Repository: pion/turn
Pull request #106 (2020-01-03 05:44:25)

Start V2
Start pion/turn V2. pion/turn is no longer a monolithic TURN server.
Instead it is a CAC (Configuration as Code) API for building TURN
clients and servers.

This release also adds TCP support.

codecov-io: # [Codecov](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=h1) Report
> Merging [#106](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=desc) into [master](https://codecov.io/gh/pion/turn/commit/28d9f281feb50575a2cb629e4e07ea7ffaa8b34d?src=pr&el=desc) will **decrease** coverage by `33.36%`.
> The diff coverage is `16.63%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/turn/pull/106/graphs/tree.svg?width=650&token=1UomjQ7lwC&height=150&src=pr)](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           master     #106       +/-   ##
===========================================
- Coverage   66.31%   32.94%   -33.37%     
===========================================
  Files          32       36        +4     
  Lines        1873     1791       -82     
===========================================
- Hits         1242      590      -652     
- Misses        498     1157      +659     
+ Partials      133       44       -89
```


| [Impacted Files](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [stun\_conn.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-c3R1bl9jb25uLmdv) | `0% <0%> (ø)` | :arrow_up: |
| [internal/allocation/reservation\_manager.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9yZXNlcnZhdGlvbl9tYW5hZ2VyLmdv) | `0% <0%> (ø)` | :arrow_up: |
| [client.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-Y2xpZW50Lmdv) | `35.58% <0%> (-34.03%)` | :arrow_down: |
| [relay\_address\_generator\_static.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-cmVsYXlfYWRkcmVzc19nZW5lcmF0b3Jfc3RhdGljLmdv) | `0% <0%> (ø)` | |
| [internal/allocation/allocation.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9hbGxvY2F0aW9uLmdv) | `0% <0%> (-86.51%)` | :arrow_down: |
| [relay\_address\_generator\_none.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-cmVsYXlfYWRkcmVzc19nZW5lcmF0b3Jfbm9uZS5nbw==) | `0% <0%> (ø)` | |
| [internal/server/turn.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-aW50ZXJuYWwvc2VydmVyL3R1cm4uZ28=) | `0% <0%> (ø)` | |
| [internal/proto/chandata.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vY2hhbmRhdGEuZ28=) | `100% <100%> (ø)` | :arrow_up: |
| [internal/server/util.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-aW50ZXJuYWwvc2VydmVyL3V0aWwuZ28=) | `20.96% <20.96%> (ø)` | |
| [server\_config.go](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree#diff-c2VydmVyX2NvbmZpZy5nbw==) | `22.72% <22.72%> (ø)` | |
| ... and [23 more](https://codecov.io/gh/pion/turn/pull/106/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=footer). Last update [28d9f28...edfb11c](https://codecov.io/gh/pion/turn/pull/106?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index ac265ae..d38e7cc 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -7,21 +7,22 @@ linters-settings:
 linters:
   enable-all: true
   disable:
+    - maligned
     - lll
     - funlen
+    - govet
+    - stylecheck
+    - godox
+    - errcheck
+    - gosec
+    - whitespace
+    - gochecknoglobals
+    - scopelint
+    - prealloc
+    - dupl
 
 issues:
   exclude-use-default: false
-  max-per-linter: 0
-  max-same-issues: 50
 
-  exclude-rules:
-    - path: internal/proto/.+_test
-      linters:
-        - scopelint
-        - gosec
-        - errcheck
-        - prealloc
-    - path: internal/proto
-      linters:
-        - gochecknoglobals
+run:
+  skip-dirs-use-default: false
diff --git a/.travis.yml b/.travis.yml
index 5efedaf..6e0ca48 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ env:
   - GO111MODULE=on
 
 before_script:
-  - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.18.0
+  - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.19.1
 
 script:
   - golangci-lint run ./...
diff --git a/client.go b/client.go
index 3594a84..8153867 100644
--- a/client.go
+++ b/client.go
@@ -13,7 +13,6 @@ import (
 	"github.com/pion/transport/vnet"
 	"github.com/pion/turn/internal/client"
 	"github.com/pion/turn/internal/proto"
-	"github.com/pkg/errors"
 )
 
 const (
@@ -398,12 +397,8 @@ func (c *Client) HandleInbound(data []byte, from net.Addr) (bool, error) {
 	switch {
 	case stun.IsMessage(data):
 		return true, c.handleSTUNMessage(data, from)
-	case len(c.turnServStr) != 0 && from.String() == c.turnServStr:
-		// received from TURN server
-		if proto.IsChannelData(data) {
-			return true, c.handleChannelData(data)
-		}
-		return true, fmt.Errorf("unexpected packet from TURN server")
+	case proto.IsChannelData(data):
+		return true, c.handleChannelData(data)
 	case len(c.stunServStr) != 0 && from.String() == c.stunServStr:
 		// received from STUN server but it is not a STUN message
 		return true, fmt.Errorf("non-STUN message from STUN server")
@@ -421,7 +416,7 @@ func (c *Client) handleSTUNMessage(data []byte, from net.Addr) error {
 
 	msg := &stun.Message{Raw: raw}
 	if err := msg.Decode(); err != nil {
-		return errors.Wrap(err, "failed to decode STUN message")
+		return fmt.Errorf("Failed to decode STUN message: %w", err)
 	}
 
 	if msg.Type.Class == stun.ClassRequest {
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..80a4199
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,12 @@
+package turn
+
+import "errors"
+
+var (
+	errRelayAddressInvalid        = errors.New("turn: RelayAddress must be valid IP to use RelayAddressGeneratorStatic")
+	errNoAvailableConns           = errors.New("turn: PacketConnConfigs and ConnConfigs are empty, unable to proceed")
+	errConnUnset                  = errors.New("turn: PacketConnConfig must have a non-nil Conn")
+	errListenerUnset              = errors.New("turn: ListenerConfig must have a non-nil Listener")
+	errListeningAddressInvalid    = errors.New("turn: RelayAddressGenerator has invalid ListeningAddress")
+	errRelayAddressGeneratorUnset = errors.New("turn: RelayAddressGenerator in RelayConfig is unset")
+)
diff --git a/examples/turn-server/main.go b/examples/turn-server/main.go
deleted file mode 100644
index 7e8f1b0..0000000
--- a/examples/turn-server/main.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package main
-
-import (
-	"log"
-	"net"
-	"os"
-	"os/signal"
-	"regexp"
-	"strconv"
-	"syscall"
-	"time"
-
-	"github.com/pion/logging"
-	"github.com/pion/turn"
-)
-
-func createAuthHandler(usersMap map[string]string) turn.AuthHandler {
-	return func(username string, srcAddr net.Addr) (string, bool) {
-		if password, ok := usersMap[username]; ok {
-			return password, true
-		}
-		return "", false
-	}
-}
-
-func main() {
-	sigs := make(chan os.Signal, 1)
-	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
-
-	usersMap := map[string]string{}
-
-	users := os.Getenv("USERS")
-	if users == "" {
-		log.Panic("USERS is a required environment variable")
-	}
-	for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(users, -1) {
-		usersMap[kv[1]] = kv[2]
-	}
-
-	realm := os.Getenv("REALM")
-	if realm == "" {
-		log.Panic("REALM is a required environment variable")
-	}
-
-	udpPortStr := os.Getenv("UDP_PORT")
-	if udpPortStr == "" {
-		udpPortStr = "3478"
-	}
-	udpPort, err := strconv.Atoi(udpPortStr)
-	if err != nil {
-		log.Panic(err)
-	}
-
-	var channelBindTimeout time.Duration
-	channelBindTimeoutStr := os.Getenv("CHANNEL_BIND_TIMEOUT")
-	if channelBindTimeoutStr != "" {
-		channelBindTimeout, err = time.ParseDuration(channelBindTimeoutStr)
-		if err != nil {
-			log.Panicf("CHANNEL_BIND_TIMEOUT=%s is an invalid time Duration", channelBindTimeoutStr)
-		}
-	}
-
-	s := turn.NewServer(&turn.ServerConfig{
-		Realm:              realm,
-		AuthHandler:        createAuthHandler(usersMap),
-		ChannelBindTimeout: channelBindTimeout,
-		ListeningPort:      udpPort,
-		LoggerFactory:      logging.NewDefaultLoggerFactory(),
-		Software:           os.Getenv("SOFTWARE"),
-	})
-
-	err = s.Start()
-	if err != nil {
-		log.Panic(err)
-	}
-
-	<-sigs
-
-	err = s.Close()
-	if err != nil {
-		log.Panic(err)
-	}
-}
diff --git a/examples/turn-server/simple/main.go b/examples/turn-server/simple/main.go
new file mode 100644
index 0000000..e39572f
--- /dev/null
+++ b/examples/turn-server/simple/main.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"net"
+	"os"
+	"os/signal"
+	"regexp"
+	"strconv"
+	"syscall"
+
+	"github.com/pion/turn"
+)
+
+func createAuthHandler(usersMap map[string]string) turn.AuthHandler {
+	return func(username string, srcAddr net.Addr) (string, bool) {
+		if password, ok := usersMap[username]; ok {
+			return password, true
+		}
+		return "", false
+	}
+}
+
+func main() {
+	publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.")
+	port := flag.Int("port", 3478, "Listening port.")
+	users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")")
+	realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")")
+	flag.Parse()
+
+	if len(*publicIP) == 0 {
+		log.Fatalf("'public-ip' is required")
+	} else if len(*users) == 0 {
+		log.Fatalf("'users' is required")
+	}
+
+	udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port))
+	if err != nil {
+		log.Panicf("Failed to create TURN server listener: %s", err)
+	}
+
+	usersMap := map[string]string{}
+	for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) {
+		usersMap[kv[1]] = kv[2]
+	}
+
+	s, err := turn.NewServer(turn.ServerConfig{
+		Realm:       *realm,
+		AuthHandler: createAuthHandler(usersMap),
+		PacketConnConfigs: []turn.PacketConnConfig{
+			{
+				PacketConn: udpListener,
+				RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
+					RelayAddress: net.ParseIP(*publicIP), // Claim that we are listening on IP passed by user (This should be your Public IP)
+					Address:      "0.0.0.0",              // But actually be listening on every interface
+				},
+			},
+		},
+	})
+	if err != nil {
+		log.Panic(err)
+	}
+
+	sigs := make(chan os.Signal, 1)
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+	<-sigs
+	if err = s.Close(); err != nil {
+		log.Panic(err)
+	}
+}
diff --git a/examples/turn-server/tcp/main.go b/examples/turn-server/tcp/main.go
new file mode 100644
index 0000000..a2675e1
--- /dev/null
+++ b/examples/turn-server/tcp/main.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"net"
+	"os"
+	"os/signal"
+	"regexp"
+	"strconv"
+	"syscall"
+
+	"github.com/pion/turn"
+)
+
+func createAuthHandler(usersMap map[string]string) turn.AuthHandler {
+	return func(username string, srcAddr net.Addr) (string, bool) {
+		if password, ok := usersMap[username]; ok {
+			return password, true
+		}
+		return "", false
+	}
+}
+
+func main() {
+	publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.")
+	port := flag.Int("port", 3478, "Listening port.")
+	users := flag.String("users", "", "List of username and password (e.g. \"user=pass,user=pass\")")
+	realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")")
+	flag.Parse()
+
+	if len(*publicIP) == 0 {
+		log.Fatalf("'public-ip' is required")
+	} else if len(*users) == 0 {
+		log.Fatalf("'users' is required")
+	}
+
+	tcpListener, err := net.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(*port))
+	if err != nil {
+		log.Panicf("Failed to create TURN server listener: %s", err)
+	}
+
+	usersMap := map[string]string{}
+	for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) {
+		usersMap[kv[1]] = kv[2]
+	}
+
+	s, err := turn.NewServer(turn.ServerConfig{
+		Realm:       *realm,
+		AuthHandler: createAuthHandler(usersMap),
+		ListenerConfigs: []turn.ListenerConfig{
+			{
+				Listener: tcpListener,
+				RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
+					RelayAddress: net.ParseIP(*publicIP),
+					Address:      "0.0.0.0",
+				},
+			},
+		},
+	})
+	if err != nil {
+		log.Panic(err)
+	}
+
+	sigs := make(chan os.Signal, 1)
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+	<-sigs
+	if err = s.Close(); err != nil {
+		log.Panic(err)
+	}
+}
diff --git a/go.mod b/go.mod
index e85d0f0..4a97f22 100644
--- a/go.mod
+++ b/go.mod
@@ -1,14 +1,12 @@
 module github.com/pion/turn
 
-go 1.12
+go 1.13
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/kr/pretty v0.1.0 // indirect
 	github.com/pion/logging v0.2.2
 	github.com/pion/stun v0.3.3
 	github.com/pion/transport v0.8.10
-	github.com/pkg/errors v0.8.1
 	github.com/stretchr/testify v1.4.0
-	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+	golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
+	golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 // indirect
 )
diff --git a/go.sum b/go.sum
index d5b9eb9..a87290d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,26 +1,26 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 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.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw=
 github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
 github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk=
 github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
