1
0
Fork 0
mirror of https://github.com/ossrs/srs.git synced 2025-03-09 15:49:59 +00:00

For regression test, add srs-bench to 3rdparty

This commit is contained in:
winlin 2021-03-04 13:23:01 +08:00
parent de87dd427d
commit 876210f6c9
1158 changed files with 256967 additions and 3 deletions

View file

@ -0,0 +1,9 @@
*.sw[poe]
examples/turn-client/tcp/tcp
examples/turn-client/udp/udp
examples/turn-server/add-software-attribute/add-software-attribute
examples/turn-server/log/log
examples/turn-server/simple/simple
examples/turn-server/tcp/tcp
examples/lt-cred-generator/lt-cred-generator
examples/turn-server/lt-cred/lt-cred

View file

@ -0,0 +1,88 @@
linters-settings:
govet:
check-shadowing: true
misspell:
locale: US
exhaustive:
default-signifies-exhaustive: true
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
linters:
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bodyclose # checks whether HTTP response body is closed successfully
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- exhaustive # check exhaustiveness of enum switch statements
- exportloopref # checks for pointers to enclosing loop variables
- gci # Gci control golang package import order and make it always deterministic.
- gochecknoglobals # Checks that no globals are present in Go code
- gochecknoinits # Checks that no init functions are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goerr113 # Golang linter to check the errors handling expressions
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
- goheader # Checks is file header matches to pattern
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- gosimple # Linter for Go source code that specializes in simplifying a code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # Detects when assignments to existing variables are not used
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- noctx # noctx finds sending http request without context.Context
- scopelint # Scopelint checks for unpinned variables in go programs
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- structcheck # Finds unused struct fields
- stylecheck # Stylecheck is a replacement for golint
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
- whitespace # Tool for detection of leading and trailing whitespace
disable:
- funlen # Tool for detection of long functions
- gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- gomnd # An analyzer to detect magic numbers.
- lll # Reports long lines
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted
- nestif # Reports deeply nested if statements
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- nolintlint # Reports ill-formed or insufficient nolint directives
- prealloc # Finds slice declarations that could potentially be preallocated
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed.
- testpackage # linter that makes you use a separate _test package
- wsl # Whitespace Linter - Forces you to use empty lines!
issues:
exclude-rules:
# Allow complex tests, better to be self contained
- path: _test\.go
linters:
- gocognit
# Allow complex main function in examples
- path: examples
text: "of func `main` is high"
linters:
- gocognit
run:
skip-dirs-use-default: false

View file

@ -0,0 +1,109 @@
before:
hooks:
- go mod tidy
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
builds:
- binary: turn-client-tcp
id: turn-client-tcp
goos:
- darwin
- windows
- linux
- freebsd
goarch:
- amd64
- 386
env:
- CGO_ENABLED=0
main: ./examples/turn-client/tcp
- binary: turn-client-udp
id: turn-client-udp
goos:
- darwin
- windows
- linux
- freebsd
goarch:
- amd64
- 386
env:
- CGO_ENABLED=0
main: ./examples/turn-client/udp
- binary: turn-server-add-software-attribute
id: turn-server-add-software-attribute
goos:
- darwin
- windows
- linux
- freebsd
goarch:
- amd64
- 386
env:
- CGO_ENABLED=0
main: ./examples/turn-server/add-software-attribute
- binary: turn-server-log
id: turn-server-log
goos:
- darwin
- windows
- linux
- freebsd
goarch:
- amd64
- 386
env:
- CGO_ENABLED=0
main: ./examples/turn-server/log
- binary: turn-server-simple
id: turn-server-simple
goos:
- darwin
- windows
- linux
- freebsd
goarch:
- amd64
- 386
env:
- CGO_ENABLED=0
main: ./examples/turn-server/simple/
- binary: turn-server-tcp
id: turn-server-tcp
goos:
- darwin
- windows
- linux
- freebsd
goarch:
- amd64
- 386
env:
- CGO_ENABLED=0
main: ./examples/turn-server/tcp/

View file

@ -0,0 +1,31 @@
# Why Pion TURN
TURN servers aren't exactly a hot technology, they are usually an after thought when building something. Most of the time
beginners build an interesting WebRTC application, but at the very end realize they need a TURN server. It is really frustrating when you
want to share your cool new project, only to realize you have to run another service.
Then you find yourself building from source, fighting with config files and making changes you don't fully understand. Pion TURN was born
hoping to solve these frustrations. These are the guiding principals/features that define pion-turn.
## Easy setup
simple-turn is a statically built TURN server, configured by environment variables. The entire install setup is 5 commands, on any platform!
The goal is that anyone should be able to run a TURN server on any platform.
## Integration first
pion-turn makes no assumptions about how you authenticate users, how you log, or even your topology! Instead of running a dedicated TURN server you
can inherit from github.com/pion/turn and set whatever logger you want.
## Embeddable
You can add this to an existing service. This means all your config files stay homogeneous instead of having the mismatch that makes it harder to manage your services.
For small setups it is usually an overkill to deploy dedicated TURN servers, this makes it easier to solve the problems you care about.
## Safe
Golang provides a great foundation to build safe network services. Especially when running a networked service that is highly concurrent bugs can be devastating.
## Readable
All network interaction is commented with a link to the spec. This makes learning and debugging easier, the TURN server was written to also serve as a guide for others.
## Tested
Every commit is tested via travis-ci Go provides fantastic facilities for testing, and more will be added as time goes on.
## Shared libraries
Every pion product is built using shared libraries, allowing others to build things using existing tested STUN and TURN tools.

View file

