Files
webrtc/icegatherer.go
2025-12-19 18:47:44 +02:00

638 lines
18 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
// +build !js
package webrtc
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/pion/ice/v4"
"github.com/pion/logging"
"github.com/pion/stun/v3"
)
// ICEGatherer gathers local host, server reflexive and relay
// candidates, as well as enabling the retrieval of local Interactive
// Connectivity Establishment (ICE) parameters which can be
// exchanged in signaling.
type ICEGatherer struct {
lock sync.RWMutex
log logging.LeveledLogger
state ICEGathererState
validatedServers []*stun.URI
gatherPolicy ICETransportPolicy
agent *ice.Agent
onLocalCandidateHandler atomic.Value // func(candidate *ICECandidate)
onStateChangeHandler atomic.Value // func(state ICEGathererState)
// Used for GatheringCompletePromise
onGatheringCompleteHandler atomic.Value // func()
api *API
// Used to set the corresponding media stream identification tag and media description index
// for ICE candidates generated by this gatherer.
sdpMid atomic.Value // string
sdpMLineIndex atomic.Uint32 // uint16
}
// ICEAddressRewriteMode controls whether a rule replaces or appends candidates.
type ICEAddressRewriteMode byte
const (
ICEAddressRewriteModeUnspecified ICEAddressRewriteMode = iota
ICEAddressRewriteReplace
ICEAddressRewriteAppend
)
func (r ICEAddressRewriteMode) toICE() ice.AddressRewriteMode {
return ice.AddressRewriteMode(r)
}
// ICEAddressRewriteRule represents a rule for remapping candidate addresses.
type ICEAddressRewriteRule struct {
External []string
Local string
Iface string
CIDR string
AsCandidateType ICECandidateType
Mode ICEAddressRewriteMode
Networks []NetworkType
}
func (r ICEAddressRewriteRule) toICE() ice.AddressRewriteRule {
candidateType := r.AsCandidateType.toICE()
mode := r.Mode.toICE()
networks := toICENetworkTypes(r.Networks)
rule := ice.AddressRewriteRule{
External: append([]string(nil), r.External...),
Local: r.Local,
Iface: r.Iface,
CIDR: r.CIDR,
AsCandidateType: candidateType,
Mode: mode,
Networks: networks,
}
return rule
}
// NewICEGatherer creates a new NewICEGatherer.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func (api *API) NewICEGatherer(opts ICEGatherOptions) (*ICEGatherer, error) {
var validatedServers []*stun.URI
if len(opts.ICEServers) > 0 {
for _, server := range opts.ICEServers {
url, err := server.urls()
if err != nil {
return nil, err
}
validatedServers = append(validatedServers, url...)
}
}
return &ICEGatherer{
state: ICEGathererStateNew,
gatherPolicy: opts.ICEGatherPolicy,
validatedServers: validatedServers,
api: api,
log: api.settingEngine.LoggerFactory.NewLogger("ice"),
sdpMid: atomic.Value{},
sdpMLineIndex: atomic.Uint32{},
}, nil
}
func (g *ICEGatherer) createAgent() error {
g.lock.Lock()
defer g.lock.Unlock()
if g.agent != nil || g.State() != ICEGathererStateNew {
return nil
}
options, err := g.buildAgentOptions()
if err != nil {
return err
}
agent, err := ice.NewAgentWithOptions(options...)
if err != nil {
return err
}
g.agent = agent
return nil
}
func (g *ICEGatherer) buildAgentOptions() ([]ice.AgentOption, error) {
candidateTypes := g.resolveCandidateTypes()
nat1To1CandiTyp := g.resolveNAT1To1CandidateType()
mDNSMode := g.sanitizedMDNSMode()
options := g.baseAgentOptions(mDNSMode)
if len(candidateTypes) > 0 {
options = append(options, ice.WithCandidateTypes(candidateTypes))
}
options = append(options, g.credentialOptions()...)
rewriteOptions, err := g.addressRewriteOptions(nat1To1CandiTyp)
if err != nil {
return nil, err
}
options = append(options, rewriteOptions...)
options = append(options, g.timeoutOptions()...)
options = append(options, g.miscOptions()...)
options = append(options, g.renominationOptions()...)
requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes
if len(requestedNetworkTypes) == 0 {
requestedNetworkTypes = supportedNetworkTypes()
}
return append(options, ice.WithNetworkTypes(toICENetworkTypes(requestedNetworkTypes))), nil
}
func (g *ICEGatherer) resolveCandidateTypes() []ice.CandidateType {
if g.api.settingEngine.candidates.ICELite {
return []ice.CandidateType{ice.CandidateTypeHost}
}
if g.gatherPolicy == ICETransportPolicyRelay {
return []ice.CandidateType{ice.CandidateTypeRelay}
}
return nil
}
func (g *ICEGatherer) resolveNAT1To1CandidateType() ice.CandidateType {
switch g.api.settingEngine.candidates.NAT1To1IPCandidateType {
case ICECandidateTypeHost:
return ice.CandidateTypeHost
case ICECandidateTypeSrflx:
return ice.CandidateTypeServerReflexive
default:
return ice.CandidateTypeUnspecified
}
}
func (g *ICEGatherer) sanitizedMDNSMode() ice.MulticastDNSMode {
mode := g.api.settingEngine.candidates.MulticastDNSMode
if mode == ice.MulticastDNSModeDisabled || mode == ice.MulticastDNSModeQueryAndGather {
return mode
}
return ice.MulticastDNSModeQueryOnly
}
func (g *ICEGatherer) baseAgentOptions(mDNSMode ice.MulticastDNSMode) []ice.AgentOption {
return []ice.AgentOption{
ice.WithICELite(g.api.settingEngine.candidates.ICELite),
ice.WithUrls(g.validatedServers),
ice.WithPortRange(g.api.settingEngine.ephemeralUDP.PortMin, g.api.settingEngine.ephemeralUDP.PortMax),
ice.WithLoggerFactory(g.api.settingEngine.LoggerFactory),
ice.WithInterfaceFilter(g.api.settingEngine.candidates.InterfaceFilter),
ice.WithIPFilter(g.api.settingEngine.candidates.IPFilter),
ice.WithNet(g.api.settingEngine.net),
ice.WithMulticastDNSMode(mDNSMode),
ice.WithTCPMux(g.api.settingEngine.iceTCPMux),
ice.WithUDPMux(g.api.settingEngine.iceUDPMux),
ice.WithProxyDialer(g.api.settingEngine.iceProxyDialer),
ice.WithBindingRequestHandler(g.api.settingEngine.iceBindingRequestHandler),
}
}
func (g *ICEGatherer) credentialOptions() []ice.AgentOption {
ufrag := g.api.settingEngine.candidates.UsernameFragment
pass := g.api.settingEngine.candidates.Password
if ufrag == "" && pass == "" {
return nil
}
return []ice.AgentOption{
ice.WithLocalCredentials(g.api.settingEngine.candidates.UsernameFragment, g.api.settingEngine.candidates.Password),
}
}
func (g *ICEGatherer) addressRewriteOptions(candidateType ice.CandidateType) ([]ice.AgentOption, error) {
rules := g.api.settingEngine.candidates.addressRewriteRules
nat1To1IPs := g.api.settingEngine.candidates.NAT1To1IPs
if len(rules) > 0 && len(nat1To1IPs) > 0 {
return nil, errAddressRewriteWithNAT1To1
}
if len(rules) > 0 {
return []ice.AgentOption{ice.WithAddressRewriteRules(rules...)}, nil
}
if len(nat1To1IPs) == 0 {
return nil, nil
}
return []ice.AgentOption{
ice.WithAddressRewriteRules(
legacyNAT1To1AddressRewriteRules(
nat1To1IPs,
candidateType,
)...,
),
}, nil
}
func (g *ICEGatherer) timeoutOptions() []ice.AgentOption {
opts := make([]ice.AgentOption, 0, 8)
if g.api.settingEngine.timeout.ICEDisconnectedTimeout != nil {
opts = append(opts, ice.WithDisconnectedTimeout(*g.api.settingEngine.timeout.ICEDisconnectedTimeout))
}
if g.api.settingEngine.timeout.ICEFailedTimeout != nil {
opts = append(opts, ice.WithFailedTimeout(*g.api.settingEngine.timeout.ICEFailedTimeout))
}
if g.api.settingEngine.timeout.ICEKeepaliveInterval != nil {
opts = append(opts, ice.WithKeepaliveInterval(*g.api.settingEngine.timeout.ICEKeepaliveInterval))
}
if g.api.settingEngine.timeout.ICEHostAcceptanceMinWait != nil {
opts = append(opts, ice.WithHostAcceptanceMinWait(*g.api.settingEngine.timeout.ICEHostAcceptanceMinWait))
}
if g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait != nil {
opts = append(opts, ice.WithSrflxAcceptanceMinWait(*g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait))
}
if g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait != nil {
opts = append(opts, ice.WithPrflxAcceptanceMinWait(*g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait))
}
if g.api.settingEngine.timeout.ICERelayAcceptanceMinWait != nil {
opts = append(opts, ice.WithRelayAcceptanceMinWait(*g.api.settingEngine.timeout.ICERelayAcceptanceMinWait))
}
if g.api.settingEngine.timeout.ICESTUNGatherTimeout != nil {
opts = append(opts, ice.WithSTUNGatherTimeout(*g.api.settingEngine.timeout.ICESTUNGatherTimeout))
}
return opts
}
func (g *ICEGatherer) miscOptions() []ice.AgentOption {
opts := make([]ice.AgentOption, 0, 4)
if g.api.settingEngine.candidates.MulticastDNSHostName != "" {
opts = append(opts, ice.WithMulticastDNSHostName(g.api.settingEngine.candidates.MulticastDNSHostName))
}
if g.api.settingEngine.candidates.IncludeLoopbackCandidate {
opts = append(opts, ice.WithIncludeLoopback())
}
if g.api.settingEngine.iceDisableActiveTCP {
opts = append(opts, ice.WithDisableActiveTCP())
}
if g.api.settingEngine.iceMaxBindingRequests != nil {
opts = append(opts, ice.WithMaxBindingRequests(*g.api.settingEngine.iceMaxBindingRequests))
}
return opts
}
func (g *ICEGatherer) renominationOptions() []ice.AgentOption {
renom := g.api.settingEngine.renomination
if !renom.enabled && !renom.automatic {
return nil
}
generator := renom.generator
opts := []ice.AgentOption{
ice.WithRenomination(func() uint32 {
return generator()
}),
}
if renom.automatic {
interval := time.Duration(0)
if renom.automaticInterval != nil {
interval = *renom.automaticInterval
}
opts = append(opts, ice.WithAutomaticRenomination(interval))
}
return opts
}
func legacyNAT1To1AddressRewriteRules(ips []string, candidateType ice.CandidateType) []ice.AddressRewriteRule {
catchAll := make([]string, 0, len(ips))
rules := make([]ice.AddressRewriteRule, 0, len(ips)+1)
for _, ip := range ips {
splits := strings.SplitN(ip, "/", 2)
if len(splits) == 2 {
rules = append(rules, ice.AddressRewriteRule{
External: []string{splits[0]},
Local: splits[1],
AsCandidateType: candidateType,
})
catchAll = append(catchAll, splits[0])
} else {
catchAll = append(catchAll, ip)
}
}
if len(catchAll) > 0 {
rules = append(rules, ice.AddressRewriteRule{
External: catchAll,
AsCandidateType: candidateType,
})
}
return rules
}
// Gather ICE candidates.
func (g *ICEGatherer) Gather() error { //nolint:cyclop
if err := g.createAgent(); err != nil {
return err
}
agent := g.getAgent()
// it is possible agent had just been closed
if agent == nil {
return fmt.Errorf("%w: unable to gather", errICEAgentNotExist)
}
g.setState(ICEGathererStateGathering)
if err := agent.OnCandidate(func(candidate ice.Candidate) {
onLocalCandidateHandler := func(*ICECandidate) {}
if handler, ok := g.onLocalCandidateHandler.Load().(func(candidate *ICECandidate)); ok && handler != nil {
onLocalCandidateHandler = handler
}
onGatheringCompleteHandler := func() {}
if handler, ok := g.onGatheringCompleteHandler.Load().(func()); ok && handler != nil {
onGatheringCompleteHandler = handler
}
sdpMid := ""
if mid, ok := g.sdpMid.Load().(string); ok {
sdpMid = mid
}
sdpMLineIndex := uint16(g.sdpMLineIndex.Load()) //nolint:gosec // G115
if candidate != nil {
c, err := newICECandidateFromICE(candidate, sdpMid, sdpMLineIndex)
if err != nil {
g.log.Warnf("Failed to convert ice.Candidate: %s", err)
return
}
onLocalCandidateHandler(&c)
} else {
g.setState(ICEGathererStateComplete)
onGatheringCompleteHandler()
onLocalCandidateHandler(nil)
}
}); err != nil {
return err
}
return agent.GatherCandidates()
}
// set media stream identification tag and media description index for this gatherer.
func (g *ICEGatherer) setMediaStreamIdentification(mid string, mLineIndex uint16) {
g.sdpMid.Store(mid)
g.sdpMLineIndex.Store(uint32(mLineIndex))
}
// Close prunes all local candidates, and closes the ports.
func (g *ICEGatherer) Close() error {
return g.close(false /* shouldGracefullyClose */)
}
// GracefulClose prunes all local candidates, and closes the ports. It also waits
// for any goroutines it started to complete. This is only safe to call outside of
// ICEGatherer callbacks or if in a callback, in its own goroutine.
func (g *ICEGatherer) GracefulClose() error {
return g.close(true /* shouldGracefullyClose */)
}
func (g *ICEGatherer) close(shouldGracefullyClose bool) error {
g.lock.Lock()
defer g.lock.Unlock()
if g.agent == nil {
return nil
}
if shouldGracefullyClose {
if err := g.agent.GracefulClose(); err != nil {
return err
}
} else {
if err := g.agent.Close(); err != nil {
return err
}
}
g.agent = nil
g.setState(ICEGathererStateClosed)
return nil
}
// GetLocalParameters returns the ICE parameters of the ICEGatherer.
func (g *ICEGatherer) GetLocalParameters() (ICEParameters, error) {
if err := g.createAgent(); err != nil {
return ICEParameters{}, err
}
agent := g.getAgent()
// it is possible agent had just been closed
if agent == nil {
return ICEParameters{}, fmt.Errorf("%w: unable to get local parameters", errICEAgentNotExist)
}
frag, pwd, err := agent.GetLocalUserCredentials()
if err != nil {
return ICEParameters{}, err
}
return ICEParameters{
UsernameFragment: frag,
Password: pwd,
ICELite: false,
}, nil
}
// GetLocalCandidates returns the sequence of valid local candidates associated with the ICEGatherer.
func (g *ICEGatherer) GetLocalCandidates() ([]ICECandidate, error) {
if err := g.createAgent(); err != nil {
return nil, err
}
agent := g.getAgent()
// it is possible agent had just been closed
if agent == nil {
return nil, fmt.Errorf("%w: unable to get local candidates", errICEAgentNotExist)
}
iceCandidates, err := agent.GetLocalCandidates()
if err != nil {
return nil, err
}
sdpMid := ""
if mid, ok := g.sdpMid.Load().(string); ok {
sdpMid = mid
}
sdpMLineIndex := uint16(g.sdpMLineIndex.Load()) //nolint:gosec // G115
return newICECandidatesFromICE(iceCandidates, sdpMid, sdpMLineIndex)
}
// OnLocalCandidate sets an event handler which fires when a new local ICE candidate is available
// Take note that the handler will be called with a nil pointer when gathering is finished.
func (g *ICEGatherer) OnLocalCandidate(f func(*ICECandidate)) {
g.onLocalCandidateHandler.Store(f)
}
// OnStateChange fires any time the ICEGatherer changes.
func (g *ICEGatherer) OnStateChange(f func(ICEGathererState)) {
g.onStateChangeHandler.Store(f)
}
// State indicates the current state of the ICE gatherer.
func (g *ICEGatherer) State() ICEGathererState {
return atomicLoadICEGathererState(&g.state)
}
func (g *ICEGatherer) setState(s ICEGathererState) {
atomicStoreICEGathererState(&g.state, s)
if handler, ok := g.onStateChangeHandler.Load().(func(state ICEGathererState)); ok && handler != nil {
handler(s)
}
}
func (g *ICEGatherer) getAgent() *ice.Agent {
g.lock.RLock()
defer g.lock.RUnlock()
return g.agent
}
func (g *ICEGatherer) collectStats(collector *statsReportCollector) {
agent := g.getAgent()
if agent == nil {
return
}
collector.Collecting()
go func(collector *statsReportCollector, agent *ice.Agent) {
for _, candidatePairStats := range agent.GetCandidatePairsStats() {
collector.Collecting()
stats, err := toICECandidatePairStats(candidatePairStats)
if err != nil {
g.log.Error(err.Error())
collector.Done()
continue
}
collector.Collect(stats.ID, stats)
}
for _, candidateStats := range agent.GetLocalCandidatesStats() {
collector.Collecting()
networkType, err := getNetworkType(candidateStats.NetworkType)
if err != nil {
g.log.Error(err.Error())
}
candidateType, err := getCandidateType(candidateStats.CandidateType)
if err != nil {
g.log.Error(err.Error())
}
stats := ICECandidateStats{
Timestamp: statsTimestampFrom(candidateStats.Timestamp),
ID: candidateStats.ID,
Type: StatsTypeLocalCandidate,
IP: candidateStats.IP,
Port: int32(candidateStats.Port), //nolint:gosec // G115, no overflow, port
Protocol: networkType.Protocol(),
CandidateType: candidateType,
Priority: int32(candidateStats.Priority), //nolint:gosec
URL: candidateStats.URL,
RelayProtocol: candidateStats.RelayProtocol,
Deleted: candidateStats.Deleted,
}
collector.Collect(stats.ID, stats)
}
for _, candidateStats := range agent.GetRemoteCandidatesStats() {
collector.Collecting()
networkType, err := getNetworkType(candidateStats.NetworkType)
if err != nil {
g.log.Error(err.Error())
}
candidateType, err := getCandidateType(candidateStats.CandidateType)
if err != nil {
g.log.Error(err.Error())
}
stats := ICECandidateStats{
Timestamp: statsTimestampFrom(candidateStats.Timestamp),
ID: candidateStats.ID,
Type: StatsTypeRemoteCandidate,
IP: candidateStats.IP,
Port: int32(candidateStats.Port), //nolint:gosec // G115, no overflow, port
Protocol: networkType.Protocol(),
CandidateType: candidateType,
Priority: int32(candidateStats.Priority), //nolint:gosec // G115
URL: candidateStats.URL,
RelayProtocol: candidateStats.RelayProtocol,
}
collector.Collect(stats.ID, stats)
}
collector.Done()
}(collector, agent)
}
func (g *ICEGatherer) getSelectedCandidatePairStats() (ICECandidatePairStats, bool) {
agent := g.getAgent()
if agent == nil {
return ICECandidatePairStats{}, false
}
selectedCandidatePairStats, isAvailable := agent.GetSelectedCandidatePairStats()
if !isAvailable {
return ICECandidatePairStats{}, false
}
stats, err := toICECandidatePairStats(selectedCandidatePairStats)
if err != nil {
g.log.Error(err.Error())
return ICECandidatePairStats{}, false
}
return stats, true
}