Repository: pion/turn
Pull request #52 (2019-06-18 07:57:48)

Support vnet
Relates to pion/webrtc#712

@Sean-Der Beside support of vnet and using pion/logger (instead of fmt.Print*), I made the following mildly major changes and wanted to draw early attention from you.

### Dropping IP level option as it is not portable (ReadFromCM => ReadFrom)
I guess the IP-level option (the "control message" - ipv4.DstFlag) was used because you thought real IP address other than 0.0.0.0 was necessary for five-tuple comparison.
I believe, if a specific IP address matters (e.g. generating relay address, etc), then we should have a dedicated socket for each local IP addresses instead of using "any-addr" (0.0.0.0). In most cases, we would only need one IP address (on eth0). This way, we can get away from having to use the any-addr.  The any-addr certainly makes things easier (you don't need to find your IP address) but price for needing IP-level option seems to be more expensive IMHO.

Let me spend a bit more time to understand how this affect fileTuple comparison. Either way though I think it is better to have `AddListener` method to allow listening on a multiple sockets in case the turn server has more than one IP address...

### Removed `turn.go` (the wrapper for client.Client and server.Server)
To support vnet, I need to access APIs (internal/client and internal/server). I found the API in the internal is pretty simple. So, I removed the wrapper, and moved server.go and client.go to the top (under name of `turn` package).   I modified cmd/simple-turn and cmd/client - as you can see, these are still simple enough I believe.

enobufs: @ernado How does gortcd deal with the listening IP gathering?
ernado: @enobufs
Via iterating over every interface and creating a listener for each one.

There is a config that is similar to "listening up".

Currently the NAT problem is not solved (if the interface IP differs from public IP it can break), I think that this should be solved via configuration (some kind of mapping)
enobufs: @ernado Thanks! That's the way to go!
Yeah, the option for external (public) IP mapping (-X option for coturn) would be desired (AWS EC2, etc). Pion/turn should have that one too...
songjiayang: @enobufs Nice work.

I have merged a pr https://github.com/pion/turn/pull/51 before, so there are some conflicting files need you to resolve.
codecov-io: # [Codecov](https://codecov.io/gh/pion/turn/pull/52?src=pr&el=h1) Report
> Merging [#52](https://codecov.io/gh/pion/turn/pull/52?src=pr&el=desc) into [master](https://codecov.io/gh/pion/turn/commit/3444b546acb0cbcad1c6646e427e9c9163faf961?src=pr&el=desc) will **increase** coverage by `10.42%`.
> The diff coverage is `15.36%`.

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

```diff
@@             Coverage Diff             @@
##           master      #52       +/-   ##
===========================================
+ Coverage   17.42%   27.85%   +10.42%     
===========================================
  Files          16       15        -1     
  Lines         677      700       +23     
===========================================
+ Hits          118      195       +77     
+ Misses        535      472       -63     
- Partials       24       33        +9
```


| [Impacted Files](https://codecov.io/gh/pion/turn/pull/52?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [stun.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-c3R1bi5nbw==) | `81.81% <ø> (ø)` | |
| [turn.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-dHVybi5nbw==) | `0% <0%> (ø)` | :arrow_up: |
| [internal/allocation/channel\_bind.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9jaGFubmVsX2JpbmQuZ28=) | `25% <0%> (-17.86%)` | :arrow_down: |
| [stunc.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-c3R1bmMuZ28=) | `54.54% <100%> (ø)` | |
| [client.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-Y2xpZW50Lmdv) | `100% <100%> (ø)` | |
| [internal/allocation/allocation.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9hbGxvY2F0aW9uLmdv) | `35.45% <18.18%> (ø)` | :arrow_up: |
| [internal/allocation/permission.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9wZXJtaXNzaW9uLmdv) | `45.45% <66.66%> (+31.16%)` | :arrow_up: |
| [server.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-c2VydmVyLmdv) | `47.89% <74.19%> (ø)` | |
| [internal/allocation/allocation\_manager.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9hbGxvY2F0aW9uX21hbmFnZXIuZ28=) | `63.49% <76.92%> (+12.64%)` | :arrow_up: |
| [internal/ipnet/ipv4\_unix.go](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree#diff-aW50ZXJuYWwvaXBuZXQvaXB2NF91bml4Lmdv) | `28.57% <0%> (-42.86%)` | :arrow_down: |
| ... and [6 more](https://codecov.io/gh/pion/turn/pull/52/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/turn/pull/52?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/52?src=pr&el=footer). Last update [3444b54...fe056b3](https://codecov.io/gh/pion/turn/pull/52?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

enobufs: Ok resolved all merge conflict with @songjiayang 's awesome allocation time refactor.  All tests passed - whew!   Merging now!

Patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d39fb86
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,1 @@
+*.sw[poe]
diff --git a/README.md b/README.md
index 6e4ab73..7093f28 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ Check out the [CONTRIBUTING.md](CONTRIBUTING.md) to join the group of amazing pe
 * [Sean DuBois](https://github.com/Sean-Der) - *Original Author*
 * [winds2016](https://github.com/winds2016) - *Windows platform testing*
 * [songjiayang](https://github.com/songjiayang) - *SongJiaYang*
+* [Yutaka Takeda](https://github.com/enobufs) - *vnet*
 
 ### License
 MIT License - see [LICENSE.md](LICENSE.md) for full text
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..97c52bf
--- /dev/null
+++ b/client.go
@@ -0,0 +1,97 @@
+package turn
+
+import (
+	"fmt"
+	"net"
+
+	"sync"
+
+	"github.com/pion/logging"
+	"github.com/pion/stun"
+	"github.com/pion/transport/vnet"
+	"github.com/pkg/errors"
+)
+
+// ClientConfig is a bag of config parameters for Client.
+type ClientConfig struct {
+	ListeningAddress string
+	LoggerFactory    logging.LoggerFactory
+	Net              *vnet.Net
+}
+
+// Client is a STUN server client
+type Client struct {
+	conn net.PacketConn
+	mux  sync.Mutex
+	net  *vnet.Net
+	log  logging.LeveledLogger
+}
+
+// NewClient returns a new Client instance. listeningAddress is the address and port to listen on, default "0.0.0.0:0"
+func NewClient(config *ClientConfig) (*Client, error) {
+	log := config.LoggerFactory.NewLogger("turnc")
+	network := "udp4"
+
+	if config.Net == nil {
+		config.Net = vnet.NewNet(nil) // defaults to native operation
+	} else {
+		log.Warn("vnet is enabled")
+	}
+
+	c := &Client{
+		net: config.Net,
+		log: log,
+	}
+
+	var err error
+	c.conn, err = c.net.ListenPacket(network, config.ListeningAddress)
+	if err != nil {
+		return nil, errors.Wrap(err, fmt.Sprintf("failed to listen on %s", config.ListeningAddress))
+	}
+
+	return c, nil
+}
+
+// SendSTUNRequest sends a new STUN request to the serverIP with serverPort
+func (c *Client) SendSTUNRequest(serverIP net.IP, serverPort int) (net.Addr, error) {
+	c.mux.Lock()
+	defer c.mux.Unlock()
+
+	c.log.Debug("sending STUN request")
+	if err := sendStunRequest(c.conn, serverIP, serverPort); err != nil {
+		return nil, err
+	}
+
+	packet := make([]byte, 1500)
+	c.log.Debug("wait for STUN response...")
+	size, _, err := c.conn.ReadFrom(packet)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to read packet from udp socket")
+	}
+
+	c.log.Debugf("received %d bytes of STUN response", size)
+	resp := &stun.Message{Raw: append([]byte{}, packet[:size]...)}
+	if err := resp.Decode(); err != nil {
+		return nil, errors.Wrap(err, "failed to handle reply")
+	}
+
+	var reflAddr stun.XORMappedAddress
+	if err := reflAddr.GetFrom(resp); err != nil {
+		return nil, err
+	}
+
+	//return fmt.Sprintf("pkt_size=%d src_addr=%s refl_addr=%s:%d", size, addr, reflAddr.IP, reflAddr.Port), nil
+	return &net.UDPAddr{
+		IP:   reflAddr.IP,
+		Port: reflAddr.Port,
+	}, nil
+}
+
+func sendStunRequest(conn net.PacketConn, serverIP net.IP, serverPort int) error {
+	msg, err := stun.Build(stun.TransactionID, stun.BindingRequest)
+	if err != nil {
+		return err
+	}
+	_, err = conn.WriteTo(msg.Raw, &net.UDPAddr{IP: serverIP, Port: serverPort})
+	return err
+}
diff --git a/cmd/client/main.go b/cmd/client/main.go
index 6392123..be1ea00 100644
--- a/cmd/client/main.go
+++ b/cmd/client/main.go
@@ -6,6 +6,7 @@ import (
 	"log"
 	"net"
 
+	"github.com/pion/logging"
 	"github.com/pion/turn"
 )
 
@@ -19,16 +20,20 @@ func main() {
 		panic(errors.New("failed to parse host IP"))
 	}
 
-	args := turn.ClientArguments{
-		BindingAddress: "0.0.0.0:0",
-		ServerIP:       ip,
-		ServerPort:     *port,
+	c, err := turn.NewClient(&turn.ClientConfig{
+		ListeningAddress: "0.0.0.0:0",
+		LoggerFactory:    logging.NewDefaultLoggerFactory(),
+	})
+	if err != nil {
+		panic(err)
 	}
 
-	resp, err := turn.StartClient(args)
+	mappedAddr, err := c.SendSTUNRequest(ip, *port)
 	if err != nil {
 		panic(err)
 	}
 
-	log.Println(resp)
+	log.Printf("mapped-address=%s:%s",
+		mappedAddr.Network(),
+		mappedAddr.String())
 }
diff --git a/cmd/simple-turn/main.go b/cmd/simple-turn/main.go
index 18a59ec..4d3d6c5 100644
--- a/cmd/simple-turn/main.go
+++ b/cmd/simple-turn/main.go
@@ -4,33 +4,37 @@ import (
 	"log"
 	"net"
 	"os"
+	"os/signal"
 	"regexp"
 	"strconv"
+	"syscall"
 	"time"
 
+	"github.com/pion/logging"
 	"github.com/pion/turn"
 )
 
-type myTurnServer struct {
-	usersMap map[string]string
-}
-
-func (m *myTurnServer) AuthenticateRequest(username string, srcAddr net.Addr) (password string, ok bool) {
-	if password, ok := m.usersMap[username]; ok {
-		return password, true
+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
 	}
-	return "", false
 }
 
 func main() {
-	m := &myTurnServer{usersMap: make(map[string]string)}
+	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) {
-		m.usersMap[kv[1]] = kv[2]
+		usersMap[kv[1]] = kv[2]
 	}
 
 	realm := os.Getenv("REALM")
@@ -40,28 +44,39 @@ func main() {
 
 	udpPortStr := os.Getenv("UDP_PORT")
 	if udpPortStr == "" {
-		log.Panic("UDP_PORT is a required environment variable")
+		udpPortStr = "3478"
 	}
 	udpPort, err := strconv.Atoi(udpPortStr)
 	if err != nil {
 		log.Panic(err)
 	}
 
-	args := turn.StartArguments{
-		Server:  m,
-		Realm:   realm,
-		UDPPort: udpPort,
-	}
-
+	var channelBindTimeout time.Duration
 	channelBindTimeoutStr := os.Getenv("CHANNEL_BIND_TIMEOUT")
 	if channelBindTimeoutStr != "" {
-		channelBindTimeout, err := time.ParseDuration(channelBindTimeoutStr)
+		channelBindTimeout, err = time.ParseDuration(channelBindTimeoutStr)
 		if err != nil {
 			log.Panicf("CHANNEL_BIND_TIMEOUT=%s is an invalid time Duration", channelBindTimeoutStr)
 		}
+	}
 
-		args.ChannelBindTimeout = channelBindTimeout
+	s := turn.NewServer(&turn.ServerConfig{
+		Realm:              realm,
+		AuthHandler:        createAuthHandler(usersMap),
+		ChannelBindTimeout: channelBindTimeout,
+		ListeningPort:      udpPort,
+		LoggerFactory:      logging.NewDefaultLoggerFactory(),
+	})
+
+	err = s.Start()
+	if err != nil {
+		log.Panic(err)
 	}
 
-	turn.Start(args)
+	<-sigs
+
+	err = s.Close()
+	if err != nil {
+		log.Panic(err)
+	}
 }
diff --git a/go.mod b/go.mod
index 8d5e652..b0bb12f 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,12 @@ module github.com/pion/turn
 go 1.12
 
 require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/gortc/turn v0.8.0
+	github.com/pion/logging v0.2.1
 	github.com/pion/stun v0.3.1
+	github.com/pion/transport v0.8.1
 	github.com/pkg/errors v0.8.1
+	github.com/stretchr/testify v1.3.0
 	golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b
 )
diff --git a/go.sum b/go.sum
index 7f4f6ed..ee68f45 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,21 @@
+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/gortc/turn v0.8.0 h1:WWQi1jkoPmc2E7qgUMcZleveKikT9Ksi3QGIl8ZtY3Q=
 github.com/gortc/turn v0.8.0/go.mod h1:gvguwaGAFyv5/9KrcW9MkCgHALYD+e99mSM7pSCYYho=
+github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk=
+github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
 github.com/pion/stun v0.3.1 h1:d09JJzOmOS8ZzIp8NppCMgrxGZpJ4Ix8qirfNYyI3BA=
 github.com/pion/stun v0.3.1/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
+github.com/pion/transport v0.8.1 h1:FUHJFd4MaIEJmlpiGx+ZH8j9JLsERnROHQPA9zNFFAs=
+github.com/pion/transport v0.8.1/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
 github.com/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.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b h1:/zjbcJPEGAyu6Is/VBOALsgdi4z9+kz/Vtdm6S+beD0=
 golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
diff --git a/internal/allocation/allocation.go b/internal/allocation/allocation.go
index f1ae4bc..393197c 100644
--- a/internal/allocation/allocation.go
+++ b/internal/allocation/allocation.go
@@ -2,12 +2,12 @@ package allocation
 
 import (
 	"encoding/binary"
-	"fmt"
 	"net"
 	"sync"
 	"time"
 
 	"github.com/gortc/turn"
+	"github.com/pion/logging"
 	"github.com/pion/stun"
 	"github.com/pion/turn/internal/ipnet"
 	"github.com/pkg/errors"
@@ -16,23 +16,18 @@ import (
 // Allocation is tied to a FiveTuple and relays traffic
 // use CreateAllocation and GetAllocation to operate
 type Allocation struct {
-	RelayAddr net.Addr
-	Protocol  Protocol
-
-	TurnSocket  ipnet.PacketConn
-	RelaySocket ipnet.PacketConn
-
-	fiveTuple *FiveTuple
-
-	permissionsLock sync.RWMutex
-	permissions     []*Permission
-
+	RelayAddr           net.Addr
+	Protocol            Protocol
+	TurnSocket          net.PacketConn
+	RelaySocket         net.PacketConn
+	fiveTuple           *FiveTuple
+	permissionsLock     sync.RWMutex
+	permissions         []*Permission
 	channelBindingsLock sync.RWMutex
 	channelBindings     []*ChannelBind
-
-	lifetimeTimer *time.Timer
-
-	closed chan interface{}
+	lifetimeTimer       *time.Timer
+	closed              chan interface{}
+	log                 logging.LeveledLogger
 }
 
 // AddPermission adds a new permission to the allocation
@@ -99,12 +94,12 @@ func (a *Allocation) AddChannelBind(c *ChannelBind, lifetime time.Duration) erro
 		c.start(lifetime)
 
 		// Channel binds also refresh permissions.
-		a.AddPermission(&Permission{Addr: c.Peer})
+		a.AddPermission(NewPermission(c.Peer, a.log))
 	} else {
 		channelByID.refresh(lifetime)
 
 		// Channel binds also refresh permissions.
-		a.AddPermission(&Permission{Addr: channelByID.Peer})
+		a.AddPermission(NewPermission(channelByID.Peer, a.log))
 	}
 
 	return nil
@@ -153,12 +148,12 @@ func (a *Allocation) GetChannelByAddr(addr net.Addr) *ChannelBind {
 func (a *Allocation) Refresh(lifetime time.Duration) {
 	if lifetime == 0 {
 		if !a.lifetimeTimer.Stop() {
-			fmt.Printf("Failed to stop allocation timer for %v \n", a.fiveTuple)
+			a.log.Errorf("Failed to stop allocation timer for %v", a.fiveTuple)
 		}
 		return
 	}
 	if !a.lifetimeTimer.Reset(lifetime) {
-		fmt.Printf("Failed to reset allocation timer for %v \n", a.fiveTuple)
+		a.log.Errorf("Failed to reset allocation timer for %v", a.fiveTuple)
 	}
 }
 
@@ -212,36 +207,36 @@ func (a *Allocation) packetHandler(m *Manager) {
 	buffer := make([]byte, rtpMTU)
 
 	for {
-		n, cm, srcAddr, err := a.RelaySocket.ReadFromCM(buffer)
+		n, srcAddr, err := a.RelaySocket.ReadFrom(buffer)
 		if err != nil {
 			if !m.DeleteAllocation(a.fiveTuple) {
-				fmt.Println("Failed to remove allocation after relay listener had closed")
+				a.log.Error("Failed to remove allocation after relay listener had closed")
 			}
 			return
 		}
 
-		if channel := a.GetChannelByAddr(&net.UDPAddr{IP: cm.Dst, Port: a.RelayAddr.(*net.UDPAddr).Port}); channel != nil {
+		if channel := a.GetChannelByAddr(a.RelaySocket.LocalAddr()); channel != nil {
 			channelData := make([]byte, 4)
 			binary.BigEndian.PutUint16(channelData[0:], uint16(channel.ID))
 			binary.BigEndian.PutUint16(channelData[2:], uint16(n))
 			channelData = append(channelData, buffer[:n]...)
 
 			if _, err = a.TurnSocket.WriteTo(channelData, a.fiveTuple.SrcAddr); err != nil {
-				fmt.Printf("Failed to send ChannelData from allocation %v %v \n", srcAddr, err)
+				a.log.Errorf("Failed to send ChannelData from allocation %v %v", srcAddr, err)
 			}
 		} else if p := a.GetPermission(&net.UDPAddr{IP: srcAddr.(*net.UDPAddr).IP, Port: srcAddr.(*net.UDPAddr).Port}); p != nil {
 			dataAttr := turn.Data(buffer[:n])
 			peerAddressAttr := turn.PeerAddress{IP: srcAddr.(*net.UDPAddr).IP, Port: srcAddr.(*net.UDPAddr).Port}
 			msg, err := stun.Build(stun.TransactionID, stun.NewType(stun.MethodData, stun.ClassIndication), peerAddressAttr, dataAttr)
 			if err != nil {
-				fmt.Printf("Failed to send DataIndication from allocation %v %v \n", srcAddr, err)
+				a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err)
 			}
 			if _, err = a.TurnSocket.WriteTo(msg.Raw, a.fiveTuple.SrcAddr); err != nil {
-				fmt.Printf("Failed to send DataIndication from allocation %v %v \n", srcAddr, err)
+				a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err)
 			}
 
 		} else {
-			fmt.Printf("Packet unhandled in relay src %v \n", srcAddr)
+			a.log.Errorf("Packet unhandled in relay src %v", srcAddr)
 		}
 	}
 
diff --git a/internal/allocation/allocation_manager.go b/internal/allocation/allocation_manager.go
index edbd11d..8cb6e77 100644
--- a/internal/allocation/allocation_manager.go
+++ b/internal/allocation/allocation_manager.go
@@ -6,14 +6,34 @@ import (
 	"sync"
 	"time"
 
-	"github.com/pion/turn/internal/ipnet"
+	"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
+}
+
 // Manager is used to hold active allocations
 type Manager struct {
 	lock        sync.RWMutex
 	allocations []*Allocation
+	log         logging.LeveledLogger
+	net         *vnet.Net
+}
+
+// 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
+	}
+	return &Manager{
+		log: config.LeveledLogger,
+		net: config.Net,
+	}
 }
 
 // GetAllocation fetches the allocation matching the passed FiveTuple
@@ -43,7 +63,7 @@ func (m *Manager) Close() error {
 }
 
 // CreateAllocation creates a new allocation and starts relaying
-func (m *Manager) CreateAllocation(fiveTuple *FiveTuple, turnSocket ipnet.PacketConn, requestedPort int, lifetime time.Duration) (*Allocation, error) {
+func (m *Manager) CreateAllocation(fiveTuple *FiveTuple, turnSocket net.PacketConn, requestedPort int, lifetime time.Duration) (*Allocation, error) {
 	if fiveTuple == nil {
 		return nil, errors.Errorf("Allocations must not be created with nil FivTuple")
 	}
@@ -67,24 +87,21 @@ func (m *Manager) CreateAllocation(fiveTuple *FiveTuple, turnSocket ipnet.Packet
 		fiveTuple:  fiveTuple,
 		TurnSocket: turnSocket,
 		closed:     make(chan interface{}),
+		log:        m.log,
 	}
 
 	network := "udp4"
-	listener, err := net.ListenPacket(network, fmt.Sprintf("0.0.0.0:%d", requestedPort))
+	conn, err := m.net.ListenPacket(network, fmt.Sprintf("0.0.0.0:%d", requestedPort))
 	if err != nil {
 		return nil, err
 	}
 
-	conn, err := ipnet.NewPacketConn(network, listener)
-	if err != nil {
-		return nil, err
-	}
 	a.RelaySocket = conn
-	a.RelayAddr = listener.LocalAddr()
+	a.RelayAddr = conn.LocalAddr()
 
 	a.lifetimeTimer = time.AfterFunc(lifetime, func() {
-		if err := listener.Close(); err != nil {
-			fmt.Printf("Failed to close listener for %v \n", a.fiveTuple)
+		if err := conn.Close(); err != nil {
+			a.log.Errorf("Failed to close listener for %v", a.fiveTuple)
 		}
 	})
 
@@ -105,7 +122,7 @@ func (m *Manager) DeleteAllocation(fiveTuple *FiveTuple) bool {
 		allocation := m.allocations[i]
 		if allocation.fiveTuple.Equal(fiveTuple) {
 			if err := allocation.Close(); err != nil {
-				fmt.Printf("Failed to close allocation: %v \n", err)
+				m.log.Errorf("Failed to close allocation: %v", err)
 			}
 			m.allocations = append(m.allocations[:i], m.allocations[i+1:]...)
 			return true
diff --git a/internal/allocation/channel_bind.go b/internal/allocation/channel_bind.go
index 61e31a4..26fc815 100644
--- a/internal/allocation/channel_bind.go
+++ b/internal/allocation/channel_bind.go
@@ -1,11 +1,11 @@
 package allocation
 
 import (
-	"fmt"
 	"net"
 	"time"
 
 	"github.com/gortc/turn"
+	"github.com/pion/logging"
 )
 
 // ChannelBind represents a TURN Channel
@@ -15,18 +15,28 @@ type ChannelBind struct {
 	ID            turn.ChannelNumber
 	allocation    *Allocation
 	lifetimeTimer *time.Timer
+	log           logging.LeveledLogger
+}
+
+// NewChannelBind creates a new ChannelBind
+func NewChannelBind(id turn.ChannelNumber, peer net.Addr, log logging.LeveledLogger) *ChannelBind {
+	return &ChannelBind{
+		ID:   id,
+		Peer: peer,
+		log:  log,
+	}
 }
 
 func (c *ChannelBind) start(lifetime time.Duration) {
 	c.lifetimeTimer = time.AfterFunc(lifetime, func() {
 		if !c.allocation.RemoveChannelBind(c.ID) {
-			fmt.Printf("Failed to remove ChannelBind for %v %x %v \n", c.ID, c.Peer, c.allocation.fiveTuple)
+			c.log.Errorf("Failed to remove ChannelBind for %v %x %v", c.ID, c.Peer, c.allocation.fiveTuple)
 		}
 	})
 }
 
 func (c *ChannelBind) refresh(lifetime time.Duration) {
 	if !c.lifetimeTimer.Reset(lifetime) {
-		fmt.Printf("Failed to reset ChannelBind timer for %v %x %v \n", c.ID, c.Peer, c.allocation.fiveTuple)
+		c.log.Errorf("Failed to reset ChannelBind timer for %v %x %v", c.ID, c.Peer, c.allocation.fiveTuple)
 	}
 }
diff --git a/internal/allocation/permission.go b/internal/allocation/permission.go
index 6e96cc0..2f7eaf4 100644
--- a/internal/allocation/permission.go
+++ b/internal/allocation/permission.go
@@ -1,9 +1,10 @@
 package allocation
 
 import (
-	"fmt"
 	"net"
 	"time"
+
+	"github.com/pion/logging"
 )
 
 const permissionTimeout = time.Duration(5) * time.Minute
@@ -15,18 +16,27 @@ type Permission struct {
 	Addr          net.Addr
 	allocation    *Allocation
 	lifetimeTimer *time.Timer
+	log           logging.LeveledLogger
+}
+
+// NewPermission create a new Permission
+func NewPermission(addr net.Addr, log logging.LeveledLogger) *Permission {
+	return &Permission{
+		Addr: addr,
+		log:  log,
+	}
 }
 
 func (p *Permission) start() {
 	p.lifetimeTimer = time.AfterFunc(permissionTimeout, func() {
 		if !p.allocation.RemovePermission(p.Addr) {
-			fmt.Printf("Failed to remove permission for %v %v \n", p.Addr, p.allocation.fiveTuple)
+			p.log.Errorf("Failed to remove permission for %v %v", p.Addr, p.allocation.fiveTuple)
 		}
 	})
 }
 
 func (p *Permission) refresh() {
 	if !p.lifetimeTimer.Reset(permissionTimeout) {
-		fmt.Printf("Failed to reset permission timer for %v %v \n", p.Addr, p.allocation.fiveTuple)
+		p.log.Errorf("Failed to reset permission timer for %v %v", p.Addr, p.allocation.fiveTuple)
 	}
 }
diff --git a/internal/client/client.go b/internal/client/client.go
deleted file mode 100644
index c70be4a..0000000
--- a/internal/client/client.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package client
-
-import (
-	"fmt"
-	"net"
-
-	"sync"
-
-	"github.com/pion/turn/internal/ipnet"
-	"github.com/pkg/errors"
-)
-
-// Client is a STUN server client
-type Client struct {
-	conn ipnet.PacketConn
-	mux  *sync.Mutex
-}
-
-// NewClient returns a new Client instance. listeningAddress is the address and port to listen on, default "0.0.0.0:0"
-func NewClient(listeningAddress string) (*Client, error) {
-	network := "udp4"
-	c, err := net.ListenPacket(network, listeningAddress)
-	if err != nil {
-		return nil, errors.Wrap(err, fmt.Sprintf("failed to listen on %s", listeningAddress))
-	}
-	conn, err := ipnet.NewPacketConn(network, c)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to create connection")
-	}
-
-	return &Client{conn, &sync.Mutex{}}, nil
-}
diff --git a/internal/client/stun.go b/internal/client/stun.go
deleted file mode 100644
index aaf2e0f..0000000
--- a/internal/client/stun.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package client
-
-import (
-	"fmt"
-	"net"
-
-	"github.com/pion/stun"
-	"github.com/pkg/errors"
-)
-
-// SendSTUNRequest sends a new STUN request to the serverIP with serverPort
-func (c *Client) SendSTUNRequest(serverIP net.IP, serverPort int) (interface{}, error) {
-	c.mux.Lock()
-	defer c.mux.Unlock()
-
-	if err := sendStunRequest(c.conn, serverIP, serverPort); err != nil {
-		return nil, err
-	}
-
-	packet := make([]byte, 1500)
-	size, cm, addr, err := c.conn.ReadFromCM(packet)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to read packet from udp socket")
-	}
-
-	resp := &stun.Message{Raw: append([]byte{}, packet[:size]...)}
-	if err := resp.Decode(); err != nil {
-		return nil, errors.Wrap(err, "failed to handle reply")
-	}
-
-	var reflAddr stun.XORMappedAddress
-	if err := reflAddr.GetFrom(resp); err != nil {
-		return nil, err
-	}
-
-	return fmt.Sprintf("pkt_size=%d dst_ip=%s src_addr=%s refl_addr=%s:%d", size, cm.Dst, addr, reflAddr.IP, reflAddr.Port), nil
-}
-
-func sendStunRequest(conn net.PacketConn, serverIP net.IP, serverPort int) error {
-	msg, err := stun.Build(stun.TransactionID, stun.BindingRequest)
-	if err != nil {
-		return err
-	}
-	_, err = conn.WriteTo(msg.Raw, &net.UDPAddr{IP: serverIP, Port: serverPort})
-	return err
-}
diff --git a/internal/server/server.go b/internal/server/server.go
deleted file mode 100644
index 3d93d48..0000000
--- a/internal/server/server.go
+++ /dev/null
@@ -1,200 +0,0 @@
-package server
-
-import (
-	"crypto/md5" // #nosec
-	"fmt"
-	"io"
-	"log"
-	"math/rand"
-	"net"
-	"strconv"
-	"sync"
-	"time"
-
-	"github.com/gortc/turn"
-	"github.com/pion/stun"
-	"github.com/pion/turn/internal/allocation"
-	"github.com/pion/turn/internal/ipnet"
-	"github.com/pkg/errors"
-)
-
-// 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)
-
-// Server is an instance of the Pion TURN server
-type Server struct {
-	lock               sync.RWMutex
-	connection         ipnet.PacketConn
-	packet             []byte
-	realm              string
-	authHandler        AuthHandler
-	manager            *allocation.Manager
-	reservationManager *allocation.ReservationManager
-	channelBindTimeout time.Duration
-}
-
-const maxStunMessageSize = 1500
-
-// NewServer creates the Pion TURN server
-func NewServer(realm string, channelBindTimeout time.Duration, a AuthHandler) *Server {
-	return &Server{
-		packet:             make([]byte, maxStunMessageSize),
-		realm:              realm,
-		authHandler:        a,
-		manager:            &allocation.Manager{},
-		reservationManager: &allocation.ReservationManager{},
-		channelBindTimeout: channelBindTimeout,
-	}
-}
-
-// Listen starts listening and handling TURN traffic
-func (s *Server) Listen(address string, port int) error {
-	listeningAddress := fmt.Sprintf("%s:%d", address, port)
-	network := "udp4"
-	c, err := net.ListenPacket(network, listeningAddress)
-	if err != nil {
-		return errors.Wrap(err, fmt.Sprintf("Failed to listen on %s", listeningAddress))
-	}
-	conn, err := ipnet.NewPacketConn(network, c)
-	if err != nil {
-		return errors.Wrap(err, "failed to create connection")
-	}
-	s.lock.Lock()
-	s.connection = conn
-	s.lock.Unlock()
-
-	for {
-		size, cm, addr, err := s.connection.ReadFromCM(s.packet)
-		if err != nil {
-			return errors.Wrap(err, "failed to read packet from udp socket")
-		}
-
-		if err := s.handleUDPPacket(addr, &net.UDPAddr{IP: cm.Dst, Port: port}, size); err != nil {
-			log.Println(err)
-		}
-	}
-}
-
-// Close closes the connection.
-func (s *Server) Close() error {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	if err := s.manager.Close(); err != nil {
-		return err
-	}
-	return s.connection.Close()
-}
-
-func (s *Server) handleUDPPacket(srcAddr, dstAddr net.Addr, size int) error {
-	if turn.IsChannelData(s.packet[:size]) {
-		return s.handleDataPacket(srcAddr, dstAddr, size)
-	}
-
-	return s.handleTURNPacket(srcAddr, dstAddr, size)
-}
-
-func (s *Server) handleDataPacket(srcAddr, dstAddr net.Addr, size int) error {
-	c := turn.ChannelData{Raw: s.packet[:size]}
-	if err := c.Decode(); err != nil {
-		return errors.Wrap(err, "Failed to create channel data from packet")
-	}
-
-	err := s.handleChannelData(srcAddr, dstAddr, &c)
-	if err != nil {
-		err = errors.Errorf("unable to handle ChannelData from %v: %v", srcAddr, err)
-	}
-
-	return err
-}
-
-func (s *Server) handleTURNPacket(srcAddr, dstAddr net.Addr, size int) error {
-	m := &stun.Message{Raw: append([]byte{}, s.packet[:size]...)}
-	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(srcAddr, dstAddr, 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(srcAddr, dstAddr 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)
-		}
-
-	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 {
-	/* #nosec */
-	h := md5.New()
-	now := time.Now().Unix()
-	if _, err := io.WriteString(h, strconv.FormatInt(now, 10)); err != nil {
-		fmt.Printf("Failed generating nonce %v \n", err)
-	}
-	if _, err := io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)); err != nil {
-		fmt.Printf("Failed generating nonce %v \n", err)
-	}
-	return fmt.Sprintf("%x", h.Sum(nil))
-}
-
-func assertMessageIntegrity(m *stun.Message, ourKey []byte) error {
-	messageIntegrityAttr := stun.MessageIntegrity(ourKey)
-	return messageIntegrityAttr.Check(m)
-}
-
-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
-}
diff --git a/internal/server/stun.go b/internal/server/stun.go
deleted file mode 100644
index 95646cb..0000000
--- a/internal/server/stun.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package server
-
-import (
-	"net"
-
-	"github.com/pion/stun"
-	"github.com/pion/turn/internal/ipnet"
-)
-
-func (s *Server) handleBindingRequest(srcAddr, dstAddr net.Addr, m *stun.Message) error {
-	ip, port, err := ipnet.AddrIPPort(srcAddr)
-	if err != nil {
-		return err
-	}
-
-	return buildAndSend(s.connection, srcAddr, &stun.Message{TransactionID: m.TransactionID}, stun.BindingSuccess,
-		&stun.XORMappedAddress{
-			IP:   ip,
-			Port: port,
-		},
-		stun.Fingerprint,
-	)
-}
diff --git a/internal/server/turn.go b/internal/server/turn.go
deleted file mode 100644
index 6bc1c06..0000000
--- a/internal/server/turn.go
+++ /dev/null
@@ -1,453 +0,0 @@
-package server
-
-import (
-	"crypto/md5" // #nosec
-	"net"
-	"strings"
-	"time"
-
-	"github.com/gortc/turn"
-	"github.com/pion/stun"
-
-	"github.com/pion/turn/internal/allocation"
-	"github.com/pion/turn/internal/ipnet"
-
-	"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) {
-		return nil, "", curriedSend(stun.ClassErrorResponse, callingMethod, m.TransactionID,
-			&stun.ErrorCodeAttribute{Code: stun.CodeUnauthorized},
-			stun.NewNonce(buildNonce()),
-			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
-func (s *Server) handleAllocateRequest(srcAddr, dstAddr net.Addr, m *stun.Message) error {
-	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return buildAndSend(s.connection, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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 allocation := s.manager.GetAllocation(fiveTuple); allocation != 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 turn.RequestedTransport
-	if err = requestedTransport.GetFrom(m); err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
-	}
-	if requestedTransport.Protocol != turn.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 turn.ReservationToken
-	if err = reservationTokenAttr.GetFrom(m); err == nil {
-		var evenPort turn.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 turn.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, s.connection, 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)
-	}
-
-	_, relayPort, err := ipnet.AddrIPPort(a.RelayAddr)
-	if err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
-	}
-
-	dstIP, _, err := ipnet.AddrIPPort(dstAddr)
-	if err != nil {
-		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
-	}
-
-	responseAttrs := []stun.Setter{
-		&turn.RelayedAddress{
-			IP:   dstIP,
-			Port: relayPort,
-		},
-		&turn.Lifetime{
-			Duration: lifetimeDuration,
-		},
-		&stun.XORMappedAddress{
-			IP:   srcIP,
-			Port: srcPort,
-		},
-	}
-
-	if reservationToken != "" {
-		s.reservationManager.CreateReservation(reservationToken, relayPort)
-		responseAttrs = append(responseAttrs, turn.ReservationToken([]byte(reservationToken)))
-	}
-
-	return curriedSend(stun.ClassSuccessResponse, stun.MethodAllocate, m.TransactionID, append(responseAttrs, messageIntegrity)...)
-}
-
-func (s *Server) handleRefreshRequest(srcAddr, dstAddr net.Addr, m *stun.Message) error {
-	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return buildAndSend(s.connection, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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,
-		&turn.Lifetime{
-			Duration: lifetimeDuration,
-		},
-		messageIntegrity,
-	)
-}
-
-func (s *Server) handleCreatePermissionRequest(srcAddr, dstAddr net.Addr, m *stun.Message) error {
-	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
-		return buildAndSend(s.connection, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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 turn.PeerAddress
-		if err := peerAddress.GetFrom(m); err != nil {
-			return err
-		}
-
-		a.AddPermission(&allocation.Permission{
-			Addr: &net.UDPAddr{
-				IP:   peerAddress.IP,
-				Port: peerAddress.Port,
-			}})
-		addCount++
-		return nil
-	}); err != nil {
-		addCount = 0
-	}
-
-	respClass := stun.ClassSuccessResponse
-	if addCount == 0 {
-		respClass = stun.ClassErrorResponse
-	}
-
-	return curriedSend(respClass, stun.MethodCreatePermission, m.TransactionID,
-		messageIntegrity)
-}
-
-func (s *Server) handleSendIndication(srcAddr, dstAddr net.Addr, m *stun.Message) error {
-	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 := turn.Data{}
-	if err := dataAttr.GetFrom(m); err != nil {
-		return err
-	}
-
-	peerAddress := turn.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
-}
-
-func (s *Server) handleChannelBindRequest(srcAddr, dstAddr net.Addr, m *stun.Message) error {
-	errorSend := func(err error, attrs ...stun.Setter) error {
-		sendErr := buildAndSend(s.connection, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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 buildAndSend(s.connection, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: m.TransactionID}, stun.NewType(method, class)}, attrs...)...)
-	}, m, stun.MethodChannelBind, s.realm, s.authHandler, srcAddr)
-	if err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	var channel turn.ChannelNumber
-	if err = channel.GetFrom(m); err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	if !channel.Valid() {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	peerAddr := turn.PeerAddress{}
-	if err = peerAddr.GetFrom(m); err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	err = a.AddChannelBind(&allocation.ChannelBind{ID: channel, Peer: &net.UDPAddr{IP: peerAddr.IP, Port: peerAddr.Port}}, s.channelBindTimeout)
-	if err != nil {
-		return errorSend(err, stun.CodeBadRequest)
-	}
-
-	return buildAndSend(s.connection, srcAddr, &stun.Message{TransactionID: m.TransactionID}, stun.BindingSuccess, messageIntegrity)
-}
-
-func (s *Server) handleChannelData(srcAddr, dstAddr net.Addr, c *turn.ChannelData) error {
-	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.GetChannelByID(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 allocationLifeTime(m *stun.Message) time.Duration {
-	lifetimeDuration := turn.DefaultLifetime
-
-	var lifetime turn.Lifetime
-	if err := lifetime.GetFrom(m); err == nil {
-		if lifetime.Duration < maximumLifetime {
-			lifetimeDuration = lifetime.Duration
-		}
-	}
-
-	return lifetimeDuration
-}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..8862887
--- /dev/null
+++ b/server.go
@@ -0,0 +1,368 @@
+package turn
+
+import (
+	"crypto/md5" // #nosec
+	"fmt"
+	"io"
+	"math/rand"
+	"net"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/gortc/turn"
+	"github.com/pion/logging"
+	"github.com/pion/stun"
+	"github.com/pion/transport/vnet"
+	"github.com/pion/turn/internal/allocation"
+	"github.com/pkg/errors"
+)
+
+const (
+	maxStunMessageSize = 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              string
+	AuthHandler        AuthHandler
+	ChannelBindTimeout time.Duration
+	ListeningPort      int
+	LoggerFactory      logging.LoggerFactory
+	Net                *vnet.Net
+}
+
+type listener struct {
+	conn    net.PacketConn
+	closeCh chan struct{}
+}
+
+// Server is an instance of the Pion TURN server
+type Server struct {
+	lock               sync.RWMutex
+	listeners          []*listener
+	listenIPs          []net.IP
+	listenPort         int
+	realm              string
+	authHandler        AuthHandler
+	manager            *allocation.Manager
+	reservationManager *allocation.ReservationManager
+	channelBindTimeout time.Duration
+	log                logging.LeveledLogger
+	net                *vnet.Net
+}
+
+// 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")
+	}
+
+	manager := allocation.NewManager(&allocation.ManagerConfig{
+		LeveledLogger: log,
+		Net:           config.Net,
+	})
+
+	listenPort := config.ListeningPort
+	if listenPort == 0 {
+		listenPort = 3478
+	}
+
+	return &Server{
+		listenPort:         listenPort,
+		realm:              config.Realm,
+		authHandler:        config.AuthHandler,
+		manager:            manager,
+		reservationManager: &allocation.ReservationManager{},
+		channelBindTimeout: config.ChannelBindTimeout,
+		log:                log,
+		net:                config.Net,
+	}
+}
+
+// AddListeningIPAddr adds a listening IP address.
+// 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 ip.IsLinkLocalUnicast() {
+		return fmt.Errorf("link-local unicast address is not allowed")
+	}
+	s.listenIPs = append(s.listenIPs, ip)
+	return nil
+}
+
+// caller must hold the mutex
+func (s *Server) gatherSystemIPAddrs() error {
+	s.log.Debug("gathering local IP address...")
+
+	ifs, err := s.net.Interfaces()
+	if err != nil {
+		return err
+	}
+	for _, ifc := range ifs {
+		if ifc.Flags&net.FlagUp == 0 {
+			continue // skip if interface is not up
+		}
+
+		if ifc.Flags&net.FlagLoopback != 0 {
+			continue // skip loopback address
+		}
+
+		addrs, err := ifc.Addrs()
+		if err != nil {
+			continue
+		}
+
+		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 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())
+			s.listenIPs = append(s.listenIPs, ip)
+		}
+	}
+
+	return 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)
+	if err != nil {
+		return errors.Wrap(err, fmt.Sprintf("Failed to listen on %s", listenAddr))
+	}
+
+	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())
+			}
+		}
+
+		close(closeCh)
+	}()
+
+	return nil
+}
+
+// Start starts the server.
+func (s *Server) Start() error {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	// If s.listenIPs is empty, gather system IPs
+	if len(s.listenIPs) == 0 {
+		err := s.gatherSystemIPAddrs()
+		if err != nil {
+			return err
+		}
+
+		if len(s.listenIPs) == 0 {
+			return fmt.Errorf("no local IP address found")
+		}
+	}
+
+	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 {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	if err := s.manager.Close(); err != nil {
+		return err
+	}
+
+	for _, l := range s.listeners {
+		err := l.conn.Close()
+		if err != nil {
+			s.log.Debugf("Close() returned error: %s", err.Error())
+		}
+
+	}
+
+	for _, l := range s.listeners {
+		<-l.closeCh
+	}
+
+	return nil
+}
+
+// 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 turn.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 {
+	c := turn.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 {
+	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)
+		}
+
+	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)
+}
+
+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
+}
diff --git a/stun.go b/stun.go
new file mode 100644
index 0000000..44297aa
--- /dev/null
+++ b/stun.go
@@ -0,0 +1,24 @@
+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 {
+	ip, port, err := ipnet.AddrIPPort(srcAddr)
+	if err != nil {
+		return err
+	}
+
+	return buildAndSend(conn, srcAddr, &stun.Message{TransactionID: m.TransactionID}, stun.BindingSuccess,
+		&stun.XORMappedAddress{
+			IP:   ip,
+			Port: port,
+		},
+		stun.Fingerprint,
+	)
+}
diff --git a/turn.go b/turn.go
index 9f2a6f5..48593d6 100644
--- a/turn.go
+++ b/turn.go
@@ -1,64 +1,471 @@
 package turn
 
 import (
-	"fmt"
+	"crypto/md5" // #nosec
 	"net"
+	"strings"
 	"time"
 
 	"github.com/gortc/turn"
-	"github.com/pion/turn/internal/client"
-	"github.com/pion/turn/internal/server"
+	"github.com/pion/stun"
+
+	"github.com/pion/turn/internal/allocation"
+	"github.com/pion/turn/internal/ipnet"
+
+	"github.com/pkg/errors"
+)
+
+const (
+	maximumLifetime = time.Hour // https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation
 )
 