@ -0,0 +1,7 @@
Copyright 2018 Pion LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,96 @@
<h1 align="center">
<a href="https://pion.ly"><img src="./.github/gopher-pion.png" alt="Pion TURN" height="250px"></a>
<br>
Pion TURN
<br>
</h1>
<h4 align="center">A toolkit for building TURN clients and servers in Go</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-turn-gray.svg?longCache=true&colorB=brightgreen" alt="Pion TURN"></a>
<a href="http://gophers.slack.com/messages/pion"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
<a href="https://github.com/pion/awesome-pion" alt="Awesome Pion"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
<br>
<a href="https://travis-ci.org/pion/turn"><img src="https://travis-ci.org/pion/turn.svg?branch=master" alt="Build Status"></a>
<a href="https://pkg.go.dev/github.com/pion/turn/v2"><img src="https://godoc.org/github.com/pion/turn?status.svg" alt="GoDoc"></a>
<a href="https://codecov.io/gh/pion/turn"><img src="https://codecov.io/gh/pion/turn/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/turn"><img src="https://goreportcard.com/badge/github.com/pion/turn" alt="Go Report Card"></a>
<a href="https://www.codacy.com/app/pion/turn"><img src="https://api.codacy.com/project/badge/Grade/d53ec6c70576476cb16c140c2964afde" alt="Codacy Badge"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
<br>
Pion TURN is a Go toolkit for building TURN servers and clients. We wrote it to solve problems we had when building RTC projects.
* **Deployable** - Use modern tooling of the Go ecosystem. Stop generating config files.
* **Embeddable** - Include `pion/turn` in your existing applications. No need to manage another service.
* **Extendable** - TURN as an API so you can easily integrate with your existing monitoring and metrics.
* **Maintainable** - `pion/turn` is simple and well documented. Designed for learning and easy debugging.
* **Portable** - Quickly deploy to multiple architectures/platforms just by setting an environment variable.
* **Safe** - Stability and safety is important for network services. Go provides everything we need.
* **Scalable** - Create allocations and mutate state at runtime. Designed to make scaling easy.
# Using
`pion/turn` is an API for building STUN/TURN clients and servers, not a binary you deploy then configure. It may require copying our examples and
making minor modifications to fit your need, no knowledge of Go is required however. You may be able to download the pre-made binaries of our examples
if you wish to get started quickly.
The advantage of this is that you don't need to deal with complicated config files, or custom APIs to modify the state of Pion TURN.
After you instantiate an instance of a Pion TURN server or client you interact with it like any library. The quickest way to get started is to look at the
[examples](examples) or [GoDoc](https://godoc.org/github.com/pion/turn)
# Examples
We try to cover most common use cases in [examples](examples). If more examples could be helpful please file an issue, we are always looking
to expand and improve `pion/turn` to make it easier for developers.
To build any example you just need to run `go build` in the directory of the example you care about.
It is also very easy to [cross compile](https://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5) Go programs.
You can also see `pion/turn` usage in [pion/ice](https://github.com/pion/ice)
# [FAQ](https://github.com/pion/webrtc/wiki/FAQ)
### RFCs
#### Implemented
* [RFC 5389: Session Traversal Utilities for NAT (STUN)](https://tools.ietf.org/html/rfc5389)
* [RFC 5766: Traversal Using Relays around NAT (TURN)](https://tools.ietf.org/html/rfc5766)
#### Planned
* [RFC 6062: Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations](https://tools.ietf.org/html/rfc6062)
* [RFC 6156: Traversal Using Relays around NAT (TURN) Extension for IPv6](https://tools.ietf.org/html/rfc6156)
### Community
Pion has an active community on the [Golang Slack](https://pion.ly/slack). Sign up and join the **#pion** channel for discussions and support.
We are always looking to support **your projects**. Please reach out if you have something to build!
### Contributing
Check out the [CONTRIBUTING.md](CONTRIBUTING.md) to join the group of amazing people making this project possible:
* [Michiel De Backker](https://github.com/backkem) - *Documentation*
* [Ingmar Wittkau](https://github.com/iwittkau) - *STUN client*
* [John Bradley](https://github.com/kc5nra) - *Original Author*
* [jose nazario](https://github.com/paralax) - *Documentation*
* [Mészáros Mihály](https://github.com/misi) - *Documentation*
* [Mike Santry](https://github.com/santrym) - *Mascot*
* [Sean DuBois](https://github.com/Sean-Der) - *Original Author*
* [winds2016](https://github.com/winds2016) - *Windows platform testing*
* [songjiayang](https://github.com/songjiayang) - *SongJiaYang*
* [Yutaka Takeda](https://github.com/enobufs) - *vnet*
* [namreg](https://github.com/namreg) - *Igor German*
* [Aleksandr Razumov](https://github.com/ernado) - *protocol*
* [Robert Eperjesi](https://github.com/epes)
* [Lukas Rezek](https://github.com/lrezek)
* [Hugo Arregui](https://github.com/hugoArregui)
* [Aaron France](https://github.com/AeroNotix)
* [Atsushi Watanabe](https://github.com/at-wat)
* [Tom Clift](https://github.com/tclift)
* [lllf](https://github.com/LittleLightLittleFire)
* nindolabs (Marouane)
* [Onwuka Gideon](https://github.com/dongido001)
* [Herman Banken](https://github.com/hermanbanken)
* [Jannis Mattheis](https://github.com/jmattheis)
### License
MIT License - see [LICENSE.md](LICENSE.md) for full text

View file

@ -0,0 +1,569 @@
package turn
import (
b64 "encoding/base64"
"fmt"
"math"
"net"
"sync"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
"github.com/pion/transport/vnet"
"github.com/pion/turn/v2/internal/client"
"github.com/pion/turn/v2/internal/proto"
)
const (
defaultRTO = 200 * time.Millisecond
maxRtxCount = 7 // total 7 requests (Rc)
maxDataBufferSize = math.MaxUint16 // message size limit for Chromium
)
// interval [msec]
// 0: 0 ms +500
// 1: 500 ms +1000
// 2: 1500 ms +2000
// 3: 3500 ms +4000
// 4: 7500 ms +8000
// 5: 15500 ms +16000
// 6: 31500 ms +32000
// -: 63500 ms failed
// ClientConfig is a bag of config parameters for Client.
type ClientConfig struct {
STUNServerAddr string // STUN server address (e.g. "stun.abc.com:3478")
TURNServerAddr string // TURN server addrees (e.g. "turn.abc.com:3478")
Username string
Password string
Realm string
Software string
RTO time.Duration
Conn net.PacketConn // Listening socket (net.PacketConn)
LoggerFactory logging.LoggerFactory
Net *vnet.Net
}
// Client is a STUN server client
type Client struct {
conn net.PacketConn // read-only
stunServ net.Addr // read-only
turnServ net.Addr // read-only
stunServStr string // read-only, used for dmuxing
turnServStr string // read-only, used for dmuxing
username stun.Username // read-only
password string // read-only
realm stun.Realm // read-only
integrity stun.MessageIntegrity // read-only
software stun.Software // read-only
trMap *client.TransactionMap // thread-safe
rto time.Duration // read-only
relayedConn *client.UDPConn // protected by mutex ***
allocTryLock client.TryLock // thread-safe
listenTryLock client.TryLock // thread-safe
net *vnet.Net // read-only
mutex sync.RWMutex // thread-safe
mutexTrMap sync.Mutex // thread-safe
log logging.LeveledLogger // read-only
}
// NewClient returns a new Client instance. listeningAddress is the address and port to listen on, default "0.0.0.0:0"
func NewClient(config *ClientConfig) (*Client, error) {
loggerFactory := config.LoggerFactory
if loggerFactory == nil {
loggerFactory = logging.NewDefaultLoggerFactory()
}
log := loggerFactory.NewLogger("turnc")
if config.Conn == nil {
return nil, errNilConn
}
if config.Net == nil {
config.Net = vnet.NewNet(nil) // defaults to native operation
} else if config.Net.IsVirtual() {
log.Warn("vnet is enabled")
}
var stunServ, turnServ net.Addr
var stunServStr, turnServStr string
var err error
if len(config.STUNServerAddr) > 0 {
log.Debugf("resolving %s", config.STUNServerAddr)
stunServ, err = config.Net.ResolveUDPAddr("udp4", config.STUNServerAddr)
if err != nil {
return nil, err
}
stunServStr = stunServ.String()
log.Debugf("stunServ: %s", stunServStr)
}
if len(config.TURNServerAddr) > 0 {
log.Debugf("resolving %s", config.TURNServerAddr)
turnServ, err = config.Net.ResolveUDPAddr("udp4", config.TURNServerAddr)
if err != nil {
return nil, err
}
turnServStr = turnServ.String()
log.Debugf("turnServ: %s", turnServStr)
}
rto := defaultRTO
if config.RTO > 0 {
rto = config.RTO
}
c := &Client{
conn: config.Conn,
stunServ: stunServ,
turnServ: turnServ,
stunServStr: stunServStr,
turnServStr: turnServStr,
username: stun.NewUsername(config.Username),
password: config.Password,
realm: stun.NewRealm(config.Realm),
software: stun.NewSoftware(config.Software),
net: config.Net,
trMap: client.NewTransactionMap(),
rto: rto,
log: log,
}
return c, nil
}
// TURNServerAddr return the TURN server address
func (c *Client) TURNServerAddr() net.Addr {
return c.turnServ
}
// STUNServerAddr return the STUN server address
func (c *Client) STUNServerAddr() net.Addr {
return c.stunServ
}
// Username returns username
func (c *Client) Username() stun.Username {
return c.username
}
// Realm return realm
func (c *Client) Realm() stun.Realm {
return c.realm
}
// WriteTo sends data to the specified destination using the base socket.
func (c *Client) WriteTo(data []byte, to net.Addr) (int, error) {
return c.conn.WriteTo(data, to)
}
// Listen will have this client start listening on the conn provided via the config.
// This is optional. If not used, you will need to call HandleInbound method
// to supply incoming data, instead.
func (c *Client) Listen() error {
if err := c.listenTryLock.Lock(); err != nil {
return fmt.Errorf("%w: %s", errAlreadyListening, err.Error())
}
go func() {
buf := make([]byte, maxDataBufferSize)
for {
n, from, err := c.conn.ReadFrom(buf)
if err != nil {
c.log.Debugf("exiting read loop: %s", err.Error())
break
}
_, err = c.HandleInbound(buf[:n], from)
if err != nil {
c.log.Debugf("exiting read loop: %s", err.Error())
break
}
}
c.listenTryLock.Unlock()
}()
return nil
}
// Close closes this client
func (c *Client) Close() {
c.mutexTrMap.Lock()
defer c.mutexTrMap.Unlock()
c.trMap.CloseAndDeleteAll()
}
// TransactionID & Base64: https://play.golang.org/p/EEgmJDI971P
// SendBindingRequestTo sends a new STUN request to the given transport address
func (c *Client) SendBindingRequestTo(to net.Addr) (net.Addr, error) {
attrs := []stun.Setter{stun.TransactionID, stun.BindingRequest}
if len(c.software) > 0 {
attrs = append(attrs, c.software)
}
msg, err := stun.Build(attrs...)
if err != nil {
return nil, err
}
trRes, err := c.PerformTransaction(msg, to, false)
if err != nil {
return nil, err
}
var reflAddr stun.XORMappedAddress
if err := reflAddr.GetFrom(trRes.Msg); err != nil {
return nil, err
}
return &net.UDPAddr{
IP: reflAddr.IP,
Port: reflAddr.Port,
}, nil
}
// SendBindingRequest sends a new STUN request to the STUN server
func (c *Client) SendBindingRequest() (net.Addr, error) {
if c.stunServ == nil {
return nil, errSTUNServerAddressNotSet
}
return c.SendBindingRequestTo(c.stunServ)
}
// Allocate sends a TURN allocation request to the given transport address
func (c *Client) Allocate() (net.PacketConn, error) {
if err := c.allocTryLock.Lock(); err != nil {
return nil, fmt.Errorf("%w: %s", errOneAllocateOnly, err.Error())
}
defer c.allocTryLock.Unlock()
relayedConn := c.relayedUDPConn()
if relayedConn != nil {
return nil, fmt.Errorf("%w: %s", errAlreadyAllocated, relayedConn.LocalAddr().String())
}
msg, err := stun.Build(
stun.TransactionID,
stun.NewType(stun.MethodAllocate, stun.ClassRequest),
proto.RequestedTransport{Protocol: proto.ProtoUDP},
stun.Fingerprint,
)
if err != nil {
return nil, err
}
trRes, err := c.PerformTransaction(msg, c.turnServ, false)
if err != nil {
return nil, err
}
res := trRes.Msg
// Anonymous allocate failed, trying to authenticate.
var nonce stun.Nonce
if err = nonce.GetFrom(res); err != nil {
return nil, err
}
if err = c.realm.GetFrom(res); err != nil {
return nil, err
}
c.realm = append([]byte(nil), c.realm...)
c.integrity = stun.NewLongTermIntegrity(
c.username.String(), c.realm.String(), c.password,
)
// Trying to authorize.
msg, err = stun.Build(
stun.TransactionID,
stun.NewType(stun.MethodAllocate, stun.ClassRequest),
proto.RequestedTransport{Protocol: proto.ProtoUDP},
&c.username,
&c.realm,
&nonce,
&c.integrity,
stun.Fingerprint,
)
if err != nil {
return nil, err
}
trRes, err = c.PerformTransaction(msg, c.turnServ, false)
if err != nil {
return nil, err
}
res = trRes.Msg
if res.Type.Class == stun.ClassErrorResponse {
var code stun.ErrorCodeAttribute
if err = code.GetFrom(res); err == nil {
return nil, fmt.Errorf("%s (error %s)", res.Type, code) //nolint:goerr113
}
return nil, fmt.Errorf("%s", res.Type) //nolint:goerr113
}
// Getting relayed addresses from response.
var relayed proto.RelayedAddress
if err := relayed.GetFrom(res); err != nil {
return nil, err
}
relayedAddr := &net.UDPAddr{
IP: relayed.IP,
Port: relayed.Port,
}
// Getting lifetime from response
var lifetime proto.Lifetime
if err := lifetime.GetFrom(res); err != nil {
return nil, err
}
relayedConn = client.NewUDPConn(&client.UDPConnConfig{
Observer: c,
RelayedAddr: relayedAddr,
Integrity: c.integrity,
Nonce: nonce,
Lifetime: lifetime.Duration,
Log: c.log,
})
c.setRelayedUDPConn(relayedConn)
return relayedConn, nil
}
// PerformTransaction performs STUN transaction
func (c *Client) PerformTransaction(msg *stun.Message, to net.Addr, ignoreResult bool) (client.TransactionResult,
error) {
trKey := b64.StdEncoding.EncodeToString(msg.TransactionID[:])
raw := make([]byte, len(msg.Raw))
copy(raw, msg.Raw)
tr := client.NewTransaction(&client.TransactionConfig{
Key: trKey,
Raw: raw,
To: to,
Interval: c.rto,
IgnoreResult: ignoreResult,
})
c.trMap.Insert(trKey, tr)
c.log.Tracef("start %s transaction %s to %s", msg.Type, trKey, tr.To.String())
_, err := c.conn.WriteTo(tr.Raw, to)
if err != nil {
return client.TransactionResult{}, err
}
tr.StartRtxTimer(c.onRtxTimeout)
// If dontWait is true, get the transaction going and return immediately
if ignoreResult {
return client.TransactionResult{}, nil
}
res := tr.WaitForResult()
if res.Err != nil {
return res, res.Err
}
return res, nil
}
// OnDeallocated is called when deallocation of relay address has been complete.
// (Called by UDPConn)
func (c *Client) OnDeallocated(relayedAddr net.Addr) {
c.setRelayedUDPConn(nil)
}
// HandleInbound handles data received.
// This method handles incoming packet demultiplex it by the source address
// and the types of the message.
// This return a booleen (handled or not) and if there was an error.
// Caller should check if the packet was handled by this client or not.
// If not handled, it is assumed that the packet is application data.
// If an error is returned, the caller should discard the packet regardless.
func (c *Client) HandleInbound(data []byte, from net.Addr) (bool, error) {
// +-------------------+-------------------------------+
// | Return Values | |
// +-------------------+ Meaning / Action |
// | handled | error | |
// |=========+=========+===============================+
// | false | nil | Handle the packet as app data |
// |---------+---------+-------------------------------+
// | true | nil | Nothing to do |
// |---------+---------+-------------------------------+
// | false | error | (shouldn't happen) |
// |---------+---------+-------------------------------+
// | true | error | Error occurred while handling |
// +---------+---------+-------------------------------+
// Possible causes of the error:
// - Malformed packet (parse error)
// - STUN message was a request
// - Non-STUN message from the STUN server
switch {
case stun.IsMessage(data):
return true, c.handleSTUNMessage(data, from)
case proto.IsChannelData(data):
return true, c.handleChannelData(data)
case len(c.stunServStr) != 0 && from.String() == c.stunServStr:
// received from STUN server but it is not a STUN message
return true, errNonSTUNMessage
default:
// assume, this is an application data
c.log.Tracef("non-STUN/TURN packect, unhandled")
}
return false, nil
}
func (c *Client) handleSTUNMessage(data []byte, from net.Addr) error {
raw := make([]byte, len(data))
copy(raw, data)
msg := &stun.Message{Raw: raw}
if err := msg.Decode(); err != nil {
return fmt.Errorf("%w: %s", errFailedToDecodeSTUN, err.Error())
}
if msg.Type.Class == stun.ClassRequest {
return fmt.Errorf("%w : %s", errUnexpectedSTUNRequestMessage, msg.String())
}
if msg.Type.Class == stun.ClassIndication {
if msg.Type.Method == stun.MethodData {
var peerAddr proto.PeerAddress
if err := peerAddr.GetFrom(msg); err != nil {
return err
}
from = &net.UDPAddr{
IP: peerAddr.IP,
Port: peerAddr.Port,
}
var data proto.Data
if err := data.GetFrom(msg); err != nil {
return err
}
c.log.Debugf("data indication received from %s", from.String())
relayedConn := c.relayedUDPConn()
if relayedConn == nil {
c.log.Debug("no relayed conn allocated")
return nil // silently discard
}
relayedConn.HandleInbound(data, from)
}
return nil
}
// This is a STUN response message (transactional)
// The type is either:
// - stun.ClassSuccessResponse
// - stun.ClassErrorResponse
trKey := b64.StdEncoding.EncodeToString(msg.TransactionID[:])
c.mutexTrMap.Lock()
tr, ok := c.trMap.Find(trKey)
if !ok {
c.mutexTrMap.Unlock()
// silently discard
c.log.Debugf("no transaction for %s", msg.String())
return nil
}
// End the transaction
tr.StopRtxTimer()
c.trMap.Delete(trKey)
c.mutexTrMap.Unlock()
if !tr.WriteResult(client.TransactionResult{
Msg: msg,
From: from,
Retries: tr.Retries(),
}) {
c.log.Debugf("no listener for %s", msg.String())
}
return nil
}
func (c *Client) handleChannelData(data []byte) error {
chData := &proto.ChannelData{
Raw: make([]byte, len(data)),
}
copy(chData.Raw, data)
if err := chData.Decode(); err != nil {
return err
}
relayedConn := c.relayedUDPConn()
if relayedConn == nil {
c.log.Debug("no relayed conn allocated")
return nil // silently discard
}
addr, ok := relayedConn.FindAddrByChannelNumber(uint16(chData.Number))
if !ok {
return fmt.Errorf("%w: %d", errChannelBindNotFound, int(chData.Number))
}
c.log.Tracef("channel data received from %s (ch=%d)", addr.String(), int(chData.Number))
relayedConn.HandleInbound(chData.Data, addr)
return nil
}
func (c *Client) onRtxTimeout(trKey string, nRtx int) {
c.mutexTrMap.Lock()
defer c.mutexTrMap.Unlock()
tr, ok := c.trMap.Find(trKey)
if !ok {
return // already gone
}
if nRtx == maxRtxCount {
// all retransmisstions failed
c.trMap.Delete(trKey)
if !tr.WriteResult(client.TransactionResult{
Err: fmt.Errorf("%w %s", errAllRetransmissionsFailed, trKey),
}) {
c.log.Debug("no listener for transaction")
}
return
}
c.log.Tracef("retransmitting transaction %s to %s (nRtx=%d)",
trKey, tr.To.String(), nRtx)
_, err := c.conn.WriteTo(tr.Raw, tr.To)
if err != nil {
c.trMap.Delete(trKey)
if !tr.WriteResult(client.TransactionResult{
Err: fmt.Errorf("%w %s", errFailedToRetransmitTransaction, trKey),
}) {
c.log.Debug("no listener for transaction")
}
return
}
tr.StartRtxTimer(c.onRtxTimeout)
}
func (c *Client) setRelayedUDPConn(conn *client.UDPConn) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.relayedConn = conn
}
func (c *Client) relayedUDPConn() *client.UDPConn {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.relayedConn
}

View file

@ -0,0 +1,20 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically copied from https://github.com/pion/.goassets repository.
#
coverage:
status:
project:
default:
# Allow decreasing 2% of total coverage to avoid noise.
threshold: 2%
patch:
default:
target: 70%
only_pulls: true
ignore:
- "examples/*"
- "examples/**/*"

View file

@ -0,0 +1,28 @@
package turn
import "errors"
var (
errRelayAddressInvalid = errors.New("turn: RelayAddress must be valid IP to use RelayAddressGeneratorStatic")
errNoAvailableConns = errors.New("turn: PacketConnConfigs and ConnConfigs are empty, unable to proceed")
errConnUnset = errors.New("turn: PacketConnConfig must have a non-nil Conn")
errListenerUnset = errors.New("turn: ListenerConfig must have a non-nil Listener")
errListeningAddressInvalid = errors.New("turn: RelayAddressGenerator has invalid ListeningAddress")
errRelayAddressGeneratorUnset = errors.New("turn: RelayAddressGenerator in RelayConfig is unset")
errMaxRetriesExceeded = errors.New("turn: max retries exceeded")
errMaxPortNotZero = errors.New("turn: MaxPort must be not 0")
errMinPortNotZero = errors.New("turn: MaxPort must be not 0")
errNilConn = errors.New("turn: conn cannot not be nil")
errTODO = errors.New("turn: TODO")
errAlreadyListening = errors.New("turn: already listening")
errFailedToClose = errors.New("turn: Server failed to close")
errFailedToRetransmitTransaction = errors.New("turn: failed to retransmit transaction")
errAllRetransmissionsFailed = errors.New("all retransmissions failed for")
errChannelBindNotFound = errors.New("no binding found for channel")
errSTUNServerAddressNotSet = errors.New("STUN server address is not set for the client")
errOneAllocateOnly = errors.New("only one Allocate() caller is allowed")
errAlreadyAllocated = errors.New("already allocated")
errNonSTUNMessage = errors.New("non-STUN message from STUN server")
errFailedToDecodeSTUN = errors.New("failed to decode STUN message")
errUnexpectedSTUNRequestMessage = errors.New("unexpected STUN request message")
)

View file

@ -0,0 +1,11 @@
module github.com/pion/turn/v2
go 1.13
require (
github.com/pion/logging v0.2.2
github.com/pion/randutil v0.1.0
github.com/pion/stun v0.3.5
github.com/pion/transport v0.10.1
github.com/stretchr/testify v1.6.1
)

View file

@ -0,0 +1,28 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,259 @@
// Package allocation contains all CRUD operations for allocations
package allocation
import (
"net"
"sync"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/ipnet"
"github.com/pion/turn/v2/internal/proto"
)
// Allocation is tied to a FiveTuple and relays traffic
// use CreateAllocation and GetAllocation to operate
type Allocation struct {
RelayAddr net.Addr
Protocol Protocol
TurnSocket net.PacketConn
RelaySocket net.PacketConn
fiveTuple *FiveTuple
permissionsLock sync.RWMutex
permissions map[string]*Permission
channelBindingsLock sync.RWMutex
channelBindings []*ChannelBind
lifetimeTimer *time.Timer
closed chan interface{}
log logging.LeveledLogger
}
func addr2IPFingerprint(addr net.Addr) string {
switch a := addr.(type) {
case *net.UDPAddr:
return a.IP.String()
case *net.TCPAddr: // Do we really need this case?
return a.IP.String()
}
return "" // shoud never happen
}
// NewAllocation creates a new instance of NewAllocation.
func NewAllocation(turnSocket net.PacketConn, fiveTuple *FiveTuple, log logging.LeveledLogger) *Allocation {
return &Allocation{
TurnSocket: turnSocket,
fiveTuple: fiveTuple,
permissions: make(map[string]*Permission, 64),
closed: make(chan interface{}),
log: log,
}
}
// GetPermission gets the Permission from the allocation
func (a *Allocation) GetPermission(addr net.Addr) *Permission {
a.permissionsLock.RLock()
defer a.permissionsLock.RUnlock()
return a.permissions[addr2IPFingerprint(addr)]
}
// AddPermission adds a new permission to the allocation
func (a *Allocation) AddPermission(p *Permission) {
fingerprint := addr2IPFingerprint(p.Addr)
a.permissionsLock.RLock()
existedPermission, ok := a.permissions[fingerprint]
a.permissionsLock.RUnlock()
if ok {
existedPermission.refresh(permissionTimeout)
return
}
p.allocation = a
a.permissionsLock.Lock()
a.permissions[fingerprint] = p
a.permissionsLock.Unlock()
p.start(permissionTimeout)
}
// RemovePermission removes the net.Addr's fingerprint from the allocation's permissions
func (a *Allocation) RemovePermission(addr net.Addr) {
a.permissionsLock.Lock()
defer a.permissionsLock.Unlock()
delete(a.permissions, addr2IPFingerprint(addr))
}
// AddChannelBind adds a new ChannelBind to the allocation, it also updates the
// permissions needed for this ChannelBind
func (a *Allocation) AddChannelBind(c *ChannelBind, lifetime time.Duration) error {
// Check that this channel id isn't bound to another transport address, and
// that this transport address isn't bound to another channel number.
channelByNumber := a.GetChannelByNumber(c.Number)
if channelByNumber != a.GetChannelByAddr(c.Peer) {
return errSameChannelDifferentPeer
}
// Add or refresh this channel.
if channelByNumber == nil {
a.channelBindingsLock.Lock()
defer a.channelBindingsLock.Unlock()
c.allocation = a
a.channelBindings = append(a.channelBindings, c)
c.start(lifetime)
// Channel binds also refresh permissions.
a.AddPermission(NewPermission(c.Peer, a.log))
} else {
channelByNumber.refresh(lifetime)
// Channel binds also refresh permissions.
a.AddPermission(NewPermission(channelByNumber.Peer, a.log))
}
return nil
}
// RemoveChannelBind removes the ChannelBind from this allocation by id
func (a *Allocation) RemoveChannelBind(number proto.ChannelNumber) bool {
a.channelBindingsLock.Lock()
defer a.channelBindingsLock.Unlock()
for i := len(a.channelBindings) - 1; i >= 0; i-- {
if a.channelBindings[i].Number == number {
a.channelBindings = append(a.channelBindings[:i], a.channelBindings[i+1:]...)
return true
}
}
return false
}
// GetChannelByNumber gets the ChannelBind from this allocation by id
func (a *Allocation) GetChannelByNumber(number proto.ChannelNumber) *ChannelBind {
a.channelBindingsLock.RLock()
defer a.channelBindingsLock.RUnlock()
for _, cb := range a.channelBindings {
if cb.Number == number {
return cb
}
}
return nil
}
// GetChannelByAddr gets the ChannelBind from this allocation by net.Addr
func (a *Allocation) GetChannelByAddr(addr net.Addr) *ChannelBind {
a.channelBindingsLock.RLock()
defer a.channelBindingsLock.RUnlock()
for _, cb := range a.channelBindings {
if ipnet.AddrEqual(cb.Peer, addr) {
return cb
}
}
return nil
}
// Refresh updates the allocations lifetime
func (a *Allocation) Refresh(lifetime time.Duration) {
if !a.lifetimeTimer.Reset(lifetime) {
a.log.Errorf("Failed to reset allocation timer for %v", a.fiveTuple)
}
}
// Close closes the allocation
func (a *Allocation) Close() error {
select {
case <-a.closed:
return nil
default:
}
close(a.closed)
a.lifetimeTimer.Stop()
a.permissionsLock.RLock()
for _, p := range a.permissions {
p.lifetimeTimer.Stop()
}
a.permissionsLock.RUnlock()
a.channelBindingsLock.RLock()
for _, c := range a.channelBindings {
c.lifetimeTimer.Stop()
}
a.channelBindingsLock.RUnlock()
return a.RelaySocket.Close()
}
// https://tools.ietf.org/html/rfc5766#section-10.3
// When the server receives a UDP datagram at a currently allocated
// relayed transport address, the server looks up the allocation
// associated with the relayed transport address. The server then
// checks to see whether the set of permissions for the allocation allow
// the relaying of the UDP datagram as described in Section 8.
//
// If relaying is permitted, then the server checks if there is a
// channel bound to the peer that sent the UDP datagram (see
// Section 11). If a channel is bound, then processing proceeds as
// described in Section 11.7.
//
// If relaying is permitted but no channel is bound to the peer, then
// the server forms and sends a Data indication. The Data indication
// MUST contain both an XOR-PEER-ADDRESS and a DATA attribute. The DATA
// attribute is set to the value of the 'data octets' field from the
// datagram, and the XOR-PEER-ADDRESS attribute is set to the source
// transport address of the received UDP datagram. The Data indication
// is then sent on the 5-tuple associated with the allocation.
const rtpMTU = 1500
func (a *Allocation) packetHandler(m *Manager) {
buffer := make([]byte, rtpMTU)
for {
n, srcAddr, err := a.RelaySocket.ReadFrom(buffer)
if err != nil {
m.DeleteAllocation(a.fiveTuple)
return
}
a.log.Debugf("relay socket %s received %d bytes from %s",
a.RelaySocket.LocalAddr().String(),
n,
srcAddr.String())
if channel := a.GetChannelByAddr(srcAddr); channel != nil {
channelData := &proto.ChannelData{
Data: buffer[:n],
Number: channel.Number,
}
channelData.Encode()
if _, err = a.TurnSocket.WriteTo(channelData.Raw, a.fiveTuple.SrcAddr); err != nil {
a.log.Errorf("Failed to send ChannelData from allocation %v %v", srcAddr, err)
}
} else if p := a.GetPermission(srcAddr); p != nil {
udpAddr := srcAddr.(*net.UDPAddr)
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 {
a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err)
}
a.log.Debugf("relaying message from %s to client at %s",
srcAddr.String(),
a.fiveTuple.SrcAddr.String())
if _, err = a.TurnSocket.WriteTo(msg.Raw, a.fiveTuple.SrcAddr); err != nil {
a.log.Errorf("Failed to send DataIndication from allocation %v %v", srcAddr, err)
}
} else {
a.log.Infof("No Permission or Channel exists for %v on allocation %v", srcAddr, a.RelayAddr.String())
}
}
}

View file

@ -0,0 +1,186 @@
package allocation
import (
"fmt"
"net"
"sync"
"time"
"github.com/pion/logging"
)
// ManagerConfig a bag of config params for Manager.
type ManagerConfig struct {
LeveledLogger logging.LeveledLogger
AllocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error)
AllocateConn func(network string, requestedPort int) (net.Conn, net.Addr, error)
}
type reservation struct {
token string
port int
}
// Manager is used to hold active allocations
type Manager struct {
lock sync.RWMutex
log logging.LeveledLogger
allocations map[string]*Allocation
reservations []*reservation
allocatePacketConn func(network string, requestedPort int) (net.PacketConn, net.Addr, error)
allocateConn func(network string, requestedPort int) (net.Conn, net.Addr, error)
}
// NewManager creates a new instance of Manager.
func NewManager(config ManagerConfig) (*Manager, error) {
switch {
case config.AllocatePacketConn == nil:
return nil, errAllocatePacketConnMustBeSet
case config.AllocateConn == nil:
return nil, errAllocateConnMustBeSet
case config.LeveledLogger == nil:
return nil, errLeveledLoggerMustBeSet
}
return &Manager{
log: config.LeveledLogger,
allocations: make(map[string]*Allocation, 64),
allocatePacketConn: config.AllocatePacketConn,
allocateConn: config.AllocateConn,
}, nil
}
// GetAllocation fetches the allocation matching the passed FiveTuple
func (m *Manager) GetAllocation(fiveTuple *FiveTuple) *Allocation {
m.lock.RLock()
defer m.lock.RUnlock()
return m.allocations[fiveTuple.Fingerprint()]
}
// Close closes the manager and closes all allocations it manages
func (m *Manager) Close() error {
m.lock.Lock()
defer m.lock.Unlock()
for _, a := range m.allocations {
if err := a.Close(); err != nil {
return err
}
}
return nil
}
// CreateAllocation creates a new allocation and starts relaying
func (m *Manager) CreateAllocation(fiveTuple *FiveTuple, turnSocket net.PacketConn, requestedPort int, lifetime time.Duration) (*Allocation, error) {
switch {
case fiveTuple == nil:
return nil, errNilFiveTuple
case fiveTuple.SrcAddr == nil:
return nil, errNilFiveTupleSrcAddr
case fiveTuple.DstAddr == nil:
return nil, errNilFiveTupleDstAddr
case turnSocket == nil:
return nil, errNilTurnSocket
case lifetime == 0:
return nil, errLifetimeZero
}
if a := m.GetAllocation(fiveTuple); a != nil {
return nil, fmt.Errorf("%w: %v", errDupeFiveTuple, fiveTuple)
}
a := NewAllocation(turnSocket, fiveTuple, m.log)
conn, relayAddr, err := m.allocatePacketConn("udp4", requestedPort)
if err != nil {
return nil, err
}
a.RelaySocket = conn
a.RelayAddr = relayAddr
m.log.Debugf("listening on relay addr: %s", a.RelayAddr.String())
a.lifetimeTimer = time.AfterFunc(lifetime, func() {
m.DeleteAllocation(a.fiveTuple)
})
m.lock.Lock()
m.allocations[fiveTuple.Fingerprint()] = a
m.lock.Unlock()
go a.packetHandler(m)
return a, nil
}
// DeleteAllocation removes an allocation
func (m *Manager) DeleteAllocation(fiveTuple *FiveTuple) {
fingerprint := fiveTuple.Fingerprint()
m.lock.Lock()
allocation := m.allocations[fingerprint]
delete(m.allocations, fingerprint)
m.lock.Unlock()
if allocation == nil {
return
}
if err := allocation.Close(); err != nil {
m.log.Errorf("Failed to close allocation: %v", err)
}
}
// CreateReservation stores the reservation for the token+port
func (m *Manager) CreateReservation(reservationToken string, port int) {
time.AfterFunc(30*time.Second, func() {
m.lock.Lock()
defer m.lock.Unlock()
for i := len(m.reservations) - 1; i >= 0; i-- {
if m.reservations[i].token == reservationToken {
m.reservations = append(m.reservations[:i], m.reservations[i+1:]...)
return
}
}
})
m.lock.Lock()
m.reservations = append(m.reservations, &reservation{
token: reservationToken,
port: port,
})
m.lock.Unlock()
}
// GetReservation returns the port for a given reservation if it exists
func (m *Manager) GetReservation(reservationToken string) (int, bool) {
m.lock.RLock()
defer m.lock.RUnlock()
for _, r := range m.reservations {
if r.token == reservationToken {
return r.port, true
}
}
return 0, false
}
// GetRandomEvenPort returns a random un-allocated udp4 port
func (m *Manager) GetRandomEvenPort() (int, error) {
conn, addr, err := m.allocatePacketConn("udp4", 0)
if err != nil {
return 0, err
}
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return 0, errFailedToCastUDPAddr
} else if err := conn.Close(); err != nil {
return 0, err
} else if udpAddr.Port%2 == 1 {
return m.GetRandomEvenPort()
}
return udpAddr.Port, nil
}

View file

@ -0,0 +1,43 @@
package allocation
import (
"net"
"time"
"github.com/pion/logging"
"github.com/pion/turn/v2/internal/proto"
)
// ChannelBind represents a TURN Channel
// https://tools.ietf.org/html/rfc5766#section-2.5
type ChannelBind struct {
Peer net.Addr
Number proto.ChannelNumber
allocation *Allocation
lifetimeTimer *time.Timer
log logging.LeveledLogger
}
// NewChannelBind creates a new ChannelBind
func NewChannelBind(number proto.ChannelNumber, peer net.Addr, log logging.LeveledLogger) *ChannelBind {
return &ChannelBind{
Number: number,
Peer: peer,
log: log,
}
}
func (c *ChannelBind) start(lifetime time.Duration) {
c.lifetimeTimer = time.AfterFunc(lifetime, func() {
if !c.allocation.RemoveChannelBind(c.Number) {
c.log.Errorf("Failed to remove ChannelBind for %v %x %v", c.Number, c.Peer, c.allocation.fiveTuple)
}
})
}
func (c *ChannelBind) refresh(lifetime time.Duration) {
if !c.lifetimeTimer.Reset(lifetime) {
c.log.Errorf("Failed to reset ChannelBind timer for %v %x %v", c.Number, c.Peer, c.allocation.fiveTuple)
}
}

View file

@ -0,0 +1,17 @@
package allocation
import "errors"
var (
errAllocatePacketConnMustBeSet = errors.New("AllocatePacketConn must be set")
errAllocateConnMustBeSet = errors.New("AllocateConn must be set")
errLeveledLoggerMustBeSet = errors.New("LeveledLogger must be set")
errSameChannelDifferentPeer = errors.New("you cannot use the same channel number with different peer")
errNilFiveTuple = errors.New("allocations must not be created with nil FivTuple")
errNilFiveTupleSrcAddr = errors.New("allocations must not be created with nil FiveTuple.SrcAddr")
errNilFiveTupleDstAddr = errors.New("allocations must not be created with nil FiveTuple.DstAddr")
errNilTurnSocket = errors.New("allocations must not be created with nil turnSocket")
errLifetimeZero = errors.New("allocations must not be created with a lifetime of 0")
errDupeFiveTuple = errors.New("allocation attempt created with duplicate FiveTuple")
errFailedToCastUDPAddr = errors.New("failed to cast net.Addr to *net.UDPAddr")
)

View file

@ -0,0 +1,36 @@
package allocation
import (
"fmt"
"net"
)
// Protocol is an enum for relay protocol
type Protocol uint8
// Network protocols for relay
const (
UDP Protocol = iota
TCP
)
// FiveTuple is the combination (client IP address and port, server IP
// address and port, and transport protocol (currently one of UDP,
// TCP, or TLS)) used to communicate between the client and the
// server. The 5-tuple uniquely identifies this communication
// stream. The 5-tuple also uniquely identifies the Allocation on
// the server.
type FiveTuple struct {
Protocol
SrcAddr, DstAddr net.Addr
}
// Equal asserts if two FiveTuples are equal
func (f *FiveTuple) Equal(b *FiveTuple) bool {
return f.Fingerprint() == b.Fingerprint()
}
// Fingerprint is the identity of a FiveTuple
func (f *FiveTuple) Fingerprint() string {
return fmt.Sprintf("%d_%s_%s", f.Protocol, f.SrcAddr.String(), f.DstAddr.String())
}

View file

@ -0,0 +1,40 @@
package allocation
import (
"net"
"time"
"github.com/pion/logging"
)
const permissionTimeout = time.Duration(5) * time.Minute
// Permission represents a TURN permission. TURN permissions mimic the address-restricted
// filtering mechanism of NATs that comply with [RFC4787].
// https://tools.ietf.org/html/rfc5766#section-2.3
type Permission struct {
Addr net.Addr
allocation *Allocation
lifetimeTimer *time.Timer
log logging.LeveledLogger
}
// NewPermission create a new Permission
func NewPermission(addr net.Addr, log logging.LeveledLogger) *Permission {
return &Permission{
Addr: addr,
log: log,
}
}
func (p *Permission) start(lifetime time.Duration) {
p.lifetimeTimer = time.AfterFunc(lifetime, func() {
p.allocation.RemovePermission(p.Addr)
})
}
func (p *Permission) refresh(lifetime time.Duration) {
if !p.lifetimeTimer.Reset(lifetime) {
p.log.Errorf("Failed to reset permission timer for %v %v", p.Addr, p.allocation.fiveTuple)
}
}

View file

@ -0,0 +1,151 @@
package client
import (
"net"
"sync"
"sync/atomic"
"time"
)
// Chanel number:
// 0x4000 through 0x7FFF: These values are the allowed channel
// numbers (16,383 possible values).
const (
minChannelNumber uint16 = 0x4000
maxChannelNumber uint16 = 0x7fff
)
type bindingState int32
const (
bindingStateIdle bindingState = iota
bindingStateRequest
bindingStateReady
bindingStateRefresh
bindingStateFailed
)
type binding struct {
number uint16 // read-only
st bindingState // thread-safe (atomic op)
addr net.Addr // read-only
mgr *bindingManager // read-only
muBind sync.Mutex // thread-safe, for ChannelBind ops
_refreshedAt time.Time // protected by mutex
mutex sync.RWMutex // thread-safe
}
func (b *binding) setState(state bindingState) {
atomic.StoreInt32((*int32)(&b.st), int32(state))
}
func (b *binding) state() bindingState {
return bindingState(atomic.LoadInt32((*int32)(&b.st)))
}
func (b *binding) setRefreshedAt(at time.Time) {
b.mutex.Lock()
defer b.mutex.Unlock()
b._refreshedAt = at
}
func (b *binding) refreshedAt() time.Time {
b.mutex.RLock()
defer b.mutex.RUnlock()
return b._refreshedAt
}
// Thread-safe binding map
type bindingManager struct {
chanMap map[uint16]*binding
addrMap map[string]*binding
next uint16
mutex sync.RWMutex
}
func newBindingManager() *bindingManager {
return &bindingManager{
chanMap: map[uint16]*binding{},
addrMap: map[string]*binding{},
next: minChannelNumber,
}
}
func (mgr *bindingManager) assignChannelNumber() uint16 {
n := mgr.next
if mgr.next == maxChannelNumber {
mgr.next = minChannelNumber
} else {
mgr.next++
}
return n
}
func (mgr *bindingManager) create(addr net.Addr) *binding {
mgr.mutex.Lock()
defer mgr.mutex.Unlock()
b := &binding{
number: mgr.assignChannelNumber(),
addr: addr,
mgr: mgr,
_refreshedAt: time.Now(),
}
mgr.chanMap[b.number] = b
mgr.addrMap[b.addr.String()] = b
return b
}
func (mgr *bindingManager) findByAddr(addr net.Addr) (*binding, bool) {
mgr.mutex.RLock()
defer mgr.mutex.RUnlock()
b, ok := mgr.addrMap[addr.String()]
return b, ok
}
func (mgr *bindingManager) findByNumber(number uint16) (*binding, bool) {
mgr.mutex.RLock()
defer mgr.mutex.RUnlock()
b, ok := mgr.chanMap[number]
return b, ok
}
func (mgr *bindingManager) deleteByAddr(addr net.Addr) bool {
mgr.mutex.Lock()
defer mgr.mutex.Unlock()
b, ok := mgr.addrMap[addr.String()]
if !ok {
return false
}
delete(mgr.addrMap, addr.String())
delete(mgr.chanMap, b.number)
return true
}
func (mgr *bindingManager) deleteByNumber(number uint16) bool {
mgr.mutex.Lock()
defer mgr.mutex.Unlock()
b, ok := mgr.chanMap[number]
if !ok {
return false
}
delete(mgr.addrMap, b.addr.String())
delete(mgr.chanMap, number)
return true
}
func (mgr *bindingManager) size() int {
mgr.mutex.RLock()
defer mgr.mutex.RUnlock()
return len(mgr.chanMap)
}

View file

@ -0,0 +1,613 @@
// Package client implements the API for a TURN client
package client
import (
"errors"
"fmt"
"io"
"math"
"net"
"sync"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/proto"
)
const (
maxReadQueueSize = 1024
permRefreshInterval = 120 * time.Second
maxRetryAttempts = 3
)
const (
timerIDRefreshAlloc int = iota
timerIDRefreshPerms
)
func noDeadline() time.Time {
return time.Time{}
}
type inboundData struct {
data []byte
from net.Addr
}
// UDPConnObserver is an interface to UDPConn observer
type UDPConnObserver interface {
TURNServerAddr() net.Addr
Username() stun.Username
Realm() stun.Realm
WriteTo(data []byte, to net.Addr) (int, error)
PerformTransaction(msg *stun.Message, to net.Addr, dontWait bool) (TransactionResult, error)
OnDeallocated(relayedAddr net.Addr)
}
// UDPConnConfig is a set of configuration params use by NewUDPConn
type UDPConnConfig struct {
Observer UDPConnObserver
RelayedAddr net.Addr
Integrity stun.MessageIntegrity
Nonce stun.Nonce
Lifetime time.Duration
Log logging.LeveledLogger
}
// UDPConn is the implementation of the Conn and PacketConn interfaces for UDP network connections.
// comatible with net.PacketConn and net.Conn
type UDPConn struct {
obs UDPConnObserver // read-only
relayedAddr net.Addr // read-only
permMap *permissionMap // thread-safe
bindingMgr *bindingManager // thread-safe
integrity stun.MessageIntegrity // read-only
_nonce stun.Nonce // needs mutex x
_lifetime time.Duration // needs mutex x
readCh chan *inboundData // thread-safe
closeCh chan struct{} // thread-safe
readTimer *time.Timer // thread-safe
refreshAllocTimer *PeriodicTimer // thread-safe
refreshPermsTimer *PeriodicTimer // thread-safe
mutex sync.RWMutex // thread-safe
log logging.LeveledLogger // read-only
}
// NewUDPConn creates a new instance of UDPConn
func NewUDPConn(config *UDPConnConfig) *UDPConn {
c := &UDPConn{
obs: config.Observer,
relayedAddr: config.RelayedAddr,
permMap: newPermissionMap(),
bindingMgr: newBindingManager(),
integrity: config.Integrity,
_nonce: config.Nonce,
_lifetime: config.Lifetime,
readCh: make(chan *inboundData, maxReadQueueSize),
closeCh: make(chan struct{}),
readTimer: time.NewTimer(time.Duration(math.MaxInt64)),
log: config.Log,
}
c.log.Debugf("initial lifetime: %d seconds", int(c.lifetime().Seconds()))
c.refreshAllocTimer = NewPeriodicTimer(
timerIDRefreshAlloc,
c.onRefreshTimers,
c.lifetime()/2,
)
c.refreshPermsTimer = NewPeriodicTimer(
timerIDRefreshPerms,
c.onRefreshTimers,
permRefreshInterval,
)
if c.refreshAllocTimer.Start() {
c.log.Debugf("refreshAllocTimer started")
}
if c.refreshPermsTimer.Start() {
c.log.Debugf("refreshPermsTimer started")
}
return c
}
// ReadFrom reads a packet from the connection,
// copying the payload into p. It returns the number of
// bytes copied into p and the return address that
// was on the packet.
// It returns the number of bytes read (0 <= n <= len(p))
// and any error encountered. Callers should always process
// the n > 0 bytes returned before considering the error err.
// ReadFrom can be made to time out and return
// an Error with Timeout() == true after a fixed time limit;
// see SetDeadline and SetReadDeadline.
func (c *UDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
select {
case ibData := <-c.readCh:
n := copy(p, ibData.data)
if n < len(ibData.data) {
return 0, nil, io.ErrShortBuffer
}
return n, ibData.from, nil
case <-c.readTimer.C:
return 0, nil, &net.OpError{
Op: "read",
Net: c.LocalAddr().Network(),
Addr: c.LocalAddr(),
Err: newTimeoutError("i/o timeout"),
}
case <-c.closeCh:
return 0, nil, &net.OpError{
Op: "read",
Net: c.LocalAddr().Network(),
Addr: c.LocalAddr(),
Err: errClosed,
}
}
}
}
// WriteTo writes a packet with payload p to addr.
// WriteTo can be made to time out and return
// an Error with Timeout() == true after a fixed time limit;
// see SetDeadline and SetWriteDeadline.
// On packet-oriented connections, write timeouts are rare.
func (c *UDPConn) WriteTo(p []byte, addr net.Addr) (int, error) { //nolint: gocognit
var err error
_, ok := addr.(*net.UDPAddr)
if !ok {
return 0, errUDPAddrCast
}
// check if we have a permission for the destination IP addr
perm, ok := c.permMap.find(addr)
if !ok {
perm = &permission{}
c.permMap.insert(addr, perm)
}
// This func-block would block, per destination IP (, or perm), until
// the perm state becomes "requested". Purpose of this is to guarantee
// the order of packets (within the same perm).
// Note that CreatePermission transaction may not be complete before
// all the data transmission. This is done assuming that the request
// will be mostly likely successful and we can tolerate some loss of
// UDP packet (or reorder), inorder to minimize the latency in most cases.
createPermission := func() error {
perm.mutex.Lock()
defer perm.mutex.Unlock()
if perm.state() == permStateIdle {
// punch a hole! (this would block a bit..)
if err = c.createPermissions(addr); err != nil {
c.permMap.delete(addr)
return err
}
perm.setState(permStatePermitted)
}
return nil
}
for i := 0; i < maxRetryAttempts; i++ {
if err = createPermission(); !errors.Is(err, errTryAgain) {
break
}
}
if err != nil {
return 0, err
}
// bind channel
b, ok := c.bindingMgr.findByAddr(addr)
if !ok {
b = c.bindingMgr.create(addr)
}
bindSt := b.state()
if bindSt == bindingStateIdle || bindSt == bindingStateRequest || bindSt == bindingStateFailed {
func() {
// block only callers with the same binding until
// the binding transaction has been complete
b.muBind.Lock()
defer b.muBind.Unlock()
// binding state may have been changed while waiting. check again.
if b.state() == bindingStateIdle {
b.setState(bindingStateRequest)
go func() {
err2 := c.bind(b)
if err2 != nil {
c.log.Warnf("bind() failed: %s", err2.Error())
b.setState(bindingStateFailed)
// keep going...
} else {
b.setState(bindingStateReady)
}
}()
}
}()
// send data using SendIndication
peerAddr := addr2PeerAddress(addr)
var msg *stun.Message
msg, err = stun.Build(
stun.TransactionID,
stun.NewType(stun.MethodSend, stun.ClassIndication),
proto.Data(p),
peerAddr,
stun.Fingerprint,
)
if err != nil {
return 0, err
}
// indication has no transaction (fire-and-forget)
return c.obs.WriteTo(msg.Raw, c.obs.TURNServerAddr())
}
// binding is either ready
// check if the binding needs a refresh
func() {
b.muBind.Lock()
defer b.muBind.Unlock()
if b.state() == bindingStateReady && time.Since(b.refreshedAt()) > 5*time.Minute {
b.setState(bindingStateRefresh)
go func() {
err = c.bind(b)
if err != nil {
c.log.Warnf("bind() for refresh failed: %s", err.Error())
b.setState(bindingStateFailed)
// keep going...
} else {
b.setRefreshedAt(time.Now())
b.setState(bindingStateReady)
}
}()
}
}()
// send via ChannelData
return c.sendChannelData(p, b.number)
}
// Close closes the connection.
// Any blocked ReadFrom or WriteTo operations will be unblocked and return errors.
func (c *UDPConn) Close() error {
c.refreshAllocTimer.Stop()
c.refreshPermsTimer.Stop()
select {
case <-c.closeCh:
return errAlreadyClosed
default:
close(c.closeCh)
}
c.obs.OnDeallocated(c.relayedAddr)
return c.refreshAllocation(0, true /* dontWait=true */)
}
// LocalAddr returns the local network address.
func (c *UDPConn) LocalAddr() net.Addr {
return c.relayedAddr
}
// SetDeadline sets the read and write deadlines associated
// with the connection. It is equivalent to calling both
// SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations
// fail with a timeout (see type Error) instead of
// blocking. The deadline applies to all future and pending
// I/O, not just the immediately following call to ReadFrom or
// WriteTo. After a deadline has been exceeded, the connection
// can be refreshed by setting a deadline in the future.
//
// An idle timeout can be implemented by repeatedly extending
// the deadline after successful ReadFrom or WriteTo calls.
//
// A zero value for t means I/O operations will not time out.
func (c *UDPConn) SetDeadline(t time.Time) error {
return c.SetReadDeadline(t)
}
// SetReadDeadline sets the deadline for future ReadFrom calls
// and any currently-blocked ReadFrom call.
// A zero value for t means ReadFrom will not time out.
func (c *UDPConn) SetReadDeadline(t time.Time) error {
var d time.Duration
if t == noDeadline() {
d = time.Duration(math.MaxInt64)
} else {
d = time.Until(t)
}
c.readTimer.Reset(d)
return nil
}
// SetWriteDeadline sets the deadline for future WriteTo calls
// and any currently-blocked WriteTo call.
// Even if write times out, it may return n > 0, indicating that
// some of the data was successfully written.
// A zero value for t means WriteTo will not time out.
func (c *UDPConn) SetWriteDeadline(t time.Time) error {
// Write never blocks.
return nil
}
func addr2PeerAddress(addr net.Addr) proto.PeerAddress {
var peerAddr proto.PeerAddress
switch a := addr.(type) {
case *net.UDPAddr:
peerAddr.IP = a.IP
peerAddr.Port = a.Port
case *net.TCPAddr:
peerAddr.IP = a.IP
peerAddr.Port = a.Port
}
return peerAddr
}
func (c *UDPConn) createPermissions(addrs ...net.Addr) error {
setters := []stun.Setter{
stun.TransactionID,
stun.NewType(stun.MethodCreatePermission, stun.ClassRequest),
}
for _, addr := range addrs {
setters = append(setters, addr2PeerAddress(addr))
}
setters = append(setters,
c.obs.Username(),
c.obs.Realm(),
c.nonce(),
c.integrity,
stun.Fingerprint)
msg, err := stun.Build(setters...)
if err != nil {
return err
}
trRes, err := c.obs.PerformTransaction(msg, c.obs.TURNServerAddr(), false)
if err != nil {
return err
}
res := trRes.Msg
if res.Type.Class == stun.ClassErrorResponse {
var code stun.ErrorCodeAttribute
if err = code.GetFrom(res); err == nil {
if code.Code == stun.CodeStaleNonce {
c.setNonceFromMsg(res)
return errTryAgain
}
return fmt.Errorf("%s (error %s)", res.Type, code) //nolint:goerr113
}
return fmt.Errorf("%s", res.Type) //nolint:goerr113
}
return nil
}
// HandleInbound passes inbound data in UDPConn
func (c *UDPConn) HandleInbound(data []byte, from net.Addr) {
// copy data
copied := make([]byte, len(data))
copy(copied, data)
select {
case c.readCh <- &inboundData{data: copied, from: from}:
default:
c.log.Warnf("receive buffer full")
}
}
// FindAddrByChannelNumber returns a peer address associated with the
// channel number on this UDPConn
func (c *UDPConn) FindAddrByChannelNumber(chNum uint16) (net.Addr, bool) {
b, ok := c.bindingMgr.findByNumber(chNum)
if !ok {
return nil, false
}
return b.addr, true
}
func (c *UDPConn) setNonceFromMsg(msg *stun.Message) {
// Update nonce
var nonce stun.Nonce
if err := nonce.GetFrom(msg); err == nil {
c.setNonce(nonce)
c.log.Debug("refresh allocation: 438, got new nonce.")
} else {
c.log.Warn("refresh allocation: 438 but no nonce.")
}
}
func (c *UDPConn) refreshAllocation(lifetime time.Duration, dontWait bool) error {
msg, err := stun.Build(
stun.TransactionID,
stun.NewType(stun.MethodRefresh, stun.ClassRequest),
proto.Lifetime{Duration: lifetime},
c.obs.Username(),
c.obs.Realm(),
c.nonce(),
c.integrity,
stun.Fingerprint,
)
if err != nil {
return fmt.Errorf("%w: %s", errFailedToBuildRefreshRequest, err.Error())
}
c.log.Debugf("send refresh request (dontWait=%v)", dontWait)
trRes, err := c.obs.PerformTransaction(msg, c.obs.TURNServerAddr(), dontWait)
if err != nil {
return fmt.Errorf("%w: %s", errFailedToRefreshAllocation, err.Error())
}
if dontWait {
c.log.Debug("refresh request sent")
return nil
}
c.log.Debug("refresh request sent, and waiting response")
res := trRes.Msg
if res.Type.Class == stun.ClassErrorResponse {
var code stun.ErrorCodeAttribute
if err = code.GetFrom(res); err == nil {
if code.Code == stun.CodeStaleNonce {
c.setNonceFromMsg(res)
return errTryAgain
}
return err
}
return fmt.Errorf("%s", res.Type) //nolint:goerr113
}
// Getting lifetime from response
var updatedLifetime proto.Lifetime
if err := updatedLifetime.GetFrom(res); err != nil {
return fmt.Errorf("%w: %s", errFailedToGetLifetime, err.Error())
}
c.setLifetime(updatedLifetime.Duration)
c.log.Debugf("updated lifetime: %d seconds", int(c.lifetime().Seconds()))
return nil
}
func (c *UDPConn) refreshPermissions() error {
addrs := c.permMap.addrs()
if len(addrs) == 0 {
c.log.Debug("no permission to refresh")
return nil
}
if err := c.createPermissions(addrs...); err != nil {
if errors.Is(err, errTryAgain) {
return errTryAgain
}
c.log.Errorf("fail to refresh permissions: %s", err.Error())
return err
}
c.log.Debug("refresh permissions successful")
return nil
}
func (c *UDPConn) bind(b *binding) error {
setters := []stun.Setter{
stun.TransactionID,
stun.NewType(stun.MethodChannelBind, stun.ClassRequest),
addr2PeerAddress(b.addr),
proto.ChannelNumber(b.number),
c.obs.Username(),
c.obs.Realm(),
c.nonce(),
c.integrity,
stun.Fingerprint,
}
msg, err := stun.Build(setters...)
if err != nil {
return err
}
trRes, err := c.obs.PerformTransaction(msg, c.obs.TURNServerAddr(), false)
if err != nil {
c.bindingMgr.deleteByAddr(b.addr)
return err
}
res := trRes.Msg
if res.Type != stun.NewType(stun.MethodChannelBind, stun.ClassSuccessResponse) {
return fmt.Errorf("unexpected response type %s", res.Type) //nolint:goerr113
}
c.log.Debugf("channel binding successful: %s %d", b.addr.String(), b.number)
// Success.
return nil
}
func (c *UDPConn) sendChannelData(data []byte, chNum uint16) (int, error) {
chData := &proto.ChannelData{
Data: data,
Number: proto.ChannelNumber(chNum),
}
chData.Encode()
return c.obs.WriteTo(chData.Raw, c.obs.TURNServerAddr())
}
func (c *UDPConn) onRefreshTimers(id int) {
c.log.Debugf("refresh timer %d expired", id)
switch id {
case timerIDRefreshAlloc:
var err error
lifetime := c.lifetime()
// limit the max retries on errTryAgain to 3
// when stale nonce returns, sencond retry should succeed
for i := 0; i < maxRetryAttempts; i++ {
err = c.refreshAllocation(lifetime, false)
if !errors.Is(err, errTryAgain) {
break
}
}
if err != nil {
c.log.Warnf("refresh allocation failed")
}
case timerIDRefreshPerms:
var err error
for i := 0; i < maxRetryAttempts; i++ {
err = c.refreshPermissions()
if !errors.Is(err, errTryAgain) {
break
}
}
if err != nil {
c.log.Warnf("refresh permissions failed")
}
}
}
func (c *UDPConn) nonce() stun.Nonce {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c._nonce
}
func (c *UDPConn) setNonce(nonce stun.Nonce) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.log.Debugf("set new nonce with %d bytes", len(nonce))
c._nonce = nonce
}
func (c *UDPConn) lifetime() time.Duration {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c._lifetime
}
func (c *UDPConn) setLifetime(lifetime time.Duration) {
c.mutex.Lock()
defer c.mutex.Unlock()
c._lifetime = lifetime
}

View file

@ -0,0 +1,37 @@
package client
import (
"errors"
)
var (
errFakeErr = errors.New("fake error")
errTryAgain = errors.New("try again")
errClosed = errors.New("use of closed network connection")
errUDPAddrCast = errors.New("addr is not a net.UDPAddr")
errAlreadyClosed = errors.New("already closed")
errDoubleLock = errors.New("try-lock is already locked")
errTransactionClosed = errors.New("transaction closed")
errWaitForResultOnNonResultTransaction = errors.New("WaitForResult called on non-result transaction")
errFailedToBuildRefreshRequest = errors.New("failed to build refresh request")
errFailedToRefreshAllocation = errors.New("failed to refresh allocation")
errFailedToGetLifetime = errors.New("failed to get lifetime from refresh response")
)
type timeoutError struct {
msg string
}
func newTimeoutError(msg string) error {
return &timeoutError{
msg: msg,
}
}
func (e *timeoutError) Error() string {
return e.msg
}
func (e *timeoutError) Timeout() bool {
return true
}

View file

@ -0,0 +1,82 @@
package client
import (
"sync"
"time"
)
// PeriodicTimerTimeoutHandler is a handler called on timeout
type PeriodicTimerTimeoutHandler func(timerID int)
// PeriodicTimer is a periodic timer
type PeriodicTimer struct {
id int
interval time.Duration
timeoutHandler PeriodicTimerTimeoutHandler
stopFunc func()
mutex sync.RWMutex
}
// NewPeriodicTimer create a new timer
func NewPeriodicTimer(id int, timeoutHandler PeriodicTimerTimeoutHandler, interval time.Duration) *PeriodicTimer {
return &PeriodicTimer{
id: id,
interval: interval,
timeoutHandler: timeoutHandler,
}
}
// Start starts the timer.
func (t *PeriodicTimer) Start() bool {
t.mutex.Lock()
defer t.mutex.Unlock()
// this is a noop if the timer is always running
if t.stopFunc != nil {
return false
}
cancelCh := make(chan struct{})
go func() {
canceling := false
for !canceling {
timer := time.NewTimer(t.interval)
select {
case <-timer.C:
t.timeoutHandler(t.id)
case <-cancelCh:
canceling = true
timer.Stop()
}
}
}()
t.stopFunc = func() {
close(cancelCh)
}
return true
}
// Stop stops the timer.
func (t *PeriodicTimer) Stop() {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.stopFunc != nil {
t.stopFunc()
t.stopFunc = nil
}
}
// IsRunning tests if the timer is running.
// Debug purpose only
func (t *PeriodicTimer) IsRunning() bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
return (t.stopFunc != nil)
}

View file

@ -0,0 +1,90 @@
package client
import (
"net"
"sync"
"sync/atomic"
)
type permState int32
const (
permStateIdle permState = iota
permStatePermitted
)
type permission struct {
st permState // thread-safe (atomic op)
mutex sync.RWMutex // thread-safe
}
func (p *permission) setState(state permState) {
atomic.StoreInt32((*int32)(&p.st), int32(state))
}
func (p *permission) state() permState {
return permState(atomic.LoadInt32((*int32)(&p.st)))
}
// Thread-safe permission map
type permissionMap struct {
permMap map[string]*permission
mutex sync.RWMutex
}
func (m *permissionMap) insert(addr net.Addr, p *permission) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return false
}
m.permMap[udpAddr.IP.String()] = p
return true
}
func (m *permissionMap) find(addr net.Addr) (*permission, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return nil, false
}
p, ok := m.permMap[udpAddr.IP.String()]
return p, ok
}
func (m *permissionMap) delete(addr net.Addr) {
m.mutex.Lock()
defer m.mutex.Unlock()
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return
}
delete(m.permMap, udpAddr.IP.String())
}
func (m *permissionMap) addrs() []net.Addr {
m.mutex.RLock()
defer m.mutex.RUnlock()
addrs := []net.Addr{}
for k := range m.permMap {
addrs = append(addrs, &net.UDPAddr{
IP: net.ParseIP(k),
})
}
return addrs
}
func newPermissionMap() *permissionMap {
return &permissionMap{
permMap: map[string]*permission{},
}
}

View file

@ -0,0 +1,185 @@
package client
import (
"net"
"sync"
"time"
"github.com/pion/stun"
)
const (
maxRtxInterval time.Duration = 1600 * time.Millisecond
)
// TransactionResult is a bag of result values of a transaction
type TransactionResult struct {
Msg *stun.Message
From net.Addr
Retries int
Err error
}
// TransactionConfig is a set of config params used by NewTransaction
type TransactionConfig struct {
Key string
Raw []byte
To net.Addr
Interval time.Duration
IgnoreResult bool // true to throw away the result of this transaction (it will not be readable using WaitForResult)
}
// Transaction represents a transaction
type Transaction struct {
Key string // read-only
Raw []byte // read-only
To net.Addr // read-only
nRtx int // modified only by the timer thread
interval time.Duration // modified only by the timer thread
timer *time.Timer // thread-safe, set only by the creator, and stopper
resultCh chan TransactionResult // thread-safe
mutex sync.RWMutex
}
// NewTransaction creates a new instance of Transaction
func NewTransaction(config *TransactionConfig) *Transaction {
var resultCh chan TransactionResult
if !config.IgnoreResult {
resultCh = make(chan TransactionResult)
}
return &Transaction{
Key: config.Key, // read-only
Raw: config.Raw, // read-only
To: config.To, // read-only
interval: config.Interval, // modified only by the timer thread
resultCh: resultCh, // thread-safe
}
}
// StartRtxTimer starts the transaction timer
func (t *Transaction) StartRtxTimer(onTimeout func(trKey string, nRtx int)) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.timer = time.AfterFunc(t.interval, func() {
t.mutex.Lock()
t.nRtx++
nRtx := t.nRtx
t.interval *= 2
if t.interval > maxRtxInterval {
t.interval = maxRtxInterval
}
t.mutex.Unlock()
onTimeout(t.Key, nRtx)
})
}
// StopRtxTimer stop the transaction timer
func (t *Transaction) StopRtxTimer() {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.timer != nil {
t.timer.Stop()
}
}
// WriteResult writes the result to the result channel
func (t *Transaction) WriteResult(res TransactionResult) bool {
if t.resultCh == nil {
return false
}
t.resultCh <- res
return true
}
// WaitForResult waits for the transaction result
func (t *Transaction) WaitForResult() TransactionResult {
if t.resultCh == nil {
return TransactionResult{
Err: errWaitForResultOnNonResultTransaction,
}
}
result, ok := <-t.resultCh
if !ok {
result.Err = errTransactionClosed
}
return result
}
// Close closes the transaction
func (t *Transaction) Close() {
if t.resultCh != nil {
close(t.resultCh)
}
}
// Retries returns the number of retransmission it has made
func (t *Transaction) Retries() int {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.nRtx
}
// TransactionMap is a thread-safe transaction map
type TransactionMap struct {
trMap map[string]*Transaction
mutex sync.RWMutex
}
// NewTransactionMap create a new instance of the transaction map
func NewTransactionMap() *TransactionMap {
return &TransactionMap{
trMap: map[string]*Transaction{},
}
}
// Insert inserts a trasaction to the map
func (m *TransactionMap) Insert(key string, tr *Transaction) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
m.trMap[key] = tr
return true
}
// Find looks up a transaction by its key
func (m *TransactionMap) Find(key string) (*Transaction, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
tr, ok := m.trMap[key]
return tr, ok
}
// Delete deletes a transaction by its key
func (m *TransactionMap) Delete(key string) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.trMap, key)
}
// CloseAndDeleteAll closes and deletes all transactions
func (m *TransactionMap) CloseAndDeleteAll() {
m.mutex.Lock()
defer m.mutex.Unlock()
for trKey, tr := range m.trMap {
tr.Close()
delete(m.trMap, trKey)
}
}
// Size returns the length of the transaction map
func (m *TransactionMap) Size() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.trMap)
}

View file

@ -0,0 +1,24 @@
package client
import (
"sync/atomic"
)
// TryLock implement the classic "try-lock" operation.
type TryLock struct {
n int32
}
// Lock tries to lock the try-lock. If successful, it returns true.
// Otherwise, it returns false immedidately.
func (c *TryLock) Lock() error {
if !atomic.CompareAndSwapInt32(&c.n, 0, 1) {
return errDoubleLock
}
return nil
}
// Unlock unlocks the try-lock.
func (c *TryLock) Unlock() {
atomic.StoreInt32(&c.n, 0)
}

View file

@ -0,0 +1,40 @@
// Package ipnet contains helper functions around net and IP
package ipnet
import (
"errors"
"net"
)
var errFailedToCastAddr = errors.New("failed to cast net.Addr to *net.UDPAddr or *net.TCPAddr")
// AddrIPPort extracts the IP and Port from a net.Addr
func AddrIPPort(a net.Addr) (net.IP, int, error) {
aUDP, ok := a.(*net.UDPAddr)
if ok {
return aUDP.IP, aUDP.Port, nil
}
aTCP, ok := a.(*net.TCPAddr)
if ok {
return aTCP.IP, aTCP.Port, nil
}
return nil, 0, errFailedToCastAddr
}
// AddrEqual asserts that two net.Addrs are equal
// Currently only supprots UDP but will be extended in the future to support others
func AddrEqual(a, b net.Addr) bool {
aUDP, ok := a.(*net.UDPAddr)
if !ok {
return false
}
bUDP, ok := b.(*net.UDPAddr)
if !ok {
return false
}
return aUDP.IP.Equal(bUDP.IP) && aUDP.Port == bUDP.Port
}

View file

@ -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
}

View file

@ -0,0 +1,140 @@
package proto
import (
"bytes"
"encoding/binary"
"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
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...)
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]
binary.BigEndian.PutUint16(c.Raw[:channelDataNumberSize], uint16(c.Number))
binary.BigEndian.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 := binary.BigEndian.Uint16(buf[:channelDataNumberSize])
c.Number = ChannelNumber(num)
l := binary.BigEndian.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
}
if int(binary.BigEndian.Uint16(buf[channelDataNumberSize:channelDataHeaderSize])) > len(buf[channelDataHeaderSize:]) {
return false
}
// Quick check for channel number.
num := binary.BigEndian.Uint16(buf[0:channelNumberSize])
return isChannelNumberValid(num)
}

View file

@ -0,0 +1,67 @@
package proto
import (
"encoding/binary"
"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)
binary.BigEndian.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(binary.BigEndian.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))
}

View file

@ -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
}

View file

@ -0,0 +1,18 @@
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
}

View file

@ -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
}

View file

@ -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") // nolint
panic(err) // nolint
}
if err != nil {
return 1
}
m2.WriteHeader()
if err := a.g.AddTo(m2); err != nil {
fmt.Println("failed to add attribute to m2") // nolint
panic(err) // nolint
}
m3.WriteHeader()
v, err := m2.Get(a.t)
if err != nil {
panic(err) // nolint
}
m3.Add(a.t, v)
if !m2.Equal(m3) {
fmt.Println(m2, "not equal", m3) // nolint
panic("not equal") // nolint
}
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) //nolint
}
return 1
}

View file

@ -0,0 +1,52 @@
package proto
import (
"encoding/binary"
"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)
binary.BigEndian.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 := binary.BigEndian.Uint32(v)
l.Duration = time.Second * time.Duration(seconds)
return nil
}

View file

@ -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

View file

@ -0,0 +1,30 @@
// Package proto implements RFC 5766 Traversal Using Relays around NAT.
//
// Merged from gortc/turn v0.80.
package proto
import (
"github.com/pion/stun"
)
// 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
)
// CreatePermissionRequest is shorthand for create permission request type.
func CreatePermissionRequest() stun.MessageType {
return stun.NewType(stun.MethodCreatePermission, stun.ClassRequest)
}
// AllocateRequest is shorthand for allocation request message type.
func AllocateRequest() stun.MessageType { return stun.NewType(stun.MethodAllocate, stun.ClassRequest) }
// SendIndication is shorthand for send indication message type.
func SendIndication() stun.MessageType { return stun.NewType(stun.MethodSend, stun.ClassIndication) }
// RefreshRequest is shorthand for refresh request message type.
func RefreshRequest() stun.MessageType { return stun.NewType(stun.MethodRefresh, stun.ClassRequest) }

