Dynamically switch between candidate pairs for connection migration and improved connectivity
Renomination allows the controlling ICE agent to nominate a different candidate pair after the initial nomination, enabling seamless connection migration in response to network changes or quality degradation.
Standard ICE nominates a single candidate pair for data transmission. Once nominated, switching to a different pair requires restarting the ICE process. Renomination extends ICE by allowing the controlling agent to nominate a new pair at any time, supporting scenarios like:
The nomination value must increase with each renomination. The controlled agent rejects nominations with values less than or equal to previously seen values.
Renomination adds a custom STUN attribute to binding requests:
renomination.go
type NominationAttribute struct { Value uint32 // 24-bit nomination value}const ( // DefaultNominationAttribute is the default STUN attribute type DefaultNominationAttribute stun.AttrType = 0x0030)
The attribute is automatically included in nomination requests when renomination is enabled:
Only the controlling agent can trigger renomination:
// Get local and remote candidateslocalCandidate := // ... your local candidateremoteCandidate := // ... your remote candidate// Renominate this candidate pairerr := agent.RenominateCandidate(localCandidate, remoteCandidate)if err != nil { // Handle error (e.g., not controlling, renomination disabled, pair not found)}
The controlled agent automatically handles incoming renominations:
selector.go
func (s *controlledSelector) shouldAcceptNomination(nomination *uint32) bool { // No nomination value = standard ICE, always accept if nomination == nil { return true } // First nomination with value if s.lastNomination == nil { s.lastNomination = nomination return true } // Accept only if new value is higher ("last nomination wins") if *nomination > *s.lastNomination { s.lastNomination = nomination return true } return false}
The controlled agent applies “last nomination wins” logic regardless of whether it has local renomination enabled. This ensures compatibility when the controlling agent uses renomination.
// Monitoring network interface changesfunc handleNetworkChange(agent *ice.Agent, newInterface string) error { // Gather new candidates on the new interface err := agent.GatherCandidates() if err != nil { return err } // Wait for new candidates time.Sleep(2 * time.Second) // Find candidate pair using the new interface pairs := agent.GetCandidatePairsStats() for _, pair := range pairs { if pair.State == ice.CandidatePairStateSucceeded { localCand := getLocalCandidate(agent, pair.LocalCandidateID) if isOnInterface(localCand, newInterface) { // Renominate to use new interface remoteCand := getRemoteCandidate(agent, pair.RemoteCandidateID) return agent.RenominateCandidate(localCand, remoteCand) } } } return fmt.Errorf("no suitable candidate on new interface")}
Renominate based on measurable quality degradation:
RTT increases beyond threshold
Packet loss exceeds acceptable rate
New interface with better metrics becomes available
Current path becomes unavailable
Avoid renominating too frequently (thrashing). Implement hysteresis.
Monitoring for Renomination
Use candidate pair statistics to make informed decisions:
stats, ok := agent.GetSelectedCandidatePairStats()if ok { // CurrentRoundTripTime, PacketsLost, BytesSent, etc. if shouldRenominate(stats) { // Find and renominate better pair }}
Compatibility
Renomination is backward compatible with standard ICE:
If remote peer doesn’t support renomination, nominations work as standard ICE
Controlled agents handle renomination even without local renomination enabled
The NOMINATION attribute is optional; absence means standard ICE behavior