What is ICE?
Interactive Connectivity Establishment (ICE) is a framework for establishing peer-to-peer connections across NATs and firewalls. It combines multiple techniques (STUN, TURN) to find the best path for communication.
ICE Components in Pion
Pion WebRTC implements ICE through several components:
ICEGatherer
The ICEGatherer collects local network addresses and contacts STUN/TURN servers:
// 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)
api * API
}
ICETransport
The ICETransport manages the connectivity checking process:
// ICETransport allows an application access to information about the ICE
// transport over which packets are sent and received.
type ICETransport struct {
lock sync . RWMutex
role ICERole
onConnectionStateChangeHandler atomic . Value // func(ICETransportState)
internalOnConnectionStateChangeHandler atomic . Value // func(ICETransportState)
onSelectedCandidatePairChangeHandler atomic . Value // func(*ICECandidatePair)
state atomic . Value // ICETransportState
gatherer * ICEGatherer
conn * ice . Conn
mux * mux . Mux
loggerFactory logging . LoggerFactory
log logging . LeveledLogger
}
ICE Candidate Types
Pion supports all standard ICE candidate types:
Host Local IP addresses discovered on the device’s network interfaces.
Server Reflexive (srflx) Public IP address as seen by a STUN server.
Peer Reflexive (prflx) Discovered during connectivity checks.
Relay Address allocated on a TURN server for relaying traffic.
ICECandidate Structure
// ICECandidate represents a ice candidate.
type ICECandidate struct {
statsID string
Foundation string `json:"foundation"`
Priority uint32 `json:"priority"`
Address string `json:"address"`
Protocol ICEProtocol `json:"protocol"`
Port uint16 `json:"port"`
Typ ICECandidateType `json:"type"`
Component uint16 `json:"component"`
RelatedAddress string `json:"relatedAddress"`
RelatedPort uint16 `json:"relatedPort"`
TCPType string `json:"tcpType"`
SDPMid string `json:"sdpMid"`
SDPMLineIndex uint16 `json:"sdpMLineIndex"`
}
Gathering Candidates
Basic Gathering
pc , err := webrtc . NewPeerConnection ( webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{
URLs : [] string { "stun:stun.l.google.com:19302" },
},
},
})
pc . OnICECandidate ( func ( candidate * webrtc . ICECandidate ) {
if candidate == nil {
fmt . Println ( "ICE gathering complete" )
return
}
fmt . Printf ( "New ICE candidate: %s \n " , candidate )
// Send candidate to remote peer
sendToRemotePeer ( candidate . ToJSON ())
})
The gathering process from the source:
// Gather ICE candidates.
func ( g * ICEGatherer ) Gather () error {
if err := g . createAgent (); err != nil {
return err
}
agent := g . getAgent ()
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
}
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 )
onLocalCandidateHandler ( nil )
}
}); err != nil {
return err
}
return agent . GatherCandidates ()
}
ICE Gathering States
New
Gatherer has been created but not started.
Gathering
Actively collecting candidates from network interfaces and servers.
Complete
All candidates have been gathered.
pc . OnICEGatheringStateChange ( func ( state webrtc . ICEGatheringState ) {
fmt . Printf ( "ICE gathering state: %s \n " , state )
})
Configuring ICE Servers
STUN Server
config := webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{
URLs : [] string { "stun:stun.l.google.com:19302" },
},
},
}
TURN Server
config := webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{
URLs : [] string { "turn:turn.example.com:3478" },
Username : "username" ,
Credential : "password" ,
},
},
}
Multiple Servers
config := webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{
URLs : [] string {
"stun:stun.l.google.com:19302" ,
"stun:stun1.l.google.com:19302" ,
},
},
{
URLs : [] string {
"turn:turn1.example.com:3478?transport=udp" ,
"turn:turn1.example.com:3478?transport=tcp" ,
"turns:turn1.example.com:5349" ,
},
Username : "user" ,
Credential : "pass" ,
},
},
}
TURN servers are used as a fallback when direct peer-to-peer connections fail. They relay all media traffic, so they can be expensive to operate.
ICE Transport Policies
config := webrtc . Configuration {
// Allow all candidate types
ICETransportPolicy : webrtc . ICETransportPolicyAll ,
// Or force relay (TURN) only
// ICETransportPolicy: webrtc.ICETransportPolicyRelay,
}
How the policy is applied:
func ( g * ICEGatherer ) resolveCandidateTypes () [] ice . CandidateType {
if g . api . settingEngine . candidates . ICELite {
return [] ice . CandidateType { ice . CandidateTypeHost }
}
switch g . gatherPolicy {
case ICETransportPolicyRelay :
return [] ice . CandidateType { ice . CandidateTypeRelay }
case ICETransportPolicyNoHost :
return [] ice . CandidateType { ice . CandidateTypeServerReflexive , ice . CandidateTypeRelay }
default :
}
return nil
}
ICE Connection States
The ICE connection goes through several states:
func ( pc * PeerConnection ) createICETransport () * ICETransport {
transport := pc . api . NewICETransport ( pc . iceGatherer )
transport . internalOnConnectionStateChangeHandler . Store ( func ( state ICETransportState ) {
var cs ICEConnectionState
switch state {
case ICETransportStateNew :
cs = ICEConnectionStateNew
case ICETransportStateChecking :
cs = ICEConnectionStateChecking
case ICETransportStateConnected :
cs = ICEConnectionStateConnected
case ICETransportStateCompleted :
cs = ICEConnectionStateCompleted
case ICETransportStateFailed :
cs = ICEConnectionStateFailed
case ICETransportStateDisconnected :
cs = ICEConnectionStateDisconnected
case ICETransportStateClosed :
cs = ICEConnectionStateClosed
}
pc . onICEConnectionStateChange ( cs )
pc . updateConnectionState ( cs , pc . dtlsTransport . State ())
})
return transport
}
Monitoring ICE State
pc . OnICEConnectionStateChange ( func ( state webrtc . ICEConnectionState ) {
fmt . Printf ( "ICE connection state: %s \n " , state )
switch state {
case webrtc . ICEConnectionStateChecking :
fmt . Println ( "Testing connectivity..." )
case webrtc . ICEConnectionStateConnected :
fmt . Println ( "Connectivity established!" )
case webrtc . ICEConnectionStateFailed :
fmt . Println ( "Connection failed - check firewall/NAT" )
case webrtc . ICEConnectionStateDisconnected :
fmt . Println ( "Connection lost" )
}
})
Adding Remote Candidates
// AddRemoteCandidate adds a candidate associated with the remote ICETransport.
func ( t * ICETransport ) AddRemoteCandidate ( remoteCandidate * ICECandidate ) error {
t . lock . RLock ()
defer t . lock . RUnlock ()
var (
candidate ice . Candidate
err error
)
if err = t . ensureGatherer (); err != nil {
return err
}
if remoteCandidate != nil {
if candidate , err = remoteCandidate . ToICE (); err != nil {
return err
}
}
agent := t . gatherer . getAgent ()
if agent == nil {
return fmt . Errorf ( " %w : unable to add remote candidates" , errICEAgentNotExist )
}
return agent . AddRemoteCandidate ( candidate )
}
Usage:
// Receive candidate from remote peer
var candidateInit webrtc . ICECandidateInit
receiveFromRemotePeer ( & candidateInit )
// Add to PeerConnection
if err := pc . AddICECandidate ( candidateInit ); err != nil {
log . Printf ( "Error adding ICE candidate: %v " , err )
}
Selected Candidate Pair
Once connectivity is established, you can inspect the selected candidate pair:
func ( t * ICETransport ) GetSelectedCandidatePair () ( * ICECandidatePair , error ) {
agent := t . gatherer . getAgent ()
if agent == nil {
return nil , nil
}
icePair , err := agent . GetSelectedCandidatePair ()
if icePair == nil || err != nil {
return nil , err
}
local , err := newICECandidateFromICE ( icePair . Local , "" , 0 )
if err != nil {
return nil , err
}
remote , err := newICECandidateFromICE ( icePair . Remote , "" , 0 )
if err != nil {
return nil , err
}
return NewICECandidatePair ( & local , & remote ), nil
}
Monitor selected pair changes:
pc . OnICEConnectionStateChange ( func ( state webrtc . ICEConnectionState ) {
if state == webrtc . ICEConnectionStateConnected {
// Get selected candidate pair
pair , err := pc . SCTP (). Transport (). ICETransport (). GetSelectedCandidatePair ()
if err == nil && pair != nil {
fmt . Printf ( "Connected via: \n " )
fmt . Printf ( " Local: %s : %d ( %s ) \n " ,
pair . Local . Address , pair . Local . Port , pair . Local . Typ )
fmt . Printf ( " Remote: %s : %d ( %s ) \n " ,
pair . Remote . Address , pair . Remote . Port , pair . Remote . Typ )
}
}
})
ICE Restart
Restart ICE to recover from connection failures:
// Create new offer with ICE restart
offer , err := pc . CreateOffer ( & webrtc . OfferOptions {
ICERestart : true ,
})
if err != nil {
panic ( err )
}
if err = pc . SetLocalDescription ( offer ); err != nil {
panic ( err )
}
sendToRemotePeer ( offer )
The restart implementation:
func ( t * ICETransport ) restart () error {
t . lock . Lock ()
defer t . lock . Unlock ()
agent := t . gatherer . getAgent ()
if agent == nil {
return fmt . Errorf ( " %w : unable to restart ICETransport" , errICEAgentNotExist )
}
if err := agent . Restart (
t . gatherer . api . settingEngine . candidates . UsernameFragment ,
t . gatherer . api . settingEngine . candidates . Password ,
); err != nil {
return err
}
return t . gatherer . Gather ()
}
Advanced ICE Configuration
Setting ICE Credentials
import " github.com/pion/webrtc/v4 "
api := webrtc . NewAPI ( webrtc . WithSettingEngine ( webrtc . SettingEngine {
// Custom ICE credentials
}))
Network Interface Filtering
settingEngine := webrtc . SettingEngine {}
settingEngine . SetInterfaceFilter ( func ( iface string ) bool {
// Only use WiFi interface
return strings . HasPrefix ( iface , "wlan" )
})
api := webrtc . NewAPI ( webrtc . WithSettingEngine ( settingEngine ))
ICE Timeouts
import " time "
settingEngine := webrtc . SettingEngine {}
settingEngine . SetICETimeouts (
30 * time . Second , // Disconnected timeout
10 * time . Second , // Failed timeout
2 * time . Second , // Keepalive interval
)
api := webrtc . NewAPI ( webrtc . WithSettingEngine ( settingEngine ))
Troubleshooting ICE
Common ICE connection failures:
Restrictive firewalls blocking UDP
No TURN server configured for relay fallback
Symmetric NAT requiring TURN
IPv6 connectivity issues
Diagnostic Tips
Check gathered candidates :
pc . OnICECandidate ( func ( c * webrtc . ICECandidate ) {
if c != nil {
fmt . Printf ( "Candidate: %s \n " , c . String ())
}
})
Monitor connection states :
pc . OnICEConnectionStateChange ( func ( state webrtc . ICEConnectionState ) {
log . Printf ( "ICE: %s " , state )
})
Enable ICE agent logging :
import " github.com/pion/logging "
loggerFactory := logging . NewDefaultLoggerFactory ()
loggerFactory . DefaultLogLevel = logging . LogLevelTrace
settingEngine := webrtc . SettingEngine {}
settingEngine . LoggerFactory = loggerFactory
Next Steps
Media Streams Learn about sending and receiving media
Data Channels Explore bidirectional data transfer