-// Server is the interface for Pion TURN server callbacks
-type Server interface {
-	AuthenticateRequest(username string, srcAddr net.Addr) (password string, ok bool)
+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
 }
 
-// Client is the interface for Pion STUN requests
-type Client interface {
-	SendSTUNRequest(serverIP net.IP, serverPort int) (interface{}, error)
+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
 }
 
-// StartArguments are the arguments for the Pion TURN server
-type StartArguments struct {
-	Server             Server
-	Realm              string
-	UDPPort            int
-	ChannelBindTimeout time.Duration
+// 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 {
+	dstAddr := conn.LocalAddr()
+	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
+		return buildAndSend(conn, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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 allocation := s.manager.GetAllocation(fiveTuple); allocation != 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 turn.RequestedTransport
+	if err = requestedTransport.GetFrom(m); err != nil {
+		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
+	}
+	if requestedTransport.Protocol != turn.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 turn.ReservationToken
+	if err = reservationTokenAttr.GetFrom(m); err == nil {
+		var evenPort turn.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 turn.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, 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)
+	}
+
+	_, relayPort, err := ipnet.AddrIPPort(a.RelayAddr)
+	if err != nil {
+		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
+	}
+
+	dstIP, _, err := ipnet.AddrIPPort(dstAddr)
+	if err != nil {
+		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
+	}
+
+	responseAttrs := []stun.Setter{
+		&turn.RelayedAddress{
+			IP:   dstIP,
+			Port: relayPort,
+		},
+		&turn.Lifetime{
+			Duration: lifetimeDuration,
+		},
+		&stun.XORMappedAddress{
+			IP:   srcIP,
+			Port: srcPort,
+		},
+	}
+
+	if reservationToken != "" {
+		s.reservationManager.CreateReservation(reservationToken, relayPort)
+		responseAttrs = append(responseAttrs, turn.ReservationToken([]byte(reservationToken)))
+	}
+
+	return curriedSend(stun.ClassSuccessResponse, stun.MethodAllocate, m.TransactionID, append(responseAttrs, messageIntegrity)...)
 }
 