-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=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/allocation/allocation.go b/internal/allocation/allocation.go
index 19b3091..a52426f 100644
--- a/internal/allocation/allocation.go
+++ b/internal/allocation/allocation.go
@@ -1,7 +1,7 @@
 package allocation
 
 import (
-	"encoding/binary"
+	"fmt"
 	"net"
 	"sync"
 	"time"
@@ -10,7 +10,6 @@ import (
 	"github.com/pion/stun"
 	"github.com/pion/turn/internal/ipnet"
 	"github.com/pion/turn/internal/proto"
-	"github.com/pkg/errors"
 )
 
 // Allocation is tied to a FiveTuple and relays traffic
@@ -56,8 +55,7 @@ func (a *Allocation) GetPermission(addr net.Addr) *Permission {
 	a.permissionsLock.RLock()
 	defer a.permissionsLock.RUnlock()
 
-	fingerprint := addr2IPFingerprint(addr)
-	return a.permissions[fingerprint]
+	return a.permissions[addr2IPFingerprint(addr)]
 }
 
 // AddPermission adds a new permission to the allocation
@@ -67,6 +65,7 @@ func (a *Allocation) AddPermission(p *Permission) {
 	a.permissionsLock.RLock()
 	existedPermission, ok := a.permissions[fingerprint]
 	a.permissionsLock.RUnlock()
+
 	if ok {
 		existedPermission.refresh(permissionTimeout)
 		return
@@ -84,8 +83,7 @@ func (a *Allocation) AddPermission(p *Permission) {
 func (a *Allocation) RemovePermission(addr net.Addr) {
 	a.permissionsLock.Lock()
 	defer a.permissionsLock.Unlock()
-	fingerprint := addr2IPFingerprint(addr)
-	delete(a.permissions, fingerprint)
+	delete(a.permissions, addr2IPFingerprint(addr))
 }
 
 // AddChannelBind adds a new ChannelBind to the allocation, it also updates the
@@ -94,9 +92,9 @@ func (a *Allocation) AddChannelBind(c *ChannelBind, lifetime time.Duration) erro
 	// Check that this channel id isn't bound to another transport address, and
 	// that this transport address isn't bound to another channel number.
 	channelByNumber := a.GetChannelByNumber(c.Number)
-	channelByPeer := a.GetChannelByAddr(c.Peer)
-	if channelByNumber != channelByPeer {
-		return errors.Errorf("You cannot use the same channel number with different peer")
+
+	if channelByNumber != a.GetChannelByAddr(c.Peer) {
+		return fmt.Errorf("You cannot use the same channel number with different peer")
 	}
 
 	// Add or refresh this channel.
@@ -230,12 +228,13 @@ func (a *Allocation) packetHandler(m *Manager) {
 			srcAddr.String())
 
 		if channel := a.GetChannelByAddr(srcAddr); channel != nil {
-			channelData := make([]byte, 4)
-			binary.BigEndian.PutUint16(channelData[0:], uint16(channel.Number))
-			binary.BigEndian.PutUint16(channelData[2:], uint16(n))
-			channelData = append(channelData, buffer[:n]...)
+			channelData := &proto.ChannelData{
+				Data:   buffer[:n],
+				Number: channel.Number,
+			}
+			channelData.Encode()
 
-			if _, err = a.TurnSocket.WriteTo(channelData, a.fiveTuple.SrcAddr); err != nil {
+			if _, err = a.TurnSocket.WriteTo(channelData.Raw, a.fiveTuple.SrcAddr); err != nil {
 				a.log.Errorf("Failed to send ChannelData from allocation %v %v", srcAddr, err)
 			}
 		} else if p := a.GetPermission(srcAddr); p != nil {
diff --git a/internal/allocation/allocation_manager.go b/internal/allocation/allocation_manager.go
index 28b9c11..791220b 100644
--- a/internal/allocation/allocation_manager.go
+++ b/internal/allocation/allocation_manager.go
@@ -7,36 +7,49 @@ import (
 	"time"
 
 	"github.com/pion/logging"
-	"github.com/pion/transport/vnet"
-	"github.com/pkg/errors"
 )
 
 // ManagerConfig a bag of config params for Manager.
 type ManagerConfig struct {
-	LeveledLogger logging.LeveledLogger
-	Net           *vnet.Net
+	LeveledLogger      logging.LeveledLogger
+	AllocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error)
+	AllocateConn       func(network string, requestedPort int) (net.Conn, net.Addr, error)
+}
+
+type reservation struct {
+	token string
+	port  int
 }
 
 // Manager is used to hold active allocations
 type Manager struct {
-	lock        sync.RWMutex
-	allocations map[string]*Allocation
-	extIPMap    map[string]net.IP
-	log         logging.LeveledLogger
-	net         *vnet.Net
+	lock sync.RWMutex
+	log  logging.LeveledLogger
+
+	allocations  map[string]*Allocation
+	reservations []*reservation
+
+	allocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error)
+	allocateConn       func(network string, requestedPort int) (net.Conn, net.Addr, error)
 }
 
 // NewManager creates a new instance of Manager.
-func NewManager(config *ManagerConfig) *Manager {
-	if config.Net == nil {
-		config.Net = vnet.NewNet(nil) // defaults to native operation
+func NewManager(config ManagerConfig) (*Manager, error) {
+	switch {
+	case config.AllocatePacketConn == nil:
+		return nil, fmt.Errorf("AllocatePacketConn must be set")
+	case config.AllocateConn == nil:
+		return nil, fmt.Errorf("AllocateConn must be set")
+	case config.LeveledLogger == nil:
+		return nil, fmt.Errorf("LeveledLogger must be set")
 	}
+
 	return &Manager{
-		log:         config.LeveledLogger,
-		net:         config.Net,
-		allocations: make(map[string]*Allocation, 64),
-		extIPMap:    make(map[string]net.IP),
-	}
+		log:                config.LeveledLogger,
+		allocations:        make(map[string]*Allocation, 64),
+		allocatePacketConn: config.AllocatePacketConn,
+		allocateConn:       config.AllocateConn,
+	}, nil
 }
 
 // GetAllocation fetches the allocation matching the passed FiveTuple
@@ -64,54 +77,36 @@ func (m *Manager) Close() error {
 func (m *Manager) CreateAllocation(
 	fiveTuple *FiveTuple,
 	turnSocket net.PacketConn,
-	relayIP net.IP, // nolint:interfacer
 	requestedPort int,
 	lifetime time.Duration) (*Allocation, error) {
 
-	if fiveTuple == nil {
-		return nil, errors.Errorf("Allocations must not be created with nil FivTuple")
-	}
-	if fiveTuple.SrcAddr == nil {
-		return nil, errors.Errorf("Allocations must not be created with nil FiveTuple.SrcAddr")
-	}
-	if fiveTuple.DstAddr == nil {
-		return nil, errors.Errorf("Allocations must not be created with nil FiveTuple.DstAddr")
+	switch {
+	case fiveTuple == nil:
+		return nil, fmt.Errorf("Allocations must not be created with nil FivTuple")
+	case fiveTuple.SrcAddr == nil:
+		return nil, fmt.Errorf("Allocations must not be created with nil FiveTuple.SrcAddr")
+	case fiveTuple.DstAddr == nil:
+		return nil, fmt.Errorf("Allocations must not be created with nil FiveTuple.DstAddr")
+	case turnSocket == nil:
+		return nil, fmt.Errorf("Allocations must not be created with nil turnSocket")
+	case lifetime == 0:
+		return nil, fmt.Errorf("Allocations must not be created with a lifetime of 0")
 	}
+
 	if a := m.GetAllocation(fiveTuple); a != nil {
-		return nil, errors.Errorf("Allocation attempt created with duplicate FiveTuple %v", fiveTuple)
+		return nil, fmt.Errorf("Allocation attempt created with duplicate FiveTuple %v", fiveTuple)
 	}
-	if turnSocket == nil {
-		return nil, errors.Errorf("Allocations must not be created with nil turnSocket")
-	}
-	if lifetime == 0 {
-		return nil, errors.Errorf("Allocations must not be created with a lifetime of 0")
-	}
-
 	a := NewAllocation(turnSocket, fiveTuple, m.log)
 
-	conn, err := m.net.ListenPacket(
-		"udp4",
-		fmt.Sprintf("%s:%d", relayIP.String(), requestedPort))
+	conn, relayAddr, err := m.allocatePacketConn("udp4", requestedPort)
 	if err != nil {
 		return nil, err
 	}
 
-	m.log.Debugf("listening on relay addr: %s", conn.LocalAddr().String())
-
 	a.RelaySocket = conn
+	a.RelayAddr = relayAddr
 
-	// Determine RelayAddr.
-	// If there's a corresponding external IP address, replace
-	// the relay IP with the external one.
-	relayAddr := conn.LocalAddr().(*net.UDPAddr)
-	if extIP, ok := m.extIPMap[relayAddr.IP.String()]; ok {
-		a.RelayAddr = &net.UDPAddr{
-			IP:   extIP,
-			Port: relayAddr.Port,
-		}
-	} else {
-		a.RelayAddr = conn.LocalAddr()
-	}
+	m.log.Debugf("listening on relay addr: %s", a.RelayAddr.String())
 
 	a.lifetimeTimer = time.AfterFunc(lifetime, func() {
 		m.DeleteAllocation(a.fiveTuple)
@@ -143,10 +138,55 @@ func (m *Manager) DeleteAllocation(fiveTuple *FiveTuple) {
 	}
 }
 
-// AddExternalIPMapping add a external IP address mapping.
-func (m *Manager) AddExternalIPMapping(extIP, locIP net.IP) {
+// CreateReservation stores the reservation for the token+port
+func (m *Manager) CreateReservation(reservationToken string, port int) {
+	time.AfterFunc(30*time.Second, func() {
+		m.lock.Lock()
+		defer m.lock.Unlock()
+		for i := len(m.reservations) - 1; i >= 0; i-- {
+			if m.reservations[i].token == reservationToken {
+				m.reservations = append(m.reservations[:i], m.reservations[i+1:]...)
+				return
+			}
+		}
+	})
+
 	m.lock.Lock()
-	defer m.lock.Unlock()
+	m.reservations = append(m.reservations, &reservation{
+		token: reservationToken,
+		port:  port,
+	})
+	m.lock.Unlock()
+}
+
+// GetReservation returns the port for a given reservation if it exists
+func (m *Manager) GetReservation(reservationToken string) (int, bool) {
+	m.lock.RLock()
+	defer m.lock.RUnlock()
+
+	for _, r := range m.reservations {
+		if r.token == reservationToken {
+			return r.port, true
+		}
+	}
+	return 0, false
+}
+
+// GetRandomEvenPort returns a random un-allocated udp4 port
+func (m *Manager) GetRandomEvenPort() (int, error) {
+	conn, addr, err := m.allocatePacketConn("udp4", 0)
+	if err != nil {
+		return 0, err
+	}
+
+	udpAddr, ok := addr.(*net.UDPAddr)
+	if !ok {
+		return 0, fmt.Errorf("failed to cast net.Addr to *net.UDPAddr")
+	} else if err := conn.Close(); err != nil {
+		return 0, err
+	} else if udpAddr.Port%2 == 1 {
+		return m.GetRandomEvenPort()
+	}
 
-	m.extIPMap[locIP.String()] = extIP
+	return udpAddr.Port, nil
 }
diff --git a/internal/allocation/reservation_manager.go b/internal/allocation/reservation_manager.go
deleted file mode 100644
index f05bcba..0000000
--- a/internal/allocation/reservation_manager.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package allocation
-
-import (
-	"fmt"
-	"net"
-	"sync"
-	"time"
-)
-
-type reservation struct {
-	token string
-	port  int
-}
-
-// ReservationManager is used to manage reservations
-type ReservationManager struct {
-	lock         sync.RWMutex
-	reservations []*reservation
-}
-
-// CreateReservation stores the reservation for the token+port
-func (m *ReservationManager) CreateReservation(reservationToken string, port int) {
-	time.AfterFunc(30*time.Second, func() {
-		m.lock.Lock()
-		defer m.lock.Unlock()
-		for i := len(m.reservations) - 1; i >= 0; i-- {
-			if m.reservations[i].token == reservationToken {
-				m.reservations = append(m.reservations[:i], m.reservations[i+1:]...)
-				return
-			}
-		}
-	})
-
-	m.lock.Lock()
-	m.reservations = append(m.reservations, &reservation{
-		token: reservationToken,
-		port:  port,
-	})
-	m.lock.Unlock()
-}
-
-// GetReservation returns the port for a given reservation if it exists
-func (m *ReservationManager) GetReservation(reservationToken string) (int, bool) {
-	m.lock.RLock()
-	defer m.lock.RUnlock()
-
-	for _, r := range m.reservations {
-		if r.token == reservationToken {
-			return r.port, true
-		}
-	}
-	return 0, false
-}
-
-// GetRandomEvenPort returns a random un-allocated udp4 port
-func GetRandomEvenPort() (int, error) {
-	listener, err := net.ListenPacket("udp4", "0.0.0.0:0")
-	if err != nil {
-		return 0, err
-	}
-
-	addr, ok := listener.LocalAddr().(*net.UDPAddr)
-	if !ok {
-		return 0, fmt.Errorf("failed to cast net.Addr to *net.UDPAddr")
-	} else if err := listener.Close(); err != nil {
-		return 0, err
-	} else if addr.Port%2 == 1 {
-		return GetRandomEvenPort()
-	}
-
-	return addr.Port, nil
-}
diff --git a/internal/ipnet/util.go b/internal/ipnet/util.go
index ce40b97..b6e9c17 100644
--- a/internal/ipnet/util.go
+++ b/internal/ipnet/util.go
@@ -8,11 +8,16 @@ import (
 // AddrIPPort extracts the IP and Port from a net.Addr
 func AddrIPPort(a net.Addr) (net.IP, int, error) {
 	aUDP, ok := a.(*net.UDPAddr)
-	if !ok {
-		return nil, 0, fmt.Errorf("failed to cast net.Addr to *net.UDPAddr")
+	if ok {
+		return aUDP.IP, aUDP.Port, nil
+	}
+
+	aTCP, ok := a.(*net.TCPAddr)
+	if ok {
+		return aTCP.IP, aTCP.Port, nil
 	}
 
-	return aUDP.IP, aUDP.Port, nil
+	return nil, 0, fmt.Errorf("failed to cast net.Addr to *net.UDPAddr or *net.TCPAddr")
 }
 
 // AddrEqual asserts that two net.Addrs are equal
diff --git a/internal/proto/chandata.go b/internal/proto/chandata.go
index f715dff..4151934 100644
--- a/internal/proto/chandata.go
+++ b/internal/proto/chandata.go
@@ -128,6 +128,11 @@ func IsChannelData(buf []byte) bool {
 	if len(buf) < channelDataHeaderSize {
 		return false
 	}
+
+	if int(bin.Uint16(buf[channelDataNumberSize:channelDataHeaderSize])) > len(buf[channelDataHeaderSize:]) {
+		return false
+	}
+
 	// Quick check for channel number.
 	num := bin.Uint16(buf[0:channelNumberSize])
 	return isChannelNumberValid(num)
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000..219c674
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1,105 @@
+package server
+
+import (
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/pion/logging"
+	"github.com/pion/stun"
+	"github.com/pion/turn/internal/allocation"
+	"github.com/pion/turn/internal/proto"
+)
+
+// Request contains all the state needed to process a single incoming datagram
+type Request struct {
+	Conn    net.PacketConn
+	SrcAddr net.Addr
+	Buff    []byte
+
+	// Shared across all Requests
+	Log                logging.LeveledLogger
+	AllocationManager  *allocation.Manager
+	Realm              string
+	ChannelBindTimeout time.Duration
+
+	// User Callbacks
+	AuthHandler func(username string, srcAddr net.Addr) (password string, ok bool)
+}
+
+// HandleRequest processes the give Request
+func HandleRequest(r Request) error {
+	r.Log.Debugf("received %d bytes of udp from %s on %s", len(r.Buff), r.SrcAddr.String(), r.Conn.LocalAddr().String())
+
+	if proto.IsChannelData(r.Buff) {
+		return handleDataPacket(r)
+	}
+
+	return handleTURNPacket(r)
+}
+
+func handleDataPacket(r Request) error {
+	r.Log.Debugf("received DataPacket from %s", r.SrcAddr.String())
+	c := proto.ChannelData{Raw: r.Buff}
+	if err := c.Decode(); err != nil {
+		return fmt.Errorf("failed to create channel data from packet: %v", err)
+	}
+
+	err := handleChannelData(r, &c)
+	if err != nil {
+		err = fmt.Errorf("unable to handle ChannelData from %v: %v", r.SrcAddr, err)
+	}
+
+	return err
+}
+
+func handleTURNPacket(r Request) error {
+	r.Log.Debug("handleTURNPacket")
+	m := &stun.Message{Raw: append([]byte{}, r.Buff...)}
+	if err := m.Decode(); err != nil {
+		return fmt.Errorf("failed to create stun message from packet: %v", err)
+	}
+
+	h, err := getMessageHandler(m.Type.Class, m.Type.Method)
+	if err != nil {
+		return fmt.Errorf("unhandled STUN packet %v-%v from %v: %v", m.Type.Method, m.Type.Class, r.SrcAddr, err)
+	}
+
+	err = h(r, m)
+	if err != nil {
+		return fmt.Errorf("failed to handle %v-%v from %v: %v", m.Type.Method, m.Type.Class, r.SrcAddr, err)
+	}
+
+	return nil
+}
+
+func getMessageHandler(class stun.MessageClass, method stun.Method) (func(r Request, m *stun.Message) error, error) {
+	switch class {
+	case stun.ClassIndication:
+		switch method {
+		case stun.MethodSend:
+			return handleSendIndication, nil
+		default:
+			return nil, fmt.Errorf("unexpected method: %s", method)
+		}
+
+	case stun.ClassRequest:
+		switch method {
+		case stun.MethodAllocate:
+			return handleAllocateRequest, nil
+		case stun.MethodRefresh:
+			return handleRefreshRequest, nil
+		case stun.MethodCreatePermission:
+			return handleCreatePermissionRequest, nil
+		case stun.MethodChannelBind:
+			return handleChannelBindRequest, nil
+		case stun.MethodBinding:
+			return handleBindingRequest, nil
+		default:
+			return nil, fmt.Errorf("unexpected method: %s", method)
+		}
+
+	default:
+		return nil, fmt.Errorf("unexpected class: %s", class)
+	}
+}
diff --git a/internal/server/stun.go b/internal/server/stun.go
new file mode 100644
index 0000000..f22d7ae
--- /dev/null
+++ b/internal/server/stun.go
@@ -0,0 +1,22 @@
+package server
+
+import (
+	"github.com/pion/stun"
+	"github.com/pion/turn/internal/ipnet"
+)
+
+func handleBindingRequest(r Request, m *stun.Message) error {
+	r.Log.Debugf("received BindingRequest from %s", r.SrcAddr.String())
+
+	ip, port, err := ipnet.AddrIPPort(r.SrcAddr)
+	if err != nil {
+		return err
+	}
+
+	attrs := buildMsg(m.TransactionID, stun.BindingSuccess, &stun.XORMappedAddress{
+		IP:   ip,
+		Port: port,
+	}, stun.Fingerprint)
+
+	return buildAndSend(r.Conn, r.SrcAddr, attrs...)
+}
diff --git a/internal/server/turn.go b/internal/server/turn.go
new file mode 100644
index 0000000..04d2697
--- /dev/null
+++ b/internal/server/turn.go
@@ -0,0 +1,358 @@
+package server
+
+import (
+	// #nosec
+
+	"fmt"
+	"net"
+
+	"github.com/pion/stun"
+	"github.com/pion/turn/internal/allocation"
+	"github.com/pion/turn/internal/ipnet"
+	"github.com/pion/turn/internal/proto"
+)
+
+// // https://tools.ietf.org/html/rfc5766#section-6.2
+func handleAllocateRequest(r Request, m *stun.Message) error {
+	r.Log.Debugf("received AllocateRequest from %s", r.SrcAddr.String())
+
+	// 1. The server MUST require that the request be authenticated.  This
+	//    authentication MUST be done using the long-term credential
+	//    mechanism of [https://tools.ietf.org/html/rfc5389#section-10.2.2]
+	//    unless the client and server agree to use another mechanism through
+	//    some procedure outside the scope of this document.
+	messageIntegrity, rtrnMsg, err := authenticateRequest(r, m, stun.MethodAllocate)
+
+	// In some cases authenticateRequest may wish to send a message back to the user
+	if rtrnMsg != nil {
+		if sendErr := buildAndSend(r.Conn, r.SrcAddr, rtrnMsg...); sendErr != nil {
+			return sendErr
+		}
+	}
+
+	// If we send an message or had on error return
+	if rtrnMsg != nil || err != nil {
+		return err
+	}
+
+	fiveTuple := &allocation.FiveTuple{
+		SrcAddr:  r.SrcAddr,
+		DstAddr:  r.Conn.LocalAddr(),
+		Protocol: allocation.UDP,
+	}
+	requestedPort := 0
+	reservationToken := ""
+
+	badRequestMsg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
+	insufficentCapacityMsg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeInsufficientCapacity})
+
+	// 2. The server checks if the 5-tuple is currently in use by an
+	//    existing allocation.  If yes, the server rejects the request with
+	//    a 437 (Allocation Mismatch) error.
+	if alloc := r.AllocationManager.GetAllocation(fiveTuple); alloc != nil {
+		msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeAllocMismatch})
+		return buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("Relay already allocated for 5-TUPLE"), msg...)
+	}
+
+	// 3. The server checks if the request contains a REQUESTED-TRANSPORT
+	//    attribute.  If the REQUESTED-TRANSPORT attribute is not included
+	//    or is malformed, the server rejects the request with a 400 (Bad
+	//    Request) error.  Otherwise, if the attribute is included but
+	//    specifies a protocol other that UDP, the server rejects the
+	//    request with a 442 (Unsupported Transport Protocol) error.
+	var requestedTransport proto.RequestedTransport
+	if err = requestedTransport.GetFrom(m); err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	} else if requestedTransport.Protocol != proto.ProtoUDP {
+		msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnsupportedTransProto})
+		return buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("RequestedTransport must be UDP"), msg...)
+	}
+
+	// 4. The request may contain a DONT-FRAGMENT attribute.  If it does,
+	//    but the server does not support sending UDP datagrams with the DF
+	//    bit set to 1 (see Section 12), then the server treats the DONT-
+	//    FRAGMENT attribute in the Allocate request as an unknown
+	//    comprehension-required attribute.
+	if m.Contains(stun.AttrDontFragment) {
+		msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnknownAttribute}, &stun.UnknownAttributes{stun.AttrDontFragment})
+		return buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("no support for DONT-FRAGMENT"), msg...)
+	}
+
+	// 5.  The server checks if the request contains a RESERVATION-TOKEN
+	//     attribute.  If yes, and the request also contains an EVEN-PORT
+	//     attribute, then the server rejects the request with a 400 (Bad
+	//     Request) error.  Otherwise, it checks to see if the token is
+	//     valid (i.e., the token is in range and has not expired and the
+	//     corresponding relayed transport address is still available).  If
+	//     the token is not valid for some reason, the server rejects the
+	//     request with a 508 (Insufficient Capacity) error.
+	var reservationTokenAttr proto.ReservationToken
+	if err = reservationTokenAttr.GetFrom(m); err == nil {
+		var evenPort proto.EvenPort
+		if err = evenPort.GetFrom(m); err == nil {
+			return buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("Request must not contain RESERVATION-TOKEN and EVEN-PORT"), badRequestMsg...)
+		}
+
+	}
+
+	// 6. The server checks if the request contains an EVEN-PORT attribute.
+	//    If yes, then the server checks that it can satisfy the request
+	//    (i.e., can allocate a relayed transport address as described
+	//    below).  If the server cannot satisfy the request, then the
+	//    server rejects the request with a 508 (Insufficient Capacity)
+	//    error.
+	var evenPort proto.EvenPort
+	if err = evenPort.GetFrom(m); err == nil {
+		randomPort := 0
+		randomPort, err = r.AllocationManager.GetRandomEvenPort()
+		if err != nil {
+			return buildAndSendErr(r.Conn, r.SrcAddr, err, insufficentCapacityMsg...)
+		}
+		requestedPort = randomPort
+		reservationToken = randSeq(8)
+	}
+
+	// 7. At any point, the server MAY choose to reject the request with a
+	//    486 (Allocation Quota Reached) error if it feels the client is
+	//    trying to exceed some locally defined allocation quota.  The
+	//    server is free to define this allocation quota any way it wishes,
+	//    but SHOULD define it based on the username used to authenticate
+	//    the request, and not on the client's transport address.
+
+	// 8. Also at any point, the server MAY choose to reject the request
+	//    with a 300 (Try Alternate) error if it wishes to redirect the
+	//    client to a different server.  The use of this error code and
+	//    attribute follow the specification in [RFC5389].
+	lifetimeDuration := allocationLifeTime(m)
+	a, err := r.AllocationManager.CreateAllocation(
+		fiveTuple,
+		r.Conn,
+		requestedPort,
+		lifetimeDuration)
+	if err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, insufficentCapacityMsg...)
+	}
+
+	// Once the allocation is created, the server replies with a success
+	// response.  The success response contains:
+	//   * An XOR-RELAYED-ADDRESS attribute containing the relayed transport
+	//     address.
+	//   * A LIFETIME attribute containing the current value of the time-to-
+	//     expiry timer.
+	//   * A RESERVATION-TOKEN attribute (if a second relayed transport
+	//     address was reserved).
+	//   * An XOR-MAPPED-ADDRESS attribute containing the client's IP address
+	//     and port (from the 5-tuple).
+
+	srcIP, srcPort, err := ipnet.AddrIPPort(r.SrcAddr)
+	if err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	}
+
+	relayIP, relayPort, err := ipnet.AddrIPPort(a.RelayAddr)
+	if err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	}
+
+	responseAttrs := []stun.Setter{
+		&proto.RelayedAddress{
+			IP:   relayIP,
+			Port: relayPort,
+		},
+		&proto.Lifetime{
+			Duration: lifetimeDuration,
+		},
+		&stun.XORMappedAddress{
+			IP:   srcIP,
+			Port: srcPort,
+		},
+	}
+
+	if reservationToken != "" {
+		r.AllocationManager.CreateReservation(reservationToken, relayPort)
+		responseAttrs = append(responseAttrs, proto.ReservationToken([]byte(reservationToken)))
+	}
+
+	msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassSuccessResponse), append(responseAttrs, messageIntegrity)...)
+	return buildAndSend(r.Conn, r.SrcAddr, msg...)
+}
+
+func handleRefreshRequest(r Request, m *stun.Message) error {
+	r.Log.Debugf("received RefreshRequest from %s", r.SrcAddr.String())
+
+	messageIntegrity, _, err := authenticateRequest(r, m, stun.MethodRefresh)
+	if err != nil {
+		return err
+	}
+
+	a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
+		SrcAddr:  r.SrcAddr,
+		DstAddr:  r.Conn.LocalAddr(),
+		Protocol: allocation.UDP,
+	})
+	if a == nil {
+		return fmt.Errorf("No allocation found for %v:%v", r.SrcAddr, r.Conn.LocalAddr())
+	}
+
+	lifetimeDuration := allocationLifeTime(m)
+	a.Refresh(lifetimeDuration)
+
+	return buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID, stun.NewType(stun.MethodRefresh, stun.ClassSuccessResponse), []stun.Setter{
+		&proto.Lifetime{
+			Duration: lifetimeDuration,
+		},
+		messageIntegrity,
+	}...)...)
+}
+
+func handleCreatePermissionRequest(r Request, m *stun.Message) error {
+	r.Log.Debugf("received CreatePermission from %s", r.SrcAddr.String())
+
+	a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
+		SrcAddr:  r.SrcAddr,
+		DstAddr:  r.Conn.LocalAddr(),
+		Protocol: allocation.UDP,
+	})
+	if a == nil {
+		return fmt.Errorf("No allocation found for %v:%v", r.SrcAddr, r.Conn.LocalAddr())
+	}
+
+	messageIntegrity, _, err := authenticateRequest(r, m, stun.MethodChannelBind)
+	if err != nil {
+		return err
+	}
+
+	addCount := 0
+
+	if err := m.ForEach(stun.AttrXORPeerAddress, func(m *stun.Message) error {
+		var peerAddress proto.PeerAddress
+		if err := peerAddress.GetFrom(m); err != nil {
+			return err
+		}
+
+		r.Log.Debugf("adding permission for %s", fmt.Sprintf("%s:%d",
+			peerAddress.IP.String(), peerAddress.Port))
+		a.AddPermission(allocation.NewPermission(
+			&net.UDPAddr{
+				IP:   peerAddress.IP,
+				Port: peerAddress.Port,
+			},
+			r.Log,
+		))
+		addCount++
+		return nil
+	}); err != nil {
+		addCount = 0
+	}
+
+	respClass := stun.ClassSuccessResponse
+	if addCount == 0 {
+		respClass = stun.ClassErrorResponse
+	}
+
+	return buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID, stun.NewType(stun.MethodChannelBind, respClass), []stun.Setter{messageIntegrity}...)...)
+}
+
+func handleSendIndication(r Request, m *stun.Message) error {
+	r.Log.Debugf("received SendIndication from %s", r.SrcAddr.String())
+	a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
+		SrcAddr:  r.SrcAddr,
+		DstAddr:  r.Conn.LocalAddr(),
+		Protocol: allocation.UDP,
+	})
+	if a == nil {
+		return fmt.Errorf("No allocation found for %v:%v", r.SrcAddr, r.Conn.LocalAddr())
+	}
+
+	dataAttr := proto.Data{}
+	if err := dataAttr.GetFrom(m); err != nil {
+		return err
+	}
+
+	peerAddress := proto.PeerAddress{}
+	if err := peerAddress.GetFrom(m); err != nil {
+		return err
+	}
+
+	msgDst := &net.UDPAddr{IP: peerAddress.IP, Port: peerAddress.Port}
+	if perm := a.GetPermission(msgDst); perm == nil {
+		return fmt.Errorf("Unable to handle send-indication, no permission added: %v", msgDst)
+	}
+
+	l, err := a.RelaySocket.WriteTo(dataAttr, msgDst)
+	if l != len(dataAttr) {
+		return fmt.Errorf("packet write smaller than packet %d != %d (expected) err: %v", l, len(dataAttr), err)
+	}
+	return err
+}
+
+func handleChannelBindRequest(r Request, m *stun.Message) error {
+	r.Log.Debugf("received ChannelBindRequest from %s", r.SrcAddr.String())
+
+	a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
+		SrcAddr:  r.SrcAddr,
+		DstAddr:  r.Conn.LocalAddr(),
+		Protocol: allocation.UDP,
+	})
+	if a == nil {
+		return fmt.Errorf("No allocation found for %v:%v", r.SrcAddr, r.Conn.LocalAddr())
+	}
+
+	badRequestMsg := buildMsg(m.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
+
+	messageIntegrity, _, err := authenticateRequest(r, m, stun.MethodChannelBind)
+	if err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	}
+
+	var channel proto.ChannelNumber
+	if err = channel.GetFrom(m); err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	}
+
+	peerAddr := proto.PeerAddress{}
+	if err = peerAddr.GetFrom(m); err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	}
+
+	r.Log.Debugf("binding channel %d to %s",
+		channel,
+		fmt.Sprintf("%s:%d", peerAddr.IP.String(), peerAddr.Port))
+	err = a.AddChannelBind(allocation.NewChannelBind(
+		channel,
+		&net.UDPAddr{IP: peerAddr.IP, Port: peerAddr.Port},
+		r.Log,
+	), r.ChannelBindTimeout)
+	if err != nil {
+		return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
+	}
+
+	return buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassSuccessResponse), []stun.Setter{messageIntegrity}...)...)
+}
+
+func handleChannelData(r Request, c *proto.ChannelData) error {
+	r.Log.Debugf("received ChannelData from %s", r.SrcAddr.String())
+
+	a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
+		SrcAddr:  r.SrcAddr,
+		DstAddr:  r.Conn.LocalAddr(),
+		Protocol: allocation.UDP,
+	})
+	if a == nil {
+		return fmt.Errorf("No allocation found for %v:%v", r.SrcAddr, r.Conn.LocalAddr())
+	}
+
+	channel := a.GetChannelByNumber(c.Number)
+	if channel == nil {
+		return fmt.Errorf("no channel bind found for %x", uint16(c.Number))
+	}
+
+	l, err := a.RelaySocket.WriteTo(c.Data, channel.Peer)
+	if err != nil {
+		return fmt.Errorf("failed writing to socket: %w", err)
+	} else if l != len(c.Data) {
+		return fmt.Errorf("packet write smaller than packet %d != %d (expected)", l, len(c.Data))
+	}
+
+	return nil
+}
diff --git a/internal/server/util.go b/internal/server/util.go
new file mode 100644
index 0000000..22f2f8d
--- /dev/null
+++ b/internal/server/util.go
@@ -0,0 +1,124 @@
+package server
+
+import (
+	// #nosec
+
+	"crypto/md5"
+	"fmt"
+	"io"
+	"math/rand"
+	"net"
+	"strconv"
+	"time"
+
+	"github.com/pion/stun"
+	"github.com/pion/turn/internal/proto"
+)
+
+const (
+	maximumAllocationLifetime = time.Hour // https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation
+)
+
+func randSeq(n int) string {
+	letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = letters[rand.Intn(len(letters))]
+	}
+	return string(b)
+}
+
+// TODO, include time info support stale nonces
+func buildNonce() (string, error) {
+	/* #nosec */
+	h := md5.New()
+	if _, err := io.WriteString(h, strconv.FormatInt(time.Now().Unix(), 10)); err != nil {
+		return "", fmt.Errorf("failed generate nonce: %v", err)
+	}
+	if _, err := io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)); err != nil {
+		return "", fmt.Errorf("failed generate nonce: %v", err)
+	}
+	return fmt.Sprintf("%x", h.Sum(nil)), nil
+}
+
+func buildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error {
+	msg, err := stun.Build(attrs...)
+	if err != nil {
+		return err
+	}
+	_, err = conn.WriteTo(msg.Raw, dst)
+	return err
+}
+
+// Send a STUN packet and return the original error to the caller
+func buildAndSendErr(conn net.PacketConn, dst net.Addr, err error, attrs ...stun.Setter) error {
+	if sendErr := buildAndSend(conn, dst, attrs...); sendErr != nil {
+		err = fmt.Errorf("Failed to send error message %w %w", sendErr, err)
+	}
+	return err
+}
+
+func buildMsg(transactionID [stun.TransactionIDSize]byte, msgType stun.MessageType, additional ...stun.Setter) []stun.Setter {
+	return append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...)
+}
+
+func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) (stun.MessageIntegrity, []stun.Setter, error) {
+	if !m.Contains(stun.AttrMessageIntegrity) {
+		nonce, err := buildNonce()
+		if err != nil {
+			return stun.MessageIntegrity{}, nil, err
+		}
+
+		return nil, buildMsg(m.TransactionID,
+			stun.NewType(callingMethod, stun.ClassErrorResponse),
+			&stun.ErrorCodeAttribute{Code: stun.CodeUnauthorized},
+			stun.NewNonce(nonce),
+			stun.NewRealm(r.Realm),
+		), nil
+
+	}
+
+	var ourKey [16]byte
+	nonceAttr := &stun.Nonce{}
+	usernameAttr := &stun.Username{}
+	realmAttr := &stun.Realm{}
+	badRequestMsg := buildMsg(m.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
+
+	if err := realmAttr.GetFrom(m); err != nil {
+		return nil, badRequestMsg, err
+	}
+
+	if err := nonceAttr.GetFrom(m); err != nil {
+		return nil, badRequestMsg, err
+	}
+
+	if err := usernameAttr.GetFrom(m); err != nil {
+		return nil, badRequestMsg, err
+	}
+
+	password, ok := r.AuthHandler(usernameAttr.String(), r.SrcAddr)
+	if !ok {
+		return nil, badRequestMsg, fmt.Errorf("No user exists for %s", usernameAttr.String())
+	}
+
+	/* #nosec */
+	ourKey = md5.Sum([]byte(usernameAttr.String() + ":" + realmAttr.String() + ":" + password))
+	if err := stun.MessageIntegrity(ourKey[:]).Check(m); err != nil {
+		return nil, badRequestMsg, err
+	}
+
+	return stun.NewLongTermIntegrity(usernameAttr.String(), realmAttr.String(), password), nil, nil
+}
+
+func allocationLifeTime(m *stun.Message) time.Duration {
+	lifetimeDuration := proto.DefaultLifetime
+
+	var lifetime proto.Lifetime
+	if err := lifetime.GetFrom(m); err == nil {
+		if lifetime.Duration < maximumAllocationLifetime {
+			lifetimeDuration = lifetime.Duration
+		}
+	}
+
+	return lifetimeDuration
+}
diff --git a/relay_address_generator_none.go b/relay_address_generator_none.go
new file mode 100644
index 0000000..a33a868
--- /dev/null
+++ b/relay_address_generator_none.go
@@ -0,0 +1,46 @@
+package turn
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+
+	"github.com/pion/transport/vnet"
+)
+
+// RelayAddressGeneratorNone returns the listener with no modifications
+type RelayAddressGeneratorNone struct {
+	// Address is passed to Listen/ListenPacket when creating the Relay
+	Address string
+
+	Net *vnet.Net
+}
+
+// Validate is caled on server startup and confirms the RelayAddressGenerator is properly configured
+func (r *RelayAddressGeneratorNone) Validate() error {
+	if r.Net == nil {
+		r.Net = vnet.NewNet(nil)
+	}
+
+	switch {
+	case r.Address == "":
+		return errListeningAddressInvalid
+	default:
+		return nil
+	}
+}
+
+// AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port to populate the allocation response with
+func (r *RelayAddressGeneratorNone) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
+	conn, err := r.Net.ListenPacket(network, r.Address+":"+strconv.Itoa(requestedPort))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return conn, conn.LocalAddr(), nil
+}
+
+// AllocateConn generates a new Conn to receive traffic on and the IP/Port to populate the allocation response with
+func (r *RelayAddressGeneratorNone) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) {
+	return nil, nil, fmt.Errorf("TODO")
+}
diff --git a/relay_address_generator_static.go b/relay_address_generator_static.go
new file mode 100644
index 0000000..11ba905
--- /dev/null
+++ b/relay_address_generator_static.go
@@ -0,0 +1,56 @@
+package turn
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+
+	"github.com/pion/transport/vnet"
+)
+
+// RelayAddressGeneratorStatic can be used to return static IP address each time a relay is created.
+// This can be used when you have a single static IP address that you want to use
+type RelayAddressGeneratorStatic struct {
+	// RelayAddress is the IP returned to the user when the relay is created
+	RelayAddress net.IP
+
+	// Address is passed to Listen/ListenPacket when creating the Relay
+	Address string
+
+	Net *vnet.Net
+}
+
+// Validate is caled on server startup and confirms the RelayAddressGenerator is properly configured
+func (r *RelayAddressGeneratorStatic) Validate() error {
+	if r.Net == nil {
+		r.Net = vnet.NewNet(nil)
+	}
+
+	switch {
+	case r.RelayAddress == nil:
+		return errRelayAddressInvalid
+	case r.Address == "":
+		return errListeningAddressInvalid
+	default:
+		return nil
+	}
+}
+
+// AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port to populate the allocation response with
+func (r *RelayAddressGeneratorStatic) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
+	conn, err := r.Net.ListenPacket(network, r.Address+":"+strconv.Itoa(requestedPort))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Replace actual listening IP with the user requested one of RelayAddressGeneratorStatic
+	relayAddr := conn.LocalAddr().(*net.UDPAddr)
+	relayAddr.IP = r.RelayAddress
+
+	return conn, relayAddr, nil
+}
+
+// AllocateConn generates a new Conn to receive traffic on and the IP/Port to populate the allocation response with
+func (r *RelayAddressGeneratorStatic) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) {
+	return nil, nil, fmt.Errorf("TODO")
+}
diff --git a/sender.go b/sender.go
deleted file mode 100644
index bb0bc1c..0000000
--- a/sender.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package turn
-
-import (
-	"net"
-
-	"github.com/pion/stun"
-)
-
-// Sender is responsible for building a message and sending it to the given addr.
-type Sender func(conn net.PacketConn, addr net.Addr, attrs ...stun.Setter) error
-
-// defaultBuildAndSend is a sender default implementation.
-func defaultBuildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error {
-	msg, err := stun.Build(attrs...)
-	if err != nil {
-		return err
-	}
-	_, err = conn.WriteTo(msg.Raw, dst)
-	return err
-}
diff --git a/server.go b/server.go
index 5e15d80..f42daf0 100644
--- a/server.go
+++ b/server.go
@@ -1,470 +1,162 @@
 package turn
 
 import (
-	"crypto/md5" // #nosec
-	"fmt"
-	"io"
-	"math/rand"
 	"net"
-	"strconv"
-	"strings"
-	"sync"
 	"time"
 
 	"github.com/pion/logging"
-	"github.com/pion/stun"
-	"github.com/pion/transport/vnet"
 	"github.com/pion/turn/internal/allocation"
 	"github.com/pion/turn/internal/proto"
-	"github.com/pkg/errors"
+	"github.com/pion/turn/internal/server"
 )
 
 const (
-	maxStunMessageSize = 1500
+	inboundMTU = 1500
 )
 
-// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN
-// with custom behavior
-type AuthHandler func(username string, srcAddr net.Addr) (password string, ok bool)
-
-// ServerConfig is a bag of config parameters for Server.
-type ServerConfig struct {
-	// Realm sets the realm for this server
-	Realm string
-	// AuthHandler is the handler called on each incoming auth requests.
-	AuthHandler AuthHandler
-	// ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes.
-	ChannelBindTimeout time.Duration
-	// ListeningPort sets the listening port number. Defaults to 3478.
-	ListeningPort int
-	// LoggerFactory must be set for logging from this server.
-	LoggerFactory logging.LoggerFactory
-	// Net is used by pion developers. Do not use in your application.
-	Net *vnet.Net
-	// Software is the STUN SOFTWARE attribute. Useful for debugging purpose.
-	Software string
-	// Sender is a custom implementation of the request Sender.
-	Sender Sender
-}
-
-type listener struct {
-	conn    net.PacketConn
-	closeCh chan struct{}
-}
-
-// Server is an instance of the Pion TURN server
+// Server is an instance of the Pion TURN Server
 type Server struct {
-	lock               sync.RWMutex
-	listeners          []*listener
-	listenIPs          []net.IP
-	relayIPs           []net.IP
-	extIPMappings      [][2]net.IP
-	listenPort         int
-	realm              string
+	log                logging.LeveledLogger
 	authHandler        AuthHandler
-	manager            *allocation.Manager
-	reservationManager *allocation.ReservationManager
+	realm              string
 	channelBindTimeout time.Duration
-	log                logging.LeveledLogger
-	net                *vnet.Net
-	software           stun.Software
-	sender             Sender
+
+	packetConnConfigs []PacketConnConfig
+	listenerConfigs   []ListenerConfig
 }
 
 // NewServer creates the Pion TURN server
