mirror of
https://github.com/hanzokms/turn.git
synced 2026-10-30 20:50:36 +00:00
468 lines
12 KiB
Go
468 lines
12 KiB
Go
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package client implements the API for a TURN client
|
|
package client
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/pion/stun/v3"
|
|
"github.com/hanzokms/turn/v4/internal/proto"
|
|
)
|
|
|
|
const (
|
|
maxReadQueueSize = 1024
|
|
permRefreshInterval = 120 * time.Second
|
|
maxRetryAttempts = 3
|
|
)
|
|
|
|
const (
|
|
timerIDRefreshAlloc int = iota
|
|
timerIDRefreshPerms
|
|
)
|
|
|
|
type inboundData struct {
|
|
data []byte
|
|
from net.Addr
|
|
}
|
|
|
|
// UDPConn is the implementation of the Conn and PacketConn interfaces for UDP network connections.
|
|
// compatible with net.PacketConn and net.Conn.
|
|
type UDPConn struct {
|
|
bindingMgr *bindingManager // Thread-safe
|
|
readCh chan *inboundData // Thread-safe
|
|
closeCh chan struct{} // Thread-safe
|
|
allocation
|
|
}
|
|
|
|
// NewUDPConn creates a new instance of UDPConn.
|
|
func NewUDPConn(config *AllocationConfig) *UDPConn {
|
|
conn := &UDPConn{
|
|
bindingMgr: newBindingManager(),
|
|
readCh: make(chan *inboundData, maxReadQueueSize),
|
|
closeCh: make(chan struct{}),
|
|
allocation: allocation{
|
|
client: config.Client,
|
|
relayedAddr: config.RelayedAddr,
|
|
serverAddr: config.ServerAddr,
|
|
readTimer: time.NewTimer(time.Duration(math.MaxInt64)),
|
|
permMap: newPermissionMap(),
|
|
username: config.Username,
|
|
realm: config.Realm,
|
|
integrity: config.Integrity,
|
|
_nonce: config.Nonce,
|
|
_lifetime: config.Lifetime,
|
|
net: config.Net,
|
|
log: config.Log,
|
|
},
|
|
}
|
|
|
|
conn.log.Debugf("Initial lifetime: %d seconds", int(conn.lifetime().Seconds()))
|
|
|
|
conn.refreshAllocTimer = NewPeriodicTimer(
|
|
timerIDRefreshAlloc,
|
|
conn.onRefreshTimers,
|
|
conn.lifetime()/2,
|
|
)
|
|
|
|
conn.refreshPermsTimer = NewPeriodicTimer(
|
|
timerIDRefreshPerms,
|
|
conn.onRefreshTimers,
|
|
permRefreshInterval,
|
|
)
|
|
|
|
if conn.refreshAllocTimer.Start() {
|
|
conn.log.Debugf("Started refresh allocation timer")
|
|
}
|
|
if conn.refreshPermsTimer.Start() {
|
|
conn.log.Debugf("Started refresh permission timer")
|
|
}
|
|
|
|
return conn
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *allocation) createPermission(perm *permission, addr net.Addr) error {
|
|
perm.mutex.Lock()
|
|
defer perm.mutex.Unlock()
|
|
|
|
if perm.state() == permStateIdle {
|
|
// Punch a hole! (this would block a bit..)
|
|
if err := a.CreatePermissions(addr); err != nil {
|
|
a.permMap.delete(addr)
|
|
|
|
return err
|
|
}
|
|
perm.setState(permStatePermitted)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteTo writes a packet with payload 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(payload []byte, addr net.Addr) (int, error) { //nolint:gocognit,cyclop
|
|
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)
|
|
}
|
|
|
|
for i := 0; i < maxRetryAttempts; i++ {
|
|
// c.createPermission() 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 most likely successful and we can tolerate some loss of
|
|
// UDP packet (or reorder), inorder to minimize the latency in most cases.
|
|
if err = c.createPermission(perm, addr); !errors.Is(err, errTryAgain) {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Bind channel
|
|
bound, ok := c.bindingMgr.findByAddr(addr)
|
|
if !ok {
|
|
bound = c.bindingMgr.create(addr)
|
|
}
|
|
|
|
bindSt := bound.state()
|
|
|
|
//nolint:nestif
|
|
if bindSt == bindingStateIdle || bindSt == bindingStateRequest || bindSt == bindingStateFailed {
|
|
func() {
|
|
// Block only callers with the same binding until
|
|
// the binding transaction has been complete
|
|
bound.muBind.Lock()
|
|
defer bound.muBind.Unlock()
|
|
|
|
// Binding state may have been changed while waiting. check again.
|
|
if bound.state() == bindingStateIdle {
|
|
bound.setState(bindingStateRequest)
|
|
go func() {
|
|
err2 := c.bind(bound)
|
|
if err2 != nil {
|
|
c.log.Warnf("Failed to bind bind(): %s", err2)
|
|
bound.setState(bindingStateFailed)
|
|
// Keep going...
|
|
} else {
|
|
bound.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(payload),
|
|
peerAddr,
|
|
stun.Fingerprint,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Indication has no transaction (fire-and-forget)
|
|
|
|
return c.client.WriteTo(msg.Raw, c.serverAddr)
|
|
}
|
|
|
|
// Binding is either ready
|
|
|
|
// Check if the binding needs a refresh
|
|
func() {
|
|
bound.muBind.Lock()
|
|
defer bound.muBind.Unlock()
|
|
|
|
if bound.state() == bindingStateReady && time.Since(bound.refreshedAt()) > 5*time.Minute {
|
|
bound.setState(bindingStateRefresh)
|
|
go func() {
|
|
if bindErr := c.bind(bound); bindErr != nil {
|
|
c.log.Warnf("Failed to bind() for refresh: %s", bindErr)
|
|
bound.setState(bindingStateFailed)
|
|
// Keep going...
|
|
} else {
|
|
bound.setRefreshedAt(time.Now())
|
|
bound.setState(bindingStateReady)
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
|
|
// Send via ChannelData
|
|
_, err = c.sendChannelData(payload, bound.number)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return len(payload), nil
|
|
}
|
|
|
|
// 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.client.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(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
|
|
}
|
|
|
|
// CreatePermissions Issues a CreatePermission request for the supplied addresses
|
|
// as described in https://datatracker.ietf.org/doc/html/rfc5766#section-9
|
|
func (a *allocation) 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,
|
|
a.username,
|
|
a.realm,
|
|
a.nonce(),
|
|
a.integrity,
|
|
stun.Fingerprint)
|
|
|
|
msg, err := stun.Build(setters...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trRes, err := a.client.PerformTransaction(msg, a.serverAddr, 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 {
|
|
a.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) bind(bound *binding) error {
|
|
setters := []stun.Setter{
|
|
stun.TransactionID,
|
|
stun.NewType(stun.MethodChannelBind, stun.ClassRequest),
|
|
addr2PeerAddress(bound.addr),
|
|
proto.ChannelNumber(bound.number),
|
|
c.username,
|
|
c.realm,
|
|
c.nonce(),
|
|
c.integrity,
|
|
stun.Fingerprint,
|
|
}
|
|
|
|
msg, err := stun.Build(setters...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trRes, err := c.client.PerformTransaction(msg, c.serverAddr, false)
|
|
if err != nil {
|
|
c.bindingMgr.deleteByAddr(bound.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", bound.addr, bound.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()
|
|
_, err := c.client.WriteTo(chData.Raw, c.serverAddr)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return len(data), nil
|
|
}
|