View file

@ -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

View file

@ -0,0 +1,61 @@
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
var errInvalidRequestedFamilyValue = errors.New("invalid value for requested family attribute")
// 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 errInvalidRequestedFamilyValue
}
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
)

View file

@ -0,0 +1,65 @@
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
}

View file

@ -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
}

View file

@ -0,0 +1,26 @@
package server
import "errors"
var (
errFailedToGenerateNonce = errors.New("failed to generate nonce")
errFailedToSendError = errors.New("failed to send error message")
errDuplicatedNonce = errors.New("duplicated Nonce generated, discarding request")
errNoSuchUser = errors.New("no such user exists")
errUnexpectedClass = errors.New("unexpected class")
errUnexpectedMethod = errors.New("unexpected method")
errFailedToHandle = errors.New("failed to handle")
errUnhandledSTUNPacket = errors.New("unhandled STUN packet")
errUnableToHandleChannelData = errors.New("unable to handle ChannelData")
errFailedToCreateSTUNPacket = errors.New("failed to create stun message from packet")
errFailedToCreateChannelData = errors.New("failed to create channel data from packet")
errRelayAlreadyAllocatedForFiveTuple = errors.New("relay already allocated for 5-TUPLE")
errRequestedTransportMustBeUDP = errors.New("RequestedTransport must be UDP")
errNoDontFragmentSupport = errors.New("no support for DONT-FRAGMENT")
errRequestWithReservationTokenAndEvenPort = errors.New("Request must not contain RESERVATION-TOKEN and EVEN-PORT")
errNoAllocationFound = errors.New("no allocation found")
errNoPermission = errors.New("unable to handle send-indication, no permission added")
errShortWrite = errors.New("packet write smaller than packet")
errNoSuchChannelBind = errors.New("no such channel bind")
errFailedWriteSocket = errors.New("failed writing to socket")
)

