Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ var (
errICEInvalidConvertCandidateType = errors.New(
"cannot convert ice.CandidateType into webrtc.ICECandidateType, invalid type",
)
errSettingEngineLiteWithNonHostCandidates = errors.New(
"SetCandidateTypes only supports ICECandidateTypeHost when ICELite is enabled",
)
errICEAgentNotExist = errors.New("ICEAgent does not exist")
errICECandiatesCoversionFailed = errors.New("unable to convert ICE candidates to ICECandidates")
errICERoleUnknown = errors.New("unknown ICE Role")
Expand Down
6 changes: 5 additions & 1 deletion icecandidatetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func getCandidateType(candidateType ice.CandidateType) (ICECandidateType, error)
}

// MarshalText implements the encoding.TextMarshaler interface.
func (t ICECandidateType) MarshalText() ([]byte, error) {
func (t ICECandidateType) MarshalText() ([]byte, error) { //nolint:staticcheck
return []byte(t.String()), nil
}

Expand All @@ -112,3 +112,7 @@ func (t *ICECandidateType) UnmarshalText(b []byte) error {

return err
}

func (r ICECandidateType) toICE() ice.CandidateType {
return ice.CandidateType(r)
}
254 changes: 196 additions & 58 deletions icegatherer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package webrtc

import (
"fmt"
"strings"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -70,89 +71,226 @@ func (api *API) NewICEGatherer(opts ICEGatherOptions) (*ICEGatherer, error) {
}, nil
}

func (g *ICEGatherer) createAgent() error { //nolint:cyclop
func (g *ICEGatherer) createAgent() error {
g.lock.Lock()
defer g.lock.Unlock()

if g.agent != nil || g.State() != ICEGathererStateNew {
return nil
}

candidateTypes := []ice.CandidateType{}
if g.api.settingEngine.candidates.ICELite {
candidateTypes = append(candidateTypes, ice.CandidateTypeHost)
} else if g.gatherPolicy == ICETransportPolicyRelay {
candidateTypes = append(candidateTypes, ice.CandidateTypeRelay)
options := g.buildAgentOptions()

agent, err := ice.NewAgentWithOptions(options...)
if err != nil {
return err
}

var nat1To1CandiTyp ice.CandidateType
switch g.api.settingEngine.candidates.NAT1To1IPCandidateType {
case ICECandidateTypeHost:
nat1To1CandiTyp = ice.CandidateTypeHost
case ICECandidateTypeSrflx:
nat1To1CandiTyp = ice.CandidateTypeServerReflexive
default:
nat1To1CandiTyp = ice.CandidateTypeUnspecified
}

mDNSMode := g.api.settingEngine.candidates.MulticastDNSMode
if mDNSMode != ice.MulticastDNSModeDisabled && mDNSMode != ice.MulticastDNSModeQueryAndGather {
// If enum is in state we don't recognized default to MulticastDNSModeQueryOnly
mDNSMode = ice.MulticastDNSModeQueryOnly
}

config := &ice.AgentConfig{
Lite: g.api.settingEngine.candidates.ICELite,
Urls: g.validatedServers,
PortMin: g.api.settingEngine.ephemeralUDP.PortMin,
PortMax: g.api.settingEngine.ephemeralUDP.PortMax,
DisconnectedTimeout: g.api.settingEngine.timeout.ICEDisconnectedTimeout,
FailedTimeout: g.api.settingEngine.timeout.ICEFailedTimeout,
KeepaliveInterval: g.api.settingEngine.timeout.ICEKeepaliveInterval,
LoggerFactory: g.api.settingEngine.LoggerFactory,
CandidateTypes: candidateTypes,
HostAcceptanceMinWait: g.api.settingEngine.timeout.ICEHostAcceptanceMinWait,
SrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait,
PrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait,
RelayAcceptanceMinWait: g.api.settingEngine.timeout.ICERelayAcceptanceMinWait,
STUNGatherTimeout: g.api.settingEngine.timeout.ICESTUNGatherTimeout,
InterfaceFilter: g.api.settingEngine.candidates.InterfaceFilter,
IPFilter: g.api.settingEngine.candidates.IPFilter,
NAT1To1IPs: g.api.settingEngine.candidates.NAT1To1IPs,
NAT1To1IPCandidateType: nat1To1CandiTyp,
IncludeLoopback: g.api.settingEngine.candidates.IncludeLoopbackCandidate,
Net: g.api.settingEngine.net,
MulticastDNSMode: mDNSMode,
MulticastDNSHostName: g.api.settingEngine.candidates.MulticastDNSHostName,
LocalUfrag: g.api.settingEngine.candidates.UsernameFragment,
LocalPwd: g.api.settingEngine.candidates.Password,
TCPMux: g.api.settingEngine.iceTCPMux,
UDPMux: g.api.settingEngine.iceUDPMux,
ProxyDialer: g.api.settingEngine.iceProxyDialer,
DisableActiveTCP: g.api.settingEngine.iceDisableActiveTCP,
MaxBindingRequests: g.api.settingEngine.iceMaxBindingRequests,
BindingRequestHandler: g.api.settingEngine.iceBindingRequestHandler,
g.agent = agent

return nil
}

func (g *ICEGatherer) buildAgentOptions() []ice.AgentOption {
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()...)
options = append(options, g.natRewriteOptions(nat1To1CandiTyp)...)
options = append(options, g.timeoutOptions()...)
options = append(options, g.miscOptions()...)

requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes
if len(requestedNetworkTypes) == 0 {
requestedNetworkTypes = supportedNetworkTypes()
}

var networkTypes []ice.NetworkType
for _, typ := range requestedNetworkTypes {
config.NetworkTypes = append(config.NetworkTypes, ice.NetworkType(typ))
networkTypes = append(networkTypes, ice.NetworkType(typ))
}

agent, err := ice.NewAgent(config)
if err != nil {
return err
return append(options, ice.WithNetworkTypes(networkTypes))
}

func (g *ICEGatherer) resolveCandidateTypes() []ice.CandidateType {
candidateTypes := g.api.settingEngine.candidates.candidateTypes

if g.gatherPolicy == ICETransportPolicyRelay {
for _, candidate := range candidateTypes {
if candidate != ice.CandidateTypeRelay {
g.log.Warnf("ICETransportPolicyRelay is set. gathering is limited to relay candidates")

break
}
}

return []ice.CandidateType{ice.CandidateTypeRelay}
}

g.agent = agent
if g.api.settingEngine.candidates.ICELite {
return []ice.CandidateType{ice.CandidateTypeHost}
}

if len(candidateTypes) > 0 {
return candidateTypes
}
Comment on lines +125 to +143
Copy link
Member Author

@JoeTurki JoeTurki Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if relay mode and ice lite are enabled at the same time, we return []ice.CandidateType{ice.CandidateTypeRelay} which isn't spec compliant, but safer so we don't leak IPs if the user didn't intended to do that, we should log a warning tho.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth commenting it in the function so it's written in the code that we intentionally "break" the spec?

Copy link
Member Author

@JoeTurki JoeTurki Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't break the spec, If a user turns on relay-only-mode and ice-lite at the same time, they are breaking the ice-lite spec, We should log an error when that happens, It's not a new behavior, it's how it works currently if you set both.

Copy link
Contributor

@philipch07 philipch07 Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I'm in favor of logging if the user accidentally breaks the spec by setting both at the same time.


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) natRewriteOptions(candidateType ice.CandidateType) []ice.AgentOption {
if len(g.api.settingEngine.candidates.NAT1To1IPs) == 0 {
return nil
}

return []ice.AgentOption{
ice.WithAddressRewriteRules(
legacyNAT1To1AddressRewriteRules(
g.api.settingEngine.candidates.NAT1To1IPs,
candidateType,
)...,
),
}
}

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 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 {
Expand Down
Loading
Loading