Skip to content

Commit

Permalink
TURN client refactor
Browse files Browse the repository at this point in the history
Implemented allocation & permission refresh
Implemented permissions
Implemented channel binding
Threa-safety improvements
Optimized inbound packet demuxing
Resolves #74
  • Loading branch information
enobufs committed Jul 15, 2019
1 parent f0df9a6 commit a50906b
Show file tree
Hide file tree
Showing 22 changed files with 2,084 additions and 176 deletions.
524 changes: 482 additions & 42 deletions client.go

Large diffs are not rendered by default.

153 changes: 91 additions & 62 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,106 @@ package turn
import (
"net"
"testing"
"time"

"github.com/pion/stun"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"

"github.com/pion/logging"
"github.com/stretchr/testify/assert"
)

func TestClient(t *testing.T) {
func createListeningTestClient(t *testing.T, loggerFactory logging.LoggerFactory) (*Client, bool) {
conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
if !assert.NoError(t, err, "should succeed") {
return nil, false
}
c, err := NewClient(&ClientConfig{
Conn: conn,
Software: &stun.NewSoftware("TEST SOFTWARE"),
LoggerFactory: loggerFactory,
})
if !assert.NoError(t, err, "should succeed") {
return nil, false
}
err = c.Listen()
if !assert.NoError(t, err, "should succeed") {
return nil, false
}

return c, true
}

func createListeningTestClientWithSTUNServ(t *testing.T, loggerFactory logging.LoggerFactory) (*Client, bool) {
conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
if !assert.NoError(t, err, "should succeed") {
return nil, false
}
c, err := NewClient(&ClientConfig{
STUNServerAddr: "stun1.l.google.com:19302",
Conn: conn,

LoggerFactory: loggerFactory,
})
if !assert.NoError(t, err, "should succeed") {
return nil, false
}
err = c.Listen()
if !assert.NoError(t, err, "should succeed") {
return nil, false
}

return c, true
}

func TestClientWithSTUN(t *testing.T) {
loggerFactory := logging.NewDefaultLoggerFactory()
log := loggerFactory.NewLogger("test")

t.Run("SendSTUNRequest Parallel", func(t *testing.T) {
c, err := NewClient(&ClientConfig{
ListeningAddress: "0.0.0.0:0",
LoggerFactory: loggerFactory,
})
if err != nil {
t.Fatal(err)
t.Run("SendBindingRequest", func(t *testing.T) {
c, ok := createListeningTestClientWithSTUNServ(t, loggerFactory)
if !ok {
return
}
defer c.Close()

resp, err := c.SendBindingRequest()
assert.NoError(t, err, "should succeed")
log.Debugf("mapped-addr: %s", resp.String())
assert.Equal(t, 0, c.trMap.Size(), "should be no transaction left")
})

t.Run("SendBindingRequestTo Parallel", func(t *testing.T) {
c, ok := createListeningTestClient(t, loggerFactory)
if !ok {
return
}
defer c.Close()

// simple channel fo go routine start signaling
started := make(chan struct{})
finished := make(chan struct{})
var err1 error
var resp1 interface{}

to, err := net.ResolveUDPAddr("udp4", "stun1.l.google.com:19302")
if !assert.NoError(t, err, "should succeed") {
return
}

// stun1.l.google.com:19302, more at https://gist.github.com/zziuni/3741933#file-stuns-L5
go func() {
close(started)
resp1, err1 = c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302)
resp1, err1 = c.SendBindingRequestTo(to)
close(finished)
}()

// block until go routine is started to make two almost parallel requests

<-started

resp2, err2 := c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302)
resp2, err2 := c.SendBindingRequestTo(to)
if err2 != nil {
t.Fatal(err)
} else {
Expand All @@ -55,61 +117,28 @@ func TestClient(t *testing.T) {
}
})

t.Run("SendSTUNRequest adds SOFTWARE attribute to message", func(t *testing.T) {
const testSoftware = "CLIENT_SOFTWARE"

cfg := &ClientConfig{
ListeningAddress: "0.0.0.0:0",
LoggerFactory: loggerFactory,
Sender: func(conn net.PacketConn, addr net.Addr, attrs ...stun.Setter) error {
msg, err := stun.Build(attrs...)
if err != nil {
return errors.Wrap(err, "could not build message")
}
var software stun.Software
if err = software.GetFrom(msg); err != nil {
return errors.Wrap(err, "could not get SOFTWARE attribute")
}

assert.Equal(t, testSoftware, software.String())

// just forward to the default sender.
return defaultBuildAndSend(conn, addr, attrs...)
},
}
software := stun.NewSoftware(testSoftware)
cfg.Software = &software
t.Run("NewClient should fail if Conn is nil", func(t *testing.T) {
_, err := NewClient(&ClientConfig{
LoggerFactory: loggerFactory,
})
assert.Error(t, err, "should fail")
})

c, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
if _, err = c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302); err != nil {
t.Fatal(err)
t.Run("SendBindingRequestTo timeout", func(t *testing.T) {
c, ok := createListeningTestClient(t, loggerFactory)
if !ok {
return
}
})
defer c.Close()

t.Run("Listen error", func(t *testing.T) {
_, err := NewClient(&ClientConfig{
ListeningAddress: "255.255.255.256:65535",
LoggerFactory: loggerFactory,
})
if err == nil {
t.Fatal("listening on 255.255.255.256:65535 should fail")
to, err := net.ResolveUDPAddr("udp4", "127.0.0.1:9")
if !assert.NoError(t, err, "should succeed") {
return
}
})

/*
// Unable to perform this test atm because there is no timeout and the test may run infinitely
t.Run("SendSTUNRequest timeout", func(t *testing.T) {
c, err := NewClient("0.0.0.0:0")
if err != nil {
t.Fatal(err)
}
_, err = c.SendSTUNRequest(net.IPv4(255, 255, 255, 255), 65535)
if err == nil {
t.Fatal("request to 255.255.255.255:65535 should fail")
}
})
*/
c.rto = 10 * time.Millisecond // force short timeout

_, err = c.SendBindingRequestTo(to)
log.Debug(err.Error())
})
}
28 changes: 20 additions & 8 deletions cmd/client/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package main

import (
"errors"
"flag"
"fmt"
"log"
"net"

Expand All @@ -12,19 +12,25 @@ import (
)

func main() {
host := flag.String("host", "74.125.143.127", "IP of TURN Server. Default is the IP of stun1.l.google.com.")
host := flag.String("host", "stun1.l.google.com", "IP of TURN Server. Default is stun1.l.google.com.")
port := flag.Int("port", 19302, "Port of TURN server.")
software := flag.String("software", "", "The STUN SOFTWARE attribute. Useful for debugging purpose.")
flag.Parse()

ip := net.ParseIP(*host)
if ip == nil {
panic(errors.New("failed to parse host IP"))
conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
if err != nil {
panic(err)
}
defer func() {
if err2 := conn.Close(); err2 != nil {
panic(err2)
}
}()

cfg := &turn.ClientConfig{
ListeningAddress: "0.0.0.0:0",
LoggerFactory: logging.NewDefaultLoggerFactory(),
STUNServerAddr: fmt.Sprintf("%s:%d", *host, *port),
Conn: conn,
LoggerFactory: logging.NewDefaultLoggerFactory(),
}

if *software != "" {
Expand All @@ -36,8 +42,14 @@ func main() {
if err != nil {
panic(err)
}
defer c.Close()

err = c.Listen()
if err != nil {
panic(err)
}

mappedAddr, err := c.SendSTUNRequest(ip, *port)
mappedAddr, err := c.SendBindingRequest()
if err != nil {
panic(err)
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ go 1.12
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gortc/turn v0.8.0
github.com/pion/logging v0.2.1
github.com/pion/logging v0.2.2
github.com/pion/stun v0.3.1
github.com/pion/transport v0.8.1
github.com/pion/transport v0.8.4
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.3.0
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ github.com/gortc/turn v0.8.0 h1:WWQi1jkoPmc2E7qgUMcZleveKikT9Ksi3QGIl8ZtY3Q=
github.com/gortc/turn v0.8.0/go.mod h1:gvguwaGAFyv5/9KrcW9MkCgHALYD+e99mSM7pSCYYho=
github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk=
github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/stun v0.3.1 h1:d09JJzOmOS8ZzIp8NppCMgrxGZpJ4Ix8qirfNYyI3BA=
github.com/pion/stun v0.3.1/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
github.com/pion/transport v0.8.1 h1:FUHJFd4MaIEJmlpiGx+ZH8j9JLsERnROHQPA9zNFFAs=
github.com/pion/transport v0.8.1/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
github.com/pion/transport v0.8.4 h1:Wios3j8IFmrli4pHiXhGMVnj1DYWiukcboZGSv8kj2M=
github.com/pion/transport v0.8.4/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
4 changes: 3 additions & 1 deletion internal/allocation/allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ func (a *Allocation) packetHandler(m *Manager) {
if err != nil {
a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err)
}
a.log.Debugf("relaying message to client at %s", a.fiveTuple.SrcAddr.String())
a.log.Debugf("relaying message from %s to client at %s",
srcAddr.String(),
a.fiveTuple.SrcAddr.String())
if _, err = a.TurnSocket.WriteTo(msg.Raw, a.fiveTuple.SrcAddr); err != nil {
a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err)
}
Expand Down
39 changes: 39 additions & 0 deletions internal/client/atomic_bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package client

import (
"sync/atomic"
)

// AtomicBool is an atomic boolean struct
type AtomicBool struct {
n int32
}

// NewAtomicBool creates a new instance of AtomicBool
func NewAtomicBool(initiallyTrue bool) *AtomicBool {
var n int32
if initiallyTrue {
n = 1
}
return &AtomicBool{n: n}
}

// SetToTrue sets this value to true
func (b *AtomicBool) SetToTrue() {
atomic.StoreInt32(&b.n, 1)
}

// SetToFalse sets this value to false
func (b *AtomicBool) SetToFalse() {
atomic.StoreInt32(&b.n, 0)
}

// True returns true if it is set to true
func (b *AtomicBool) True() bool {
return atomic.LoadInt32(&b.n) != int32(0)
}

// False return true if it is set to false
func (b *AtomicBool) False() bool {
return atomic.LoadInt32(&b.n) == int32(0)
}
24 changes: 24 additions & 0 deletions internal/client/atomic_bool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package client

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestAtomicBool(t *testing.T) {
b0 := NewAtomicBool(false)
assert.False(t, b0.True(), "should false")
assert.True(t, b0.False(), "should false")

b1 := NewAtomicBool(true)
assert.True(t, b1.True(), "should true")
assert.False(t, b1.False(), "should true")

b0.SetToTrue()
assert.True(t, b0.True(), "should true")
assert.False(t, b0.False(), "should true")
b0.SetToFalse()
assert.False(t, b0.True(), "should false")
assert.True(t, b0.False(), "should false")
}
Loading

0 comments on commit a50906b

Please sign in to comment.