View file

@ -0,0 +1,109 @@
// Package server implements the private API to implement a TURN server
package server
import (
"fmt"
"net"
"sync"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/allocation"
"github.com/pion/turn/v2/internal/proto"
)
// Request contains all the state needed to process a single incoming datagram
type Request struct {
// Current Request State
Conn net.PacketConn
SrcAddr net.Addr
Buff []byte
// Server State
AllocationManager *allocation.Manager
Nonces *sync.Map
// User Configuration
AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)
Log logging.LeveledLogger
Realm string
ChannelBindTimeout time.Duration
}
// HandleRequest processes the give Request
func HandleRequest(r Request) error {
r.Log.Debugf("received %d bytes of udp from %s on %s", len(r.Buff), r.SrcAddr.String(), r.Conn.LocalAddr().String())
if proto.IsChannelData(r.Buff) {
return handleDataPacket(r)
}
return handleTURNPacket(r)
}
func handleDataPacket(r Request) error {
r.Log.Debugf("received DataPacket from %s", r.SrcAddr.String())
c := proto.ChannelData{Raw: r.Buff}
if err := c.Decode(); err != nil {
return fmt.Errorf("%w: %v", errFailedToCreateChannelData, err)
}
err := handleChannelData(r, &c)
if err != nil {
err = fmt.Errorf("%w from %v: %v", errUnableToHandleChannelData, r.SrcAddr, err)
}
return err
}
func handleTURNPacket(r Request) error {
r.Log.Debug("handleTURNPacket")
m := &stun.Message{Raw: append([]byte{}, r.Buff...)}
if err := m.Decode(); err != nil {
return fmt.Errorf("%w: %v", errFailedToCreateSTUNPacket, err)
}
h, err := getMessageHandler(m.Type.Class, m.Type.Method)
if err != nil {
return fmt.Errorf("%w %v-%v from %v: %v", errUnhandledSTUNPacket, m.Type.Method, m.Type.Class, r.SrcAddr, err)
}
err = h(r, m)
if err != nil {
return fmt.Errorf("%w %v-%v from %v: %v", errFailedToHandle, m.Type.Method, m.Type.Class, r.SrcAddr, err)
}
return nil
}
func getMessageHandler(class stun.MessageClass, method stun.Method) (func(r Request, m *stun.Message) error, error) {
switch class {
case stun.ClassIndication:
switch method {
case stun.MethodSend:
return handleSendIndication, nil
default:
return nil, fmt.Errorf("%w: %s", errUnexpectedMethod, method)
}
case stun.ClassRequest:
switch method {
case stun.MethodAllocate:
return handleAllocateRequest, nil
case stun.MethodRefresh:
return handleRefreshRequest, nil
case stun.MethodCreatePermission:
return handleCreatePermissionRequest, nil
case stun.MethodChannelBind:
return handleChannelBindRequest, nil
case stun.MethodBinding:
return handleBindingRequest, nil
default:
return nil, fmt.Errorf("%w: %s", errUnexpectedMethod, method)
}
default:
return nil, fmt.Errorf("%w: %s", errUnexpectedClass, class)
}
}

