mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-14 19:00:50 +01:00
247 lines
7.4 KiB
Go
247 lines
7.4 KiB
Go
package iface
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
log "github.com/sirupsen/logrus"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/pion/logging"
|
|
"github.com/pion/stun"
|
|
)
|
|
|
|
// UniversalUDPMux allows multiple connections to go over a single UDP port for
|
|
// host, server reflexive and relayed candidates.
|
|
// Actual connection muxing is happening in the UDPMux.
|
|
type UniversalUDPMux interface {
|
|
UDPMux
|
|
GetXORMappedAddr(stunAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error)
|
|
GetRelayedAddr(turnAddr net.Addr, deadline time.Duration) (*net.Addr, error)
|
|
GetConnForURL(ufrag string, url string) (net.PacketConn, error)
|
|
}
|
|
|
|
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn overriding ReadFrom.
|
|
// It the passes packets to the UDPMux that does the actual connection muxing.
|
|
type UniversalUDPMuxDefault struct {
|
|
*UDPMuxDefault
|
|
params UniversalUDPMuxParams
|
|
|
|
// since we have a shared socket, for srflx candidates it makes sense to have a shared mapped address across all the agents
|
|
// stun.XORMappedAddress indexed by the STUN server addr
|
|
xorMappedMap map[string]*xorMapped
|
|
}
|
|
|
|
// UniversalUDPMuxParams are parameters for UniversalUDPMux server reflexive.
|
|
type UniversalUDPMuxParams struct {
|
|
Logger logging.LeveledLogger
|
|
UDPConn net.PacketConn
|
|
XORMappedAddrCacheTTL time.Duration
|
|
}
|
|
|
|
// NewUniversalUDPMuxDefault creates an implementation of UniversalUDPMux embedding UDPMux
|
|
func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDefault {
|
|
if params.Logger == nil {
|
|
params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice")
|
|
}
|
|
if params.XORMappedAddrCacheTTL == 0 {
|
|
params.XORMappedAddrCacheTTL = time.Second * 25
|
|
}
|
|
|
|
m := &UniversalUDPMuxDefault{
|
|
params: params,
|
|
xorMappedMap: make(map[string]*xorMapped),
|
|
}
|
|
|
|
// embed UDPMux
|
|
udpMuxParams := UDPMuxParams{
|
|
Logger: params.Logger,
|
|
UDPConn: m.params.UDPConn,
|
|
}
|
|
m.UDPMuxDefault = NewUDPMuxDefault(udpMuxParams)
|
|
|
|
return m
|
|
}
|
|
|
|
// GetRelayedAddr creates relayed connection to the given TURN service and returns the relayed addr.
|
|
// Not implemented yet.
|
|
func (m *UniversalUDPMuxDefault) GetRelayedAddr(turnAddr net.Addr, deadline time.Duration) (*net.Addr, error) {
|
|
return nil, errors.New("not implemented yet")
|
|
}
|
|
|
|
// GetConnForURL add uniques to the muxed connection by concatenating ufrag and URL (e.g. STUN URL) to be able to support multiple STUN/TURN servers
|
|
// and return a unique connection per server.
|
|
func (m *UniversalUDPMuxDefault) GetConnForURL(ufrag string, url string) (net.PacketConn, error) {
|
|
return m.UDPMuxDefault.GetConn(fmt.Sprintf("%s%s", ufrag, url))
|
|
}
|
|
|
|
func (m *UniversalUDPMuxDefault) HandlePacket(p []byte, n int, addr net.Addr) error {
|
|
if stun.IsMessage(p[:n]) {
|
|
msg := &stun.Message{
|
|
Raw: append([]byte{}, p[:n]...),
|
|
}
|
|
|
|
if err := msg.Decode(); err != nil {
|
|
log.Warnf("Failed to handle decode ICE from %s: %v\n", addr.String(), err)
|
|
// todo proper error
|
|
return nil
|
|
}
|
|
|
|
udpAddr, ok := addr.(*net.UDPAddr)
|
|
if !ok {
|
|
// message about this err will be logged in the UDPMux
|
|
return nil
|
|
}
|
|
|
|
if m.isXORMappedResponse(msg, udpAddr.String()) {
|
|
err := m.handleXORMappedResponse(udpAddr, msg)
|
|
if err != nil {
|
|
log.Debugf("%w: %v", errors.New("failed to get XOR-MAPPED-ADDRESS response"), err)
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return m.UDPMuxDefault.HandlePacket(p, n, addr)
|
|
}
|
|
|
|
// isXORMappedResponse indicates whether the message is a XORMappedAddress and is coming from the known STUN server.
|
|
func (m *UniversalUDPMuxDefault) isXORMappedResponse(msg *stun.Message, stunAddr string) bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
// check first if it is a STUN server address because remote peer can also send similar messages but as a BindingSuccess
|
|
_, ok := m.xorMappedMap[stunAddr]
|
|
_, err := msg.Get(stun.AttrXORMappedAddress)
|
|
return err == nil && ok
|
|
}
|
|
|
|
// handleXORMappedResponse parses response from the STUN server, extracts XORMappedAddress attribute
|
|
// and set the mapped address for the server
|
|
func (m *UniversalUDPMuxDefault) handleXORMappedResponse(stunAddr *net.UDPAddr, msg *stun.Message) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
mappedAddr, ok := m.xorMappedMap[stunAddr.String()]
|
|
if !ok {
|
|
return errors.New("no address mapping")
|
|
}
|
|
|
|
var addr stun.XORMappedAddress
|
|
if err := addr.GetFrom(msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
m.xorMappedMap[stunAddr.String()] = mappedAddr
|
|
mappedAddr.SetAddr(&addr)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetXORMappedAddr returns *stun.XORMappedAddress if already present for a given STUN server.
|
|
// Makes a STUN binding request to discover mapped address otherwise.
|
|
// Blocks until the stun.XORMappedAddress has been discovered or deadline.
|
|
// Method is safe for concurrent use.
|
|
func (m *UniversalUDPMuxDefault) GetXORMappedAddr(serverAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error) {
|
|
m.mu.Lock()
|
|
mappedAddr, ok := m.xorMappedMap[serverAddr.String()]
|
|
// if we already have a mapping for this STUN server (address already received)
|
|
// and if it is not too old we return it without making a new request to STUN server
|
|
if ok {
|
|
if mappedAddr.expired() {
|
|
mappedAddr.closeWaiters()
|
|
delete(m.xorMappedMap, serverAddr.String())
|
|
ok = false
|
|
} else if mappedAddr.pending() {
|
|
ok = false
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
if ok {
|
|
return mappedAddr.addr, nil
|
|
}
|
|
|
|
// otherwise, make a STUN request to discover the address
|
|
// or wait for already sent request to complete
|
|
waitAddrReceived, err := m.sendStun(serverAddr)
|
|
if err != nil {
|
|
return nil, errors.New("failed to send STUN packet")
|
|
}
|
|
|
|
// block until response was handled by the connWorker routine and XORMappedAddress was updated
|
|
select {
|
|
case <-waitAddrReceived:
|
|
// when channel closed, addr was obtained
|
|
m.mu.Lock()
|
|
mappedAddr := *m.xorMappedMap[serverAddr.String()]
|
|
m.mu.Unlock()
|
|
if mappedAddr.addr == nil {
|
|
return nil, errors.New("no address mapping")
|
|
}
|
|
return mappedAddr.addr, nil
|
|
case <-time.After(deadline):
|
|
return nil, errors.New("timeout while waiting for XORMappedAddr")
|
|
}
|
|
}
|
|
|
|
// sendStun sends a STUN request via UDP conn.
|
|
//
|
|
// The returned channel is closed when the STUN response has been received.
|
|
// Method is safe for concurrent use.
|
|
func (m *UniversalUDPMuxDefault) sendStun(serverAddr net.Addr) (chan struct{}, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// if record present in the map, we already sent a STUN request,
|
|
// just wait when waitAddrReceived will be closed
|
|
addrMap, ok := m.xorMappedMap[serverAddr.String()]
|
|
if !ok {
|
|
addrMap = &xorMapped{
|
|
expiresAt: time.Now().Add(m.params.XORMappedAddrCacheTTL),
|
|
waitAddrReceived: make(chan struct{}),
|
|
}
|
|
m.xorMappedMap[serverAddr.String()] = addrMap
|
|
}
|
|
|
|
req, err := stun.Build(stun.BindingRequest, stun.TransactionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err = m.params.UDPConn.WriteTo(req.Raw, serverAddr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return addrMap.waitAddrReceived, nil
|
|
}
|
|
|
|
type xorMapped struct {
|
|
addr *stun.XORMappedAddress
|
|
waitAddrReceived chan struct{}
|
|
expiresAt time.Time
|
|
}
|
|
|
|
func (a *xorMapped) closeWaiters() {
|
|
select {
|
|
case <-a.waitAddrReceived:
|
|
// notify was close, ok, that means we received duplicate response
|
|
// just exit
|
|
break
|
|
default:
|
|
// notify tha twe have a new addr
|
|
close(a.waitAddrReceived)
|
|
}
|
|
}
|
|
|
|
func (a *xorMapped) pending() bool {
|
|
return a.addr == nil
|
|
}
|
|
|
|
func (a *xorMapped) expired() bool {
|
|
return a.expiresAt.Before(time.Now())
|
|
}
|
|
|
|
func (a *xorMapped) SetAddr(addr *stun.XORMappedAddress) {
|
|
a.addr = addr
|
|
a.closeWaiters()
|
|
}
|