An ICE Agent is the core component that manages the ICE protocol execution, including candidate gathering, connectivity checks, and connection maintenance.
What is an ICE Agent?
The Agent type (defined in agent.go:40) orchestrates the entire ICE process:
type Agent struct {
loop * taskloop . Loop
constructed bool
connectionState ConnectionState
gatheringState GatheringState
isControlling atomic . Bool
localCandidates map [ NetworkType ][] Candidate
remoteCandidates map [ NetworkType ][] Candidate
checklist [] * CandidatePair
selectedPair atomic . Value
localUfrag string
localPwd string
remoteUfrag string
remotePwd string
// Configuration
disconnectedTimeout time . Duration
failedTimeout time . Duration
keepaliveInterval time . Duration
checkInterval time . Duration
}
Creating an Agent
Using Options (Recommended)
import " github.com/pion/ice/v4 "
agent , err := ice . NewAgentWithOptions (
ice . WithNetworkTypes ([] ice . NetworkType {
ice . NetworkTypeUDP4 ,
ice . NetworkTypeUDP6 ,
}),
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
ice . CandidateTypeServerReflexive ,
ice . CandidateTypeRelay ,
}),
ice . WithUrls ([] * stun . URI {
{ Scheme : stun . SchemeTypeSTUN , Host : "stun.l.google.com" , Port : 19302 },
}),
)
if err != nil {
panic ( err )
}
defer agent . Close ()
Using Config (Deprecated)
config := & ice . AgentConfig {
Urls : [] * stun . URI {
{ Scheme : stun . SchemeTypeSTUN , Host : "stun.l.google.com" , Port : 19302 },
},
NetworkTypes : [] ice . NetworkType {
ice . NetworkTypeUDP4 ,
},
}
agent , err := ice . NewAgent ( config )
The AgentConfig approach is deprecated. Use NewAgentWithOptions() for new code.
Agent Lifecycle
The ICE agent progresses through several phases during its lifetime:
1. Creation and Initialization
// Create agent
agent , err := ice . NewAgentWithOptions ()
// Register event handlers
agent . OnCandidate ( func ( c ice . Candidate ) {
if c != nil {
// New candidate gathered
} else {
// Gathering complete
}
})
agent . OnConnectionStateChange ( func ( state ice . ConnectionState ) {
fmt . Printf ( "State: %s \n " , state )
})
agent . OnSelectedCandidatePairChange ( func ( local , remote ice . Candidate ) {
fmt . Printf ( "Selected pair: %s <-> %s \n " , local , remote )
})
2. Credential Exchange
// Get local credentials to send to peer
ufrag , pwd , err := agent . GetLocalUserCredentials ()
if err != nil {
panic ( err )
}
// Send ufrag and pwd to remote peer via signaling
sendToRemotePeer ( ufrag , pwd )
// Receive remote credentials from peer
remoteUfrag , remotePwd := receiveFromRemotePeer ()
3. Candidate Gathering
// Start gathering candidates
err = agent . GatherCandidates ()
if err != nil {
panic ( err )
}
// Candidates will be delivered via OnCandidate callback
// Send each candidate to remote peer via signaling
The gathering state transitions from GatheringStateNew → GatheringStateGathering → GatheringStateComplete.
4. Connectivity Establishment
// Set remote credentials
err = agent . SetRemoteCredentials ( remoteUfrag , remotePwd )
if err != nil {
panic ( err )
}
// Add remote candidates
for _ , candidate := range remoteCandidates {
err = agent . AddRemoteCandidate ( candidate )
if err != nil {
panic ( err )
}
}
// Connect as controlling agent
conn , err := agent . Dial ( context . Background (), remoteUfrag , remotePwd )
if err != nil {
panic ( err )
}
// Or accept as controlled agent
// conn, err := agent.Accept(context.Background(), remoteUfrag, remotePwd)
5. Connection Usage
// Use the connection like any net.Conn
data := [] byte ( "Hello, peer!" )
n , err := conn . Write ( data )
buffer := make ([] byte , 1500 )
n , err = conn . Read ( buffer )
6. Cleanup
// Close gracefully (waits for goroutines)
err = agent . GracefulClose ()
// Or close immediately
// err = agent.Close()
Connection States
The agent’s connection state (defined in ice.go:10-34) tracks the ICE negotiation progress:
State Description ConnectionStateNewAgent is gathering addresses ConnectionStateCheckingPerforming connectivity checks ConnectionStateConnectedSuccessfully connected with a working pair ConnectionStateCompletedAll checks finished (unused in v4) ConnectionStateFailedUnable to establish connection ConnectionStateDisconnectedPreviously connected, now experiencing issues ConnectionStateClosedAgent has been closed
State Transitions
State changes are triggered by:
New → Checking : When Dial() or Accept() is called
Checking → Connected : When a candidate pair succeeds
Connected → Disconnected : After disconnectedTimeout without traffic
Disconnected → Failed : After failedTimeout from disconnected state
Connected → Connected : Can switch selected pairs during renomination
Any → Closed : When Close() is called
Implementation in agent.go:731-747:
func ( a * Agent ) updateConnectionState ( newState ConnectionState ) {
if a . connectionState != newState {
if newState == ConnectionStateFailed {
// Clean up on failure
a . removeUfragFromMux ()
a . checklist = make ([] * CandidatePair , 0 )
a . setSelectedPair ( nil )
a . deleteAllCandidates ()
}
a . connectionState = newState
a . connectionStateNotifier . EnqueueConnectionState ( newState )
}
}
Controlling vs Controlled Roles
ICE agents take on one of two roles during connection establishment:
Controlling Agent
Initiates nomination of candidate pairs
Sends binding requests with USE-CANDIDATE attribute
Typically the peer that initiated the session (caller)
Set by calling Dial()
// Becomes controlling agent
conn , err := agent . Dial ( ctx , remoteUfrag , remotePwd )
Controlled Agent
Responds to nomination from controlling agent
Accepts nominated pairs
Typically the peer that received the session (callee)
Set by calling Accept()
// Becomes controlled agent
conn , err := agent . Accept ( ctx , remoteUfrag , remotePwd )
Role Conflicts
If both agents claim the same role, ICE resolves the conflict using the tie-breaker value (randomly generated 64-bit number). The agent with the higher tie-breaker value keeps its role; the other switches.
Implementation in agent.go:1452-1481:
func ( a * Agent ) handleRoleConflict ( msg * stun . Message , local , remote Candidate , remoteTieBreaker * AttrControl ) {
localIsGreaterOrEqual := a . tieBreaker >= remoteTieBreaker . Tiebreaker
if ( a . isControlling . Load () && localIsGreaterOrEqual ) ||
( ! a . isControlling . Load () && ! localIsGreaterOrEqual ) {
// Send role conflict error
} else {
// Switch our role
a . isControlling . Store ( ! a . isControlling . Load ())
a . setSelector ()
}
}
Agent Configuration Options
Pion ICE provides extensive configuration through options:
Network Configuration
agent , err := ice . NewAgentWithOptions (
// Specify network types
ice . WithNetworkTypes ([] ice . NetworkType {
ice . NetworkTypeUDP4 ,
ice . NetworkTypeUDP6 ,
ice . NetworkTypeTCP4 ,
}),
// Port range for local candidates
ice . WithPortRange ( 10000 , 20000 ),
// Interface filtering
ice . WithInterfaceFilter ( func ( iface string ) bool {
return iface != "docker0" // Skip Docker interfaces
}),
)
Candidate Configuration
agent , err := ice . NewAgentWithOptions (
// Candidate types to gather
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
ice . CandidateTypeServerReflexive ,
ice . CandidateTypeRelay ,
}),
// STUN/TURN servers
ice . WithUrls ([] * stun . URI {
{ Scheme : stun . SchemeTypeSTUN , Host : "stun.l.google.com" , Port : 19302 },
{ Scheme : stun . SchemeTypeTURN , Host : "turn.example.com" , Port : 3478 ,
Username : "user" , Password : "pass" },
}),
)
Timeout Configuration
Configured in agent_config.go:16-59:
disconnectedTimeout := 5 * time . Second
failedTimeout := 25 * time . Second
keepaliveInterval := 2 * time . Second
agent , err := ice . NewAgentWithOptions (
ice . WithDisconnectedTimeout ( disconnectedTimeout ),
ice . WithFailedTimeout ( failedTimeout ),
ice . WithKeepaliveInterval ( keepaliveInterval ),
)
Understanding timeout values
Disconnected Timeout (default 5s):
How long without traffic before transitioning to ConnectionStateDisconnected
Set to 0 to never go to disconnected
Failed Timeout (default 25s):
How long in disconnected state before transitioning to ConnectionStateFailed
Set to 0 to never go to failed
Keepalive Interval (default 2s):
How often to send STUN binding requests to maintain the connection
Set to 0 to disable keepalives (not recommended)
Check Interval (default 200ms):
How often to perform connectivity checks while in checking state
Advanced Configuration
agent , err := ice . NewAgentWithOptions (
// ICE Lite mode (servers with public IPs)
ice . WithLite ( true ),
// Custom logging
ice . WithLoggerFactory ( customLoggerFactory ),
// NAT 1:1 IP mapping
ice . WithAddressRewriteRules ([] ice . AddressRewriteRule {
{
External : [] string { "203.0.113.1" },
AsCandidateType : ice . CandidateTypeHost ,
},
}),
// Maximum binding requests before failing
ice . WithMaxBindingRequests ( 7 ),
// Multicast DNS
ice . WithMulticastDNSMode ( ice . MulticastDNSModeQueryAndGather ),
)
Restarting an Agent
You can restart an agent to perform ICE restart (generates new credentials):
// Restart with new credentials
err = agent . Restart ( "" , "" ) // Empty strings auto-generate new credentials
// Or provide specific credentials
err = agent . Restart ( newUfrag , newPwd )
// After restart, gather candidates again
err = agent . GatherCandidates ()
From agent.go:1694-1754, restarting:
Cancels ongoing gathering
Clears all candidates
Resets checklist and selected pair
Generates new credentials if not provided
Returns to ConnectionStateChecking (if not new)
Event Handlers
OnCandidate
agent . OnCandidate ( func ( c ice . Candidate ) {
if c == nil {
fmt . Println ( "Gathering complete" )
} else {
fmt . Printf ( "New candidate: %s \n " , c )
// Send to remote peer via signaling
}
})
OnConnectionStateChange
agent . OnConnectionStateChange ( func ( state ice . ConnectionState ) {
switch state {
case ice . ConnectionStateConnected :
fmt . Println ( "Connected!" )
case ice . ConnectionStateFailed :
fmt . Println ( "Connection failed" )
case ice . ConnectionStateDisconnected :
fmt . Println ( "Connection lost" )
}
})
OnSelectedCandidatePairChange
agent . OnSelectedCandidatePairChange ( func ( local , remote ice . Candidate ) {
fmt . Printf ( "Selected pair changed to: %s <-> %s \n " ,
local . String (), remote . String ())
})
Best Practices
Always set event handlers before calling GatherCandidates() to ensure you don’t miss any candidates.
Use GracefulClose() instead of Close() when shutting down to ensure all goroutines complete cleanly.
ICE Lite is suitable when:
Your application runs on a server with a public IP address
You want to reduce server complexity
The remote peer will be a full ICE agent
You don’t need relay candidates
Lite agents only gather host candidates and don’t perform active connectivity checks.