View file

@ -0,0 +1,22 @@
package server
import (
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/ipnet"
)
func handleBindingRequest(r Request, m *stun.Message) error {
r.Log.Debugf("received BindingRequest from %s", r.SrcAddr.String())
ip, port, err := ipnet.AddrIPPort(r.SrcAddr)
if err != nil {
return err
}
attrs := buildMsg(m.TransactionID, stun.BindingSuccess, &stun.XORMappedAddress{
IP: ip,
Port: port,
}, stun.Fingerprint)
return buildAndSend(r.Conn, r.SrcAddr, attrs...)
}

View file

@ -0,0 +1,352 @@
package server
import (
"fmt"
"net"
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/allocation"
"github.com/pion/turn/v2/internal/ipnet"
"github.com/pion/turn/v2/internal/proto"
)
// // https://tools.ietf.org/html/rfc5766#section-6.2
func handleAllocateRequest(r Request, m *stun.Message) error {
r.Log.Debugf("received AllocateRequest from %s", r.SrcAddr.String())
// 1. The server MUST require that the request be authenticated. This
// authentication MUST be done using the long-term credential
// mechanism of [https://tools.ietf.org/html/rfc5389#section-10.2.2]
// unless the client and server agree to use another mechanism through
// some procedure outside the scope of this document.
messageIntegrity, hasAuth, err := authenticateRequest(r, m, stun.MethodAllocate)
if !hasAuth {
return err
}
fiveTuple := &allocation.FiveTuple{
SrcAddr: r.SrcAddr,
DstAddr: r.Conn.LocalAddr(),
Protocol: allocation.UDP,
}
requestedPort := 0
reservationToken := ""
badRequestMsg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
insufficentCapacityMsg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeInsufficientCapacity})
// 2. The server checks if the 5-tuple is currently in use by an
// existing allocation. If yes, the server rejects the request with
// a 437 (Allocation Mismatch) error.
if alloc := r.AllocationManager.GetAllocation(fiveTuple); alloc != nil {
msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeAllocMismatch})
return buildAndSendErr(r.Conn, r.SrcAddr, errRelayAlreadyAllocatedForFiveTuple, msg...)
}
// 3. The server checks if the request contains a REQUESTED-TRANSPORT
// attribute. If the REQUESTED-TRANSPORT attribute is not included
// or is malformed, the server rejects the request with a 400 (Bad
// Request) error. Otherwise, if the attribute is included but
// specifies a protocol other that UDP, the server rejects the
// request with a 442 (Unsupported Transport Protocol) error.
var requestedTransport proto.RequestedTransport
if err = requestedTransport.GetFrom(m); err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
} else if requestedTransport.Protocol != proto.ProtoUDP {
msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnsupportedTransProto})
return buildAndSendErr(r.Conn, r.SrcAddr, errRequestedTransportMustBeUDP, msg...)
}
// 4. The request may contain a DONT-FRAGMENT attribute. If it does,
// but the server does not support sending UDP datagrams with the DF
// bit set to 1 (see Section 12), then the server treats the DONT-
// FRAGMENT attribute in the Allocate request as an unknown
// comprehension-required attribute.
if m.Contains(stun.AttrDontFragment) {
msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeUnknownAttribute}, &stun.UnknownAttributes{stun.AttrDontFragment})
return buildAndSendErr(r.Conn, r.SrcAddr, errNoDontFragmentSupport, msg...)
}
// 5. The server checks if the request contains a RESERVATION-TOKEN
// attribute. If yes, and the request also contains an EVEN-PORT
// attribute, then the server rejects the request with a 400 (Bad
// Request) error. Otherwise, it checks to see if the token is
// valid (i.e., the token is in range and has not expired and the
// corresponding relayed transport address is still available). If
// the token is not valid for some reason, the server rejects the
// request with a 508 (Insufficient Capacity) error.
var reservationTokenAttr proto.ReservationToken
if err = reservationTokenAttr.GetFrom(m); err == nil {
var evenPort proto.EvenPort
if err = evenPort.GetFrom(m); err == nil {
return buildAndSendErr(r.Conn, r.SrcAddr, errRequestWithReservationTokenAndEvenPort, badRequestMsg...)
}
}
// 6. The server checks if the request contains an EVEN-PORT attribute.
// If yes, then the server checks that it can satisfy the request
// (i.e., can allocate a relayed transport address as described
// below). If the server cannot satisfy the request, then the
// server rejects the request with a 508 (Insufficient Capacity)
// error.
var evenPort proto.EvenPort
if err = evenPort.GetFrom(m); err == nil {
randomPort := 0
randomPort, err = r.AllocationManager.GetRandomEvenPort()
if err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, insufficentCapacityMsg...)
}
requestedPort = randomPort
reservationToken = randSeq(8)
}
// 7. At any point, the server MAY choose to reject the request with a
// 486 (Allocation Quota Reached) error if it feels the client is
// trying to exceed some locally defined allocation quota. The
// server is free to define this allocation quota any way it wishes,
// but SHOULD define it based on the username used to authenticate
// the request, and not on the client's transport address.
// 8. Also at any point, the server MAY choose to reject the request
// with a 300 (Try Alternate) error if it wishes to redirect the
// client to a different server. The use of this error code and
// attribute follow the specification in [RFC5389].
lifetimeDuration := allocationLifeTime(m)
a, err := r.AllocationManager.CreateAllocation(
fiveTuple,
r.Conn,
requestedPort,
lifetimeDuration)
if err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, insufficentCapacityMsg...)
}
// Once the allocation is created, the server replies with a success
// response. The success response contains:
// * An XOR-RELAYED-ADDRESS attribute containing the relayed transport
// address.
// * A LIFETIME attribute containing the current value of the time-to-
// expiry timer.
// * A RESERVATION-TOKEN attribute (if a second relayed transport
// address was reserved).
// * An XOR-MAPPED-ADDRESS attribute containing the client's IP address
// and port (from the 5-tuple).
srcIP, srcPort, err := ipnet.AddrIPPort(r.SrcAddr)
if err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
relayIP, relayPort, err := ipnet.AddrIPPort(a.RelayAddr)
if err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
responseAttrs := []stun.Setter{
&proto.RelayedAddress{
IP: relayIP,
Port: relayPort,
},
&proto.Lifetime{
Duration: lifetimeDuration,
},
&stun.XORMappedAddress{
IP: srcIP,
Port: srcPort,
},
}
if reservationToken != "" {
r.AllocationManager.CreateReservation(reservationToken, relayPort)
responseAttrs = append(responseAttrs, proto.ReservationToken([]byte(reservationToken)))
}
msg := buildMsg(m.TransactionID, stun.NewType(stun.MethodAllocate, stun.ClassSuccessResponse), append(responseAttrs, messageIntegrity)...)
return buildAndSend(r.Conn, r.SrcAddr, msg...)
}
func handleRefreshRequest(r Request, m *stun.Message) error {
r.Log.Debugf("received RefreshRequest from %s", r.SrcAddr.String())
messageIntegrity, hasAuth, err := authenticateRequest(r, m, stun.MethodRefresh)
if !hasAuth {
return err
}
lifetimeDuration := allocationLifeTime(m)
fiveTuple := &allocation.FiveTuple{
SrcAddr: r.SrcAddr,
DstAddr: r.Conn.LocalAddr(),
Protocol: allocation.UDP,
}
if lifetimeDuration != 0 {
a := r.AllocationManager.GetAllocation(fiveTuple)
if a == nil {
return fmt.Errorf("%w %v:%v", errNoAllocationFound, r.SrcAddr, r.Conn.LocalAddr())
}
a.Refresh(lifetimeDuration)
} else {
r.AllocationManager.DeleteAllocation(fiveTuple)
}
return buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID, stun.NewType(stun.MethodRefresh, stun.ClassSuccessResponse), []stun.Setter{
&proto.Lifetime{
Duration: lifetimeDuration,
},
messageIntegrity,
}...)...)
}
func handleCreatePermissionRequest(r Request, m *stun.Message) error {
r.Log.Debugf("received CreatePermission from %s", r.SrcAddr.String())
a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
SrcAddr: r.SrcAddr,
DstAddr: r.Conn.LocalAddr(),
Protocol: allocation.UDP,
})
if a == nil {
return fmt.Errorf("%w %v:%v", errNoAllocationFound, r.SrcAddr, r.Conn.LocalAddr())
}
messageIntegrity, hasAuth, err := authenticateRequest(r, m, stun.MethodCreatePermission)
if !hasAuth {
return err
}
addCount := 0
if err := m.ForEach(stun.AttrXORPeerAddress, func(m *stun.Message) error {
var peerAddress proto.PeerAddress
if err := peerAddress.GetFrom(m); err != nil {
return err
}
r.Log.Debugf("adding permission for %s", fmt.Sprintf("%s:%d",
peerAddress.IP.String(), peerAddress.Port))
a.AddPermission(allocation.NewPermission(
&net.UDPAddr{
IP: peerAddress.IP,
Port: peerAddress.Port,
},
r.Log,
))
addCount++
return nil
}); err != nil {
addCount = 0
}
respClass := stun.ClassSuccessResponse
if addCount == 0 {
respClass = stun.ClassErrorResponse
}
return buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID, stun.NewType(stun.MethodCreatePermission, respClass), []stun.Setter{messageIntegrity}...)...)
}
func handleSendIndication(r Request, m *stun.Message) error {
r.Log.Debugf("received SendIndication from %s", r.SrcAddr.String())
a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
SrcAddr: r.SrcAddr,
DstAddr: r.Conn.LocalAddr(),
Protocol: allocation.UDP,
})
if a == nil {
return fmt.Errorf("%w %v:%v", errNoAllocationFound, r.SrcAddr, r.Conn.LocalAddr())
}
dataAttr := proto.Data{}
if err := dataAttr.GetFrom(m); err != nil {
return err
}
peerAddress := proto.PeerAddress{}
if err := peerAddress.GetFrom(m); err != nil {
return err
}
msgDst := &net.UDPAddr{IP: peerAddress.IP, Port: peerAddress.Port}
if perm := a.GetPermission(msgDst); perm == nil {
return fmt.Errorf("%w: %v", errNoPermission, msgDst)
}
l, err := a.RelaySocket.WriteTo(dataAttr, msgDst)
if l != len(dataAttr) {
return fmt.Errorf("%w %d != %d (expected) err: %v", errShortWrite, l, len(dataAttr), err)
}
return err
}
func handleChannelBindRequest(r Request, m *stun.Message) error {
r.Log.Debugf("received ChannelBindRequest from %s", r.SrcAddr.String())
a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
SrcAddr: r.SrcAddr,
DstAddr: r.Conn.LocalAddr(),
Protocol: allocation.UDP,
})
if a == nil {
return fmt.Errorf("%w %v:%v", errNoAllocationFound, r.SrcAddr, r.Conn.LocalAddr())
}
badRequestMsg := buildMsg(m.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
messageIntegrity, hasAuth, err := authenticateRequest(r, m, stun.MethodChannelBind)
if !hasAuth {
return err
}
var channel proto.ChannelNumber
if err = channel.GetFrom(m); err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
peerAddr := proto.PeerAddress{}
if err = peerAddr.GetFrom(m); err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
r.Log.Debugf("binding channel %d to %s",
channel,
fmt.Sprintf("%s:%d", peerAddr.IP.String(), peerAddr.Port))
err = a.AddChannelBind(allocation.NewChannelBind(
channel,
&net.UDPAddr{IP: peerAddr.IP, Port: peerAddr.Port},
r.Log,
), r.ChannelBindTimeout)
if err != nil {
return buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
return buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID, stun.NewType(stun.MethodChannelBind, stun.ClassSuccessResponse), []stun.Setter{messageIntegrity}...)...)
}
func handleChannelData(r Request, c *proto.ChannelData) error {
r.Log.Debugf("received ChannelData from %s", r.SrcAddr.String())
a := r.AllocationManager.GetAllocation(&allocation.FiveTuple{
SrcAddr: r.SrcAddr,
DstAddr: r.Conn.LocalAddr(),
Protocol: allocation.UDP,
})
if a == nil {
return fmt.Errorf("%w %v:%v", errNoAllocationFound, r.SrcAddr, r.Conn.LocalAddr())
}
channel := a.GetChannelByNumber(c.Number)
if channel == nil {
return fmt.Errorf("%w %x", errNoSuchChannelBind, uint16(c.Number))
}
l, err := a.RelaySocket.WriteTo(c.Data, channel.Peer)
if err != nil {
return fmt.Errorf("%w: %s", errFailedWriteSocket, err.Error())
} else if l != len(c.Data) {
return fmt.Errorf("%w %d != %d (expected)", errShortWrite, l, len(c.Data))
}
return nil
}

