Ctrl K
ComunidadGuías…

internal/turnsrv/turnsrv.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package turnsrv is the embedded TURN/STUN service for private-call P3
// (pion/turn/v5).
//
// Role: the private-call media plane is client-to-client P2P (tgcalls/WebRTC); the
// server acts as a relay only when NAT traversal fails. This package exposes just
// three things externally: the reachable service address (written into the
// phoneConnectionWebrtc entries of phoneCall.connections), the temporary
// credentials issued per call, and the on/off switch.
//
// Credentials follow the TURN REST spec (draft-uberti-behave-turn-rest-00, same as
// coturn): username = "<unix expiry>:<identity>", password =
// base64(HMAC-SHA1(secret, username)). This means switching to an external coturn
// in the future only requires sharing the secret, with zero changes on the
// signaling side.
package turnsrv

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net"
	"time"

	"github.com/pion/logging"
	"github.com/pion/turn/v5"
	"go.uber.org/zap"
)

// Config is the operating parameters of the embedded TURN.
type Config struct {
	// UDPPort is the TURN/STUN listening port (independent of the SFU port: both
	// must exclusively consume the STUN traffic on their own socket and cannot
	// share one). The Windows firewall must allow it.
	UDPPort int
	// AdvertiseIP is the client-reachable address written into
	// phoneConnectionWebrtc and used for relay allocations. ⚠ With 127.0.0.1 the
	// relay candidate a real device gets is unreachable (the media plane fails
	// silently).
	AdvertiseIP string
	// Realm is the realm of the TURN long-term credential (any stable string works).
	Realm string
	// SharedSecret is the HMAC key for REST credentials; when empty it is generated
	// randomly at process level (self-consistent for a single instance; multiple
	// instances / an external coturn must explicitly configure the same value).
	SharedSecret string
	// RelayMinPort/RelayMaxPort bound the UDP port range for relay allocations (the
	// firewall's allowed range).
	RelayMinPort int
	RelayMaxPort int
	// CredentialTTL is the validity period of issued credentials. ⚠ It must be >=
	// the maximum duration of a single call: libwebrtc TurnPort's allocation
	// refresh reuses the initial credentials, so an expired credential cuts off an
	// in-progress relayed call (conclusion from reading the TDesktop code);
	// default 24h.
	CredentialTTL time.Duration
	Logger        *zap.Logger
}

// Service is the TURN boundary visible to the signaling layer.
type Service interface {
	// Enabled reports whether relaying is actually available (when false,
	// connections can only deliver an empty list and behavior falls back to P1's
	// LAN direct connection).
	Enabled() bool
	// Credentials issues temporary credentials for one call (user is any identity;
	// the convention is callID).
	Credentials(user string) (username, password string, err error)
	// IP/Port return the client-reachable service address.
	IP() string
	Port() int
	Close() error
}

// Disabled returns the not-enabled implementation.
func Disabled() Service { return disabled{} }

type disabled struct{}

func (disabled) Enabled() bool { return false }
func (disabled) Credentials(string) (string, string, error) {
	return "", "", fmt.Errorf("turnsrv: disabled")
}
func (disabled) IP() string   { return "" }
func (disabled) Port() int    { return 0 }
func (disabled) Close() error { return nil }

type pionTURN struct {
	cfg    Config
	server *turn.Server
}

// New starts the embedded TURN: binds the UDP port and wires up REST credential
// verification and the relay port range.
func New(cfg Config) (Service, error) {
	if cfg.UDPPort <= 0 {
		return nil, fmt.Errorf("turnsrv: invalid udp port %d", cfg.UDPPort)
	}
	if cfg.AdvertiseIP == "" {
		cfg.AdvertiseIP = "127.0.0.1"
	}
	if cfg.Realm == "" {
		cfg.Realm = "telesrv"
	}
	if cfg.SharedSecret == "" {
		secret, err := randomSecret()
		if err != nil {
			return nil, err
		}
		cfg.SharedSecret = secret
	}
	if cfg.RelayMinPort <= 0 || cfg.RelayMaxPort < cfg.RelayMinPort {
		return nil, fmt.Errorf("turnsrv: invalid relay port range [%d,%d]", cfg.RelayMinPort, cfg.RelayMaxPort)
	}
	if cfg.CredentialTTL <= 0 {
		cfg.CredentialTTL = 24 * time.Hour
	}
	if cfg.Logger == nil {
		cfg.Logger = zap.NewNop()
	}
	relayIP := net.ParseIP(cfg.AdvertiseIP)
	if relayIP == nil {
		return nil, fmt.Errorf("turnsrv: invalid advertise ip %q", cfg.AdvertiseIP)
	}
	conn, err := net.ListenPacket("udp4", fmt.Sprintf("0.0.0.0:%d", cfg.UDPPort))
	if err != nil {
		return nil, fmt.Errorf("turnsrv: listen udp %d: %w", cfg.UDPPort, err)
	}
	pionLog := logging.NewDefaultLoggerFactory()
	server, err := turn.NewServer(turn.ServerConfig{
		Realm:         cfg.Realm,
		LoggerFactory: pionLog,
		AuthHandler:   turn.LongTermTURNRESTAuthHandler(cfg.SharedSecret, pionLog.NewLogger("turn-auth")),
		PacketConnConfigs: []turn.PacketConnConfig{{
			PacketConn: conn,
			RelayAddressGenerator: &turn.RelayAddressGeneratorPortRange{
				RelayAddress: relayIP,
				MinPort:      uint16(cfg.RelayMinPort),
				MaxPort:      uint16(cfg.RelayMaxPort),
				Address:      "0.0.0.0",
			},
		}},
	})
	if err != nil {
		_ = conn.Close()
		return nil, fmt.Errorf("turnsrv: new server: %w", err)
	}
	cfg.Logger.Info("turn listening",
		zap.Int("udp_port", cfg.UDPPort),
		zap.String("advertise_ip", cfg.AdvertiseIP),
		zap.Int("relay_min_port", cfg.RelayMinPort),
		zap.Int("relay_max_port", cfg.RelayMaxPort))
	if cfg.AdvertiseIP == "127.0.0.1" {
		cfg.Logger.Warn("TELESRV_TURN_ADVERTISE_IP is 127.0.0.1: the relay candidate a real device gets is unreachable (the cross-network media plane fails silently); cross-device calls must set it to a client-reachable IP")
	}
	return &pionTURN{cfg: cfg, server: server}, nil
}

func (t *pionTURN) Enabled() bool { return true }

func (t *pionTURN) Credentials(user string) (string, string, error) {
	username, password, err := turn.GenerateLongTermTURNRESTCredentials(t.cfg.SharedSecret, user, t.cfg.CredentialTTL)
	if err != nil {
		return "", "", fmt.Errorf("turnsrv: generate credentials: %w", err)
	}
	return username, password, nil
}

func (t *pionTURN) IP() string   { return t.cfg.AdvertiseIP }
func (t *pionTURN) Port() int    { return t.cfg.UDPPort }
func (t *pionTURN) Close() error { return t.server.Close() }

func randomSecret() (string, error) {
	buf := make([]byte, 24)
	if _, err := rand.Read(buf); err != nil {
		return "", fmt.Errorf("turnsrv: random secret: %w", err)
	}
	return hex.EncodeToString(buf), nil
}