-// GetChannelBindTimeout is a helper method for StartArguments's default ChannelBindTimeout setting
-func (args StartArguments) GetChannelBindTimeout() time.Duration {
-	if args.ChannelBindTimeout == 0 {
-		return turn.DefaultLifetime
+// caller must hold the mutex
+func (s *Server) handleRefreshRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
+	dstAddr := conn.LocalAddr()
+	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
+		return buildAndSend(conn, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: transactionID}, stun.NewType(method, class)}, attrs...)...)
+	}
+	messageIntegrity, _, err := authenticateRequest(curriedSend, m, stun.MethodCreatePermission, s.realm, s.authHandler, srcAddr)
+	if err != nil {
+		return err
 	}
 
-	return args.ChannelBindTimeout
+	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,
+		&turn.Lifetime{
+			Duration: lifetimeDuration,
+		},
+		messageIntegrity,
+	)
 }
 
-// ClientArguments are the arguments for the Pion client
-type ClientArguments struct {
-	BindingAddress string
-	ServerIP       net.IP
-	ServerPort     int
+// caller must hold the mutex
+func (s *Server) handleCreatePermissionRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
+	dstAddr := conn.LocalAddr()
+	curriedSend := func(class stun.MessageClass, method stun.Method, transactionID [stun.TransactionIDSize]byte, attrs ...stun.Setter) error {
+		return buildAndSend(conn, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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 turn.PeerAddress
+		if err := peerAddress.GetFrom(m); err != nil {
+			return err
+		}
+
+		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)
 }
 
-// Start the Pion TURN server
-func Start(args StartArguments) {
-	fmt.Println(server.NewServer(args.Realm, args.GetChannelBindTimeout(), args.Server.AuthenticateRequest).Listen("0.0.0.0", args.UDPPort))
+// caller must hold the mutex
+func (s *Server) handleSendIndication(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
+	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 := turn.Data{}
+	if err := dataAttr.GetFrom(m); err != nil {
+		return err
+	}
+
+	peerAddress := turn.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
 }
 
-// Create creates the Pion TURN server, but does not call listen
-func Create(args StartArguments) *server.Server {
-	return server.NewServer(args.Realm, args.GetChannelBindTimeout(), args.Server.AuthenticateRequest)
+// caller must hold the mutex
+func (s *Server) handleChannelBindRequest(conn net.PacketConn, srcAddr net.Addr, m *stun.Message) error {
+	dstAddr := conn.LocalAddr()
+	errorSend := func(err error, attrs ...stun.Setter) error {
+		sendErr := buildAndSend(conn, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: 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 buildAndSend(conn, srcAddr, append([]stun.Setter{&stun.Message{TransactionID: m.TransactionID}, stun.NewType(method, class)}, attrs...)...)
+	}, m, stun.MethodChannelBind, s.realm, s.authHandler, srcAddr)
+	if err != nil {
+		return errorSend(err, stun.CodeBadRequest)
+	}
+
+	var channel turn.ChannelNumber
+	if err = channel.GetFrom(m); err != nil {
+		return errorSend(err, stun.CodeBadRequest)
+	}
+
+	peerAddr := turn.PeerAddress{}
+	if err = peerAddr.GetFrom(m); err != nil {
+		return errorSend(err, stun.CodeBadRequest)
+	}
+
+	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 buildAndSend(conn, srcAddr, &stun.Message{TransactionID: m.TransactionID}, stun.BindingSuccess, messageIntegrity)
 }
 
-// StartClient starts a Pion client
-func StartClient(args ClientArguments) (interface{}, error) {
-	c, err := client.NewClient(args.BindingAddress)
+func (s *Server) handleChannelData(conn net.PacketConn, srcAddr net.Addr, c *turn.ChannelData) error {
+	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.GetChannelByID(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 nil, err
+		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 c.SendSTUNRequest(args.ServerIP, args.ServerPort)
+
+	return nil
+}
+
+func allocationLifeTime(m *stun.Message) time.Duration {
+	lifetimeDuration := turn.DefaultLifetime
+
+	var lifetime turn.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
new file mode 100644
index 0000000..3f4fa10
--- /dev/null
+++ b/client_test.go
@@ -0,0 +1,77 @@
+package turn
+
+import (
+	"net"
+	"testing"
+
+	"github.com/pion/logging"
+)
+
+func TestClient(t *testing.T) {
+	loggerFactory := logging.NewDefaultLoggerFactory()
+
+	t.Run("SendSTUNRequest Parallel", func(t *testing.T) {
+		c, err := NewClient(&ClientConfig{
+			ListeningAddress: "0.0.0.0:0",
+			LoggerFactory:    loggerFactory,
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// simple channel fo go routine start signaling
+		started := make(chan struct{})
+		finished := make(chan struct{})
+		var err1 error
+		var resp1 interface{}
+
+		// stun1.l.google.com:19302, more at https://gist.github.com/zziuni/3741933#file-stuns-L5
+		go func() {
+			close(started)
+			resp1, err1 = c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302)
+			close(finished)
+		}()
+
+		// block until go routine is started to make two almost parallel requests
+
+		<-started
+
+		resp2, err2 := c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302)
+		if err2 != nil {
+			t.Fatal(err)
+		} else {
+			t.Log(resp2)
+		}
+
+		<-finished
+		if err1 != nil {
+			t.Fatal(err)
+		} else {
+			t.Log(resp1)
+		}
+	})
+
+	t.Run("Listen error", func(t *testing.T) {
+		_, err := NewClient(&ClientConfig{
+			ListeningAddress: "255.255.255.256:65535",
+			LoggerFactory:    loggerFactory,
+		})
+		if err == nil {
+			t.Fatal("listening on 255.255.255.256:65535 should fail")
+		}
+	})
+
+	/*
+		// Unable to perform this test atm because there is no timeout and the test may run infinitely
+		t.Run("SendSTUNRequest timeout", func(t *testing.T) {
+			c, err := NewClient("0.0.0.0:0")
+			if err != nil {
+				t.Fatal(err)
+			}
+			_, err = c.SendSTUNRequest(net.IPv4(255, 255, 255, 255), 65535)
+			if err == nil {
+				t.Fatal("request to 255.255.255.255:65535 should fail")
+			}
+		})
+	*/
+}
diff --git a/internal/allocation/allocation_manager_test.go b/internal/allocation/allocation_manager_test.go
index 53377ab..bc1fb26 100644
--- a/internal/allocation/allocation_manager_test.go
+++ b/internal/allocation/allocation_manager_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/gortc/turn"
+	"github.com/pion/logging"
 	"github.com/pion/turn/internal/ipnet"
 )
 
@@ -40,7 +41,9 @@ func TestAllocation(t *testing.T) {
 
 // test invalid Allocation creations
 func subTestCreateInvalidAllocation(t *testing.T, turnSocket ipnet.PacketConn) {
-	m := &Manager{}
+	loggerFactory := logging.NewDefaultLoggerFactory()
+	config := &ManagerConfig{LeveledLogger: loggerFactory.NewLogger("test")}
+	m := NewManager(config)
 	if a, err := m.CreateAllocation(nil, turnSocket, 0, turn.DefaultLifetime); a != nil || err == nil {
 		t.Errorf("Illegally created allocation with nil FiveTuple")
 	}
@@ -54,7 +57,9 @@ func subTestCreateInvalidAllocation(t *testing.T, turnSocket ipnet.PacketConn) {
 
 // test valid Allocation creations
 func subTestCreateAllocation(t *testing.T, turnSocket ipnet.PacketConn) {
-	m := &Manager{}
+	loggerFactory := logging.NewDefaultLoggerFactory()
+	config := &ManagerConfig{LeveledLogger: loggerFactory.NewLogger("test")}
+	m := NewManager(config)
 	fiveTuple := randomFiveTuple()
 	if a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, turn.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
@@ -67,7 +72,9 @@ func subTestCreateAllocation(t *testing.T, turnSocket ipnet.PacketConn) {
 
 // test that two allocations can't be created with the same FiveTuple
 func subTestCreateAllocationDuplicateFiveTuple(t *testing.T, turnSocket ipnet.PacketConn) {
-	m := &Manager{}
+	loggerFactory := logging.NewDefaultLoggerFactory()
+	config := &ManagerConfig{LeveledLogger: loggerFactory.NewLogger("test")}
+	m := NewManager(config)
 	fiveTuple := randomFiveTuple()
 	if a, err := m.CreateAllocation(fiveTuple, turnSocket, 0, turn.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
deleted file mode 100644
index a8190e7..0000000
--- a/internal/client/client_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package client
-
-import (
-	"net"
-	"testing"
-)
-
-func TestClient_SendSTUNRequest_Parallel(t *testing.T) {
-	c, err := NewClient("0.0.0.0:0")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// simple channel fo go routine start signaling
-	started := make(chan struct{})
-	finished := make(chan struct{})
-	var err1 error
-	var resp1 interface{}
-
-	// stun1.l.google.com:19302, more at https://gist.github.com/zziuni/3741933#file-stuns-L5
-	go func() {
-		close(started)
-		resp1, err1 = c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302)
-		close(finished)
-	}()
-
-	// block until go routine is started to make two almost parallel requests
-
-	<-started
-
-	resp2, err2 := c.SendSTUNRequest(net.IPv4(74, 125, 143, 127), 19302)
-	if err2 != nil {
-		t.Fatal(err)
-	} else {
-		t.Log(resp2)
-	}
-
-	<-finished
-	if err1 != nil {
-		t.Fatal(err)
-	} else {
-		t.Log(resp1)
-	}
-
-}
-
-func TestNewClient_Errors(t *testing.T) {
-
-	_, err := NewClient("255.255.255.256:65535")
-	if err == nil {
-		t.Fatal("listening on 255.255.255.256:65535 should fail")
-	}
-
-	// Unable to perform this test atm because there is no timeout and the test may run infinitely
-	//c, err := NewClient("0.0.0.0:0")
-	//if err != nil {
-	//	t.Fatal(err)
-	//}
-	//_, err = c.SendSTUNRequest(net.IPv4(255, 255, 255, 255), 65535)
-	//if err == nil {
-	//	t.Fatal("request to 255.255.255.255:65535 should fail")
-	//}
-}
diff --git a/server_test.go b/server_test.go
new file mode 100644
index 0000000..471ee02
--- /dev/null
+++ b/server_test.go
@@ -0,0 +1,71 @@
+package turn
+
+import (
+	"net"
+	"testing"
+	"time"
+
+	"github.com/pion/logging"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestServer(t *testing.T) {
+	loggerFactory := logging.NewDefaultLoggerFactory()
+	log := loggerFactory.NewLogger("test")
+
+	t.Run("simple", func(t *testing.T) {
+		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",
+			LoggerFactory: loggerFactory,
+		})
+
+		err := server.AddListeningIPAddr("127.0.0.1")
+		assert.NoError(t, err, "should succeed")
+
+		doneCh := make(chan struct{})
+
+		go func() {
+			log.Debug("start listening...")
+			err2 := server.Start()
+			if err2 != nil {
+				t.Logf("Start returned with err: %v", err2)
+			}
+			close(doneCh)
+		}()
+
+		// make sure the server is listening before running
+		// the client.
+		time.Sleep(100 * time.Microsecond)
+
+		log.Debug("creating a client.")
+		client, err := NewClient(&ClientConfig{
+			ListeningAddress: "0.0.0.0:0",
+			LoggerFactory:    loggerFactory,
+		})
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+
+		log.Debug("sending a binding request.")
+		resp, err := client.SendSTUNRequest(net.IPv4(127, 0, 0, 1), 3478)
+		assert.NoError(t, err, "should succeed")
+		t.Logf("resp: %v", resp)
+
+		log.Debug("now closing the server...")
+
+		// Close server
+		err = server.Close()
+		assert.NoError(t, err, "should succeed")
+		log.Debug("wainting the server to terminate...")
+		<-doneCh
+	})
+}
diff --git a/server_vnet_test.go b/server_vnet_test.go
new file mode 100644
index 0000000..7efe6ef
--- /dev/null
+++ b/server_vnet_test.go
@@ -0,0 +1,137 @@
+package turn
+
+import (
+	"net"
+	"testing"
+	"time"
+
+	"github.com/pion/logging"
+	"github.com/pion/transport/vnet"
+	"github.com/stretchr/testify/assert"
+)
+
+func buildVNet() (*vnet.Router, *vnet.Net, *vnet.Net, error) {
+	loggerFactory := logging.NewDefaultLoggerFactory()
+
+	// WAN
+	wan, err := vnet.NewRouter(&vnet.RouterConfig{
+		CIDR:          "0.0.0.0/0",
+		LoggerFactory: loggerFactory,
+	})
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	wanNet := vnet.NewNet(&vnet.NetConfig{
+		StaticIP: "1.2.3.4", // will be assigned to eth0
+	})
+
+	err = wan.AddNet(wanNet)
+	if err != nil {
+		return nil, nil, 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, nil, nil, err
+	}
+
+	lanNet := vnet.NewNet(&vnet.NetConfig{})
+	err = lan.AddNet(lanNet)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	err = wan.AddRouter(lan)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	err = wan.Start()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	return wan, wanNet, lanNet, nil
+}
+
+func TestServerVNet(t *testing.T) {
+	loggerFactory := logging.NewDefaultLoggerFactory()
+	log := loggerFactory.NewLogger("test")
+
+	t.Run("simple", func(t *testing.T) {
+		wan, wanNet, lanNet, err := buildVNet()
+		assert.NoError(t, err, "should succeed")
+		defer wan.Stop() // nolint:errcheck
+
+		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:           wanNet,
+			LoggerFactory: loggerFactory,
+		})
+
+		err = server.AddListeningIPAddr("1.2.3.4")
+		assert.NoError(t, err, "should succeed")
+
+		doneCh := make(chan struct{})
+
+		go func() {
+			log.Debug("start listening...")
+			err2 := server.Start()
+			if err2 != nil {
+				t.Logf("Start returned with err: %v", err2)
+			}
+			close(doneCh)
+		}()
+
+		// make sure the server is listening before running
+		// the client.
+		time.Sleep(100 * time.Microsecond)
+
+		log.Debug("creating a client.")
+		client, err := NewClient(&ClientConfig{
+			ListeningAddress: "0.0.0.0:0",
+			Net:              lanNet,
+			LoggerFactory:    loggerFactory,
+		})
+		if !assert.NoError(t, err, "should succeed") {
+			return
+		}
+
+		log.Debug("sending a binding request.")
+		reflAddr, err := client.SendSTUNRequest(net.IPv4(1, 2, 3, 4), 3478)
+		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")
+
+		// Close server
+		err = server.Close()
+		assert.NoError(t, err, "should succeed")
+
+		<-doneCh
+	})
+}
diff --git a/internal/server/turn_test.go b/turn_test.go
similarity index 98%
rename from internal/server/turn_test.go
rename to turn_test.go
index d1e043e..bdc8bbc 100644
--- a/internal/server/turn_test.go
+++ b/turn_test.go
@@ -1,4 +1,4 @@
-package server
+package turn
 
 import (
 	"testing"