View file

@ -0,0 +1,133 @@
package server
import (
"crypto/md5" //nolint:gosec,gci
"fmt"
"io"
"math/rand"
"net"
"strconv"
"time"
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/proto"
)
const (
maximumAllocationLifetime = time.Hour // https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation
nonceLifetime = time.Hour // https://tools.ietf.org/html/rfc5766#section-4
)
func randSeq(n int) string {
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))] //nolint:gosec
}
return string(b)
}
func buildNonce() (string, error) {
/* #nosec */
h := md5.New()
if _, err := io.WriteString(h, strconv.FormatInt(time.Now().Unix(), 10)); err != nil {
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err)
}
if _, err := io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)); err != nil { //nolint:gosec
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err)
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func buildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error {
msg, err := stun.Build(attrs...)
if err != nil {
return err
}
_, err = conn.WriteTo(msg.Raw, dst)
return err
}
// Send a STUN packet and return the original error to the caller
func buildAndSendErr(conn net.PacketConn, dst net.Addr, err error, attrs ...stun.Setter) error {
if sendErr := buildAndSend(conn, dst, attrs...); sendErr != nil {
err = fmt.Errorf("%w %v %v", errFailedToSendError, sendErr, err)
}
return err
}
func buildMsg(transactionID [stun.TransactionIDSize]byte, msgType stun.MessageType, additional ...stun.Setter) []stun.Setter {
return append([]stun.Setter{&stun.Message{TransactionID: transactionID}, msgType}, additional...)
}
func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) (stun.MessageIntegrity, bool, error) {
respondWithNonce := func(responseCode stun.ErrorCode) (stun.MessageIntegrity, bool, error) {
nonce, err := buildNonce()
if err != nil {
return nil, false, err
}
// Nonce has already been taken
if _, keyCollision := r.Nonces.LoadOrStore(nonce, time.Now()); keyCollision {
return nil, false, errDuplicatedNonce
}
return nil, false, buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID,
stun.NewType(callingMethod, stun.ClassErrorResponse),
&stun.ErrorCodeAttribute{Code: responseCode},
stun.NewNonce(nonce),
stun.NewRealm(r.Realm),
)...)
}
if !m.Contains(stun.AttrMessageIntegrity) {
return respondWithNonce(stun.CodeUnauthorized)
}
nonceAttr := &stun.Nonce{}
usernameAttr := &stun.Username{}
realmAttr := &stun.Realm{}
badRequestMsg := buildMsg(m.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})
if err := nonceAttr.GetFrom(m); err != nil {
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
// Assert Nonce exists and is not expired
nonceCreationTime, ok := r.Nonces.Load(string(*nonceAttr))
if !ok || time.Since(nonceCreationTime.(time.Time)) >= nonceLifetime {
r.Nonces.Delete(nonceAttr)
return respondWithNonce(stun.CodeStaleNonce)
}
if err := realmAttr.GetFrom(m); err != nil {
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
} else if err := usernameAttr.GetFrom(m); err != nil {
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
ourKey, ok := r.AuthHandler(usernameAttr.String(), realmAttr.String(), r.SrcAddr)
if !ok {
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("%w %s", errNoSuchUser, usernameAttr.String()), badRequestMsg...)
}
if err := stun.MessageIntegrity(ourKey).Check(m); err != nil {
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}
return stun.MessageIntegrity(ourKey), true, nil
}
func allocationLifeTime(m *stun.Message) time.Duration {
lifetimeDuration := proto.DefaultLifetime
var lifetime proto.Lifetime
if err := lifetime.GetFrom(m); err == nil {
if lifetime.Duration < maximumAllocationLifetime {
lifetimeDuration = lifetime.Duration
}
}
return lifetimeDuration
}