-func NewServer(config *ServerConfig) *Server {
-	log := config.LoggerFactory.NewLogger("turn")
-
-	if config.Net == nil {
-		config.Net = vnet.NewNet(nil) // defaults to native operation
-	} else {
-		log.Warn("vnet is enabled")
-	}
-
-	if config.Sender == nil {
-		config.Sender = defaultBuildAndSend
-	}
-
-	manager := allocation.NewManager(&allocation.ManagerConfig{
-		LeveledLogger: log,
-		Net:           config.Net,
-	})
-
-	listenPort := config.ListeningPort
-	if listenPort == 0 {
-		listenPort = 3478
+func NewServer(config ServerConfig) (*Server, error) {
+	if err := config.validate(); err != nil {
+		return nil, err
 	}
 
-	channelBindTimeout := config.ChannelBindTimeout
-	if channelBindTimeout == 0 {
-		channelBindTimeout = proto.DefaultLifetime
+	loggerFactory := config.LoggerFactory
+	if loggerFactory == nil {
+		loggerFactory = logging.NewDefaultLoggerFactory()
 	}
 
-	return &Server{
-		listenPort:         listenPort,
-		realm:              config.Realm,
+	s := &Server{
+		log:                loggerFactory.NewLogger("turn"),
 		authHandler:        config.AuthHandler,
-		manager:            manager,
-		reservationManager: &allocation.ReservationManager{},
-		channelBindTimeout: channelBindTimeout,
-		log:                log,
-		net:                config.Net,
-		software:           stun.NewSoftware(config.Software),
-		sender:             config.Sender,
+		realm:              config.Realm,
+		channelBindTimeout: config.ChannelBindTimeout,
 	}
-}
 
-// AddListeningIPAddr adds a listening IP address.
-// If not specified, it will automatically assigns the listening IP addresses
-// from the system.
-// This method must be called before calling Start().
-func (s *Server) AddListeningIPAddr(addrStr string) error {
-	ip := net.ParseIP(addrStr)
-	if ip.To4() == nil {
-		return fmt.Errorf("Non-IPv4 address is not supported")
+	if s.channelBindTimeout == 0 {
+		s.channelBindTimeout = proto.DefaultLifetime
 	}
 
-	if ip.IsLinkLocalUnicast() {
-		return fmt.Errorf("link-local unicast address is not allowed")
+	for _, p := range config.PacketConnConfigs {
+		go s.packetConnReadLoop(p.PacketConn, p.RelayAddressGenerator)
 	}
-	s.listenIPs = append(s.listenIPs, ip)
-	return nil
-}
 
-// AddRelayIPAddr adds a listening IP address.
-// Note: current implementation can have only one relay IP address.
-// If not specified, it will automatically assigns the relay IP addresses
-// from the system.
-// This method must be called before calling Start().
-func (s *Server) AddRelayIPAddr(addrStr string) error {
-	ip := net.ParseIP(addrStr)
-	if ip.To4() == nil {
-		return fmt.Errorf("Non-IPv4 address is not supported")
-	}
+	for _, l := range config.ListenerConfigs {
+		go func() {
+			conn, err := l.Listener.Accept()
+			if err != nil {
+				s.log.Debugf("exit accept loop on error: %s", err.Error())
+				return
+			}
 
-	if ip.IsLinkLocalUnicast() {
-		return fmt.Errorf("link-local unicast address is not allowed")
+			go s.connReadLoop(conn, l.RelayAddressGenerator)
+		}()
 	}
 
-	if ip.IsUnspecified() {
-		return fmt.Errorf("unspecified IP is not allowed")
-	}
-	s.relayIPs = append(s.relayIPs, ip)
-	return nil
+	return s, nil
 }
 
-// AddExternalIPAddr adds a mapping to an external IP address.
-// This is useful when the TURN server is behind a 1-to-1 NAT, or a D-NAT
-// (which is typical when hosting on AWS EC2).
-// The argument should be a dotted-decimal representation of external IP address.
-// If there is more than on IP address, the argument should be a cancatenation of
-// external IP and local IP delimited by a '/'.
-// Ex 1: "185.199.108.153"
-// Ex 2: "185.199.108.153/10.0.1.2"
-// By default, no address mapping is used.
-// This method must be called before calling Start().
-func (s *Server) AddExternalIPAddr(mapping string) error {
-	var extIP, locIP net.IP
-	ipStrs := strings.Split(mapping, "/")
-	extIP = net.ParseIP(ipStrs[0])
-	if extIP == nil {
-		return fmt.Errorf("invalid IP address: %s", ipStrs[0])
+// Close stops the TURN Server. It cleans up any associated state and closes all connections it is managing
+func (s *Server) Close() error {
+	for _, p := range s.packetConnConfigs {
+		p.PacketConn.Close()
 	}
-	if len(ipStrs) > 1 {
-		locIP = net.ParseIP(ipStrs[1])
-		if locIP == nil {
-			return fmt.Errorf("invalid IP address: %s", ipStrs[1])
-		}
+
+	for _, l := range s.listenerConfigs {
+		l.Listener.Close()
 	}
-	s.extIPMappings = append(s.extIPMappings, [2]net.IP{extIP, locIP})
+
 	return nil
 }
 
-// caller must hold the mutex
-func (s *Server) gatherSystemIPAddrs() ([]net.IP, error) {
-	s.log.Debug("gathering local IP address...")
-
-	var ips []net.IP
-
-	ifs, err := s.net.Interfaces()
+func (s *Server) connReadLoop(c net.Conn, r RelayAddressGenerator) {
+	buf := make([]byte, inboundMTU)
+	allocationManager, err := allocation.NewManager(allocation.ManagerConfig{
+		AllocatePacketConn: r.AllocatePacketConn,
+		AllocateConn:       r.AllocateConn,
+		LeveledLogger:      s.log,
+	})
 	if err != nil {
-		return nil, err
+		s.log.Errorf("exit read loop on error: %s", err.Error())
+		return
 	}
-	for _, ifc := range ifs {
-		if ifc.Flags&net.FlagUp == 0 {
-			continue // skip if interface is not up
+	defer func() {
+		if err := allocationManager.Close(); err != nil {
+			s.log.Errorf("Failed to close AllocationManager: %s", err.Error())
 		}
+	}()
 
-		if ifc.Flags&net.FlagLoopback != 0 {
-			continue // skip loopback address
-		}
+	stunConn := NewSTUNConn(c)
+	for {
+		n, addr, err := stunConn.ReadFrom(buf)
 
-		addrs, err := ifc.Addrs()
 		if err != nil {
-			continue
+			s.log.Debugf("exit read loop on error: %s", err.Error())
+			return
 		}
 
-		for _, addr := range addrs {
-			var ip net.IP
-			switch addr := addr.(type) {
-			case *net.IPNet:
-				ip = addr.IP
-			case *net.IPAddr:
-				ip = addr.IP
-			}
-
-			if ip == nil {
-				return nil, fmt.Errorf("invalid IP address: %s", addr.String())
-			}
-
-			if ip.To4() == nil {
-				continue // skip non-IPv4 address
-			}
-
-			if ip.IsLinkLocalUnicast() {
-				continue
-			}
-
-			s.log.Debugf("- found local IP: %s", ip.String())
-			ips = append(ips, ip)
+		if err := server.HandleRequest(server.Request{
+			Conn:               stunConn,
+			SrcAddr:            addr,
+			Buff:               buf[:n],
+			Log:                s.log,
+			AuthHandler:        s.authHandler,
+			Realm:              s.realm,
+			AllocationManager:  allocationManager,
+			ChannelBindTimeout: s.channelBindTimeout,
+		}); err != nil {
+			s.log.Errorf("error when handling datagram: %v", err)
 		}
 	}
 
-	return ips, nil
 }
 
-// Listen starts listening and handling TURN traffic
-// caller must hold the mutex
-func (s *Server) listen(localIP net.IP) error {
-	network := "udp4"
-	listenAddr := fmt.Sprintf("%s:%d", localIP.String(), s.listenPort)
-	conn, err := s.net.ListenPacket(network, listenAddr)
+func (s *Server) packetConnReadLoop(p net.PacketConn, r RelayAddressGenerator) {
+	buf := make([]byte, inboundMTU)
+	allocationManager, err := allocation.NewManager(allocation.ManagerConfig{
+		AllocatePacketConn: r.AllocatePacketConn,
+		AllocateConn:       r.AllocateConn,
+		LeveledLogger:      s.log,
+	})
 	if err != nil {
-		return errors.Wrap(err, fmt.Sprintf("Failed to listen on %s", listenAddr))
+		s.log.Errorf("exit read loop on error: %s", err.Error())
+		return
 	}
-
-	laddr := conn.LocalAddr()
-	s.log.Infof("Listening on %s:%s", laddr.Network(), laddr.String())
-
-	closeCh := make(chan struct{})
-	s.listeners = append(s.listeners, &listener{
-		conn:    conn,
-		closeCh: closeCh,
-	})
-
-	go func() {
-		buf := make([]byte, maxStunMessageSize)
-		for {
-			n, addr, err := conn.ReadFrom(buf)
-			if err != nil {
-				s.log.Debugf("exit read loop on error: %s", err.Error())
-				break
-			}
-
-			if err := s.handleUDPPacket(conn, addr, buf[:n]); err != nil {
-				s.log.Error(err.Error())
-			}
+	defer func() {
+		if err := allocationManager.Close(); err != nil {
+			s.log.Errorf("Failed to close AllocationManager: %s", err.Error())
 		}
-
-		close(closeCh)
 	}()
 
-	return nil
-}
+	for {
+		n, addr, err := p.ReadFrom(buf)
 
-// Start starts the server.
-func (s *Server) Start() error {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	var ips []net.IP
-	if len(s.listenIPs) == 0 {
-		var err error
-		ips, err = s.gatherSystemIPAddrs()
 		if err != nil {
-			return err
-		}
-		if len(ips) == 0 {
-			return fmt.Errorf("no local IP address found")
+			s.log.Debugf("exit read loop on error: %s", err.Error())
+			return
 		}
 
-		s.listenIPs = ips
-	}
-
-	// If s.relayIPs is empty, use s.listenIPs
-	if len(s.relayIPs) == 0 {
-		s.relayIPs = s.listenIPs
-	}
-
-	// Set external IP mapping for the allocation manager
-	for _, mapping := range s.extIPMappings {
-		extIPIsV4 := (mapping[0].To4() != nil)
-		if mapping[1] == nil {
-			for _, relayIP := range s.relayIPs {
-				relayIPIsV4 := (relayIP.To4() != nil)
-				if (extIPIsV4 && relayIPIsV4) || (!extIPIsV4 && !relayIPIsV4) {
-					s.manager.AddExternalIPMapping(mapping[0], relayIP)
-				}
-			}
-		} else {
-			relayIPIsV4 := (mapping[1].To4() != nil)
-			if (extIPIsV4 && relayIPIsV4) || (!extIPIsV4 && !relayIPIsV4) {
-				s.manager.AddExternalIPMapping(mapping[0], mapping[1])
-			}
-		}
-	}
-
-	for _, localIP := range s.listenIPs {
-		err := s.listen(localIP)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// Close closes the connection.
-func (s *Server) Close() error {
-	var toJoin []*listener
-	var err error
-
-	func() {
-		s.lock.RLock()
-		defer s.lock.RUnlock()
-
-		if err2 := s.manager.Close(); err2 != nil {
-			err = err2
-		}
-
-		for _, l := range s.listeners {
-			err2 := l.conn.Close()
-			if err2 != nil {
-				s.log.Debugf("Close() returned error: %s", err2.Error())
-				continue
-			}
-			toJoin = append(toJoin, l)
-		}
-	}()
-
-	for _, l := range s.listeners {
-		<-l.closeCh
-	}
-
-	return err
-}
-
-// caller must hold the mutex
-func (s *Server) handleUDPPacket(conn net.PacketConn, srcAddr net.Addr, buf []byte) error {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	s.log.Debugf("received %d bytes of udp from %s on %s",
-		len(buf),
-		srcAddr.String(),
-		conn.LocalAddr().String(),
-	)
-	if proto.IsChannelData(buf) {
-		return s.handleDataPacket(conn, srcAddr, buf)
-	}
-
-	return s.handleTURNPacket(conn, srcAddr, buf)
-}
-
-// caller must hold the mutex
-func (s *Server) handleDataPacket(conn net.PacketConn, srcAddr net.Addr, buf []byte) error {
-	s.log.Debugf("received DataPacket from %s", srcAddr.String())
-	c := proto.ChannelData{Raw: buf}
-	if err := c.Decode(); err != nil {
-		return errors.Wrap(err, "Failed to create channel data from packet")
-	}
-
-	err := s.handleChannelData(conn, srcAddr, &c)
-	if err != nil {
-		err = errors.Errorf("unable to handle ChannelData from %v: %v", srcAddr, err)
-	}
-
-	return err
-}
-
-// caller must hold the mutex
-func (s *Server) handleTURNPacket(conn net.PacketConn, srcAddr net.Addr, buf []byte) error {
-	s.log.Debug("handleTURNPacket")
-	m := &stun.Message{Raw: append([]byte{}, buf...)}
-	if err := m.Decode(); err != nil {
-		return errors.Wrap(err, "failed to create stun message from packet")
-	}
-
-	h, err := s.getMessageHandler(m.Type.Class, m.Type.Method)
-	if err != nil {
-		return errors.Errorf("unhandled STUN packet %v-%v from %v: %v", m.Type.Method, m.Type.Class, srcAddr, err)
-	}
-
-	err = h(conn, srcAddr, m)
-	if err != nil {
-		return errors.Errorf("failed to handle %v-%v from %v: %v", m.Type.Method, m.Type.Class, srcAddr, err)
-	}
-
-	return nil
-}
-
-type messageHandler func(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error
-
-func (s *Server) getMessageHandler(class stun.MessageClass, method stun.Method) (messageHandler, error) {
-	switch class {
-	case stun.ClassIndication:
-		switch method {
-		case stun.MethodSend:
-			return s.handleSendIndication, nil
-		default:
-			return nil, errors.Errorf("unexpected method: %s", method)
-		}
-
-	case stun.ClassRequest:
-		switch method {
-		case stun.MethodAllocate:
-			return s.handleAllocateRequest, nil
-		case stun.MethodRefresh:
-			return s.handleRefreshRequest, nil
-		case stun.MethodCreatePermission:
-			return s.handleCreatePermissionRequest, nil
-		case stun.MethodChannelBind:
-			return s.handleChannelBindRequest, nil
-		case stun.MethodBinding:
-			return s.handleBindingRequest, nil
-		default:
-			return nil, errors.Errorf("unexpected method: %s", method)
+		if err := server.HandleRequest(server.Request{
+			Conn:               p,
+			SrcAddr:            addr,
+			Buff:               buf[:n],
+			Log:                s.log,
+			AuthHandler:        s.authHandler,
+			Realm:              s.realm,
+			AllocationManager:  allocationManager,
+			ChannelBindTimeout: s.channelBindTimeout,
+		}); err != nil {
+			s.log.Errorf("error when handling datagram: %v", err)
 		}
-
-	default:
-		return nil, errors.Errorf("unexpected class: %s", class)
 	}
 }
-
-func randSeq(n int) string {
-	letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
-	b := make([]rune, n)
-	for i := range b {
-		b[i] = letters[rand.Intn(len(letters))]
-	}
-	return string(b)
-}
-
-// TODO, include time info support stale nonces
-func buildNonce() (string, error) {
-	/* #nosec */
-	h := md5.New()
-	now := time.Now().Unix()
-	if _, err := io.WriteString(h, strconv.FormatInt(now, 10)); err != nil {
-		return "", errors.Wrap(err, fmt.Sprintf("Failed generating nonce %v \n", err))
-	}
-	if _, err := io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)); err != nil {
-		return "", errors.Wrap(err, fmt.Sprintf("Failed generating nonce %v \n", err))
-	}
-	return fmt.Sprintf("%x", h.Sum(nil)), nil
-}
-
-func assertMessageIntegrity(m *stun.Message, ourKey []byte) error {
-	messageIntegrityAttr := stun.MessageIntegrity(ourKey)
-	return messageIntegrityAttr.Check(m)
-}
diff --git a/server_config.go b/server_config.go
new file mode 100644
index 0000000..aaa98a7
--- /dev/null
+++ b/server_config.go
@@ -0,0 +1,105 @@
+package turn
+
+import (
+	"net"
+	"time"
+
+	"github.com/pion/logging"
+)
+
+// RelayAddressGenerator is used to generate a RelayAddress when creating an allocation.
+// You can use one of the provided ones or provide your own.
+type RelayAddressGenerator interface {
+	// Validate confirms that the RelayAddressGenerator is properly initialized
+	Validate() error
+
+	// Allocate a PacketConn (UDP) RelayAddress
+	AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error)
+
+	// Allocate a Conn (TCP) RelayAddress
+	AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error)
+}
+
+// PacketConnConfig is a single net.PacketConn to listen/write on. This will be used for UDP listeners
+type PacketConnConfig struct {
+	PacketConn net.PacketConn
+
+	// When an allocation is generated the RelayAddressGenerator
+	// creates the net.PacketConn and returns the IP/Port it is available at
+	RelayAddressGenerator RelayAddressGenerator
+}
+
+func (c *PacketConnConfig) validate() error {
+	if c.PacketConn == nil {
+		return errConnUnset
+	}
+	if c.RelayAddressGenerator == nil {
+		return errRelayAddressGeneratorUnset
+	}
+
+	return c.RelayAddressGenerator.Validate()
+}
+
+// ListenerConfig is a single net.Listener to accept connections on. This will be used for TCP, TLS and DTLS listeners
+type ListenerConfig struct {
+	Listener net.Listener
+
+	// When an allocation is generated the RelayAddressGenerator
+	// creates the net.PacketConn and returns the IP/Port it is available at
+	RelayAddressGenerator RelayAddressGenerator
+}
+
+func (c *ListenerConfig) validate() error {
+	if c.Listener == nil {
+		return errListenerUnset
+	}
+
+	if c.RelayAddressGenerator == nil {
+		return errRelayAddressGeneratorUnset
+	}
+
+	return c.RelayAddressGenerator.Validate()
+}
+
+// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
+type AuthHandler func(username string, srcAddr net.Addr) (password string, ok bool)
+
+// ServerConfig configures the Pion TURN Server
+type ServerConfig struct {
+	// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners
+	// Each listener can have custom behavior around the creation of Relays
+	PacketConnConfigs []PacketConnConfig
+	ListenerConfigs   []ListenerConfig
+
+	// LoggerFactory must be set for logging from this server.
+	LoggerFactory logging.LoggerFactory
+
+	// Realm sets the realm for this server
+	Realm string
+
+	// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
+	AuthHandler AuthHandler
+
+	// ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes.
+	ChannelBindTimeout time.Duration
+}
+
+func (s *ServerConfig) validate() error {
+	if len(s.PacketConnConfigs) == 0 && len(s.ListenerConfigs) == 0 {
+		return errNoAvailableConns
+	}
+
+	for _, s := range s.PacketConnConfigs {
+		if err := s.validate(); err != nil {
+			return err
+		}
+	}
+
+	for _, s := range s.ListenerConfigs {
+		if err := s.validate(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/stun.go b/stun.go
deleted file mode 100644
index becdfb1..0000000
--- a/stun.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package turn
-
-import (
-	"net"
-
-	"github.com/pion/stun"
-	"github.com/pion/turn/internal/ipnet"
-)
-
-// caller must hold the mutex
-func (s *Server) handleBindingRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
-	s.log.Debugf("received BindingRequest from %s", srcAddr.String())
-	ip, port, err := ipnet.AddrIPPort(srcAddr)
-	if err != nil {
-		return err
-	}
-
-	attrs := s.makeAttrs(m.TransactionID, stun.BindingSuccess, &stun.XORMappedAddress{
-		IP:   ip,
-		Port: port,
-	}, stun.Fingerprint)
-
-	return s.sender(conn, srcAddr, attrs...)
-}
diff --git a/stun_conn.go b/stun_conn.go
index 6cde09c..63df9e7 100644
--- a/stun_conn.go
+++ b/stun_conn.go
@@ -40,17 +40,16 @@ func consumeSingleTURNFrame(p []byte) (int, error) {
 	}
 
 	var datagramSize uint16
-	switch {
-	case stun.IsMessage(p):
+	if stun.IsMessage(p) {
 		datagramSize = binary.BigEndian.Uint16(p[2:4]) + stunHeaderSize
-	case proto.IsChannelData(p):
+	} else if num := binary.BigEndian.Uint16(p[0:4]); proto.ChannelNumber(num).Valid() {
 		datagramSize = binary.BigEndian.Uint16(p[channelDataNumberSize:channelDataHeaderSize])
 		if paddingOverflow := (datagramSize + channelDataPadding) % channelDataPadding; paddingOverflow != 0 {
 			datagramSize = (datagramSize + channelDataPadding) - paddingOverflow
 		}
 
 		datagramSize += channelDataHeaderSize
-	default:
+	} else {
 		return 0, errInvalidTURNFrame
 	}
 
diff --git a/turn.go b/turn.go
deleted file mode 100644
index 16437df..0000000
--- a/turn.go
+++ /dev/null
@@ -1,489 +0,0 @@
-package turn
-
-import (
-	"crypto/md5" // #nosec
-	"fmt"
-	"net"
-	"strings"
-	"time"
-
-	"github.com/pion/stun"
-	"github.com/pion/turn/internal/allocation"
-	"github.com/pion/turn/internal/ipnet"
-	"github.com/pion/turn/internal/proto"
-	"github.com/pkg/errors"
-)
-
-const (
-	maximumLifetime = time.Hour // https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation
-)
-
-type curriedSend func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error
-
-func authenticateRequest(curriedSend curriedSend, m *stun.Message, callingMethod stun.Method, realm string, authHandler AuthHandler, srcAddr net.Addr) (stun.MessageIntegrity, string, error) {
-	handleErr := func(err error) (stun.MessageIntegrity, string, error) {
-		if sendErr := curriedSend(stun.ClassErrorResponse, callingMethod, m.TransactionID,
-			&stun.ErrorCodeAttribute{Code: stun.CodeBadRequest},
-		); sendErr != nil {
-			err = errors.Errorf(strings.Join([]string{sendErr.Error(), err.Error()}, "\n"))
-		}
-		return stun.MessageIntegrity{}, "", err
-	}
-
-	if !m.Contains(stun.AttrMessageIntegrity) {
-		nonce, err := buildNonce()
-		if err != nil {
-			return stun.MessageIntegrity{}, "", err
-		}
-
-		return nil, "", curriedSend(stun.ClassErrorResponse, callingMethod, m.TransactionID,
-			&stun.ErrorCodeAttribute{Code: stun.CodeUnauthorized},
-			stun.NewNonce(nonce),
-			stun.NewRealm(realm),
-		)
-
-	}
-
-	var ourKey [16]byte
-	nonceAttr := &stun.Nonce{}
-	usernameAttr := &stun.Username{}
-	realmAttr := &stun.Realm{}
-
-	if err := realmAttr.GetFrom(m); err != nil {
-		return handleErr(err)
-	}
-
-	if err := nonceAttr.GetFrom(m); err != nil {
-		return handleErr(err)
-	}
-
-	if err := usernameAttr.GetFrom(m); err != nil {
-		return handleErr(err)
-	}
-
-	password, ok := authHandler(usernameAttr.String(), srcAddr)
-	if !ok {
-		return handleErr(errors.Errorf("No user exists for %s", usernameAttr.String()))
-	}
-
-	/* #nosec */
-	ourKey = md5.Sum([]byte(usernameAttr.String() + ":" + realmAttr.String() + ":" + password))
-	if err := assertMessageIntegrity(m, ourKey[:]); err != nil {
-		return handleErr(err)
-	}
-
-	return stun.NewLongTermIntegrity(usernameAttr.String(), realmAttr.String(), password), usernameAttr.String(), nil
-}
-
-func assertDontFragment(curriedSend curriedSend, m *stun.Message, attr stun.Setter) error {
-	if m.Contains(stun.AttrDontFragment) {
-		err := errors.Errorf("no support for DONT-FRAGMENT")
-		if sendErr := curriedSend(stun.ClassErrorResponse, stun.MethodAllocate, m.TransactionID,
-			&stun.ErrorCodeAttribute{Code: stun.CodeUnknownAttribute},
-			&stun.UnknownAttributes{stun.AttrDontFragment},
-			attr,
-		); sendErr != nil {
-			err = errors.Errorf(strings.Join([]string{sendErr.Error(), err.Error()}, "\n"))
-		}
-		return err
-	}
-	return nil
-}
-
-// https://tools.ietf.org/html/rfc5766#section-6.2
-// caller must hold the mutex
-func (s *Server) handleAllocateRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
-	s.log.Debugf("received AllocateRequest from %s", srcAddr.String())
-	dstAddr := conn.LocalAddr()
-	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return s.sender(conn, srcAddr, s.makeAttrs(transactionID, stun.NewType(method, class), attrs...)...)
-	}
-	respondWithError := func(err error, messageIntegrity stun.MessageIntegrity, errorCode stun.ErrorCode) error {
-		if sendErr := curriedSend(stun.ClassErrorResponse, stun.MethodAllocate, m.TransactionID,
-			&stun.ErrorCodeAttribute{Code: errorCode},
-			messageIntegrity,
-		); sendErr != nil {
-			err = errors.Errorf(strings.Join([]string{sendErr.Error(), err.Error()}, "\n"))
-		}
-		return err
-	}
-
-	// 1. The server MUST require that the request be authenticated.  This
-	//    authentication MUST be done using the long-term credential
-	//    mechanism of [https://tools.ietf.org/html/rfc5389#section-10.2.2]
-	//    unless the client and server agree to use another mechanism through
-	//    some procedure outside the scope of this document.
-	messageIntegrity, _, err := authenticateRequest(curriedSend, m, stun.MethodAllocate, s.realm, s.authHandler, srcAddr)
-	if err != nil {
-		return err
-	}
-	if messageIntegrity == nil {
-		return nil
-	}
-
-	fiveTuple := &allocation.FiveTuple{
-		SrcAddr:  srcAddr,
-		DstAddr:  dstAddr,
-		Protocol: allocation.UDP,
-	}
-	requestedPort := 0
-	reservationToken := ""
-
-	// 2. The server checks if the 5-tuple is currently in use by an
-	//    existing allocation.  If yes, the server rejects the request with
-	//    a 437 (Allocation Mismatch) error.
-	if alloc := s.manager.GetAllocation(fiveTuple); alloc != nil {
-		return respondWithError(errors.Errorf("Relay already allocated for 5-TUPLE"), messageIntegrity, stun.CodeAllocMismatch)
-	}
-
-	// 3. The server checks if the request contains a REQUESTED-TRANSPORT
-	//    attribute.  If the REQUESTED-TRANSPORT attribute is not included
-	//    or is malformed, the server rejects the request with a 400 (Bad
-	//    Request) error.  Otherwise, if the attribute is included but
-	//    specifies a protocol other that UDP, the server rejects the
-	//    request with a 442 (Unsupported Transport Protocol) error.
-	var requestedTransport proto.RequestedTransport
-	if err = requestedTransport.GetFrom(m); err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
-	}
-	if requestedTransport.Protocol != proto.ProtoUDP {
-		return respondWithError(err, messageIntegrity, stun.CodeUnsupportedTransProto)
-	}
-
-	// 4. The request may contain a DONT-FRAGMENT attribute.  If it does,
-	//    but the server does not support sending UDP datagrams with the DF
-	//    bit set to 1 (see Section 12), then the server treats the DONT-
-	//    FRAGMENT attribute in the Allocate request as an unknown
-	//    comprehension-required attribute.
-	if err = assertDontFragment(curriedSend, m, messageIntegrity); err != nil {
-		return err
-	}
-
-	// 5.  The server checks if the request contains a RESERVATION-TOKEN
-	//     attribute.  If yes, and the request also contains an EVEN-PORT
-	//     attribute, then the server rejects the request with a 400 (Bad
-	//     Request) error.  Otherwise, it checks to see if the token is
-	//     valid (i.e., the token is in range and has not expired and the
-	//     corresponding relayed transport address is still available).  If
-	//     the token is not valid for some reason, the server rejects the
-	//     request with a 508 (Insufficient Capacity) error.
-	var reservationTokenAttr proto.ReservationToken
-	if err = reservationTokenAttr.GetFrom(m); err == nil {
-		var evenPort proto.EvenPort
-		if err = evenPort.GetFrom(m); err == nil {
-			return respondWithError(errors.Errorf("Request must not contain RESERVATION-TOKEN and EVEN-PORT"), messageIntegrity, stun.CodeBadRequest)
-		}
-
-		allocationPort, reservationFound := s.reservationManager.GetReservation(string(reservationTokenAttr))
-		if !reservationFound {
-			return respondWithError(errors.Errorf("No reservation found with token %s", string(reservationTokenAttr)), messageIntegrity, stun.CodeBadRequest)
-		}
-		requestedPort = allocationPort + 1
-	}
-
-	// 6. The server checks if the request contains an EVEN-PORT attribute.
-	//    If yes, then the server checks that it can satisfy the request
-	//    (i.e., can allocate a relayed transport address as described
-	//    below).  If the server cannot satisfy the request, then the
-	//    server rejects the request with a 508 (Insufficient Capacity)
-	//    error.
-	var evenPort proto.EvenPort
-	if err = evenPort.GetFrom(m); err == nil {
-		randomPort := 0
-		randomPort, err = allocation.GetRandomEvenPort()
-		if err != nil {
-			return respondWithError(err, messageIntegrity, stun.CodeInsufficientCapacity)
-		}
-		requestedPort = randomPort
-		reservationToken = randSeq(8)
-	}
-
-	// 7. At any point, the server MAY choose to reject the request with a
-	//    486 (Allocation Quota Reached) error if it feels the client is
-	//    trying to exceed some locally defined allocation quota.  The
-	//    server is free to define this allocation quota any way it wishes,
-	//    but SHOULD define it based on the username used to authenticate
-	//    the request, and not on the client's transport address.
-
-	// 8. Also at any point, the server MAY choose to reject the request
-	//    with a 300 (Try Alternate) error if it wishes to redirect the
-	//    client to a different server.  The use of this error code and
-	//    attribute follow the specification in [RFC5389].
-	// Check current usage vs redis usage of other servers
-	// if bad, redirect { stun.AttrErrorCode, 300 }
-	lifetimeDuration := allocationLifeTime(m)
-	a, err := s.manager.CreateAllocation(
-		fiveTuple,
-		conn,
-		s.relayIPs[0], // TODO: allow more than one relay IP
-		requestedPort,
-		lifetimeDuration)
-	if err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeInsufficientCapacity)
-	}
-
-	// Once the allocation is created, the server replies with a success
-	// response.  The success response contains:
-	//   * An XOR-RELAYED-ADDRESS attribute containing the relayed transport
-	//     address.
-	//   * A LIFETIME attribute containing the current value of the time-to-
-	//     expiry timer.
-	//   * A RESERVATION-TOKEN attribute (if a second relayed transport
-	//     address was reserved).
-	//   * An XOR-MAPPED-ADDRESS attribute containing the client's IP address
-	//     and port (from the 5-tuple).
-
-	srcIP, srcPort, err := ipnet.AddrIPPort(srcAddr)
-	if err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
-	}
-
-	relayIP, relayPort, err := ipnet.AddrIPPort(a.RelayAddr)
-	if err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
-	}
-
-	responseAttrs := []stun.Setter{
-		&proto.RelayedAddress{
-			IP:   relayIP,
-			Port: relayPort,
-		},
-		&proto.Lifetime{
-			Duration: lifetimeDuration,
-		},
-		&stun.XORMappedAddress{
-			IP:   srcIP,
-			Port: srcPort,
-		},
-	}
-
-	if reservationToken != "" {
-		s.reservationManager.CreateReservation(reservationToken, relayPort)
-		responseAttrs = append(responseAttrs, proto.ReservationToken([]byte(reservationToken)))
-	}
-
-	return curriedSend(stun.ClassSuccessResponse, stun.MethodAllocate, m.TransactionID, append(responseAttrs, messageIntegrity)...)
-}
-
-// caller must hold the mutex
-func (s *Server) handleRefreshRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
-	s.log.Debugf("received RefreshRequest from %s", srcAddr.String())
-	dstAddr := conn.LocalAddr()
-	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return s.sender(conn, srcAddr, s.makeAttrs(transactionID, stun.NewType(method, class), attrs...)...)
-	}
-	messageIntegrity, _, err := authenticateRequest(curriedSend, m, stun.MethodCreatePermission, s.realm, s.authHandler, srcAddr)
-	if err != nil {
-		return err
-	}
-
-	a := s.manager.GetAllocation(&allocation.FiveTuple{
-		SrcAddr:  srcAddr,
-		DstAddr:  dstAddr,
-		Protocol: allocation.UDP,
-	})
-	if a == nil {
-		return errors.Errorf("No allocation found for %v:%v", srcAddr, dstAddr)
-	}
-
-	lifetimeDuration := allocationLifeTime(m)
-	a.Refresh(lifetimeDuration)
-
-	return curriedSend(stun.ClassSuccessResponse, stun.MethodRefresh, m.TransactionID,
-		&proto.Lifetime{
-			Duration: lifetimeDuration,
-		},
-		messageIntegrity,
-	)
-}
-
-// caller must hold the mutex
-func (s *Server) handleCreatePermissionRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
-	s.log.Debugf("received CreatePermission from %s", srcAddr.String())
-	dstAddr := conn.LocalAddr()
-	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return s.sender(conn, srcAddr, s.makeAttrs(transactionID, stun.NewType(method, class), attrs...)...)
-	}
-
-	a := s.manager.GetAllocation(&allocation.FiveTuple{
-		SrcAddr:  srcAddr,
-		DstAddr:  dstAddr,
-		Protocol: allocation.UDP,
-	})
-	if a == nil {
-		return errors.Errorf("No allocation found for %v:%v", srcAddr, dstAddr)
-	}
-
-	messageIntegrity, _, err := authenticateRequest(curriedSend, m, stun.MethodCreatePermission, s.realm, s.authHandler, srcAddr)
-	if err != nil {
-		return err
-	}
-	addCount := 0
-
-	if err := m.ForEach(stun.AttrXORPeerAddress, func(m *stun.Message) error {
-		var peerAddress proto.PeerAddress
-		if err := peerAddress.GetFrom(m); err != nil {
-			return err
-		}
-
-		s.log.Debugf("adding permission for %s", fmt.Sprintf("%s:%d",
-			peerAddress.IP.String(), peerAddress.Port))
-		a.AddPermission(allocation.NewPermission(
-			&net.UDPAddr{
-				IP:   peerAddress.IP,
-				Port: peerAddress.Port,
-			},
-			s.log,
-		))
-		addCount++
-		return nil
-	}); err != nil {
-		addCount = 0
-	}
-
-	respClass := stun.ClassSuccessResponse
-	if addCount == 0 {
-		respClass = stun.ClassErrorResponse
-	}
-
-	return curriedSend(respClass, stun.MethodCreatePermission, m.TransactionID,
-		messageIntegrity)
-}
-
-// caller must hold the mutex
-func (s *Server) handleSendIndication(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
-	s.log.Debugf("received SendIndication from %s", srcAddr.String())
-	dstAddr := conn.LocalAddr()
-	a := s.manager.GetAllocation(&allocation.FiveTuple{
-		SrcAddr:  srcAddr,
-		DstAddr:  dstAddr,
-		Protocol: allocation.UDP,
-	})
-	if a == nil {
-		return errors.Errorf("No allocation found for %v:%v", srcAddr, dstAddr)
-	}
-
-	dataAttr := proto.Data{}
-	if err := dataAttr.GetFrom(m); err != nil {
-		return err
-	}
-
-	peerAddress := proto.PeerAddress{}
-	if err := peerAddress.GetFrom(m); err != nil {
-		return err
-	}
-
-	msgDst := &net.UDPAddr{IP: peerAddress.IP, Port: peerAddress.Port}
-	if perm := a.GetPermission(msgDst); perm == nil {
-		return errors.Errorf("Unable to handle send-indication, no permission added: %v", msgDst)
-	}
-
-	l, err := a.RelaySocket.WriteTo(dataAttr, msgDst)
-	if l != len(dataAttr) {
-		return errors.Errorf("packet write smaller than packet %d != %d (expected) err: %v", l, len(dataAttr), err)
-	}
-	return err
-}
-
-// caller must hold the mutex
-func (s *Server) handleChannelBindRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
-	s.log.Debugf("received ChannelBindRequest from %s", srcAddr.String())
-	dstAddr := conn.LocalAddr()
-	errorSend := func(err error, attrs ...stun.Setter) error {
-		sendErr := s.sender(conn, srcAddr, s.makeAttrs(m.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassErrorResponse), attrs...)...)
-		if sendErr != nil {
-			err = errors.Errorf(strings.Join([]string{sendErr.Error(), err.Error()}, "\n"))
-		}
-		return err
-	}
-
-	a := s.manager.GetAllocation(&allocation.FiveTuple{
-		SrcAddr:  srcAddr,
-		DstAddr:  dstAddr,
-		Protocol: allocation.UDP,
-	})
-	if a == nil {
-		return errors.Errorf("No allocation found for %v:%v", srcAddr, dstAddr)
-	}
-
-	messageIntegrity, _, err := authenticateRequest(func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return s.sender(conn, srcAddr, s.makeAttrs(m.TransactionID, stun.NewType(method, class))...)
-	}, m, stun.MethodChannelBind, s.realm, s.authHandler, srcAddr)
-	if err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	var channel proto.ChannelNumber
-	if err = channel.GetFrom(m); err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	peerAddr := proto.PeerAddress{}
-	if err = peerAddr.GetFrom(m); err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	s.log.Debugf("binding channel %d to %s",
-		channel,
-		fmt.Sprintf("%s:%d", peerAddr.IP.String(), peerAddr.Port))
-	err = a.AddChannelBind(allocation.NewChannelBind(
-		channel,
-		&net.UDPAddr{IP: peerAddr.IP, Port: peerAddr.Port},
-		s.log,
-	), s.channelBindTimeout)
-	if err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	return s.sender(conn, srcAddr, s.makeAttrs(m.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassSuccessResponse), messageIntegrity)...)
-}
-
-func (s *Server) handleChannelData(conn net.PacketConn, srcAddr net.Addr, c *proto.ChannelData) error {
-	s.log.Debugf("received ChannelData from %s", srcAddr.String())
-	dstAddr := conn.LocalAddr()
-	a := s.manager.GetAllocation(&allocation.FiveTuple{
-		SrcAddr:  srcAddr,
-		DstAddr:  dstAddr,
-		Protocol: allocation.UDP,
-	})
-	if a == nil {
-		return errors.Errorf("No allocation found for %v:%v", srcAddr, dstAddr)
-	}
-
-	channel := a.GetChannelByNumber(c.Number)
-	if channel == nil {
-		return errors.Errorf("No channel bind found for %x \n", uint16(c.Number))
-	}
-
-	l, err := a.RelaySocket.WriteTo(c.Data, channel.Peer)
-	if err != nil {
-		return errors.Wrap(err, "failed writing to socket")
-	}
-
-	if l != len(c.Data) {
-		return errors.Errorf("packet write smaller than packet %d != %d (expected)", l, len(c.Data))
-	}
-
-	return nil
-}
-
-func (s *Server) makeAttrs(transactionID [stun.TransactionIDSize]byte, msgType stun.MessageType, additional ...stun.Setter) []stun.Setter {
-	attrs := append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...)
-	if len(s.software) > 0 {
-		attrs = append(attrs, s.software)
-	}
-	return attrs
-}
-
-func allocationLifeTime(m *stun.Message) time.Duration {
-	lifetimeDuration := proto.DefaultLifetime
-
-	var lifetime proto.Lifetime
-	if err := lifetime.GetFrom(m); err == nil {
-		if lifetime.Duration < maximumLifetime {
-			lifetimeDuration = lifetime.Duration
-		}
-	}
-
-	return lifetimeDuration
-}


Test patch:
diff --git a/client_test.go b/client_test.go
index 1aad566..8440e74 100644
--- a/client_test.go
+++ b/client_test.go
@@ -81,7 +81,6 @@ func TestClientWithSTUN(t *testing.T) {
 		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") {
@@ -91,7 +90,7 @@ func TestClientWithSTUN(t *testing.T) {
 		// stun1.l.google.com:19302, more at https://gist.github.com/zziuni/3741933#file-stuns-L5
 		go func() {
 			close(started)
-			resp1, err1 = c.SendBindingRequestTo(to)
+			_, err1 = c.SendBindingRequestTo(to)
 			close(finished)
 		}()
 
@@ -99,18 +98,13 @@ func TestClientWithSTUN(t *testing.T) {
 
 		<-started
 
-		resp2, err2 := c.SendBindingRequestTo(to)
-		if err2 != nil {
+		if _, err = c.SendBindingRequestTo(to); err != nil {
 			t.Fatal(err)
-		} else {
-			t.Log(resp2)
 		}
 
 		<-finished
 		if err1 != nil {
 			t.Fatal(err)
-		} else {
-			t.Log(resp1)
 		}
 	})
 
diff --git a/internal/allocation/allocation_manager_test.go b/internal/allocation/allocation_manager_test.go
index 40dae55..4652fb2 100644
--- a/internal/allocation/allocation_manager_test.go
+++ b/internal/allocation/allocation_manager_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/pion/logging"
 	"github.com/pion/turn/internal/proto"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestManager(t *testing.T) {
@@ -23,7 +24,6 @@ func TestManager(t *testing.T) {
 		{"DeleteAllocation", subTestDeleteAllocation},
 		{"AllocationTimeout", subTestAllocationTimeout},
 		{"Close", subTestManagerClose},
-		{"ExternalIPMapping", subTestExternalIPMapping},
 	}
 
 	network := "udp4"
@@ -42,23 +42,27 @@ func TestManager(t *testing.T) {
 
 // test invalid Allocation creations
 func subTestCreateInvalidAllocation(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
-	if a, err := m.CreateAllocation(nil, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a != nil || err == nil {
+	m, err := newTestManager()
+	assert.NoError(t, err)
+
+	if a, err := m.CreateAllocation(nil, turnSocket, 0, proto.DefaultLifetime); a != nil || err == nil {
 		t.Errorf("Illegally created allocation with nil FiveTuple")
 	}
-	if a, err := m.CreateAllocation(randomFiveTuple(), nil, net.IPv4zero, 0, proto.DefaultLifetime); a != nil || err == nil {
+	if a, err := m.CreateAllocation(randomFiveTuple(), nil, 0, proto.DefaultLifetime); a != nil || err == nil {
 		t.Errorf("Illegally created allocation with nil turnSocket")
 	}
-	if a, err := m.CreateAllocation(randomFiveTuple(), turnSocket, net.IPv4zero, 0, 0); a != nil || err == nil {
+	if a, err := m.CreateAllocation(randomFiveTuple(), turnSocket, 0, 0); a != nil || err == nil {
 		t.Errorf("Illegally created allocation with 0 lifetime")
 	}
 }
 
 // test valid Allocation creations
 func subTestCreateAllocation(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
+	m, err := newTestManager()
+	assert.NoError(t, err)
+
 	fiveTuple := randomFiveTuple()
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a == nil || err != nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
 	}
 
@@ -69,21 +73,25 @@ func subTestCreateAllocation(t *testing.T, turnSocket net.PacketConn) {
 
 // test that two allocations can't be created with the same FiveTuple
 func subTestCreateAllocationDuplicateFiveTuple(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
+	m, err := newTestManager()
+	assert.NoError(t, err)
+
 	fiveTuple := randomFiveTuple()
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a == nil || err != nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
 	}
 
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a != nil || err == nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime); a != nil || err == nil {
 		t.Errorf("Was able to create allocation with same FiveTuple twice")
 	}
 }
 
 func subTestDeleteAllocation(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
+	m, err := newTestManager()
+	assert.NoError(t, err)
+
 	fiveTuple := randomFiveTuple()
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a == nil || err != nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, proto.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
 	}
 
@@ -99,14 +107,16 @@ func subTestDeleteAllocation(t *testing.T, turnSocket net.PacketConn) {
 
 // test that allocation should be closed if timeout
 func subTestAllocationTimeout(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
+	m, err := newTestManager()
+	assert.NoError(t, err)
+
 	allocations := make([]*Allocation, 5)
 	lifetime := time.Second
 
 	for index := range allocations {
 		fiveTuple := randomFiveTuple()
 
-		a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, lifetime)
+		a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, lifetime)
 		if err != nil {
 			t.Errorf("Failed to create allocation with %v", fiveTuple)
 		}
@@ -125,12 +135,14 @@ func subTestAllocationTimeout(t *testing.T, turnSocket net.PacketConn) {
 
 // test for manager close
 func subTestManagerClose(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
+	m, err := newTestManager()
+	assert.NoError(t, err)
+
 	allocations := make([]*Allocation, 2)
 
-	a1, _ := m.CreateAllocation(randomFiveTuple(), turnSocket, net.IPv4zero, 0, time.Second)
+	a1, _ := m.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Second)
 	allocations[0] = a1
-	a2, _ := m.CreateAllocation(randomFiveTuple(), turnSocket, net.IPv4zero, 0, time.Minute)
+	a2, _ := m.CreateAllocation(randomFiveTuple(), turnSocket, 0, time.Minute)
 	allocations[1] = a2
 
 	// make a1 timeout
@@ -147,41 +159,6 @@ func subTestManagerClose(t *testing.T, turnSocket net.PacketConn) {
 	}
 }
 
-// test for external mapping
-func subTestExternalIPMapping(t *testing.T, turnSocket net.PacketConn) {
-	m := newTestManager()
-	fiveTuple := randomFiveTuple()
-
-	extIP := net.ParseIP("1.2.3.4")
-	m.AddExternalIPMapping(extIP, net.IPv4zero)
-
-	a, err := m.CreateAllocation(
-		fiveTuple,
-		turnSocket,
-		net.IPv4zero,
-		0,
-		proto.DefaultLifetime)
-
-	if a == nil || err != nil {
-		t.Errorf("Failed to create allocation %v %v", a, err)
-	}
-
-	relAddr, ok := a.RelayAddr.(*net.UDPAddr)
-	if !ok {
-		t.Errorf("RelayAddr must be a net.UDPAddr")
-	}
-
-	// IP addr should match the external IP
-	if !relAddr.IP.Equal(extIP) {
-		t.Errorf("should have the external IP")
-	}
-
-	m.DeleteAllocation(fiveTuple)
-	if a = m.GetAllocation(fiveTuple); a != nil {
-		t.Errorf("Get allocation with %v should be nil after delete", fiveTuple)
-	}
-}
-
 func randomFiveTuple() *FiveTuple {
 	/* #nosec */
 	return &FiveTuple{
@@ -190,9 +167,21 @@ func randomFiveTuple() *FiveTuple {
 	}
 }
 
-func newTestManager() *Manager {
+func newTestManager() (*Manager, error) {
 	loggerFactory := logging.NewDefaultLoggerFactory()
-	config := &ManagerConfig{LeveledLogger: loggerFactory.NewLogger("test")}
+
+	config := ManagerConfig{
+		LeveledLogger: loggerFactory.NewLogger("test"),
+		AllocatePacketConn: func(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
+			conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
+			if err != nil {
+				return nil, nil, err
+			}
+
+			return conn, conn.LocalAddr(), nil
+		},
+		AllocateConn: func(network string, requestedPort int) (net.Conn, net.Addr, error) { return nil, nil, nil },
+	}
 	return NewManager(config)
 }
 
diff --git a/internal/allocation/allocation_test.go b/internal/allocation/allocation_test.go
index 3903486..e5be5d4 100644
--- a/internal/allocation/allocation_test.go
+++ b/internal/allocation/allocation_test.go
@@ -216,7 +216,7 @@ func subTestAllocationClose(t *testing.T) {
 func subTestPacketHandler(t *testing.T) {
 	network := "udp"
 
-	m := newTestManager()
+	m, _ := newTestManager()
 
 	// turn server initialization
 	turnSocket, err := net.ListenPacket(network, "127.0.0.1:0")
@@ -247,7 +247,7 @@ func subTestPacketHandler(t *testing.T) {
 	a, err := m.CreateAllocation(&FiveTuple{
 		SrcAddr: clientListener.LocalAddr(),
 		DstAddr: turnSocket.LocalAddr(),
-	}, turnSocket, net.ParseIP("127.0.0.1"), 0, proto.DefaultLifetime)
+	}, turnSocket, 0, proto.DefaultLifetime)
 
 	assert.Nil(t, err, "should succeed")
 
diff --git a/turn_test.go b/internal/server/turn_test.go
similarity index 94%
rename from turn_test.go
rename to internal/server/turn_test.go
index 41424b3..303142f 100644
--- a/turn_test.go
+++ b/internal/server/turn_test.go
@@ -1,4 +1,4 @@
-package turn
+package server
 
 import (
 	"testing"
@@ -32,7 +32,7 @@ func TestAllocationLifeTime(t *testing.T) {
 	// If lifetime is bigger than maximumLifetime
 	{
 		lifetime := proto.Lifetime{
-			Duration: maximumLifetime * 2,
+			Duration: maximumAllocationLifetime * 2,
 		}
 
 		m2 := &stun.Message{}
diff --git a/server_test.go b/server_test.go
index 0f19031..028dc3f 100644
--- a/server_test.go
+++ b/server_test.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/pion/logging"
+	"github.com/pion/transport/vnet"
 	"github.com/pion/turn/internal/proto"
 	"github.com/stretchr/testify/assert"
 )
@@ -19,27 +20,35 @@ func TestServer(t *testing.T) {
 	}
 
 	t.Run("simple", func(t *testing.T) {
+		udpListener, err := net.ListenPacket("udp4", "0.0.0.0:3478")
+		if err != nil {
+			t.Fatalf("Failed to create TURN server listener: %s", err)
+		}
 
-		server := NewServer(&ServerConfig{
+		server, err := NewServer(ServerConfig{
 			AuthHandler: func(username string, srcAddr net.Addr) (password string, ok bool) {
 				if pw, ok := credMap[username]; ok {
 					return pw, true
 				}
 				return "", false
 			},
+			PacketConnConfigs: []PacketConnConfig{
+				{
+					PacketConn: udpListener,
+					RelayAddressGenerator: &RelayAddressGeneratorStatic{
+						RelayAddress: net.ParseIP("127.0.0.1"),
+						Address:      "0.0.0.0",
+					},
+				},
+			},
+
 			Realm:         "pion.ly",
 			LoggerFactory: loggerFactory,
 		})
+		assert.NoError(t, err)
 
 		assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match")
 
-		err := server.AddListeningIPAddr("127.0.0.1")
-		assert.NoError(t, err, "should succeed")
-
-		log.Debug("start listening...")
-		err = server.Start()
-		assert.NoError(t, err, "should succeed")
-
 		// make sure the server is listening before running
 		// the client.
 		time.Sleep(100 * time.Microsecond)
@@ -63,10 +72,9 @@ func TestServer(t *testing.T) {
 		defer client.Close()
 
 		log.Debug("sending a binding request.")
-		to := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3478}
-		resp, err := client.SendBindingRequestTo(to)
+
+		_, err = client.SendBindingRequestTo(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3478})
 		assert.NoError(t, err, "should succeed")
-		t.Logf("resp: %v", resp)
 
 		log.Debug("now closing the server...")
 
@@ -74,117 +82,161 @@ func TestServer(t *testing.T) {
 		err = server.Close()
 		assert.NoError(t, err, "should succeed")
 	})
+}
 
-	t.Run("Relay IPs default to the listening IPs", func(t *testing.T) {
-		server := NewServer(&ServerConfig{
-			AuthHandler: func(username string, srcAddr net.Addr) (password string, ok bool) {
-				if pw, ok := credMap[username]; ok {
-					return pw, true
-				}
-				return "", false
-			},
-			Realm:         "pion.ly",
-			LoggerFactory: loggerFactory,
-		})
+type VNet struct {
+	wan    *vnet.Router
+	net0   *vnet.Net // net (0) on the WAN
+	net1   *vnet.Net // net (1) on the WAN
+	netL0  *vnet.Net // net (0) on the LAN
+	server *Server
+}
 
-		assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match")
+func (v *VNet) Close() error {
+	err := v.server.Close()
+	v.wan.Stop()
+	if err != nil {
+		return err
+	}
+	return nil
+}
 
-		err := server.AddListeningIPAddr("127.0.0.1")
-		assert.NoError(t, err, "should succeed")
+func buildVNet() (*VNet, error) {
+	loggerFactory := logging.NewDefaultLoggerFactory()
 
-		log.Debug("start listening...")
-		err = server.Start()
-		assert.NoError(t, err, "should succeed")
+	// WAN
+	wan, err := vnet.NewRouter(&vnet.RouterConfig{
+		CIDR:          "0.0.0.0/0",
+		LoggerFactory: loggerFactory,
+	})
+	if err != nil {
+		return nil, err
+	}
 
-		// make sure the server is listening before running
-		// the client.
-		time.Sleep(100 * time.Microsecond)
+	net0 := vnet.NewNet(&vnet.NetConfig{
+		StaticIP: "1.2.3.4", // will be assigned to eth0
+	})
 
-		assert.Equal(t, 1, len(server.relayIPs), "should match")
-		assert.True(t, server.relayIPs[0].Equal(net.IPv4(127, 0, 0, 1)), "should match")
+	err = wan.AddNet(net0)
+	if err != nil {
+		return nil, err
+	}
 
-		// Close server
-		err = server.Close()
-		assert.NoError(t, err, "should succeed")
+	net1 := vnet.NewNet(&vnet.NetConfig{
+		StaticIP: "1.2.3.5", // will be assigned to eth0
 	})
 
-	t.Run("AddRelayIPAddr", func(t *testing.T) {
-		server := NewServer(&ServerConfig{
-			AuthHandler: func(username string, srcAddr net.Addr) (password string, ok bool) {
-				if pw, ok := credMap[username]; ok {
-					return pw, true
-				}
-				return "", false
-			},
-			Realm:         "pion.ly",
-			LoggerFactory: loggerFactory,
-		})
-
-		assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match")
+	err = wan.AddNet(net1)
+	if err != nil {
+		return nil, err
+	}
 
-		err := server.AddListeningIPAddr("127.0.0.1")
-		assert.NoError(t, err, "should succeed")
+	// LAN
+	lan, err := vnet.NewRouter(&vnet.RouterConfig{
+		StaticIP: "5.6.7.8", // this router's external IP on eth0
+		CIDR:     "192.168.0.0/24",
+		NATType: &vnet.NATType{
+			MappingBehavior:   vnet.EndpointIndependent,
+			FilteringBehavior: vnet.EndpointIndependent,
+		},
+		LoggerFactory: loggerFactory,
+	})
+	if err != nil {
+		return nil, err
+	}
 
-		err = server.AddRelayIPAddr("127.0.0.2")
-		assert.NoError(t, err, "should succeed")
+	netL0 := vnet.NewNet(&vnet.NetConfig{})
 
-		err = server.AddRelayIPAddr("127.0.0.3")
-		assert.NoError(t, err, "should succeed")
+	if err = lan.AddNet(netL0); err != nil {
+		return nil, err
+	}
 
-		log.Debug("start listening...")
-		err = server.Start()
-		assert.NoError(t, err, "should succeed")
+	if err = wan.AddRouter(lan); err != nil {
+		return nil, err
+	}
 
-		// make sure the server is listening before running
-		// the client.
-		time.Sleep(100 * time.Microsecond)
+	if err = wan.Start(); err != nil {
+		return nil, err
+	}
 
-		assert.Equal(t, 2, len(server.relayIPs), "should match")
-		assert.True(t, server.relayIPs[0].Equal(net.IPv4(127, 0, 0, 2)), "should match")
-		assert.True(t, server.relayIPs[1].Equal(net.IPv4(127, 0, 0, 3)), "should match")
+	// start server...
+	credMap := map[string]string{}
+	credMap["user"] = "pass"
 
-		// Close server
-		err = server.Close()
-		assert.NoError(t, err, "should succeed")
-	})
+	udpListener, err := net0.ListenPacket("udp4", "0.0.0.0:3478")
+	if err != nil {
+		return nil, err
+	}
 
-	t.Run("Adds SOFTWARE attribute to response", func(t *testing.T) {
-		const testSoftware = "SERVER_SOFTWARE"
-		cfg := &ServerConfig{
-			AuthHandler: func(username string, srcAddr net.Addr) (password string, ok bool) {
-				if pw, ok := credMap[username]; ok {
-					return pw, true
-				}
-				return "", false
+	server, err := NewServer(ServerConfig{
+		AuthHandler: func(username string, srcAddr net.Addr) (password string, ok bool) {
+			if pw, ok := credMap[username]; ok {
+				return pw, true
+			}
+			return "", false
+		},
+		Realm: "pion.ly",
+		PacketConnConfigs: []PacketConnConfig{
+			{
+				PacketConn: udpListener,
+				RelayAddressGenerator: &RelayAddressGeneratorNone{
+					Address: "1.2.3.4",
+					Net:     net0,
+				},
 			},
-			Realm:         "pion.ly",
-			Software:      testSoftware,
-			LoggerFactory: loggerFactory,
-		}
+		},
+		LoggerFactory: loggerFactory,
+	})
+	if err != nil {
+		return nil, err
+	}
 
-		server := NewServer(cfg)
+	// register host names
+	err = wan.AddHost("stun.pion.ly", "1.2.3.4")
+	if err != nil {
+		return nil, err
+	}
+	err = wan.AddHost("turn.pion.ly", "1.2.3.4")
+	if err != nil {
+		return nil, err
+	}
+	err = wan.AddHost("echo.pion.ly", "1.2.3.5")
+	if err != nil {
+		return nil, err
+	}
 
-		err := server.AddListeningIPAddr("127.0.0.1")
-		assert.NoError(t, err, "should succeed")
+	return &VNet{
+		wan:    wan,
+		net0:   net0,
+		net1:   net1,
+		netL0:  netL0,
+		server: server,
+	}, nil
+}
 
-		log.Debug("start listening...")
-		err = server.Start()
-		assert.NoError(t, err, "should succeed")
+func TestServerVNet(t *testing.T) {
+	loggerFactory := logging.NewDefaultLoggerFactory()
+	log := loggerFactory.NewLogger("test")
 
-		// make sure the server is listening before running
-		// the client.
-		time.Sleep(100 * time.Microsecond)
+	t.Run("SendBindingRequest", func(t *testing.T) {
+		v, err := buildVNet()
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+		defer v.Close()
 
-		lconn, err := net.ListenPacket("udp4", "0.0.0.0:0")
+		lconn, err := v.netL0.ListenPacket("udp4", "0.0.0.0:0")
 		if !assert.NoError(t, err, "should succeed") {
 			return
 		}
-		defer lconn.Close() // nolint:errcheck,gosec
+		defer lconn.Close()
 
 		log.Debug("creating a client.")
 		client, err := NewClient(&ClientConfig{
-			Conn:          lconn,
-			LoggerFactory: loggerFactory,
+			STUNServerAddr: "1.2.3.4:3478",
+			Conn:           lconn,
+			Net:            v.netL0,
+			LoggerFactory:  loggerFactory,
 		})
 		if !assert.NoError(t, err, "should succeed") {
 			return
@@ -196,62 +248,116 @@ func TestServer(t *testing.T) {
 		defer client.Close()
 
 		log.Debug("sending a binding request.")
-		resp, err := client.SendBindingRequestTo(&net.UDPAddr{
-			IP:   net.IPv4(127, 0, 0, 1),
-			Port: 3478,
-		})
-		assert.NoError(t, err, "should succeed")
-		t.Logf("resp: %v", resp)
-
-		log.Debug("now closing the server...")
+		reflAddr, err := client.SendBindingRequest()
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+		log.Debugf("mapped-address: %v", reflAddr.String())
+		udpAddr := reflAddr.(*net.UDPAddr)
 
-		// Close server
-		err = server.Close()
-		assert.NoError(t, err, "should succeed")
+		// The mapped-address should have IP address that was assigned
+		// to the LAN router.
+		assert.True(t, udpAddr.IP.Equal(net.IPv4(5, 6, 7, 8)), "should match")
 	})
 
-	t.Run("AddExternalIPAddr", func(t *testing.T) {
-		server := NewServer(&ServerConfig{
-			LoggerFactory: loggerFactory,
+	t.Run("Echo via relay", func(t *testing.T) {
+		v, err := buildVNet()
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+
+		lconn, err := v.netL0.ListenPacket("udp4", "0.0.0.0:0")
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+
+		log.Debug("creating a client.")
+		client, err := NewClient(&ClientConfig{
+			STUNServerAddr: "stun.pion.ly:3478",
+			TURNServerAddr: "turn.pion.ly:3478",
+			Username:       "user",
+			Password:       "pass",
+			Conn:           lconn,
+			Net:            v.netL0,
+			LoggerFactory:  loggerFactory,
 		})
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+		err = client.Listen()
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
 
-		err := server.AddExternalIPAddr("1.2.3.4")
-		assert.NoError(t, err, "should succeed")
+		log.Debug("sending a binding request.")
+		conn, err := client.Allocate()
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
 
-		assert.Equal(t, 1, len(server.extIPMappings), "should be 1")
-		assert.True(
-			t,
-			server.extIPMappings[0][0].Equal(net.ParseIP("1.2.3.4")),
-			"should be true")
-		assert.Nil(t, server.extIPMappings[0][1], "should be nil")
-	})
+		log.Debugf("laddr: %s", conn.LocalAddr().String())
 
-	t.Run("AddExternalIPAddr 2", func(t *testing.T) {
-		server := NewServer(&ServerConfig{
-			LoggerFactory: loggerFactory,
-		})
+		echoConn, err := v.net1.ListenPacket("udp4", "1.2.3.5:5678")
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+
+		go func() {
+			buf := make([]byte, 1500)
+			for {
+				n, from, err2 := echoConn.ReadFrom(buf)
+				if err2 != nil {
+					break
+				}
+
+				// verify the message was received from the relay address
+				assert.Equal(t, conn.LocalAddr().String(), from.String(), "should match")
+				assert.Equal(t, "Hello", string(buf[:n]), "should match")
+
+				// echo the data
+				_, err2 = echoConn.WriteTo(buf[:n], from)
+				if !assert.NoError(t, err2, "should succeed") {
+					break
+				}
+			}
+		}()
+
+		buf := make([]byte, 1500)
+
+		for i := 0; i < 10; i++ {
+			log.Debug("sending \"Hello\"..")
+			_, err = conn.WriteTo([]byte("Hello"), echoConn.LocalAddr())
+			if !assert.NoError(t, err, "should succeed") {
+				return
+			}
 
-		err := server.AddExternalIPAddr("1.2.3.4/10.0.0.2")
+			_, from, err2 := conn.ReadFrom(buf)
+			assert.NoError(t, err2, "should succeed")
+
+			// verify the message was received from the relay address
+			assert.Equal(t, echoConn.LocalAddr().String(), from.String(), "should match")
+
+			time.Sleep(100 * time.Millisecond)
+		}
+
+		time.Sleep(100 * time.Millisecond)
+		err = conn.Close()
 		assert.NoError(t, err, "should succeed")
-		err = server.AddExternalIPAddr("1.2.3.5/10.0.0.3")
+		log.Debug("RELAY CONN CLOSED")
+
+		err = echoConn.Close()
 		assert.NoError(t, err, "should succeed")
+		log.Debug("ECHO SERVER CLOSED")
 
-		assert.Equal(t, 2, len(server.extIPMappings), "should be 2")
-		assert.True(
-			t,
-			server.extIPMappings[0][0].Equal(net.ParseIP("1.2.3.4")),
-			"should be true")
-		assert.True(
-			t,
-			server.extIPMappings[0][1].Equal(net.ParseIP("10.0.0.2")),
-			"should be true")
-		assert.True(
-			t,
-			server.extIPMappings[1][0].Equal(net.ParseIP("1.2.3.5")),
-			"should be true")
-		assert.True(
-			t,
-			server.extIPMappings[1][1].Equal(net.ParseIP("10.0.0.3")),
-			"should be true")
+		client.Close()
+		log.Debug("TURN CLIENT CLOSED")
+
+		err = lconn.Close()
+		assert.NoError(t, err, "should succeed")
+		log.Debug("LOCAL CONN CLOSED")
+
+		err = v.Close()
+		assert.NoError(t, err, "should succeed")
+		log.Debug("VNET CLOSED")
 	})
 }
diff --git a/server_vnet_test.go b/server_vnet_test.go
deleted file mode 100644
index 7f4994a..0000000
--- a/server_vnet_test.go
+++ /dev/null
@@ -1,293 +0,0 @@
-package turn
-
-import (
-	"net"
-	"testing"
-	"time"
-
-	"github.com/pion/logging"
-	"github.com/pion/transport/vnet"
-	"github.com/stretchr/testify/assert"
-)
-
-type VNet struct {
-	wan    *vnet.Router
-	net0   *vnet.Net // net (0) on the WAN
-	net1   *vnet.Net // net (1) on the WAN
-	netL0  *vnet.Net // net (0) on the LAN
-	server *Server
-}
-
-func (v *VNet) Close() error {
-	err := v.server.Close()
-	v.wan.Stop() // nolint:errcheck,gosec
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func buildVNet() (*VNet, error) {
-	loggerFactory := logging.NewDefaultLoggerFactory()
-
-	// WAN
-	wan, err := vnet.NewRouter(&vnet.RouterConfig{
-		CIDR:          "0.0.0.0/0",
-		LoggerFactory: loggerFactory,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	net0 := vnet.NewNet(&vnet.NetConfig{
-		StaticIP: "1.2.3.4", // will be assigned to eth0
-	})
-
-	err = wan.AddNet(net0)
-	if err != nil {
-		return nil, err
-	}
-
-	net1 := vnet.NewNet(&vnet.NetConfig{
-		StaticIP: "1.2.3.5", // will be assigned to eth0
-	})
-
-	err = wan.AddNet(net1)
-	if err != nil {
-		return nil, err
-	}
-
-	// LAN
-	lan, err := vnet.NewRouter(&vnet.RouterConfig{
-		StaticIP: "5.6.7.8", // this router's external IP on eth0
-		CIDR:     "192.168.0.0/24",
-		NATType: &vnet.NATType{
-			MappingBehavior:   vnet.EndpointIndependent,
-			FilteringBehavior: vnet.EndpointIndependent,
-		},
-		LoggerFactory: loggerFactory,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	netL0 := vnet.NewNet(&vnet.NetConfig{})
-	err = lan.AddNet(netL0)
-	if err != nil {
-		return nil, err
-	}
-
-	err = wan.AddRouter(lan)
-	if err != nil {
-		return nil, err
-	}
-
-	err = wan.Start()
-	if err != nil {
-		return nil, err
-	}
-
-	// start server...
-	credMap := map[string]string{}
-	credMap["user"] = "pass"
-
-	server := NewServer(&ServerConfig{
-		AuthHandler: func(username string, srcAddr net.Addr) (password string, ok bool) {
-			if pw, ok := credMap[username]; ok {
-				return pw, true
-			}
-			return "", false
-		},
-		Realm:         "pion.ly",
-		Net:           net0,
-		LoggerFactory: loggerFactory,
-	})
-
-	err = server.AddListeningIPAddr("1.2.3.4")
-	if err != nil {
-		return nil, err
-	}
-
-	// register host names
-	err = wan.AddHost("stun.pion.ly", "1.2.3.4")
-	if err != nil {
-		return nil, err
-	}
-	err = wan.AddHost("turn.pion.ly", "1.2.3.4")
-	if err != nil {
-		return nil, err
-	}
-	err = wan.AddHost("echo.pion.ly", "1.2.3.5")
-	if err != nil {
-		return nil, err
-	}
-
-	err = server.Start()
-	if err != nil {
-		wan.Stop() // nolint:errcheck,gosec
-		return nil, err
-	}
-
-	return &VNet{
-		wan:    wan,
-		net0:   net0,
-		net1:   net1,
-		netL0:  netL0,
-		server: server,
-	}, nil
-}
-
-func TestServerVNet(t *testing.T) {
-	loggerFactory := logging.NewDefaultLoggerFactory()
-	log := loggerFactory.NewLogger("test")
-
-	t.Run("SendBindingRequest", func(t *testing.T) {
-		v, err := buildVNet()
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-		defer v.Close() // nolint:errcheck
-
-		lconn, err := v.netL0.ListenPacket("udp4", "0.0.0.0:0")
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-		defer lconn.Close() // nolint:errcheck,gosec
-
-		log.Debug("creating a client.")
-		client, err := NewClient(&ClientConfig{
-			STUNServerAddr: "1.2.3.4:3478",
-			Conn:           lconn,
-			Net:            v.netL0,
-			LoggerFactory:  loggerFactory,
-		})
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-		err = client.Listen()
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-		defer client.Close()
-
-		log.Debug("sending a binding request.")
-		reflAddr, err := client.SendBindingRequest()
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-		log.Debugf("mapped-address: %v", reflAddr.String())
-		udpAddr := reflAddr.(*net.UDPAddr)
-
-		// The mapped-address should have IP address that was assigned
-		// to the LAN router.
-		assert.True(t, udpAddr.IP.Equal(net.IPv4(5, 6, 7, 8)), "should match")
-	})
-
-	t.Run("Echo via relay", func(t *testing.T) {
-		v, err := buildVNet()
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-
-		lconn, err := v.netL0.ListenPacket("udp4", "0.0.0.0:0")
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-
-		log.Debug("creating a client.")
-		client, err := NewClient(&ClientConfig{
-			STUNServerAddr: "stun.pion.ly:3478",
-			TURNServerAddr: "turn.pion.ly:3478",
-			Username:       "user",
-			Password:       "pass",
-			Conn:           lconn,
-			Net:            v.netL0,
-			LoggerFactory:  loggerFactory,
-		})
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-		err = client.Listen()
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-
-		log.Debug("sending a binding request.")
-		conn, err := client.Allocate()
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-
-		log.Debugf("laddr: %s", conn.LocalAddr().String())
-
-		echoConn, err := v.net1.ListenPacket("udp4", "1.2.3.5:5678")
-		if !assert.NoError(t, err, "should succeed") {
-			return
-		}
-
-		go func() {
-			buf := make([]byte, 1500)
-			for {
-				n, from, err2 := echoConn.ReadFrom(buf)
-				if err2 != nil {
-					break
-				}
-				log.Debugf("echo: received %d bytes from %s: %s", n, from.String(), string(buf[:n]))
-
-				// verify the message was received from the relay address
-				if !assert.Equal(t, conn.LocalAddr().String(), from.String(), "should match") {
-					break
-				}
-
-				// verify the message received is correct
-				if !assert.Equal(t, "Hello", string(buf[:n]), "should match") {
-					break
-				}
-
-				// echo the data
-				_, err2 = echoConn.WriteTo(buf[:n], from)
-				if !assert.NoError(t, err2, "should succeed") {
-					break
-				}
-			}
-		}()
-
-		buf := make([]byte, 1500)
-
-		for i := 0; i < 10; i++ {
-			log.Debug("sending \"Hello\"..")
-			_, err = conn.WriteTo([]byte("Hello"), echoConn.LocalAddr())
-			if !assert.NoError(t, err, "should succeed") {
-				return
-			}
-
-			_, from, err2 := conn.ReadFrom(buf)
-			assert.NoError(t, err2, "should succeed")
-
-			// verify the message was received from the relay address
-			assert.Equal(t, echoConn.LocalAddr().String(), from.String(), "should match")
-
-			time.Sleep(100 * time.Millisecond)
-		}
-
-		time.Sleep(100 * time.Millisecond)
-		err = conn.Close()
-		assert.NoError(t, err, "should succeed")
-		log.Debug("RELAY CONN CLOSED")
-
-		err = echoConn.Close()
-		assert.NoError(t, err, "should succeed")
-		log.Debug("ECHO SERVER CLOSED")
-
-		client.Close()
-		log.Debug("TURN CLIENT CLOSED")
-
-		err = lconn.Close()
-		assert.NoError(t, err, "should succeed")
-		log.Debug("LOCAL CONN CLOSED")
-
-		err = v.Close()
-		assert.NoError(t, err, "should succeed")
-		log.Debug("VNET CLOSED")
-	})
-}