Repository: pion/turn
Pull request #83 (2019-07-18 12:06:19)

Move protocol implementation to pion/turn repo
I've copied all protocol implementation from gortc/turn package to
the internal/proto. This should reduce coupling.

codecov-io: # [Codecov](https://codecov.io/gh/pion/turn/pull/83?src=pr&el=h1) Report
> Merging [#83](https://codecov.io/gh/pion/turn/pull/83?src=pr&el=desc) into [master](https://codecov.io/gh/pion/turn/commit/49817164c4a059cf7dbccfa14f5414768621f387?src=pr&el=desc) will **increase** coverage by `5.3%`.
> The diff coverage is `98.31%`.

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

```diff
@@            Coverage Diff            @@
##           master      #83     +/-   ##
=========================================
+ Coverage   59.32%   64.63%   +5.3%     
=========================================
  Files          22       35     +13     
  Lines        1699     1954    +255     
=========================================
+ Hits         1008     1263    +255     
  Misses        561      561             
  Partials      130      130
```


| [Impacted Files](https://codecov.io/gh/pion/turn/pull/83?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/proto/relayedaddr.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vcmVsYXllZGFkZHIuZ28=) | `100% <100%> (ø)` | |
| [internal/allocation/allocation.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYWxsb2NhdGlvbi9hbGxvY2F0aW9uLmdv) | `83.07% <100%> (ø)` | :arrow_up: |
| [internal/proto/lifetime.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vbGlmZXRpbWUuZ28=) | `100% <100%> (ø)` | |
| [internal/proto/dontfrag.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vZG9udGZyYWcuZ28=) | `100% <100%> (ø)` | |
| [internal/proto/peeraddr.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vcGVlcmFkZHIuZ28=) | `100% <100%> (ø)` | |
| [internal/proto/data.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vZGF0YS5nbw==) | `100% <100%> (ø)` | |
| [internal/proto/reqfamily.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vcmVxZmFtaWx5Lmdv) | `100% <100%> (ø)` | |
| [internal/proto/reqtrans.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vcmVxdHJhbnMuZ28=) | `100% <100%> (ø)` | |
| [internal/proto/chann.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vY2hhbm4uZ28=) | `100% <100%> (ø)` | |
| [internal/proto/rsrvtoken.go](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG8vcnNydnRva2VuLmdv) | `100% <100%> (ø)` | |
| ... and [22 more](https://codecov.io/gh/pion/turn/pull/83/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/.github/lint-disallowed-functions-in-library.sh b/.github/lint-disallowed-functions-in-library.sh
index f316c18..27060a1 100644
--- a/.github/lint-disallowed-functions-in-library.sh
+++ b/.github/lint-disallowed-functions-in-library.sh
@@ -3,7 +3,7 @@ set -e
 
 # Disallow usages of functions that cause the program to exit in the library code
 SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
-EXCLUDE_DIRECTORIES="--exclude-dir=examples --exclude-dir=.git --exclude-dir=.github "
+EXCLUDE_DIRECTORIES="--exclude-dir=examples --exclude-dir=.git --exclude-dir=.github --exclude=fuzz.go"
 DISALLOWED_FUNCTIONS=('os.Exit(' 'panic(' 'Fatal(' 'Fatalf(' 'Fatalln(')
 
 
diff --git a/.golangci.yml b/.golangci.yml
index 17c3de6..08b4eca 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -12,4 +12,15 @@ linters:
 issues:
   exclude-use-default: false
   max-per-linter: 0
-  max-same-issues: 50
\ No newline at end of file
+  max-same-issues: 50
+
+  exclude-rules:
+    - path: internal/proto/.+_test
+      linters:
+        - scopelint
+        - gosec
+        - errcheck
+        - prealloc
+    - path: internal/proto
+      linters:
+        - gochecknoglobals
diff --git a/README.md b/README.md
index a3a5b9c..c2aa9f2 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,7 @@ Check out the [CONTRIBUTING.md](CONTRIBUTING.md) to join the group of amazing pe
 * [songjiayang](https://github.com/songjiayang) - *SongJiaYang*
 * [Yutaka Takeda](https://github.com/enobufs) - *vnet*
 * [namreg](https://github.com/namreg) - *Igor German*
+* [Aleksandr Razumov](https://github.com/ernado) - *protocol*
 
 ### License
 MIT License - see [LICENSE.md](LICENSE.md) for full text
diff --git a/client.go b/client.go
index 2a5bd83..81b32d4 100644
--- a/client.go
+++ b/client.go
@@ -8,11 +8,11 @@ import (
 	"sync"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/logging"
 	"github.com/pion/stun"
 	"github.com/pion/transport/vnet"
 	"github.com/pion/turn/internal/client"
+	"github.com/pion/turn/internal/proto"
 	"github.com/pkg/errors"
 )
 
@@ -241,7 +241,7 @@ func (c *Client) Allocate() (net.PacketConn, error) {
 	msg, err := stun.Build(
 		stun.TransactionID,
 		stun.NewType(stun.MethodAllocate, stun.ClassRequest),
-		turn.RequestedTransportUDP,
+		proto.RequestedTransportUDP,
 		stun.Fingerprint,
 	)
 	if err != nil {
@@ -271,7 +271,7 @@ func (c *Client) Allocate() (net.PacketConn, error) {
 	msg, err = stun.Build(
 		stun.TransactionID,
 		stun.NewType(stun.MethodAllocate, stun.ClassRequest),
-		turn.RequestedTransportUDP,
+		proto.RequestedTransportUDP,
 		&c.username,
 		&c.realm,
 		&nonce,
@@ -297,7 +297,7 @@ func (c *Client) Allocate() (net.PacketConn, error) {
 	}
 
 	// Getting relayed addresses from response.
-	var relayed turn.RelayedAddress
+	var relayed proto.RelayedAddress
 	if err := relayed.GetFrom(res); err != nil {
 		return nil, err
 	}
@@ -307,7 +307,7 @@ func (c *Client) Allocate() (net.PacketConn, error) {
 	}
 
 	// Getting lifetime from response
-	var lifetime turn.Lifetime
+	var lifetime proto.Lifetime
 	if err := lifetime.GetFrom(res); err != nil {
 		return nil, err
 	}
@@ -400,7 +400,7 @@ func (c *Client) HandleInbound(data []byte, from net.Addr) (bool, error) {
 		return true, c.handleSTUNMessage(data, from)
 	case len(c.turnServStr) != 0 && from.String() == c.turnServStr:
 		// received from TURN server
-		if turn.IsChannelData(data) {
+		if proto.IsChannelData(data) {
 			return true, c.handleChannelData(data)
 		}
 		return true, fmt.Errorf("unexpected packet from TURN server")
@@ -430,7 +430,7 @@ func (c *Client) handleSTUNMessage(data []byte, from net.Addr) error {
 
 	if msg.Type.Class == stun.ClassIndication {
 		if msg.Type.Method == stun.MethodData {
-			var peerAddr turn.PeerAddress
+			var peerAddr proto.PeerAddress
 			if err := peerAddr.GetFrom(msg); err != nil {
 				return err
 			}
@@ -439,7 +439,7 @@ func (c *Client) handleSTUNMessage(data []byte, from net.Addr) error {
 				Port: peerAddr.Port,
 			}
 
-			var data turn.Data
+			var data proto.Data
 			if err := data.GetFrom(msg); err != nil {
 				return err
 			}
@@ -486,7 +486,7 @@ func (c *Client) handleSTUNMessage(data []byte, from net.Addr) error {
 }
 
 func (c *Client) handleChannelData(data []byte) error {
-	chData := &turn.ChannelData{
+	chData := &proto.ChannelData{
 		Raw: make([]byte, len(data)),
 	}
 	copy(chData.Raw, data)
diff --git a/go.mod b/go.mod
index b2dd817..9e07a57 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,9 @@ go 1.12
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/gortc/turn v0.8.0
 	github.com/pion/logging v0.2.2
 	github.com/pion/stun v0.3.1
 	github.com/pion/transport v0.8.6
 	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 33786fa..418417a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,6 @@
 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/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -18,8 +16,3 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 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=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/allocation/allocation.go b/internal/allocation/allocation.go
index a58fea1..acd464d 100644
--- a/internal/allocation/allocation.go
+++ b/internal/allocation/allocation.go
@@ -6,10 +6,10 @@ import (
 	"sync"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/logging"
 	"github.com/pion/stun"
 	"github.com/pion/turn/internal/ipnet"
+	"github.com/pion/turn/internal/proto"
 	"github.com/pkg/errors"
 )
 
@@ -121,7 +121,7 @@ func (a *Allocation) AddChannelBind(c *ChannelBind, lifetime time.Duration) erro
 }
 
 // RemoveChannelBind removes the ChannelBind from this allocation by id
-func (a *Allocation) RemoveChannelBind(number turn.ChannelNumber) bool {
+func (a *Allocation) RemoveChannelBind(number proto.ChannelNumber) bool {
 	a.channelBindingsLock.Lock()
 	defer a.channelBindingsLock.Unlock()
 
@@ -136,7 +136,7 @@ func (a *Allocation) RemoveChannelBind(number turn.ChannelNumber) bool {
 }
 
 // GetChannelByNumber gets the ChannelBind from this allocation by id
-func (a *Allocation) GetChannelByNumber(number turn.ChannelNumber) *ChannelBind {
+func (a *Allocation) GetChannelByNumber(number proto.ChannelNumber) *ChannelBind {
 	a.channelBindingsLock.RLock()
 	defer a.channelBindingsLock.RUnlock()
 	for _, cb := range a.channelBindings {
@@ -246,8 +246,8 @@ func (a *Allocation) packetHandler(m *Manager) {
 			}
 		} else if p := a.GetPermission(srcAddr); p != nil {
 			udpAddr := srcAddr.(*net.UDPAddr)
-			peerAddressAttr := turn.PeerAddress{IP: udpAddr.IP, Port: udpAddr.Port}
-			dataAttr := turn.Data(buffer[:n])
+			peerAddressAttr := proto.PeerAddress{IP: udpAddr.IP, Port: udpAddr.Port}
+			dataAttr := proto.Data(buffer[:n])
 
 			msg, err := stun.Build(stun.TransactionID, stun.NewType(stun.MethodData, stun.ClassIndication), peerAddressAttr, dataAttr)
 			if err != nil {
diff --git a/internal/allocation/channel_bind.go b/internal/allocation/channel_bind.go
index 5fe4722..f01c5f5 100644
--- a/internal/allocation/channel_bind.go
+++ b/internal/allocation/channel_bind.go
@@ -4,15 +4,15 @@ import (
 	"net"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/logging"
+	"github.com/pion/turn/internal/proto"
 )
 
 // ChannelBind represents a TURN Channel
 // https://tools.ietf.org/html/rfc5766#section-2.5
 type ChannelBind struct {
 	Peer   net.Addr
-	Number turn.ChannelNumber
+	Number proto.ChannelNumber
 
 	allocation    *Allocation
 	lifetimeTimer *time.Timer
@@ -20,7 +20,7 @@ type ChannelBind struct {
 }
 
 // NewChannelBind creates a new ChannelBind
-func NewChannelBind(number turn.ChannelNumber, peer net.Addr, log logging.LeveledLogger) *ChannelBind {
+func NewChannelBind(number proto.ChannelNumber, peer net.Addr, log logging.LeveledLogger) *ChannelBind {
 	return &ChannelBind{
 		Number: number,
 		Peer:   peer,
diff --git a/internal/client/conn.go b/internal/client/conn.go
index 24e6912..a3628b6 100644
--- a/internal/client/conn.go
+++ b/internal/client/conn.go
@@ -8,9 +8,9 @@ import (
 	"sync"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/logging"
 	"github.com/pion/stun"
+	"github.com/pion/turn/internal/proto"
 )
 
 const (
@@ -236,7 +236,7 @@ func (c *UDPConn) WriteTo(p []byte, addr net.Addr) (int, error) {
 		msg, err = stun.Build(
 			stun.TransactionID,
 			stun.NewType(stun.MethodSend, stun.ClassIndication),
-			turn.Data(p),
+			proto.Data(p),
 			peerAddr,
 			stun.Fingerprint,
 		)
@@ -342,8 +342,8 @@ func (c *UDPConn) SetWriteDeadline(t time.Time) error {
 	return nil
 }
 
-func addr2PeerAddress(addr net.Addr) turn.PeerAddress {
-	var peerAddr turn.PeerAddress
+func addr2PeerAddress(addr net.Addr) proto.PeerAddress {
+	var peerAddr proto.PeerAddress
 	switch a := addr.(type) {
 	case *net.UDPAddr:
 		peerAddr.IP = a.IP
@@ -425,7 +425,7 @@ func (c *UDPConn) refreshAllocation(lifetime time.Duration, dontWait bool) error
 	msg, err := stun.Build(
 		stun.TransactionID,
 		stun.NewType(stun.MethodRefresh, stun.ClassRequest),
-		turn.Lifetime{Duration: lifetime},
+		proto.Lifetime{Duration: lifetime},
 		c.obs.Username(),
 		c.obs.Realm(),
 		c.nonce(),
@@ -472,7 +472,7 @@ func (c *UDPConn) refreshAllocation(lifetime time.Duration, dontWait bool) error
 	}
 
 	// Getting lifetime from response
-	var updatedLifetime turn.Lifetime
+	var updatedLifetime proto.Lifetime
 	if err := updatedLifetime.GetFrom(res); err != nil {
 		return fmt.Errorf("failed to get lifetime from refresh response: %s", err.Error())
 	}
@@ -502,7 +502,7 @@ func (c *UDPConn) bind(b *binding) error {
 		stun.TransactionID,
 		stun.NewType(stun.MethodChannelBind, stun.ClassRequest),
 		addr2PeerAddress(b.addr),
-		turn.ChannelNumber(b.number),
+		proto.ChannelNumber(b.number),
 		c.obs.Username(),
 		c.obs.Realm(),
 		c.nonce(),
@@ -533,9 +533,9 @@ func (c *UDPConn) bind(b *binding) error {
 }
 
 func (c *UDPConn) sendChannelData(data []byte, chNum uint16) (int, error) {
-	chData := &turn.ChannelData{
+	chData := &proto.ChannelData{
 		Data:   data,
-		Number: turn.ChannelNumber(chNum),
+		Number: proto.ChannelNumber(chNum),
 	}
 	chData.Encode()
 	return c.obs.WriteTo(chData.Raw, c.obs.TURNServerAddr())
diff --git a/internal/proto/addr.go b/internal/proto/addr.go
new file mode 100644
index 0000000..b1d654d
--- /dev/null
+++ b/internal/proto/addr.go
@@ -0,0 +1,65 @@
+package proto
+
+import (
+	"fmt"
+	"net"
+)
+
+// Addr is ip:port.
+type Addr struct {
+	IP   net.IP
+	Port int
+}
+
+// Network implements net.Addr.
+func (Addr) Network() string { return "turn" }
+
+// FromUDPAddr sets addr to UDPAddr.
+func (a *Addr) FromUDPAddr(n *net.UDPAddr) {
+	a.IP = n.IP
+	a.Port = n.Port
+}
+
+// Equal returns true if b == a.
+func (a Addr) Equal(b Addr) bool {
+	if a.Port != b.Port {
+		return false
+	}
+	return a.IP.Equal(b.IP)
+}
+
+// EqualIP returns true if a and b have equal IP addresses.
+func (a Addr) EqualIP(b Addr) bool {
+	return a.IP.Equal(b.IP)
+}
+
+func (a Addr) String() string {
+	return fmt.Sprintf("%s:%d", a.IP, a.Port)
+}
+
+// FiveTuple represents 5-TUPLE value.
+type FiveTuple struct {
+	Client Addr
+	Server Addr
+	Proto  Protocol
+}
+
+func (t FiveTuple) String() string {
+	return fmt.Sprintf("%s->%s (%s)",
+		t.Client, t.Server, t.Proto,
+	)
+}
+
+// Equal returns true if b == t.
+func (t FiveTuple) Equal(b FiveTuple) bool {
+	if t.Proto != b.Proto {
+		return false
+	}
+	if !t.Client.Equal(b.Client) {
+		return false
+	}
+	if !t.Server.Equal(b.Server) {
+		return false
+	}
+	return true
+}
diff --git a/internal/proto/chandata.go b/internal/proto/chandata.go
new file mode 100644
index 0000000..69ab77b
--- /dev/null
+++ b/internal/proto/chandata.go
@@ -0,0 +1,138 @@
+package proto
+
+import (
+	"bytes"
+	"errors"
+	"io"
+)
+
+// ChannelData represents The ChannelData Message.
+//
+// See RFC 5766 Section 11.4
+type ChannelData struct {
+	Data    []byte // can be subslice of Raw
+	Length  int    // ignored while encoding, len(Data) is used
+	Padding bool   // use  padding
+	Number  ChannelNumber
+	Raw     []byte
+}
+
+// Equal returns true if b == c.
+func (c *ChannelData) Equal(b *ChannelData) bool {
+	if c == nil && b == nil {
+		return true
+	}
+	if c == nil || b == nil {
+		return false
+	}
+	if c.Number != b.Number {
+		return false
+	}
+	if len(c.Data) != len(b.Data) {
+		return false
+	}
+	return bytes.Equal(c.Data, b.Data)
+}
+
+// grow ensures that internal buffer will fit v more bytes and
+// increases it capacity if necessary.
+//
+// Similar to stun.Message.grow method.
+func (c *ChannelData) grow(v int) {
+	n := len(c.Raw) + v
+	for cap(c.Raw) < n {
+		c.Raw = append(c.Raw, 0)
+	}
+	c.Raw = c.Raw[:n]
+}
+
+// Reset resets Length, Data and Raw length.
+func (c *ChannelData) Reset() {
+	c.Raw = c.Raw[:0]
+	c.Length = 0
+	c.Data = c.Data[:0]
+}
+
+// Encode encodes ChannelData Message to Raw.
+func (c *ChannelData) Encode() {
+	c.Raw = c.Raw[:0]
+	c.WriteHeader()
+	c.Raw = append(c.Raw, c.Data...)
+	if !c.Padding {
+		return
+	}
+	padded := nearestPaddedValueLength(len(c.Raw))
+	if bytesToAdd := padded - len(c.Raw); bytesToAdd > 0 {
+		for i := 0; i < bytesToAdd; i++ {
+			c.Raw = append(c.Raw, 0)
+		}
+	}
+}
+
+const padding = 4
+
+func nearestPaddedValueLength(l int) int {
+	n := padding * (l / padding)
+	if n < l {
+		n += padding
+	}
+	return n
+}
+
+// WriteHeader writes channel number and length.
+func (c *ChannelData) WriteHeader() {
+	if len(c.Raw) < channelDataHeaderSize {
+		// Making WriteHeader call valid even when c.Raw
+		// is nil or len(c.Raw) is less than needed for header.
+		c.grow(channelDataHeaderSize)
+	}
+	// Early bounds check to guarantee safety of writes below.
+	_ = c.Raw[:channelDataHeaderSize]
+	bin.PutUint16(c.Raw[:channelDataNumberSize], uint16(c.Number))
+	bin.PutUint16(c.Raw[channelDataNumberSize:channelDataHeaderSize],
+		uint16(len(c.Data)),
+	)
+}
+
+// ErrBadChannelDataLength means that channel data length is not equal
+// to actual data length.
+var ErrBadChannelDataLength = errors.New("channelData length != len(Data)")
+
+// Decode decodes The ChannelData Message from Raw.
+func (c *ChannelData) Decode() error {
+	buf := c.Raw
+	if len(buf) < channelDataHeaderSize {
+		return io.ErrUnexpectedEOF
+	}
+	num := bin.Uint16(buf[:channelDataNumberSize])
+	c.Number = ChannelNumber(num)
+	l := bin.Uint16(buf[channelDataNumberSize:channelDataHeaderSize])
+	c.Data = buf[channelDataHeaderSize:]
+	c.Length = int(l)
+	if !c.Number.Valid() {
+		return ErrInvalidChannelNumber
+	}
+	if int(l) < len(c.Data) {
+		c.Data = c.Data[:int(l)]
+	}
+	if int(l) > len(buf[channelDataHeaderSize:]) {
+		return ErrBadChannelDataLength
+	}
+	return nil
+}
+
+const (
+	channelDataLengthSize = 2
+	channelDataNumberSize = channelDataLengthSize
+	channelDataHeaderSize = channelDataLengthSize + channelDataNumberSize
+)
+
+// IsChannelData returns true if buf looks like the ChannelData Message.
+func IsChannelData(buf []byte) bool {
+	if len(buf) < channelDataHeaderSize {
+		return false
+	}
+	// Quick check for channel number.
+	num := bin.Uint16(buf[0:channelNumberSize])
+	return isChannelNumberValid(num)
+}
diff --git a/internal/proto/chann.go b/internal/proto/chann.go
new file mode 100644
index 0000000..0bf8d6c
--- /dev/null
+++ b/internal/proto/chann.go
@@ -0,0 +1,66 @@
+package proto
+
+import (
+	"errors"
+	"strconv"
+
+	"github.com/pion/stun"
+)
+
+// ChannelNumber represents CHANNEL-NUMBER attribute.
+//
+// The CHANNEL-NUMBER attribute contains the number of the channel.
+//
+// RFC 5766 Section 14.1
+type ChannelNumber uint16 // encoded as uint16
+
+func (n ChannelNumber) String() string { return strconv.Itoa(int(n)) }
+
+// 16 bits of uint + 16 bits of RFFU = 0.
+const channelNumberSize = 4
+
+// AddTo adds CHANNEL-NUMBER to message.
+func (n ChannelNumber) AddTo(m *stun.Message) error {
+	v := make([]byte, channelNumberSize)
+	bin.PutUint16(v[:2], uint16(n))
+	// v[2:4] are zeroes (RFFU = 0)
+	m.Add(stun.AttrChannelNumber, v)
+	return nil
+}
+
+// GetFrom decodes CHANNEL-NUMBER from message.
+func (n *ChannelNumber) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrChannelNumber)
+	if err != nil {
+		return err
+	}
+	if err = stun.CheckSize(stun.AttrChannelNumber, len(v), channelNumberSize); err != nil {
+		return err
+	}
+	_ = v[channelNumberSize-1] // asserting length
+	*n = ChannelNumber(bin.Uint16(v[:2]))
+	// v[2:4] is RFFU and equals to 0.
+	return nil
+}
+
+// See https://tools.ietf.org/html/rfc5766#section-11:
+//
+// 0x4000 through 0x7FFF: These values are the allowed channel
+// numbers (16,383 possible values).
+const (
+	MinChannelNumber = 0x4000
+	MaxChannelNumber = 0x7FFF
+)
+
+// ErrInvalidChannelNumber means that channel number is not valid as by RFC 5766 Section 11.
+var ErrInvalidChannelNumber = errors.New("channel number not in [0x4000, 0x7FFF]")
+
+// isChannelNumberValid returns true if c in [0x4000, 0x7FFF].
+func isChannelNumberValid(c uint16) bool {
+	return c >= MinChannelNumber && c <= MaxChannelNumber
+}
+
+// Valid returns true if channel number has correct value that complies RFC 5766 Section 11 range.
+func (n ChannelNumber) Valid() bool {
+	return isChannelNumberValid(uint16(n))
+}
diff --git a/internal/proto/data.go b/internal/proto/data.go
new file mode 100644
index 0000000..64243e0
--- /dev/null
+++ b/internal/proto/data.go
@@ -0,0 +1,30 @@
+package proto
+
+import "github.com/pion/stun"
+
+// Data represents DATA attribute.
+//
+// The DATA attribute is present in all Send and Data indications.  The
+// value portion of this attribute is variable length and consists of
+// the application data (that is, the data that would immediately follow
+// the UDP header if the data was been sent directly between the client
+// and the peer).
+//
+// RFC 5766 Section 14.4
+type Data []byte
+
+// AddTo adds DATA to message.
+func (d Data) AddTo(m *stun.Message) error {
+	m.Add(stun.AttrData, d)
+	return nil
+}
+
+// GetFrom decodes DATA from message.
+func (d *Data) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrData)
+	if err != nil {
+		return err
+	}
+	*d = v
+	return nil
+}
diff --git a/internal/proto/dontfrag.go b/internal/proto/dontfrag.go
new file mode 100644
index 0000000..d9e0bf4
--- /dev/null
+++ b/internal/proto/dontfrag.go
@@ -0,0 +1,21 @@
+package proto
+
+import "github.com/pion/stun"
+
+// DontFragmentAttr represents DONT-FRAGMENT attribute.
+type DontFragmentAttr struct{}
+
+// AddTo adds DONT-FRAGMENT attribute to message.
+func (DontFragmentAttr) AddTo(m *stun.Message) error {
+	m.Add(stun.AttrDontFragment, nil)
+	return nil
+}
+
+// IsSet returns true if DONT-FRAGMENT attribute is set.
+func (DontFragmentAttr) IsSet(m *stun.Message) bool {
+	_, err := m.Get(stun.AttrDontFragment)
+	return err == nil
+}
+
+// DontFragment is shorthand for DontFragmentAttr.
+var DontFragment DontFragmentAttr
diff --git a/internal/proto/evenport.go b/internal/proto/evenport.go
new file mode 100644
index 0000000..a5a3882
--- /dev/null
+++ b/internal/proto/evenport.go
@@ -0,0 +1,55 @@
+package proto
+
+import "github.com/pion/stun"
+
+// EvenPort represents EVEN-PORT attribute.
+//
+// This attribute allows the client to request that the port in the
+// relayed transport address be even, and (optionally) that the server
+// reserve the next-higher port number.
+//
+// RFC 5766 Section 14.6
+type EvenPort struct {
+	// ReservePort means that the server is requested to reserve
+	// the next-higher port number (on the same IP address)
+	// for a subsequent allocation.
+	ReservePort bool
+}
+
+func (p EvenPort) String() string {
+	if p.ReservePort {
+		return "reserve: true"
+	}
+	return "reserve: false"
+}
+
+const (
+	evenPortSize = 1
+	firstBitSet  = (1 << 8) - 1 // 0b100000000
+)
+
+// AddTo adds EVEN-PORT to message.
+func (p EvenPort) AddTo(m *stun.Message) error {
+	v := make([]byte, evenPortSize)
+	if p.ReservePort {
+		// Set first bit to 1.
+		v[0] = firstBitSet
+	}
+	m.Add(stun.AttrEvenPort, v)
+	return nil
+}
+
+// GetFrom decodes EVEN-PORT from message.
+func (p *EvenPort) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrEvenPort)
+	if err != nil {
+		return err
+	}
+	if err = stun.CheckSize(stun.AttrEvenPort, len(v), evenPortSize); err != nil {
+		return err
+	}
+	if v[0]&firstBitSet > 0 {
+		p.ReservePort = true
+	}
+	return nil
+}
diff --git a/internal/proto/fuzz.go b/internal/proto/fuzz.go
new file mode 100644
index 0000000..905b390
--- /dev/null
+++ b/internal/proto/fuzz.go
@@ -0,0 +1,111 @@
+// +build gofuzz
+
+package proto
+
+import (
+	"fmt"
+
+	"github.com/pion/stun"
+)
+
+type attr interface {
+	stun.Getter
+	stun.Setter
+}
+
+type attrs []struct {
+	g attr
+	t stun.AttrType
+}
+
+func (a attrs) pick(v byte) struct {
+	g attr
+	t stun.AttrType
+} {
+	idx := int(v) % len(a)
+	return a[idx]
+}
+
+func FuzzSetters(data []byte) int {
+	var (
+		m1 = &stun.Message{
+			Raw: make([]byte, 0, 2048),
+		}
+		m2 = &stun.Message{
+			Raw: make([]byte, 0, 2048),
+		}
+		m3 = &stun.Message{
+			Raw: make([]byte, 0, 2048),
+		}
+	)
+	attributes := attrs{
+		{new(RequestedTransport), stun.AttrRequestedTransport},
+		{new(RelayedAddress), stun.AttrXORRelayedAddress},
+		{new(ChannelNumber), stun.AttrChannelNumber},
+		{new(Data), stun.AttrData},
+		{new(EvenPort), stun.AttrEvenPort},
+		{new(Lifetime), stun.AttrLifetime},
+		{new(ReservationToken), stun.AttrReservationToken},
+	}
+	var firstByte = byte(0)
+	if len(data) > 0 {
+		firstByte = data[0]
+	}
+	a := attributes.pick(firstByte)
+	value := data
+	if len(data) > 1 {
+		value = value[1:]
+	}
+	m1.WriteHeader()
+	m1.Add(a.t, value)
+	err := a.g.GetFrom(m1)
+	if err == stun.ErrAttributeNotFound {
+		fmt.Println("unexpected 404")
+		panic(err)
+	}
+	if err != nil {
+		return 1
+	}
+	m2.WriteHeader()
+	if err := a.g.AddTo(m2); err != nil {
+		fmt.Println("failed to add attribute to m2")
+		panic(err)
+	}
+	m3.WriteHeader()
+	v, err := m2.Get(a.t)
+	if err != nil {
+		panic(err)
+	}
+	m3.Add(a.t, v)
+
+	if !m2.Equal(m3) {
+		fmt.Println(m2, "not equal", m3)
+		panic("not equal")
+	}
+	return 1
+}
+
+var d = &ChannelData{}
+
+func FuzzChannelData(data []byte) int {
+	d.Reset()
+	if b := bin.Uint16(data[0:4]); b > 20000 {
+		bin.PutUint16(data[0:4], MinChannelNumber-1)
+	} else if b > 40000 {
+		bin.PutUint16(data[0:4], MinChannelNumber+(MaxChannelNumber-MinChannelNumber)%b)
+	}
+	d.Raw = append(d.Raw, data...)
+	if d.Decode() != nil {
+		return 0
+	}
+	d2 := &ChannelData{}
+	d.Encode()
+	if !d.Number.Valid() {
+		return 1
+	}
+	d2.Raw = d.Raw
+	if err := d2.Decode(); err != nil {
+		panic(err)
+	}
+	return 1
+}
diff --git a/internal/proto/lifetime.go b/internal/proto/lifetime.go
new file mode 100644
index 0000000..2ee063b
--- /dev/null
+++ b/internal/proto/lifetime.go
@@ -0,0 +1,55 @@
+package proto
+
+import (
+	"time"
+
+	"github.com/pion/stun"
+)
+
+// DefaultLifetime in RFC 5766 is 10 minutes.
+//
+// RFC 5766 Section 2.2
+const DefaultLifetime = time.Minute * 10
+
+// Lifetime represents LIFETIME attribute.
+//
+// The LIFETIME attribute represents the duration for which the server
+// will maintain an allocation in the absence of a refresh. The value
+// portion of this attribute is 4-bytes long and consists of a 32-bit
+// unsigned integral value representing the number of seconds remaining
+// until expiration.
+//
+// RFC 5766 Section 14.2
+type Lifetime struct {
+	time.Duration
+}
+
+// uint32 seconds
+const lifetimeSize = 4 // 4 bytes, 32 bits
+
+// AddTo adds LIFETIME to message.
+func (l Lifetime) AddTo(m *stun.Message) error {
+	v := make([]byte, lifetimeSize)
+	bin.PutUint32(v, uint32(l.Seconds()))
+	m.Add(stun.AttrLifetime, v)
+	return nil
+}
+
+// GetFrom decodes LIFETIME from message.
+func (l *Lifetime) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrLifetime)
+	if err != nil {
+		return err
+	}
+	if err = stun.CheckSize(stun.AttrLifetime, len(v), lifetimeSize); err != nil {
+		return err
+	}
+	_ = v[lifetimeSize-1] // asserting length
+	seconds := bin.Uint32(v)
+	l.Duration = time.Second * time.Duration(seconds)
+	return nil
+}
+
+// ZeroLifetime is shorthand for setting zero lifetime
+// that indicates to close allocation.
+var ZeroLifetime stun.Setter = Lifetime{}
diff --git a/internal/proto/peeraddr.go b/internal/proto/peeraddr.go
new file mode 100644
index 0000000..b357b82
--- /dev/null
+++ b/internal/proto/peeraddr.go
@@ -0,0 +1,42 @@
+package proto
+
+import (
+	"net"
+
+	"github.com/pion/stun"
+)
+
+// PeerAddress implements XOR-PEER-ADDRESS attribute.
+//
+// The XOR-PEER-ADDRESS specifies the address and port of the peer as
+// seen from the TURN server. (For example, the peer's server-reflexive
+// transport address if the peer is behind a NAT.)
+//
+// RFC 5766 Section 14.3
+type PeerAddress struct {
+	IP   net.IP
+	Port int
+}
+
+func (a PeerAddress) String() string {
+	return stun.XORMappedAddress(a).String()
+}
+
+// AddTo adds XOR-PEER-ADDRESS to message.
+func (a PeerAddress) AddTo(m *stun.Message) error {
+	return stun.XORMappedAddress(a).AddToAs(m, stun.AttrXORPeerAddress)
+}
+
+// GetFrom decodes XOR-PEER-ADDRESS from message.
+func (a *PeerAddress) GetFrom(m *stun.Message) error {
+	return (*stun.XORMappedAddress)(a).GetFromAs(m, stun.AttrXORPeerAddress)
+}
+
+// XORPeerAddress implements XOR-PEER-ADDRESS attribute.
+//
+// The XOR-PEER-ADDRESS specifies the address and port of the peer as
+// seen from the TURN server. (For example, the peer's server-reflexive
+// transport address if the peer is behind a NAT.)
+//
+// RFC 5766 Section 14.3
+type XORPeerAddress = PeerAddress
diff --git a/internal/proto/proto.go b/internal/proto/proto.go
new file mode 100644
index 0000000..e4a3db5
--- /dev/null
+++ b/internal/proto/proto.go
@@ -0,0 +1,32 @@
+// Package proto implements RFC 5766 Traversal Using Relays around NAT.
+//
+// Merged from gortc/turn v0.80.
+package proto
+
+import (
+	"encoding/binary"
+
+	"github.com/pion/stun"
+)
+
+// bin is shorthand for binary.BigEndian.
+var bin = binary.BigEndian
+
+// Default ports for TURN from RFC 5766 Section 4.
+const (
+	// DefaultPort for TURN is same as STUN.
+	DefaultPort = stun.DefaultPort
+	// DefaultTLSPort is for TURN over TLS and is same as STUN.
+	DefaultTLSPort = stun.DefaultTLSPort
+)
+
+var (
+	// AllocateRequest is shorthand for allocation request message type.
+	AllocateRequest = stun.NewType(stun.MethodAllocate, stun.ClassRequest)
+	// CreatePermissionRequest is shorthand for create permission request type.
+	CreatePermissionRequest = stun.NewType(stun.MethodCreatePermission, stun.ClassRequest)
+	// SendIndication is shorthand for send indication message type.
+	SendIndication = stun.NewType(stun.MethodSend, stun.ClassIndication)
+	// RefreshRequest is shorthand for refresh request message type.
+	RefreshRequest = stun.NewType(stun.MethodRefresh, stun.ClassRequest)
+)
diff --git a/internal/proto/relayedaddr.go b/internal/proto/relayedaddr.go
new file mode 100644
index 0000000..2169cb7
--- /dev/null
+++ b/internal/proto/relayedaddr.go
@@ -0,0 +1,40 @@
+package proto
+
+import (
+	"net"
+
+	"github.com/pion/stun"
+)
+
+// RelayedAddress implements XOR-RELAYED-ADDRESS attribute.
+//
+// It specifies the address and port that the server allocated to the
+// client. It is encoded in the same way as XOR-MAPPED-ADDRESS.
+//
+// RFC 5766 Section 14.5
+type RelayedAddress struct {
+	IP   net.IP
+	Port int
+}
+
+func (a RelayedAddress) String() string {
+	return stun.XORMappedAddress(a).String()
+}
+
+// AddTo adds XOR-PEER-ADDRESS to message.
+func (a RelayedAddress) AddTo(m *stun.Message) error {
+	return stun.XORMappedAddress(a).AddToAs(m, stun.AttrXORRelayedAddress)
+}
+
+// GetFrom decodes XOR-PEER-ADDRESS from message.
+func (a *RelayedAddress) GetFrom(m *stun.Message) error {
+	return (*stun.XORMappedAddress)(a).GetFromAs(m, stun.AttrXORRelayedAddress)
+}
+
+// XORRelayedAddress implements XOR-RELAYED-ADDRESS attribute.
+//
+// It specifies the address and port that the server allocated to the
+// client. It is encoded in the same way as XOR-MAPPED-ADDRESS.
+//
+// RFC 5766 Section 14.5
+type XORRelayedAddress = RelayedAddress
diff --git a/internal/proto/reqfamily.go b/internal/proto/reqfamily.go
new file mode 100644
index 0000000..dae4f3a
--- /dev/null
+++ b/internal/proto/reqfamily.go
@@ -0,0 +1,59 @@
+package proto
+
+import (
+	"errors"
+
+	"github.com/pion/stun"
+)
+
+// RequestedAddressFamily represents the REQUESTED-ADDRESS-FAMILY Attribute as
+// defined in RFC 6156 Section 4.1.1.
+type RequestedAddressFamily byte
+
+const requestedFamilySize = 4
+
+// GetFrom decodes REQUESTED-ADDRESS-FAMILY from message.
+func (f *RequestedAddressFamily) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrRequestedAddressFamily)
+	if err != nil {
+		return err
+	}
+	if err = stun.CheckSize(stun.AttrRequestedAddressFamily, len(v), requestedFamilySize); err != nil {
+		return err
+	}
+	switch v[0] {
+	case byte(RequestedFamilyIPv4), byte(RequestedFamilyIPv6):
+		*f = RequestedAddressFamily(v[0])
+	default:
+		return errors.New("invalid value for requested family attribute")
+	}
+	return nil
+}
+
+func (f RequestedAddressFamily) String() string {
+	switch f {
+	case RequestedFamilyIPv4:
+		return "IPv4"
+	case RequestedFamilyIPv6:
+		return "IPv6"
+	default:
+		return "unknown"
+	}
+}
+
+// AddTo adds REQUESTED-ADDRESS-FAMILY to message.
+func (f RequestedAddressFamily) AddTo(m *stun.Message) error {
+	v := make([]byte, requestedFamilySize)
+	v[0] = byte(f)
+	// b[1:4] is RFFU = 0.
+	// The RFFU field MUST be set to zero on transmission and MUST be
+	// ignored on reception. It is reserved for future uses.
+	m.Add(stun.AttrRequestedAddressFamily, v)
+	return nil
+}
+
+// Values for RequestedAddressFamily as defined in RFC 6156 Section 4.1.1.
+const (
+	RequestedFamilyIPv4 RequestedAddressFamily = 0x01
+	RequestedFamilyIPv6 RequestedAddressFamily = 0x02
+)
diff --git a/internal/proto/reqtrans.go b/internal/proto/reqtrans.go
new file mode 100644
index 0000000..01ec93b
--- /dev/null
+++ b/internal/proto/reqtrans.go
@@ -0,0 +1,71 @@
+package proto
+
+import (
+	"strconv"
+
+	"github.com/pion/stun"
+)
+
+// Protocol is IANA assigned protocol number.
+type Protocol byte
+
+const (
+	// ProtoUDP is IANA assigned protocol number for UDP.
+	ProtoUDP Protocol = 17
+)
+
+func (p Protocol) String() string {
+	switch p {
+	case ProtoUDP:
+		return "UDP"
+	default:
+		return strconv.Itoa(int(p))
+	}
+}
+
+// RequestedTransport represents REQUESTED-TRANSPORT attribute.
+//
+// This attribute is used by the client to request a specific transport
+// protocol for the allocated transport address. RFC 5766 only allows the use of
+// codepoint 17 (User Datagram Protocol).
+//
+// RFC 5766 Section 14.7
+type RequestedTransport struct {
+	Protocol Protocol
+}
+
+func (t RequestedTransport) String() string {
+	return "protocol: " + t.Protocol.String()
+}
+
+const requestedTransportSize = 4
+
+// AddTo adds REQUESTED-TRANSPORT to message.
+func (t RequestedTransport) AddTo(m *stun.Message) error {
+	v := make([]byte, requestedTransportSize)
+	v[0] = byte(t.Protocol)
+	// b[1:4] is RFFU = 0.
+	// The RFFU field MUST be set to zero on transmission and MUST be
+	// ignored on reception. It is reserved for future uses.
+	m.Add(stun.AttrRequestedTransport, v)
+	return nil
+}
+
+// GetFrom decodes REQUESTED-TRANSPORT from message.
+func (t *RequestedTransport) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrRequestedTransport)
+	if err != nil {
+		return err
+	}
+	if err = stun.CheckSize(stun.AttrRequestedTransport, len(v), requestedTransportSize); err != nil {
+		return err
+	}
+	t.Protocol = Protocol(v[0])
+	return nil
+}
+
+// RequestedTransportUDP is setter for requested transport attribute with
+// value ProtoUDP (17).
+var RequestedTransportUDP stun.Setter = RequestedTransport{
+	Protocol: ProtoUDP,
+}
diff --git a/internal/proto/rsrvtoken.go b/internal/proto/rsrvtoken.go
new file mode 100644
index 0000000..6e2ed4d
--- /dev/null
+++ b/internal/proto/rsrvtoken.go
@@ -0,0 +1,39 @@
+package proto
+
+import "github.com/pion/stun"
+
+// ReservationToken represents RESERVATION-TOKEN attribute.
+//
+// The RESERVATION-TOKEN attribute contains a token that uniquely
+// identifies a relayed transport address being held in reserve by the
+// server. The server includes this attribute in a success response to
+// tell the client about the token, and the client includes this
+// attribute in a subsequent Allocate request to request the server use
+// that relayed transport address for the allocation.
+//
+// RFC 5766 Section 14.9
+type ReservationToken []byte
+
+const reservationTokenSize = 8 // 8 bytes
+
+// AddTo adds RESERVATION-TOKEN to message.
+func (t ReservationToken) AddTo(m *stun.Message) error {
+	if err := stun.CheckSize(stun.AttrReservationToken, len(t), reservationTokenSize); err != nil {
+		return err
+	}
+	m.Add(stun.AttrReservationToken, t)
+	return nil
+}
+
+// GetFrom decodes RESERVATION-TOKEN from message.
+func (t *ReservationToken) GetFrom(m *stun.Message) error {
+	v, err := m.Get(stun.AttrReservationToken)
+	if err != nil {
+		return err
+	}
+	if err = stun.CheckSize(stun.AttrReservationToken, len(v), reservationTokenSize); err != nil {
+		return err
+	}
+	*t = v
+	return nil
+}
diff --git a/internal/proto/uri.go b/internal/proto/uri.go
new file mode 100644
index 0000000..255fb6b
--- /dev/null
+++ b/internal/proto/uri.go
@@ -0,0 +1,72 @@
+package proto
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"strconv"
+)
+
+// Scheme definitions from RFC 7065 Section 3.2.
+const (
+	Scheme       = "turn"
+	SchemeSecure = "turns"
+)
+
+// Transport definitions as in RFC 7065.
+const (
+	TransportTCP = "tcp"
+	TransportUDP = "udp"
+)
+
+// URI as defined in RFC 7065.
+type URI struct {
+	Scheme    string
+	Host      string
+	Port      int
+	Transport string
+}
+
+func (u URI) String() string {
+	transportSuffix := ""
+	if len(u.Transport) > 0 {
+		transportSuffix = "?transport=" + u.Transport
+	}
+	if u.Port != 0 {
+		return fmt.Sprintf("%s:%s:%d%s",
+			u.Scheme, u.Host, u.Port, transportSuffix,
+		)
+	}
+	return u.Scheme + ":" + u.Host + transportSuffix
+}
+
+// ParseURI parses URI from string.
+func ParseURI(rawURI string) (URI, error) {
+	// Carefully reusing URI parser from net/url.
+	u, urlParseErr := url.Parse(rawURI)
+	if urlParseErr != nil {
+		return URI{}, urlParseErr
+	}
+	if u.Scheme != Scheme && u.Scheme != SchemeSecure {
+		return URI{}, fmt.Errorf("unknown uri scheme %q", u.Scheme)
+	}
+	if u.Opaque == "" {
+		return URI{}, errors.New("invalid uri format: expected opaque")
+	}
+	// Using URL methods to split host.
+	u.Host = u.Opaque
+	host, rawPort := u.Hostname(), u.Port()
+	uri := URI{
+		Scheme:    u.Scheme,
+		Host:      host,
+		Transport: u.Query().Get("transport"),
+	}
+	if len(rawPort) > 0 {
+		port, portErr := strconv.Atoi(rawPort)
+		if portErr != nil {
+			return uri, fmt.Errorf("failed to parse %q as port: %v", rawPort, portErr)
+		}
+		uri.Port = port
+	}
+	return uri, nil
+}
diff --git a/server.go b/server.go
index 5569da2..fea5ad1 100644
--- a/server.go
+++ b/server.go
@@ -10,11 +10,11 @@ import (
 	"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/pion/turn/internal/proto"
 	"github.com/pkg/errors"
 )
 
@@ -95,7 +95,7 @@ func NewServer(config *ServerConfig) *Server {
 
 	channelBindTimeout := config.ChannelBindTimeout
 	if channelBindTimeout == 0 {
-		channelBindTimeout = turn.DefaultLifetime
+		channelBindTimeout = proto.DefaultLifetime
 	}
 
 	return &Server{
@@ -318,7 +318,7 @@ func (s *Server) handleUDPPacket(conn net.PacketConn, srcAddr net.Addr, buf []by
 		srcAddr.String(),
 		conn.LocalAddr().String(),
 	)
-	if turn.IsChannelData(buf) {
+	if proto.IsChannelData(buf) {
 		return s.handleDataPacket(conn, srcAddr, buf)
 	}
 
@@ -328,7 +328,7 @@ func (s *Server) handleUDPPacket(conn net.PacketConn, srcAddr net.Addr, buf []by
 // 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 := turn.ChannelData{Raw: buf}
+	c := proto.ChannelData{Raw: buf}
 	if err := c.Decode(); err != nil {
 		return errors.Wrap(err, "Failed to create channel data from packet")
 	}
diff --git a/turn.go b/turn.go
index c2219d9..e998d9a 100644
--- a/turn.go
+++ b/turn.go
@@ -7,12 +7,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gortc/turn"
 	"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"
 )
 
@@ -144,11 +142,11 @@ func (s *Server) handleAllocateRequest(conn net.PacketConn, srcAddr net.Addr, m
 	//    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
+	var requestedTransport proto.RequestedTransport
 	if err = requestedTransport.GetFrom(m); err != nil {
 		return respondWithError(err, messageIntegrity, stun.CodeBadRequest)
 	}
-	if requestedTransport.Protocol != turn.ProtoUDP {
+	if requestedTransport.Protocol != proto.ProtoUDP {
 		return respondWithError(err, messageIntegrity, stun.CodeUnsupportedTransProto)
 	}
 
@@ -169,9 +167,9 @@ func (s *Server) handleAllocateRequest(conn net.PacketConn, srcAddr net.Addr, m
 	//     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
+	var reservationTokenAttr proto.ReservationToken
 	if err = reservationTokenAttr.GetFrom(m); err == nil {
-		var evenPort turn.EvenPort
+		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)
 		}
@@ -189,7 +187,7 @@ func (s *Server) handleAllocateRequest(conn net.PacketConn, srcAddr net.Addr, m
 	//    below).  If the server cannot satisfy the request, then the
 	//    server rejects the request with a 508 (Insufficient Capacity)
 	//    error.
-	var evenPort turn.EvenPort
+	var evenPort proto.EvenPort
 	if err = evenPort.GetFrom(m); err == nil {
 		randomPort := 0
 		randomPort, err = allocation.GetRandomEvenPort()
@@ -250,11 +248,11 @@ func (s *Server) handleAllocateRequest(conn net.PacketConn, srcAddr net.Addr, m
 	}
 
 	responseAttrs := []stun.Setter{
-		&turn.RelayedAddress{
+		&proto.RelayedAddress{
 			IP:   dstIP,
 			Port: relayPort,
 		},
-		&turn.Lifetime{
+		&proto.Lifetime{
 			Duration: lifetimeDuration,
 		},
 		&stun.XORMappedAddress{
@@ -265,7 +263,7 @@ func (s *Server) handleAllocateRequest(conn net.PacketConn, srcAddr net.Addr, m
 
 	if reservationToken != "" {
 		s.reservationManager.CreateReservation(reservationToken, relayPort)
-		responseAttrs = append(responseAttrs, turn.ReservationToken([]byte(reservationToken)))
+		responseAttrs = append(responseAttrs, proto.ReservationToken([]byte(reservationToken)))
 	}
 
 	return curriedSend(stun.ClassSuccessResponse, stun.MethodAllocate, m.TransactionID, append(responseAttrs, messageIntegrity)...)
@@ -296,7 +294,7 @@ func (s *Server) handleRefreshRequest(conn net.PacketConn, srcAddr net.Addr, m *
 	a.Refresh(lifetimeDuration)
 
 	return curriedSend(stun.ClassSuccessResponse, stun.MethodRefresh, m.TransactionID,
-		&turn.Lifetime{
+		&proto.Lifetime{
 			Duration: lifetimeDuration,
 		},
 		messageIntegrity,
@@ -327,7 +325,7 @@ func (s *Server) handleCreatePermissionRequest(conn net.PacketConn, srcAddr net.
 	addCount := 0
 
 	if err := m.ForEach(stun.AttrXORPeerAddress, func(m *stun.Message) error {
-		var peerAddress turn.PeerAddress
+		var peerAddress proto.PeerAddress
 		if err := peerAddress.GetFrom(m); err != nil {
 			return err
 		}
@@ -369,12 +367,12 @@ func (s *Server) handleSendIndication(conn net.PacketConn, srcAddr net.Addr, m *
 		return errors.Errorf("No allocation found for %v:%v", srcAddr, dstAddr)
 	}
 
-	dataAttr := turn.Data{}
+	dataAttr := proto.Data{}
 	if err := dataAttr.GetFrom(m); err != nil {
 		return err
 	}
 
-	peerAddress := turn.PeerAddress{}
+	peerAddress := proto.PeerAddress{}
 	if err := peerAddress.GetFrom(m); err != nil {
 		return err
 	}
@@ -419,12 +417,12 @@ func (s *Server) handleChannelBindRequest(conn net.PacketConn, srcAddr net.Addr,
 		return errorSend(err, stun.CodeBadRequest)
 	}
 
-	var channel turn.ChannelNumber
+	var channel proto.ChannelNumber
 	if err = channel.GetFrom(m); err != nil {
 		return errorSend(err, stun.CodeBadRequest)
 	}
 
-	peerAddr := turn.PeerAddress{}
+	peerAddr := proto.PeerAddress{}
 	if err = peerAddr.GetFrom(m); err != nil {
 		return errorSend(err, stun.CodeBadRequest)
 	}
@@ -444,7 +442,7 @@ func (s *Server) handleChannelBindRequest(conn net.PacketConn, srcAddr net.Addr,
 	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 *turn.ChannelData) error {
+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{
@@ -482,9 +480,9 @@ func (s *Server) makeAttrs(transactionID [stun.TransactionIDSize]byte, msgType s
 }
 
 func allocationLifeTime(m *stun.Message) time.Duration {
-	lifetimeDuration := turn.DefaultLifetime
+	lifetimeDuration := proto.DefaultLifetime
 
-	var lifetime turn.Lifetime
+	var lifetime proto.Lifetime
 	if err := lifetime.GetFrom(m); err == nil {
 		if lifetime.Duration < maximumLifetime {
 			lifetimeDuration = lifetime.Duration


Test patch:
diff --git a/internal/allocation/allocation_manager_test.go b/internal/allocation/allocation_manager_test.go
index a731497..e0ca9c4 100644
--- a/internal/allocation/allocation_manager_test.go
+++ b/internal/allocation/allocation_manager_test.go
@@ -8,8 +8,8 @@ import (
 	"testing"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/logging"
+	"github.com/pion/turn/internal/proto"
 )
 
 func TestManager(t *testing.T) {
@@ -42,10 +42,10 @@ 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, turn.DefaultLifetime); a != nil || err == nil {
+	if a, err := m.CreateAllocation(nil, turnSocket, net.IPv4zero, 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, turn.DefaultLifetime); a != nil || err == nil {
+	if a, err := m.CreateAllocation(randomFiveTuple(), nil, net.IPv4zero, 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 {
@@ -57,7 +57,7 @@ func subTestCreateInvalidAllocation(t *testing.T, turnSocket net.PacketConn) {
 func subTestCreateAllocation(t *testing.T, turnSocket net.PacketConn) {
 	m := newTestManager()
 	fiveTuple := randomFiveTuple()
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, turn.DefaultLifetime); a == nil || err != nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
 	}
 
@@ -70,11 +70,11 @@ func subTestCreateAllocation(t *testing.T, turnSocket net.PacketConn) {
 func subTestCreateAllocationDuplicateFiveTuple(t *testing.T, turnSocket net.PacketConn) {
 	m := newTestManager()
 	fiveTuple := randomFiveTuple()
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, turn.DefaultLifetime); a == nil || err != nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 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, turn.DefaultLifetime); a != nil || err == nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a != nil || err == nil {
 		t.Errorf("Was able to create allocation with same FiveTuple twice")
 	}
 }
@@ -82,7 +82,7 @@ func subTestCreateAllocationDuplicateFiveTuple(t *testing.T, turnSocket net.Pack
 func subTestDeleteAllocation(t *testing.T, turnSocket net.PacketConn) {
 	m := newTestManager()
 	fiveTuple := randomFiveTuple()
-	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, turn.DefaultLifetime); a == nil || err != nil {
+	if a, err := m.CreateAllocation(fiveTuple, turnSocket, net.IPv4zero, 0, proto.DefaultLifetime); a == nil || err != nil {
 		t.Errorf("Failed to create allocation %v %v", a, err)
 	}
 
diff --git a/internal/allocation/allocation_test.go b/internal/allocation/allocation_test.go
index 6240b09..acd633d 100644
--- a/internal/allocation/allocation_test.go
+++ b/internal/allocation/allocation_test.go
@@ -6,11 +6,10 @@ import (
 	"testing"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/stun"
-	"github.com/stretchr/testify/assert"
-
 	"github.com/pion/turn/internal/ipnet"
+	"github.com/pion/turn/internal/proto"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestAllocation(t *testing.T) {
@@ -107,19 +106,19 @@ func subTestAddChannelBind(t *testing.T) {
 	a := NewAllocation(nil, nil, nil)
 
 	addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
-	c := NewChannelBind(turn.MinChannelNumber, addr, nil)
+	c := NewChannelBind(proto.MinChannelNumber, addr, nil)
 
-	err := a.AddChannelBind(c, turn.DefaultLifetime)
+	err := a.AddChannelBind(c, proto.DefaultLifetime)
 	assert.Nil(t, err, "should succeed")
 	assert.Equal(t, a, c.allocation, "allocation should be the caller.")
 
-	c2 := NewChannelBind(turn.MinChannelNumber+1, addr, nil)
-	err = a.AddChannelBind(c2, turn.DefaultLifetime)
+	c2 := NewChannelBind(proto.MinChannelNumber+1, addr, nil)
+	err = a.AddChannelBind(c2, proto.DefaultLifetime)
 	assert.NotNil(t, err, "should failed with conflicted peer address")
 
 	addr2, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3479")
-	c3 := NewChannelBind(turn.MinChannelNumber, addr2, nil)
-	err = a.AddChannelBind(c3, turn.DefaultLifetime)
+	c3 := NewChannelBind(proto.MinChannelNumber, addr2, nil)
+	err = a.AddChannelBind(c3, proto.DefaultLifetime)
 	assert.NotNil(t, err, "should fail with conflicted number.")
 }
 
@@ -127,14 +126,14 @@ func subTestGetChannelByNumber(t *testing.T) {
 	a := NewAllocation(nil, nil, nil)
 
 	addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
-	c := NewChannelBind(turn.MinChannelNumber, addr, nil)
+	c := NewChannelBind(proto.MinChannelNumber, addr, nil)
 
-	_ = a.AddChannelBind(c, turn.DefaultLifetime)
+	_ = a.AddChannelBind(c, proto.DefaultLifetime)
 
 	existChannel := a.GetChannelByNumber(c.Number)
 	assert.Equal(t, c, existChannel)
 
-	notExistChannel := a.GetChannelByNumber(turn.MinChannelNumber + 1)
+	notExistChannel := a.GetChannelByNumber(proto.MinChannelNumber + 1)
 	assert.Nil(t, notExistChannel, "should be nil for not existed channel.")
 }
 
@@ -142,9 +141,9 @@ func subTestGetChannelByAddr(t *testing.T) {
 	a := NewAllocation(nil, nil, nil)
 
 	addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
-	c := NewChannelBind(turn.MinChannelNumber, addr, nil)
+	c := NewChannelBind(proto.MinChannelNumber, addr, nil)
 
-	_ = a.AddChannelBind(c, turn.DefaultLifetime)
+	_ = a.AddChannelBind(c, proto.DefaultLifetime)
 
 	existChannel := a.GetChannelByAddr(c.Peer)
 	assert.Equal(t, c, existChannel)
@@ -158,9 +157,9 @@ func subTestRemoveChannelBind(t *testing.T) {
 	a := NewAllocation(nil, nil, nil)
 
 	addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:3478")
-	c := NewChannelBind(turn.MinChannelNumber, addr, nil)
+	c := NewChannelBind(proto.MinChannelNumber, addr, nil)
 
-	_ = a.AddChannelBind(c, turn.DefaultLifetime)
+	_ = a.AddChannelBind(c, proto.DefaultLifetime)
 
 	a.RemoveChannelBind(c.Number)
 
@@ -182,12 +181,12 @@ func subTestAllocationClose(t *testing.T) {
 	a := NewAllocation(nil, nil, nil)
 	a.RelaySocket = l
 	// add mock lifetimeTimer
-	a.lifetimeTimer = time.AfterFunc(turn.DefaultLifetime, func() {})
+	a.lifetimeTimer = time.AfterFunc(proto.DefaultLifetime, func() {})
 
 	// add channel
 	addr, _ := net.ResolveUDPAddr(network, "127.0.0.1:3478")
-	c := NewChannelBind(turn.MinChannelNumber, addr, nil)
-	_ = a.AddChannelBind(c, turn.DefaultLifetime)
+	c := NewChannelBind(proto.MinChannelNumber, addr, nil)
+	_ = a.AddChannelBind(c, proto.DefaultLifetime)
 
 	// add permission
 	a.AddPermission(NewPermission(addr, nil))
@@ -231,7 +230,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, turn.DefaultLifetime)
+	}, turnSocket, net.ParseIP("127.0.0.1"), 0, proto.DefaultLifetime)
 
 	assert.Nil(t, err, "should succeed")
 
@@ -248,8 +247,8 @@ func subTestPacketHandler(t *testing.T) {
 	// add permission with peer1 address
 	a.AddPermission(NewPermission(peerListener1.LocalAddr(), m.log))
 	// add channel with min channel number and peer2 address
-	channelBind := NewChannelBind(turn.MinChannelNumber, peerListener2.LocalAddr(), m.log)
-	_ = a.AddChannelBind(channelBind, turn.DefaultLifetime)
+	channelBind := NewChannelBind(proto.MinChannelNumber, peerListener2.LocalAddr(), m.log)
+	_ = a.AddChannelBind(channelBind, proto.DefaultLifetime)
 
 	_, port, _ := ipnet.AddrIPPort(a.RelaySocket.LocalAddr())
 	relayAddrWithHostStr := fmt.Sprintf("127.0.0.1:%d", port)
@@ -267,7 +266,7 @@ func subTestPacketHandler(t *testing.T) {
 	err = stun.Decode(data, &msg)
 	assert.Nil(t, err, "decode data to stun message failed")
 
-	var msgData turn.Data
+	var msgData proto.Data
 	err = msgData.GetFrom(&msg)
 	assert.Nil(t, err, "get data from stun message failed")
 	assert.Equal(t, targetText, string(msgData), "get message doesn't equal the target text")
@@ -278,9 +277,9 @@ func subTestPacketHandler(t *testing.T) {
 	data = <-dataCh
 
 	// resolve channel data
-	assert.True(t, turn.IsChannelData(data), "should be channel data")
+	assert.True(t, proto.IsChannelData(data), "should be channel data")
 
-	channelData := turn.ChannelData{
+	channelData := proto.ChannelData{
 		Raw: data,
 	}
 	err = channelData.Decode()
diff --git a/internal/allocation/channel_bind_test.go b/internal/allocation/channel_bind_test.go
index 5afb235..da5ee0f 100644
--- a/internal/allocation/channel_bind_test.go
+++ b/internal/allocation/channel_bind_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/gortc/turn"
+	"github.com/pion/turn/internal/proto"
 )
 
 func TestChannelBind(t *testing.T) {
@@ -43,7 +43,7 @@ func newChannelBind(lifetime time.Duration) *ChannelBind {
 
 	addr, _ := net.ResolveUDPAddr("udp", "0.0.0.0:0")
 	c := &ChannelBind{
-		Number: turn.MinChannelNumber,
+		Number: proto.MinChannelNumber,
 		Peer:   addr,
 	}
 
diff --git a/internal/proto/addr_test.go b/internal/proto/addr_test.go
new file mode 100644
index 0000000..b9f1b5e
--- /dev/null
+++ b/internal/proto/addr_test.go
@@ -0,0 +1,99 @@
+package proto
+
+import (
+	"fmt"
+	"net"
+	"testing"
+)
+
+func TestAddr_FromUDPAddr(t *testing.T) {
+	u := &net.UDPAddr{
+		IP:   net.IPv4(127, 0, 0, 1),
+		Port: 1234,
+	}
+	a := new(Addr)
+	a.FromUDPAddr(u)
+	if !u.IP.Equal(a.IP) || u.Port != a.Port || u.String() != a.String() {
+		t.Error("not equal")
+	}
+	if a.Network() != "turn" {
+		t.Error("unexpected network")
+	}
+}
+
+func TestAddr_EqualIP(t *testing.T) {
+	a := Addr{
+		IP:   net.IPv4(127, 0, 0, 1),
+		Port: 1337,
+	}
+	b := Addr{
+		IP:   net.IPv4(127, 0, 0, 1),
+		Port: 1338,
+	}
+	if a.Equal(b) {
+		t.Error("a != b")
+	}
+	if !a.EqualIP(b) {
+		t.Error("a.IP should equal to b.IP")
+	}
+}
+
+func TestFiveTuple_Equal(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		a, b FiveTuple
+		v    bool
+	}{
+		{
+			name: "blank",
+			v:    true,
+		},
+		{
+			name: "proto",
+			a: FiveTuple{
+				Proto: ProtoUDP,
+			},
+		},
+		{
+			name: "server",
+			a: FiveTuple{
+				Server: Addr{
+					Port: 100,
+				},
+			},
+		},
+		{
+			name: "client",
+			a: FiveTuple{
+				Client: Addr{
+					Port: 100,
+				},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			if v := tc.a.Equal(tc.b); v != tc.v {
+				t.Errorf("%s [%v!=%v] %s",
+					tc.a, v, tc.v, tc.b,
+				)
+			}
+		})
+	}
+}
+
+func TestFiveTuple_String(t *testing.T) {
+	s := fmt.Sprint(FiveTuple{
+		Proto: ProtoUDP,
+		Server: Addr{
+			Port: 100,
+			IP:   net.IPv4(127, 0, 0, 1),
+		},
+		Client: Addr{
+			Port: 200,
+			IP:   net.IPv4(127, 0, 0, 1),
+		},
+	})
+	if s != "127.0.0.1:200->127.0.0.1:100 (UDP)" {
+		t.Error("unexpected stringer output")
+	}
+}
diff --git a/internal/proto/chandata_test.go b/internal/proto/chandata_test.go
new file mode 100644
index 0000000..2ba061f
--- /dev/null
+++ b/internal/proto/chandata_test.go
@@ -0,0 +1,285 @@
+package proto
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/hex"
+	"io"
+	"testing"
+)
+
+func TestChannelData_Encode(t *testing.T) {
+	d := &ChannelData{
+		Data:   []byte{1, 2, 3, 4},
+		Number: MinChannelNumber + 1,
+	}
+	d.Encode()
+	b := &ChannelData{}
+	b.Raw = append(b.Raw, d.Raw...)
+	if err := b.Decode(); err != nil {
+		t.Error(err)
+	}
+	if !b.Equal(d) {
+		t.Error("not equal")
+	}
+	if !IsChannelData(b.Raw) || !IsChannelData(d.Raw) {
+		t.Error("unexpected IsChannelData")
+	}
+}
+
+func TestChannelData_Equal(t *testing.T) {
+	for _, tc := range []struct {
+		name  string
+		a, b  *ChannelData
+		value bool
+	}{
+		{
+			name:  "nil",
+			value: true,
+		},
+		{
+			name: "nil to non-nil",
+			b:    &ChannelData{},
+		},
+		{
+			name: "equal",
+			b: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 3},
+			},
+			a: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 3},
+			},
+			value: true,
+		},
+		{
+			name: "number",
+			b: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 3},
+			},
+			a: &ChannelData{
+				Number: MinChannelNumber + 1,
+				Data:   []byte{1, 2, 3},
+			},
+		},
+		{
+			name: "length",
+			b: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 3},
+			},
+			a: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 3, 4},
+			},
+		},
+		{
+			name: "data",
+			b: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 3},
+			},
+			a: &ChannelData{
+				Number: MinChannelNumber,
+				Data:   []byte{1, 2, 2},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			if v := tc.a.Equal(tc.b); v != tc.value {
+				t.Errorf("unexpected: %v != %v", tc.value, v)
+			}
+		})
+	}
+}
+
+func TestChannelData_Decode(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		buf  []byte
+		err  error
+	}{
+		{
+			name: "nil",
+			err:  io.ErrUnexpectedEOF,
+		},
+		{
+			name: "small",
+			buf:  []byte{1, 2, 3},
+			err:  io.ErrUnexpectedEOF,
+		},
+		{
+			name: "zeroes",
+			buf:  []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+			err:  ErrInvalidChannelNumber,
+		},
+		{
+			name: "bad chan number",
+			buf:  []byte{63, 255, 0, 0, 0, 4, 0, 0, 1, 2, 3, 4},
+			err:  ErrInvalidChannelNumber,
+		},
+		{
+			name: "bad length",
+			buf:  []byte{0x40, 0x40, 0x02, 0x23, 0x16, 0, 0, 0, 0, 0, 0, 0},
+			err:  ErrBadChannelDataLength,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			m := &ChannelData{
+				Raw: tc.buf,
+			}
+			if err := m.Decode(); err != tc.err {
+				t.Errorf("unexpected: %v != %v", tc.err, err)
+			}
+		})
+	}
+}
+
+func TestChannelData_Reset(t *testing.T) {
+	d := &ChannelData{
+		Data:   []byte{1, 2, 3, 4},
+		Number: MinChannelNumber + 1,
+	}
+	d.Encode()
+	buf := make([]byte, len(d.Raw))
+	copy(buf, d.Raw)
+	d.Reset()
+	d.Raw = buf
+	if err := d.Decode(); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestIsChannelData(t *testing.T) {
+	for _, tc := range []struct {
+		name  string
+		buf   []byte
+		value bool
+	}{
+		{
+			name: "nil",
+		},
+		{
+			name: "small",
+			buf:  []byte{1, 2, 3, 4},
+		},
+		{
+			name: "zeroes",
+			buf:  []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			if v := IsChannelData(tc.buf); v != tc.value {
+				t.Errorf("unexpected: %v != %v", tc.value, v)
+			}
+		})
+	}
+}
+
+func BenchmarkIsChannelData(b *testing.B) {
+	buf := []byte{64, 0, 0, 0, 0, 4, 0, 0, 1, 2, 3}
+	b.ReportAllocs()
+	b.SetBytes(int64(len(buf)))
+	for i := 0; i < b.N; i++ {
+		IsChannelData(buf)
+	}
+}
+
+func BenchmarkChannelData_Encode(b *testing.B) {
+	d := &ChannelData{
+		Data:   []byte{1, 2, 3, 4},
+		Number: MinChannelNumber + 1,
+	}
+	b.ReportAllocs()
+	b.SetBytes(4 + channelDataHeaderSize)
+	for i := 0; i < b.N; i++ {
+		d.Encode()
+	}
+}
+
+func BenchmarkChannelData_Decode(b *testing.B) {
+	d := &ChannelData{
+		Data:   []byte{1, 2, 3, 4},
+		Number: MinChannelNumber + 1,
+	}
+	d.Encode()
+	buf := make([]byte, len(d.Raw))
+	copy(buf, d.Raw)
+	b.ReportAllocs()
+	b.SetBytes(4 + channelDataHeaderSize)
+	for i := 0; i < b.N; i++ {
+		d.Reset()
+		d.Raw = buf
+		if err := d.Decode(); err != nil {
+			b.Error(err)
+		}
+	}
+}
+
+func TestChannelData_Padding(t *testing.T) {
+	c := &ChannelData{
+		Data:    []byte{1},
+		Padding: true,
+		Number:  MinChannelNumber + 1,
+	}
+	c.Encode()
+	n := &ChannelData{
+		Raw: make([]byte, len(c.Raw)),
+	}
+	copy(n.Raw, c.Raw)
+	if err := n.Decode(); err != nil {
+		t.Error(err)
+	}
+	if !n.Equal(c) {
+		t.Error("should be equal")
+	}
+}
+
+func TestChromeChannelData(t *testing.T) {
+	var (
+		r = bytes.NewReader(loadData(t, "02_chandata.hex"))
+		s = bufio.NewScanner(r)
+
+		data     [][]byte
+		messages []*ChannelData
+	)
+	// Decoding hex data into binary.
+	for s.Scan() {
+		b, err := hex.DecodeString(s.Text())
+		if err != nil {
+			t.Fatal(err)
+		}
+		data = append(data, b)
+	}
+	// All hex streams decoded to raw binary format and stored in data slice.
+	// Decoding packets to messages.
+	for i, packet := range data {
+		var (
+			m = new(ChannelData)
+		)
+		m.Raw = packet
+		if err := m.Decode(); err != nil {
+			t.Errorf("Packet %d: %v", i, err)
+		}
+		encoded := &ChannelData{
+			Data:   m.Data,
+			Number: m.Number,
+		}
+		encoded.Encode()
+		decoded := new(ChannelData)
+		decoded.Raw = encoded.Raw
+		if err := decoded.Decode(); err != nil {
+			t.Error(err)
+		}
+		if !decoded.Equal(m) {
+			t.Error("should be equal")
+		}
+
+		messages = append(messages, m)
+	}
+	if len(messages) != 2 {
+		t.Error("unexpected message slice list")
+	}
+}
diff --git a/internal/proto/chann_test.go b/internal/proto/chann_test.go
new file mode 100644
index 0000000..ce5ccbc
--- /dev/null
+++ b/internal/proto/chann_test.go
@@ -0,0 +1,118 @@
+package proto
+
+import (
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func BenchmarkChannelNumber(b *testing.B) {
+	b.Run("AddTo", func(b *testing.B) {
+		b.ReportAllocs()
+		m := new(stun.Message)
+		for i := 0; i < b.N; i++ {
+			n := ChannelNumber(12)
+			if err := n.AddTo(m); err != nil {
+				b.Fatal(err)
+			}
+			m.Reset()
+		}
+	})
+	b.Run("GetFrom", func(b *testing.B) {
+		m := new(stun.Message)
+		ChannelNumber(12).AddTo(m)
+		for i := 0; i < b.N; i++ {
+			var n ChannelNumber
+			if err := n.GetFrom(m); err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+}
+
+func TestChannelNumber(t *testing.T) {
+	t.Run("String", func(t *testing.T) {
+		n := ChannelNumber(112)
+		if n.String() != "112" {
+			t.Errorf("bad string %s, expedted 112", n.String())
+		}
+	})
+	t.Run("NoAlloc", func(t *testing.T) {
+		m := &stun.Message{}
+		if wasAllocs(func() {
+			// Case with ChannelNumber on stack.
+			n := ChannelNumber(6)
+			n.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+
+		n := ChannelNumber(12)
+		nP := &n
+		if wasAllocs(func() {
+			// On heap.
+			nP.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		n := ChannelNumber(6)
+		if err := n.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			var numDecoded ChannelNumber
+			if err := numDecoded.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if numDecoded != n {
+				t.Errorf("Decoded %d, expected %d", numDecoded, n)
+			}
+			if wasAllocs(func() {
+				var num ChannelNumber
+				num.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				nHandle := new(ChannelNumber)
+				if err := nHandle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+				m.Add(stun.AttrChannelNumber, []byte{1, 2, 3})
+				if !stun.IsAttrSizeInvalid(nHandle.GetFrom(m)) {
+					t.Error("IsAttrSizeInvalid should be true")
+				}
+			})
+		})
+	})
+}
+
+func TestChannelNumber_Valid(t *testing.T) {
+	for _, tc := range []struct {
+		n     ChannelNumber
+		value bool
+	}{
+		{MinChannelNumber - 1, false},
+		{MinChannelNumber, true},
+		{MinChannelNumber + 1, true},
+		{MaxChannelNumber, true},
+		{MaxChannelNumber + 1, false},
+	} {
+		t.Run(tc.n.String(), func(t *testing.T) {
+			if v := tc.n.Valid(); v != tc.value {
+				t.Errorf("unexpected: %v != %v", tc.value, v)
+			}
+		})
+	}
+}
diff --git a/internal/proto/chrome_test.go b/internal/proto/chrome_test.go
new file mode 100644
index 0000000..3aac81e
--- /dev/null
+++ b/internal/proto/chrome_test.go
@@ -0,0 +1,42 @@
+package proto
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/hex"
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestChromeAllocRequest(t *testing.T) {
+	var (
+		r = bytes.NewReader(loadData(t, "01_chromeallocreq.hex"))
+		s = bufio.NewScanner(r)
+
+		data     [][]byte
+		messages []*stun.Message
+	)
+	// Decoding hex data into binary.
+	for s.Scan() {
+		b, err := hex.DecodeString(s.Text())
+		if err != nil {
+			t.Fatal(err)
+		}
+		data = append(data, b)
+	}
+	// All hex streams decoded to raw binary format and stored in data slice.
+	// Decoding packets to messages.
+	for i, packet := range data {
+		var (
+			m = new(stun.Message)
+		)
+		if _, err := m.Write(packet); err != nil {
+			t.Errorf("Packet %d: %v", i, err)
+		}
+		messages = append(messages, m)
+	}
+	if len(messages) != 4 {
+		t.Error("unexpected message slice list")
+	}
+}
diff --git a/internal/proto/data_test.go b/internal/proto/data_test.go
new file mode 100644
index 0000000..57835a0
--- /dev/null
+++ b/internal/proto/data_test.go
@@ -0,0 +1,86 @@
+package proto
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func BenchmarkData(b *testing.B) {
+	b.Run("AddTo", func(b *testing.B) {
+		m := new(stun.Message)
+		d := make(Data, 10)
+		for i := 0; i < b.N; i++ {
+			d.AddTo(m)
+			m.Reset()
+		}
+	})
+	b.Run("AddToRaw", func(b *testing.B) {
+		m := new(stun.Message)
+		d := make([]byte, 10)
+		// Overhead should be low.
+		for i := 0; i < b.N; i++ {
+			m.Add(stun.AttrData, d)
+			m.Reset()
+		}
+	})
+}
+
+func TestData(t *testing.T) {
+	t.Run("NoAlloc", func(t *testing.T) {
+		m := new(stun.Message)
+		v := []byte{1, 2, 3, 4}
+		if wasAllocs(func() {
+			// On stack.
+			d := Data(v)
+			d.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+
+		d := &Data{1, 2, 3, 4}
+		if wasAllocs(func() {
+			// On heap.
+			d.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		d := Data{1, 2, 33, 44, 0x13, 0xaf}
+		if err := d.AddTo(m); err != nil {
+			t.Fatal(err)
+		}
+		m.WriteHeader()
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			var dataDecoded Data
+			if err := dataDecoded.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if !bytes.Equal(dataDecoded, d) {
+				t.Error(dataDecoded, "!=", d, "(expected)")
+			}
+			if wasAllocs(func() {
+				var dataDecoded Data
+				dataDecoded.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				var handle Data
+				if err := handle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+			})
+		})
+	})
+}
diff --git a/internal/proto/dontfrag_test.go b/internal/proto/dontfrag_test.go
new file mode 100644
index 0000000..b690a43
--- /dev/null
+++ b/internal/proto/dontfrag_test.go
@@ -0,0 +1,38 @@
+package proto
+
+import (
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestDontFragment(t *testing.T) {
+	t.Run("False", func(t *testing.T) {
+		m := new(stun.Message)
+		m.WriteHeader()
+		if DontFragment.IsSet(m) {
+			t.Error("should not be set")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		if err := DontFragment.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("IsSet", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			if !DontFragment.IsSet(m) {
+				t.Error("should be set")
+			}
+			if wasAllocs(func() {
+				DontFragment.IsSet(m)
+			}) {
+				t.Error("unexpected allocations")
+			}
+		})
+	})
+}
diff --git a/internal/proto/evenport_test.go b/internal/proto/evenport_test.go
new file mode 100644
index 0000000..cd9f319
--- /dev/null
+++ b/internal/proto/evenport_test.go
@@ -0,0 +1,78 @@
+package proto
+
+import (
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestEvenPort(t *testing.T) {
+	t.Run("String", func(t *testing.T) {
+		p := EvenPort{}
+		if p.String() != "reserve: false" {
+			t.Errorf("bad value %q for reselve: false", p.String())
+		}
+		p.ReservePort = true
+		if p.String() != "reserve: true" {
+			t.Errorf("bad value %q for reselve: true", p.String())
+		}
+	})
+	t.Run("False", func(t *testing.T) {
+		m := new(stun.Message)
+		p := EvenPort{
+			ReservePort: false,
+		}
+		if err := p.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		decoded := new(stun.Message)
+		var port EvenPort
+		decoded.Write(m.Raw)
+		if err := port.GetFrom(m); err != nil {
+			t.Fatal(err)
+		}
+		if port != p {
+			t.Fatal("not equal")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		p := EvenPort{
+			ReservePort: true,
+		}
+		if err := p.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			port := EvenPort{}
+			if err := port.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if port != p {
+				t.Errorf("Decoded %q, expected %q", port.String(), p.String())
+			}
+			if wasAllocs(func() {
+				port.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				var handle EvenPort
+				if err := handle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+				m.Add(stun.AttrEvenPort, []byte{1, 2, 3})
+				if !stun.IsAttrSizeInvalid(handle.GetFrom(m)) {
+					t.Error("IsAttrSizeInvalid should be true")
+				}
+			})
+		})
+	})
+}
diff --git a/internal/proto/fuzz_test.go b/internal/proto/fuzz_test.go
new file mode 100644
index 0000000..5898188
--- /dev/null
+++ b/internal/proto/fuzz_test.go
@@ -0,0 +1,53 @@
+// +build gofuzz
+
+package proto
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func corpus(t *testing.T, function, typ string) [][]byte {
+	var data [][]byte
+	p := filepath.Join("fuzz", function, typ)
+	f, err := os.Open(p)
+	if err != nil {
+		if os.IsNotExist(err) {
+			t.Skip("does not exist")
+		}
+		t.Fatal(err)
+	}
+	list, err := f.Readdir(-1)
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, d := range list {
+		if strings.Contains(d.Name(), ".") {
+			// Skipping non-raw files.
+			continue
+		}
+		df, err := os.Open(filepath.Join(p, d.Name()))
+		if err != nil {
+			t.Fatal(err)
+		}
+		buf := make([]byte, 5000)
+		n, _ := df.Read(buf)
+		data = append(data, buf[:n])
+		df.Close()
+	}
+	return data
+}
+
+func TestFuzzSetters_Crashers(t *testing.T) {
+	for _, buf := range corpus(t, "turn-setters", "crashers") {
+		FuzzSetters(buf)
+	}
+}
+
+func TestFuzzSetters_Coverage(t *testing.T) {
+	for _, buf := range corpus(t, "turn-setters", "corpus") {
+		FuzzSetters(buf)
+	}
+}
diff --git a/internal/proto/lifetime_test.go b/internal/proto/lifetime_test.go
new file mode 100644
index 0000000..dc6fdff
--- /dev/null
+++ b/internal/proto/lifetime_test.go
@@ -0,0 +1,121 @@
+package proto
+
+import (
+	"testing"
+
+	"fmt"
+	"time"
+
+	"github.com/pion/stun"
+)
+
+func ExampleLifetime() {
+	// Encoding lifetime to message.
+	m := new(stun.Message)
+	Lifetime{time.Minute}.AddTo(m)
+	m.WriteHeader()
+
+	// Decoding message.
+	mDec := new(stun.Message)
+	if _, err := m.WriteTo(mDec); err != nil {
+		panic(err)
+	}
+	// Decoding lifetime from message.
+	l := Lifetime{}
+	l.GetFrom(m)
+	fmt.Println("Decoded:", l)
+	// Output:
+	// Decoded: 1m0s
+}
+
+func BenchmarkLifetime(b *testing.B) {
+	b.Run("AddTo", func(b *testing.B) {
+		b.ReportAllocs()
+		m := new(stun.Message)
+		for i := 0; i < b.N; i++ {
+			l := Lifetime{time.Second}
+			if err := l.AddTo(m); err != nil {
+				b.Fatal(err)
+			}
+			m.Reset()
+		}
+	})
+	b.Run("GetFrom", func(b *testing.B) {
+		m := new(stun.Message)
+		Lifetime{time.Minute}.AddTo(m)
+		for i := 0; i < b.N; i++ {
+			l := Lifetime{}
+			if err := l.GetFrom(m); err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+}
+
+func TestLifetime(t *testing.T) {
+	t.Run("String", func(t *testing.T) {
+		l := Lifetime{time.Second * 10}
+		if l.String() != "10s" {
+			t.Errorf("bad string %s, expedted 10s", l)
+		}
+	})
+	t.Run("NoAlloc", func(t *testing.T) {
+		m := &stun.Message{}
+		if wasAllocs(func() {
+			// On stack.
+			l := Lifetime{
+				Duration: time.Minute,
+			}
+			l.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+
+		l := &Lifetime{time.Second}
+		if wasAllocs(func() {
+			// On heap.
+			l.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		l := Lifetime{time.Second * 10}
+		if err := l.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			life := Lifetime{}
+			if err := life.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if life != l {
+				t.Errorf("Decoded %q, expected %q", life, l)
+			}
+			if wasAllocs(func() {
+				life.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				nHandle := new(Lifetime)
+				if err := nHandle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+				m.Add(stun.AttrLifetime, []byte{1, 2, 3})
+				if !stun.IsAttrSizeInvalid(nHandle.GetFrom(m)) {
+					t.Error("IsAttrSizeInvalid should be true")
+				}
+			})
+		})
+	})
+}
diff --git a/internal/proto/peeraddr_test.go b/internal/proto/peeraddr_test.go
new file mode 100644
index 0000000..cf486c6
--- /dev/null
+++ b/internal/proto/peeraddr_test.go
@@ -0,0 +1,32 @@
+package proto
+
+import (
+	"net"
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestPeerAddress(t *testing.T) {
+	// Simple tests because already tested in stun.
+	a := PeerAddress{
+		IP:   net.IPv4(111, 11, 1, 2),
+		Port: 333,
+	}
+	t.Run("String", func(t *testing.T) {
+		if a.String() != "111.11.1.2:333" {
+			t.Error("invalid string")
+		}
+	})
+	m := new(stun.Message)
+	if err := a.AddTo(m); err != nil {
+		t.Fatal(err)
+	}
+	m.WriteHeader()
+	decoded := new(stun.Message)
+	decoded.Write(m.Raw)
+	var aGot PeerAddress
+	if err := aGot.GetFrom(decoded); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/internal/proto/proto_test.go b/internal/proto/proto_test.go
new file mode 100644
index 0000000..edef69a
--- /dev/null
+++ b/internal/proto/proto_test.go
@@ -0,0 +1,33 @@
+package proto
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+const allocRuns = 10
+
+// wasAllocs returns true if f allocates memory.
+func wasAllocs(f func()) bool {
+	return testing.AllocsPerRun(allocRuns, f) > 0
+}
+
+func loadData(tb testing.TB, name string) []byte {
+	name = filepath.Join("testdata", name)
+	f, err := os.Open(name)
+	if err != nil {
+		tb.Fatal(err)
+	}
+	defer func() {
+		if errClose := f.Close(); errClose != nil {
+			tb.Fatal(errClose)
+		}
+	}()
+	v, err := ioutil.ReadAll(f)
+	if err != nil {
+		tb.Fatal(err)
+	}
+	return v
+}
diff --git a/internal/proto/relayedaddr_test.go b/internal/proto/relayedaddr_test.go
new file mode 100644
index 0000000..32c6d77
--- /dev/null
+++ b/internal/proto/relayedaddr_test.go
@@ -0,0 +1,32 @@
+package proto
+
+import (
+	"net"
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestRelayedAddress(t *testing.T) {
+	// Simple tests because already tested in stun.
+	a := RelayedAddress{
+		IP:   net.IPv4(111, 11, 1, 2),
+		Port: 333,
+	}
+	t.Run("String", func(t *testing.T) {
+		if a.String() != "111.11.1.2:333" {
+			t.Error("invalid string")
+		}
+	})
+	m := new(stun.Message)
+	if err := a.AddTo(m); err != nil {
+		t.Fatal(err)
+	}
+	m.WriteHeader()
+	decoded := new(stun.Message)
+	decoded.Write(m.Raw)
+	var aGot RelayedAddress
+	if err := aGot.GetFrom(decoded); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/internal/proto/reqfamily_test.go b/internal/proto/reqfamily_test.go
new file mode 100644
index 0000000..d996416
--- /dev/null
+++ b/internal/proto/reqfamily_test.go
@@ -0,0 +1,88 @@
+package proto
+
+import (
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestRequestedAddressFamily(t *testing.T) {
+	t.Run("String", func(t *testing.T) {
+		if RequestedFamilyIPv4.String() != "IPv4" {
+			t.Errorf("bad string %q, expected %q", RequestedFamilyIPv4,
+				"IPv4",
+			)
+		}
+		if RequestedFamilyIPv6.String() != "IPv6" {
+			t.Errorf("bad string %q, expected %q", RequestedFamilyIPv6,
+				"IPv6",
+			)
+		}
+		if RequestedAddressFamily(0x04).String() != "unknown" {
+			t.Error("should be unknown")
+		}
+	})
+	t.Run("NoAlloc", func(t *testing.T) {
+		m := &stun.Message{}
+		if wasAllocs(func() {
+			// On stack.
+			r := RequestedFamilyIPv4
+			r.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+
+		r := new(RequestedAddressFamily)
+		*r = RequestedFamilyIPv4
+		if wasAllocs(func() {
+			// On heap.
+			r.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		r := RequestedFamilyIPv4
+		if err := r.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			var req RequestedAddressFamily
+			if err := req.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if req != r {
+				t.Errorf("Decoded %q, expected %q", req, r)
+			}
+			if wasAllocs(func() {
+				r.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				var handle RequestedAddressFamily
+				if err := handle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+				m.Add(stun.AttrRequestedAddressFamily, []byte{1, 2, 3})
+				if !stun.IsAttrSizeInvalid(handle.GetFrom(m)) {
+					t.Error("IsAttrSizeInvalid should be true")
+				}
+				m.Reset()
+				m.Add(stun.AttrRequestedAddressFamily, []byte{5, 0, 0, 0})
+				if handle.GetFrom(m) == nil {
+					t.Error("should error on invalid value")
+				}
+			})
+		})
+	})
+}
diff --git a/internal/proto/reqtrans_test.go b/internal/proto/reqtrans_test.go
new file mode 100644
index 0000000..46f1c77
--- /dev/null
+++ b/internal/proto/reqtrans_test.go
@@ -0,0 +1,93 @@
+package proto
+
+import (
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestRequestedTransport(t *testing.T) {
+	t.Run("String", func(t *testing.T) {
+		r := RequestedTransport{
+			Protocol: ProtoUDP,
+		}
+		if r.String() != "protocol: UDP" {
+			t.Errorf("bad string %q, expected %q", r,
+				"protocol: UDP",
+			)
+		}
+		r.Protocol = 254
+		if r.String() != "protocol: 254" {
+			if r.String() != "protocol: UDP" {
+				t.Errorf("bad string %q, expected %q", r,
+					"protocol: 254",
+				)
+			}
+		}
+	})
+	t.Run("NoAlloc", func(t *testing.T) {
+		m := &stun.Message{}
+		if wasAllocs(func() {
+			// On stack.
+			r := RequestedTransport{
+				Protocol: ProtoUDP,
+			}
+			r.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+
+		r := &RequestedTransport{
+			Protocol: ProtoUDP,
+		}
+		if wasAllocs(func() {
+			// On heap.
+			r.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		r := RequestedTransport{
+			Protocol: ProtoUDP,
+		}
+		if err := r.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			req := RequestedTransport{
+				Protocol: ProtoUDP,
+			}
+			if err := req.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if req != r {
+				t.Errorf("Decoded %q, expected %q", req, r)
+			}
+			if wasAllocs(func() {
+				r.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				var handle RequestedTransport
+				if err := handle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+				m.Add(stun.AttrRequestedTransport, []byte{1, 2, 3})
+				if !stun.IsAttrSizeInvalid(handle.GetFrom(m)) {
+					t.Error("IsAttrSizeInvalid should be true")
+				}
+			})
+		})
+	})
+}
diff --git a/internal/proto/rsrvtoken_test.go b/internal/proto/rsrvtoken_test.go
new file mode 100644
index 0000000..72e1bb3
--- /dev/null
+++ b/internal/proto/rsrvtoken_test.go
@@ -0,0 +1,77 @@
+package proto
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/pion/stun"
+)
+
+func TestReservationToken(t *testing.T) {
+	t.Run("NoAlloc", func(t *testing.T) {
+		m := &stun.Message{}
+		tok := make([]byte, 8)
+		if wasAllocs(func() {
+			// On stack.
+			tk := ReservationToken(tok)
+			tk.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+
+		tk := make(ReservationToken, 8)
+		if wasAllocs(func() {
+			// On heap.
+			tk.AddTo(m)
+			m.Reset()
+		}) {
+			t.Error("Unexpected allocations")
+		}
+	})
+	t.Run("AddTo", func(t *testing.T) {
+		m := new(stun.Message)
+		tk := make(ReservationToken, 8)
+		tk[2] = 33
+		tk[7] = 1
+		if err := tk.AddTo(m); err != nil {
+			t.Error(err)
+		}
+		m.WriteHeader()
+		t.Run("HandleErr", func(t *testing.T) {
+			badTk := ReservationToken{34, 45}
+			if !stun.IsAttrSizeInvalid(badTk.AddTo(m)) {
+				t.Error("IsAttrSizeInvalid should be true")
+			}
+		})
+		t.Run("GetFrom", func(t *testing.T) {
+			decoded := new(stun.Message)
+			if _, err := decoded.Write(m.Raw); err != nil {
+				t.Fatal("failed to decode message:", err)
+			}
+			var tok ReservationToken
+			if err := tok.GetFrom(decoded); err != nil {
+				t.Fatal(err)
+			}
+			if !bytes.Equal(tok, tk) {
+				t.Errorf("Decoded %v, expected %v", tok, tk)
+			}
+			if wasAllocs(func() {
+				tok.GetFrom(decoded)
+			}) {
+				t.Error("Unexpected allocations")
+			}
+			t.Run("HandleErr", func(t *testing.T) {
+				m := new(stun.Message)
+				var handle ReservationToken
+				if err := handle.GetFrom(m); err != stun.ErrAttributeNotFound {
+					t.Errorf("%v should be not found", err)
+				}
+				m.Add(stun.AttrReservationToken, []byte{1, 2, 3})
+				if !stun.IsAttrSizeInvalid(handle.GetFrom(m)) {
+					t.Error("IsAttrSizeInvalid should be true")
+				}
+			})
+		})
+	})
+}
diff --git a/internal/proto/testdata/01_chromeallocreq.hex b/internal/proto/testdata/01_chromeallocreq.hex
new file mode 100644
index 0000000..e7ec7a3
--- /dev/null
+++ b/internal/proto/testdata/01_chromeallocreq.hex
@@ -0,0 +1,4 @@
+000300242112a442626b4a6849664c3630526863802f0016687474703a2f2f6c6f63616c686f73743a333030302f00000019000411000000
+011300582112a442626b4a6849664c36305268630009001000000401556e617574686f72697a656400150010356130323039623563623830363130360014000b61312e63796465762e7275758022001a436f7475726e2d342e352e302e33202764616e204569646572272300
+0003006c2112a442324e50695a437a4634535034802f0016687474703a2f2f6c6f63616c686f73743a333030302f000000190004110000000006000665726e61646f00000014000b61312e63796465762e7275000015001035613032303962356362383036313036000800145c8743f3b64bec0880cdd8d476d37b801a6c3d33
+010300582112a442324e50695a437a4634535034001600080001fb922b1ab211002000080001adb2f49f38ae000d0004000002588022001a436f7475726e2d342e352e302e33202764616e204569646572277475000800145d7e85b767a519ffce91dbf0a96775e370db92e3
diff --git a/internal/proto/testdata/02_chandata.hex b/internal/proto/testdata/02_chandata.hex
new file mode 100644
index 0000000..9b23911
--- /dev/null
+++ b/internal/proto/testdata/02_chandata.hex
@@ -0,0 +1,2 @@
+40000064000100502112a442453731722f2b322b6e4e7a5800060009443758343a33776c59000000c0570004000003e7802a00081d5136dab65b169300250000002400046e001eff0008001465d11a330e104a9f5f598af4abc6a805f26003cf802800046b334442
+4000022316fefd0000000000000011012c0b000120000100000000012000011d00011a308201163081bda003020102020900afe52871340bd13e300a06082a8648ce3d0403023011310f300d06035504030c06576562525443301e170d3138303831313033353230305a170d3138303931313033353230305a3011310f300d06035504030c065765625254433059301306072a8648ce3d020106082a8648ce3d030107034200048080e348bd41469cfb7a7df316676fd72a06211765a50a0f0b07526c872dcf80093ed5caa3f5a40a725dd74b41b79bdd19ee630c5313c8601d6983286c8722c1300a06082a8648ce3d0403020348003045022100d13a0a131bc2a9f27abd3d4c547f7ef172996a0c0755c707b6a3e048d8762ded0220055fc8182818a644a3d3b5b157304cc3f1421fadb06263bfb451cd28be4bc9ee16fefd0000000000000012002d10000021000200000000002120f7e23c97df45a96e13cb3e76b37eff5e73e2aee0b6415d29443d0bd24f578b7e16fefd000000000000001300580f00004c000300000000004c040300483046022100fdbb74eab1aca1532e6ac0ab267d5b83a24bb4d5d7d504936e2785e6e388b2bd022100f6a457b9edd9ead52a9d0e9a19240b3a68b95699546c044f863cf8349bc8046214fefd000000000000001400010116fefd0001000000000004003000010000000000040aae2421e7d549632a7def8ed06898c3c5b53f5b812a963a39ab6cdd303b79bdb237f3314c1da21b
diff --git a/internal/proto/uri_test.go b/internal/proto/uri_test.go
new file mode 100644
index 0000000..bd46605
--- /dev/null
+++ b/internal/proto/uri_test.go
@@ -0,0 +1,160 @@
+package proto
+
+import "testing"
+
+func TestParseURI(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		in   string
+		out  URI
+	}{
+		{
+			name: "default",
+			in:   "turn:example.org",
+			out: URI{
+				Host:   "example.org",
+				Scheme: Scheme,
+			},
+		},
+		{
+			name: "secure",
+			in:   "turns:example.org",
+			out: URI{
+				Host:   "example.org",
+				Scheme: SchemeSecure,
+			},
+		},
+		{
+			name: "with port",
+			in:   "turn:example.org:8000",
+			out: URI{
+				Host:   "example.org",
+				Scheme: Scheme,
+				Port:   8000,
+			},
+		},
+		{
+			name: "with port and transport",
+			in:   "turn:example.org:8000?transport=tcp",
+			out: URI{
+				Host:      "example.org",
+				Scheme:    Scheme,
+				Port:      8000,
+				Transport: TransportTCP,
+			},
+		},
+		{
+			name: "with transport",
+			in:   "turn:example.org?transport=udp",
+			out: URI{
+				Host:      "example.org",
+				Scheme:    Scheme,
+				Transport: TransportUDP,
+			},
+		},
+		{
+			name: "with port and custom transport",
+			in:   "turns:example.org:8000?transport=quic",
+			out: URI{
+				Host:      "example.org",
+				Scheme:    SchemeSecure,
+				Port:      8000,
+				Transport: "quic",
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			out, parseErr := ParseURI(tc.in)
+			if parseErr != nil {
+				t.Fatal(parseErr)
+			}
+			if out != tc.out {
+				t.Errorf("%s != %s", out, tc.out)
+			}
+		})
+	}
+	t.Run("MustFail", func(t *testing.T) {
+		for _, tc := range []struct {
+			name string
+			in   string
+		}{
+			{
+				name: "hierarchical",
+				in:   "turn://example.org",
+			},
+			{
+				name: "bad port",
+				in:   "turn:example.org:port",
+			},
+			{
+				name: "bad scheme",
+				in:   "tcp:example.org",
+			},
+			{
+				name: "invalid uri scheme",
+				in:   "turn_s:test",
+			},
+		} {
+			t.Run(tc.name, func(t *testing.T) {
+				_, parseErr := ParseURI(tc.in)
+				if parseErr == nil {
+					t.Fatal("should fail, but did not")
+				}
+			})
+		}
+	})
+}
+
+func TestURI_String(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		uri  URI
+		out  string
+	}{
+		{
+			name: "blank",
+			out:  ":",
+		},
+		{
+			name: "simple",
+			uri: URI{
+				Host:   "example.org",
+				Scheme: Scheme,
+			},
+			out: "turn:example.org",
+		},
+		{
+			name: "secure",
+			uri: URI{
+				Host:   "example.org",
+				Scheme: SchemeSecure,
+			},
+			out: "turns:example.org",
+		},
+		{
+			name: "secure with port",
+			uri: URI{
+				Host:   "example.org",
+				Scheme: SchemeSecure,
+				Port:   443,
+			},
+			out: "turns:example.org:443",
+		},
+		{
+			name: "secure with transport",
+			uri: URI{
+				Host:      "example.org",
+				Scheme:    SchemeSecure,
+				Port:      443,
+				Transport: "tcp",
+			},
+			out: "turns:example.org:443?transport=tcp",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			if v := tc.uri.String(); v != tc.out {
+				t.Errorf("%q != %q", v, tc.out)
+			}
+		})
+	}
+}
diff --git a/server_test.go b/server_test.go
index 78e21c9..6782776 100644
--- a/server_test.go
+++ b/server_test.go
@@ -5,8 +5,8 @@ import (
 	"testing"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/logging"
+	"github.com/pion/turn/internal/proto"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -31,7 +31,7 @@ func TestServer(t *testing.T) {
 			LoggerFactory: loggerFactory,
 		})
 
-		assert.Equal(t, turn.DefaultLifetime, server.channelBindTimeout, "should match")
+		assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match")
 
 		err := server.AddListeningIPAddr("127.0.0.1")
 		assert.NoError(t, err, "should succeed")
@@ -87,7 +87,7 @@ func TestServer(t *testing.T) {
 			LoggerFactory: loggerFactory,
 		})
 
-		assert.Equal(t, turn.DefaultLifetime, server.channelBindTimeout, "should match")
+		assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match")
 
 		err := server.AddListeningIPAddr("127.0.0.1")
 		assert.NoError(t, err, "should succeed")
@@ -120,7 +120,7 @@ func TestServer(t *testing.T) {
 			LoggerFactory: loggerFactory,
 		})
 
-		assert.Equal(t, turn.DefaultLifetime, server.channelBindTimeout, "should match")
+		assert.Equal(t, proto.DefaultLifetime, server.channelBindTimeout, "should match")
 
 		err := server.AddListeningIPAddr("127.0.0.1")
 		assert.NoError(t, err, "should succeed")
diff --git a/turn_test.go b/turn_test.go
index bdc8bbc..41424b3 100644
--- a/turn_test.go
+++ b/turn_test.go
@@ -4,19 +4,19 @@ import (
 	"testing"
 	"time"
 
-	"github.com/gortc/turn"
 	"github.com/pion/stun"
+	"github.com/pion/turn/internal/proto"
 )
 
 func TestAllocationLifeTime(t *testing.T) {
-	lifetime := turn.Lifetime{
+	lifetime := proto.Lifetime{
 		Duration: 5 * time.Second,
 	}
 
 	m := &stun.Message{}
 	lifetimeDuration := allocationLifeTime(m)
 
-	if lifetimeDuration != turn.DefaultLifetime {
+	if lifetimeDuration != proto.DefaultLifetime {
 		t.Errorf("Allocation lifetime should be default time duration")
 	}
 
@@ -31,7 +31,7 @@ func TestAllocationLifeTime(t *testing.T) {
 
 	// If lifetime is bigger than maximumLifetime
 	{
-		lifetime := turn.Lifetime{
+		lifetime := proto.Lifetime{
 			Duration: maximumLifetime * 2,
 		}
 
@@ -39,8 +39,8 @@ func TestAllocationLifeTime(t *testing.T) {
 		_ = lifetime.AddTo(m2)
 
 		lifetimeDuration := allocationLifeTime(m2)
-		if lifetimeDuration != turn.DefaultLifetime {
-			t.Errorf("Expect lifetimeDuration is %s, but %s", turn.DefaultLifetime, lifetimeDuration)
+		if lifetimeDuration != proto.DefaultLifetime {
+			t.Errorf("Expect lifetimeDuration is %s, but %s", proto.DefaultLifetime, lifetimeDuration)
 		}
 	}