View file

@ -0,0 +1,56 @@
package turn
import ( //nolint:gci
"crypto/hmac"
"crypto/sha1" //nolint:gosec,gci
"encoding/base64"
"net"
"strconv"
"time"
"github.com/pion/logging"
)
// GenerateCredentials can be used to create credentials valid for [duration] time
func GenerateLongTermCredentials(sharedSecret string, duration time.Duration) (string, string, error) {
t := time.Now().Add(duration).Unix()
username := strconv.FormatInt(t, 10)
password, err := longTermCredentials(username, sharedSecret)
return username, password, err
}
func longTermCredentials(username string, sharedSecret string) (string, error) {
mac := hmac.New(sha1.New, []byte(sharedSecret))
_, err := mac.Write([]byte(username))
if err != nil {
return "", err // Not sure if this will ever happen
}
password := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(password), nil
}
// NewAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials.
// https://tools.ietf.org/search/rfc5389#section-10.2
func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
if l == nil {
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
}
return func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
l.Tracef("Authentication username=%q realm=%q srcAddr=%v\n", username, realm, srcAddr)
t, err := strconv.Atoi(username)
if err != nil {
l.Errorf("Invalid time-windowed username %q", username)
return nil, false
}
if int64(t) < time.Now().Unix() {
l.Errorf("Expired time-windowed username %q", username)
return nil, false
}
password, err := longTermCredentials(username, sharedSecret)
if err != nil {
l.Error(err.Error())
return nil, false
}
return GenerateAuthKey(username, realm, password), true
}
}

View file

@ -0,0 +1,45 @@
package turn
import (
"net"
"strconv"
"github.com/pion/transport/vnet"
)
// RelayAddressGeneratorNone returns the listener with no modifications
type RelayAddressGeneratorNone struct {
// Address is passed to Listen/ListenPacket when creating the Relay
Address string
Net *vnet.Net
}
// Validate is caled on server startup and confirms the RelayAddressGenerator is properly configured
func (r *RelayAddressGeneratorNone) Validate() error {
if r.Net == nil {
r.Net = vnet.NewNet(nil)
}
switch {
case r.Address == "":
return errListeningAddressInvalid
default:
return nil
}
}
// AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port to populate the allocation response with
func (r *RelayAddressGeneratorNone) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
conn, err := r.Net.ListenPacket(network, r.Address+":"+strconv.Itoa(requestedPort))
if err != nil {
return nil, nil, err
}
return conn, conn.LocalAddr(), nil
}
// AllocateConn generates a new Conn to receive traffic on and the IP/Port to populate the allocation response with
func (r *RelayAddressGeneratorNone) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) {
return nil, nil, errTODO
}

View file

@ -0,0 +1,92 @@
package turn
import (
"fmt"
"net"
"github.com/pion/randutil"
"github.com/pion/transport/vnet"
)
// RelayAddressGeneratorPortRange can be used to only allocate connections inside a defined port range.
// Similar to the RelayAddressGeneratorStatic a static ip address can be set.
type RelayAddressGeneratorPortRange struct {
// RelayAddress is the IP returned to the user when the relay is created
RelayAddress net.IP
// MinPort the minimum port to allocate
MinPort uint16
// MaxPort the maximum (inclusive) port to allocate
MaxPort uint16
// MaxRetries the amount of tries to allocate a random port in the defined range
MaxRetries int
// Rand the random source of numbers
Rand randutil.MathRandomGenerator
// Address is passed to Listen/ListenPacket when creating the Relay
Address string
Net *vnet.Net
}
// Validate is called on server startup and confirms the RelayAddressGenerator is properly configured
func (r *RelayAddressGeneratorPortRange) Validate() error {
if r.Net == nil {
r.Net = vnet.NewNet(nil)
}
if r.Rand == nil {
r.Rand = randutil.NewMathRandomGenerator()
}
if r.MaxRetries == 0 {
r.MaxRetries = 10
}
switch {
case r.MinPort == 0:
return errMinPortNotZero
case r.MaxPort == 0:
return errMaxPortNotZero
case r.RelayAddress == nil:
return errRelayAddressInvalid
case r.Address == "":
return errListeningAddressInvalid
default:
return nil
}
}
// AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port to populate the allocation response with
func (r *RelayAddressGeneratorPortRange) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
if requestedPort != 0 {
conn, err := r.Net.ListenPacket(network, fmt.Sprintf("%s:%d", r.Address, requestedPort))
if err != nil {
return nil, nil, err
}
relayAddr := conn.LocalAddr().(*net.UDPAddr)
relayAddr.IP = r.RelayAddress
return conn, relayAddr, nil
}
for try := 0; try < r.MaxRetries; try++ {
port := r.MinPort + uint16(r.Rand.Intn(int((r.MaxPort+1)-r.MinPort)))
conn, err := r.Net.ListenPacket(network, fmt.Sprintf("%s:%d", r.Address, port))
if err != nil {
continue
}
relayAddr := conn.LocalAddr().(*net.UDPAddr)
relayAddr.IP = r.RelayAddress
return conn, relayAddr, nil
}
return nil, nil, errMaxRetriesExceeded
}
// AllocateConn generates a new Conn to receive traffic on and the IP/Port to populate the allocation response with
func (r *RelayAddressGeneratorPortRange) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) {
return nil, nil, errTODO
}

View file

@ -0,0 +1,55 @@
package turn
import (
"net"
"strconv"
"github.com/pion/transport/vnet"
)
// RelayAddressGeneratorStatic can be used to return static IP address each time a relay is created.
// This can be used when you have a single static IP address that you want to use
type RelayAddressGeneratorStatic struct {
// RelayAddress is the IP returned to the user when the relay is created
RelayAddress net.IP
// Address is passed to Listen/ListenPacket when creating the Relay
Address string
Net *vnet.Net
}
// Validate is caled on server startup and confirms the RelayAddressGenerator is properly configured
func (r *RelayAddressGeneratorStatic) Validate() error {
if r.Net == nil {
r.Net = vnet.NewNet(nil)
}
switch {
case r.RelayAddress == nil:
return errRelayAddressInvalid
case r.Address == "":
return errListeningAddressInvalid
default:
return nil
}
}
// AllocatePacketConn generates a new PacketConn to receive traffic on and the IP/Port to populate the allocation response with
func (r *RelayAddressGeneratorStatic) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
conn, err := r.Net.ListenPacket(network, r.Address+":"+strconv.Itoa(requestedPort))
if err != nil {
return nil, nil, err
}
// Replace actual listening IP with the user requested one of RelayAddressGeneratorStatic
relayAddr := conn.LocalAddr().(*net.UDPAddr)
relayAddr.IP = r.RelayAddress
return conn, relayAddr, nil
}
// AllocateConn generates a new Conn to receive traffic on and the IP/Port to populate the allocation response with
func (r *RelayAddressGeneratorStatic) AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error) {
return nil, nil, errTODO
}

View file

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
],
"commitBody": "Generated by renovateBot",
"packageRules": [
{
"packagePatterns": ["^golang.org/x/"],
"schedule": ["on the first day of the month"]
}
]
}

View file

@ -0,0 +1,161 @@
// Package turn contains the public API for pion/turn, a toolkit for building TURN clients and servers
package turn
import (
"fmt"
"net"
"sync"
"time"
"github.com/pion/logging"
"github.com/pion/turn/v2/internal/allocation"
"github.com/pion/turn/v2/internal/proto"
"github.com/pion/turn/v2/internal/server"
)
const (
inboundMTU = 1500
)
// Server is an instance of the Pion TURN Server
type Server struct {
log logging.LeveledLogger
authHandler AuthHandler
realm string
channelBindTimeout time.Duration
nonces *sync.Map
packetConnConfigs []PacketConnConfig
listenerConfigs []ListenerConfig
}
// NewServer creates the Pion TURN server
func NewServer(config ServerConfig) (*Server, error) {
if err := config.validate(); err != nil {
return nil, err
}
loggerFactory := config.LoggerFactory
if loggerFactory == nil {
loggerFactory = logging.NewDefaultLoggerFactory()
}
s := &Server{
log: loggerFactory.NewLogger("turn"),
authHandler: config.AuthHandler,
realm: config.Realm,
channelBindTimeout: config.ChannelBindTimeout,
packetConnConfigs: config.PacketConnConfigs,
listenerConfigs: config.ListenerConfigs,
nonces: &sync.Map{},
}
if s.channelBindTimeout == 0 {
s.channelBindTimeout = proto.DefaultLifetime
}
for i := range s.packetConnConfigs {
go func(p PacketConnConfig) {
allocationManager, err := allocation.NewManager(allocation.ManagerConfig{
AllocatePacketConn: p.RelayAddressGenerator.AllocatePacketConn,
AllocateConn: p.RelayAddressGenerator.AllocateConn,
LeveledLogger: s.log,
})
if err != nil {
s.log.Errorf("exit read loop on error: %s", err.Error())
return
}
defer func() {
if err := allocationManager.Close(); err != nil {
s.log.Errorf("Failed to close AllocationManager: %s", err.Error())
}
}()
s.readLoop(p.PacketConn, allocationManager)
}(s.packetConnConfigs[i])
}
for _, listener := range s.listenerConfigs {
go func(l ListenerConfig) {
allocationManager, err := allocation.NewManager(allocation.ManagerConfig{
AllocatePacketConn: l.RelayAddressGenerator.AllocatePacketConn,
AllocateConn: l.RelayAddressGenerator.AllocateConn,
LeveledLogger: s.log,
})
if err != nil {
s.log.Errorf("exit read loop on error: %s", err.Error())
return
}
defer func() {
if err := allocationManager.Close(); err != nil {
s.log.Errorf("Failed to close AllocationManager: %s", err.Error())
}
}()
for {
conn, err := l.Listener.Accept()
if err != nil {
s.log.Debugf("exit accept loop on error: %s", err.Error())
return
}
go s.readLoop(NewSTUNConn(conn), allocationManager)
}
}(listener)
}
return s, nil
}
// Close stops the TURN Server. It cleans up any associated state and closes all connections it is managing
func (s *Server) Close() error {
var errors []error
for _, p := range s.packetConnConfigs {
if err := p.PacketConn.Close(); err != nil {
errors = append(errors, err)
}
}
for _, l := range s.listenerConfigs {
if err := l.Listener.Close(); err != nil {
errors = append(errors, err)
}
}
if len(errors) == 0 {
return nil
}
err := errFailedToClose
for _, e := range errors {
err = fmt.Errorf("%s; Close error (%v) ", err.Error(), e) //nolint:goerr113
}
return err
}
func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager) {
buf := make([]byte, inboundMTU)
for {
n, addr, err := p.ReadFrom(buf)
if err != nil {
s.log.Debugf("exit read loop on error: %s", err.Error())
return
}
if err := server.HandleRequest(server.Request{
Conn: p,
SrcAddr: addr,
Buff: buf[:n],
Log: s.log,
AuthHandler: s.authHandler,
Realm: s.realm,
AllocationManager: allocationManager,
ChannelBindTimeout: s.channelBindTimeout,
Nonces: s.nonces,
}); err != nil {
s.log.Errorf("error when handling datagram: %v", err)
}
}
}

View file

@ -0,0 +1,116 @@
package turn
import (
"crypto/md5" //nolint:gosec,gci
"fmt"
"net"
"strings"
"time"
"github.com/pion/logging"
)
// RelayAddressGenerator is used to generate a RelayAddress when creating an allocation.
// You can use one of the provided ones or provide your own.
type RelayAddressGenerator interface {
// Validate confirms that the RelayAddressGenerator is properly initialized
Validate() error
// Allocate a PacketConn (UDP) RelayAddress
AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error)
// Allocate a Conn (TCP) RelayAddress
AllocateConn(network string, requestedPort int) (net.Conn, net.Addr, error)
}
// PacketConnConfig is a single net.PacketConn to listen/write on. This will be used for UDP listeners
type PacketConnConfig struct {
PacketConn net.PacketConn
// When an allocation is generated the RelayAddressGenerator
// creates the net.PacketConn and returns the IP/Port it is available at
RelayAddressGenerator RelayAddressGenerator
}
func (c *PacketConnConfig) validate() error {
if c.PacketConn == nil {
return errConnUnset
}
if c.RelayAddressGenerator == nil {
return errRelayAddressGeneratorUnset
}
return c.RelayAddressGenerator.Validate()
}
// ListenerConfig is a single net.Listener to accept connections on. This will be used for TCP, TLS and DTLS listeners
type ListenerConfig struct {
Listener net.Listener
// When an allocation is generated the RelayAddressGenerator
// creates the net.PacketConn and returns the IP/Port it is available at
RelayAddressGenerator RelayAddressGenerator
}
func (c *ListenerConfig) validate() error {
if c.Listener == nil {
return errListenerUnset
}
if c.RelayAddressGenerator == nil {
return errRelayAddressGeneratorUnset
}
return c.RelayAddressGenerator.Validate()
}
// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
type AuthHandler func(username, realm string, srcAddr net.Addr) (key []byte, ok bool)
// GenerateAuthKey is a convince function to easily generate keys in the format used by AuthHandler
func GenerateAuthKey(username, realm, password string) []byte {
// #nosec
h := md5.New()
fmt.Fprint(h, strings.Join([]string{username, realm, password}, ":"))
return h.Sum(nil)
}
// ServerConfig configures the Pion TURN Server
type ServerConfig struct {
// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners
// Each listener can have custom behavior around the creation of Relays
PacketConnConfigs []PacketConnConfig
ListenerConfigs []ListenerConfig
// LoggerFactory must be set for logging from this server.
LoggerFactory logging.LoggerFactory
// Realm sets the realm for this server
Realm string
// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
AuthHandler AuthHandler
// ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes.
ChannelBindTimeout time.Duration
}
func (s *ServerConfig) validate() error {
if len(s.PacketConnConfigs) == 0 && len(s.ListenerConfigs) == 0 {
return errNoAvailableConns
}
for _, s := range s.PacketConnConfigs {
if err := s.validate(); err != nil {
return err
}
}
for _, s := range s.ListenerConfigs {
if err := s.validate(); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,121 @@
package turn
import (
"encoding/binary"
"errors"
"net"
"time"
"github.com/pion/stun"
"github.com/pion/turn/v2/internal/proto"
)
var (
errInvalidTURNFrame = errors.New("data is not a valid TURN frame, no STUN or ChannelData found")
errIncompleteTURNFrame = errors.New("data contains incomplete STUN or TURN frame")
)
// STUNConn wraps a net.Conn and implements
// net.PacketConn by being STUN aware and
// packetizing the stream
type STUNConn struct {
nextConn net.Conn
buff []byte
}
const (
stunHeaderSize = 20
channelDataLengthSize = 2
channelDataNumberSize = channelDataLengthSize
channelDataHeaderSize = channelDataLengthSize + channelDataNumberSize
channelDataPadding = 4
)
// Given a buffer give the last offset of the TURN frame
// If the buffer isn't a valid STUN or ChannelData packet
// or the length doesn't match return false
func consumeSingleTURNFrame(p []byte) (int, error) {
// Too short to determine if ChannelData or STUN
if len(p) < 9 {
return 0, errIncompleteTURNFrame
}
var datagramSize uint16
if stun.IsMessage(p) {
datagramSize = binary.BigEndian.Uint16(p[2:4]) + stunHeaderSize
} else if num := binary.BigEndian.Uint16(p[0:4]); proto.ChannelNumber(num).Valid() {
datagramSize = binary.BigEndian.Uint16(p[channelDataNumberSize:channelDataHeaderSize])
if paddingOverflow := (datagramSize + channelDataPadding) % channelDataPadding; paddingOverflow != 0 {
datagramSize = (datagramSize + channelDataPadding) - paddingOverflow
}
datagramSize += channelDataHeaderSize
} else {
return 0, errInvalidTURNFrame
}
if len(p) < int(datagramSize) {
return 0, errIncompleteTURNFrame
}
return int(datagramSize), nil
}
// ReadFrom implements ReadFrom from net.PacketConn
func (s *STUNConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
// First pass any buffered data from previous reads
n, err = consumeSingleTURNFrame(s.buff)
if errors.Is(err, errInvalidTURNFrame) {
return 0, nil, err
} else if err == nil {
copy(p, s.buff[:n])
s.buff = s.buff[n:]
return n, s.nextConn.RemoteAddr(), nil
}
// Then read from the nextConn, appending to our buff
n, err = s.nextConn.Read(p)
if err != nil {
return 0, nil, err
}
s.buff = append(s.buff, append([]byte{}, p[:n]...)...)
return s.ReadFrom(p)
}
// WriteTo implements WriteTo from net.PacketConn
func (s *STUNConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return s.nextConn.Write(p)
}
// Close implements Close from net.PacketConn
func (s *STUNConn) Close() error {
return s.nextConn.Close()
}
// LocalAddr implements LocalAddr from net.PacketConn
func (s *STUNConn) LocalAddr() net.Addr {
return s.nextConn.LocalAddr()
}
// SetDeadline implements SetDeadline from net.PacketConn
func (s *STUNConn) SetDeadline(t time.Time) error {
return s.nextConn.SetDeadline(t)
}
// SetReadDeadline implements SetReadDeadline from net.PacketConn
func (s *STUNConn) SetReadDeadline(t time.Time) error {
return s.nextConn.SetReadDeadline(t)
}
// SetWriteDeadline implements SetWriteDeadline from net.PacketConn
func (s *STUNConn) SetWriteDeadline(t time.Time) error {
return s.nextConn.SetWriteDeadline(t)
}
// NewSTUNConn creates a STUNConn
func NewSTUNConn(nextConn net.Conn) *STUNConn {
return &STUNConn{nextConn: nextConn}
}