Skip to main content
This guide references examples from the Pion ICE repository, explaining key patterns and use cases.

Available Examples

The Pion ICE library includes several examples demonstrating different features:

Ping-Pong

Basic peer-to-peer connection with message exchange

Continual Gathering

Dynamic candidate gathering with network monitoring

Automatic Renomination

Automatic switching to better candidate pairs

NAT Rules

Address rewriting for complex NAT scenarios

Ping-Pong Example

The ping-pong example demonstrates a basic ICE connection between two peers.

Key Patterns

Setting up the agent:
examples/ping-pong/main.go
iceAgent, err = ice.NewAgentWithOptions(
    ice.WithNetworkTypes([]ice.NetworkType{ice.NetworkTypeUDP4}),
)
Handling candidates:
examples/ping-pong/main.go
err = iceAgent.OnCandidate(func(c ice.Candidate) {
    if c == nil {
        return
    }
    
    // Send candidate to remote peer via signaling
    _, err = http.PostForm(fmt.Sprintf("http://localhost:%d/remoteCandidate", remoteHTTPPort),
        url.Values{
            "candidate": {c.Marshal()},
        })
})
Establishing connection:
examples/ping-pong/main.go
// Controlling agent
if isControlling {
    conn, err = iceAgent.Dial(context.TODO(), remoteUfrag, remotePwd)
} else {
    conn, err = iceAgent.Accept(context.TODO(), remoteUfrag, remotePwd)
}
Exchanging data:
examples/ping-pong/main.go
// Send messages
val, err := randutil.GenerateCryptoRandomString(15, "abcdefghijklmnopqrstuvwxyz")
if _, err = conn.Write([]byte(val)); err != nil {
    panic(err)
}

// Receive messages
buf := make([]byte, 1500)
for {
    n, err := conn.Read(buf)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Received: '%s'\n", string(buf[:n]))
}

Running the Example

# Terminal 1 - Controlled agent
go run examples/ping-pong/main.go

# Terminal 2 - Controlling agent
go run examples/ping-pong/main.go -controlling

Continual Gathering Example

Demonstrates dynamic candidate gathering with network interface monitoring.

Key Patterns

Configuring continual gathering:
examples/continual-gathering/main.go
agent, err := ice.NewAgentWithOptions(
    ice.WithNetworkTypes([]ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}),
    ice.WithCandidateTypes([]ice.CandidateType{ice.CandidateTypeHost}),
    ice.WithContinualGatheringPolicy(ice.GatherContinually),
    ice.WithNetworkMonitorInterval(2 * time.Second),
)
Tracking candidates over time:
examples/continual-gathering/main.go
candidateCount := 0
candidateMap := make(map[string]ice.Candidate)

err = agent.OnCandidate(func(candidate ice.Candidate) {
    if candidate == nil {
        if policy == ice.GatherOnce {
            fmt.Println("Gathering completed")
        }
        return
    }
    
    candidateCount++
    candidateID := candidate.String()
    
    if _, exists := candidateMap[candidateID]; !exists {
        candidateMap[candidateID] = candidate
        fmt.Printf("[%s] Candidate #%d: %s\n", 
            time.Now().Format("15:04:05"), candidateCount, candidate)
    }
})
Monitoring gathering state:
examples/continual-gathering/main.go
go func() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        state, _ := agent.GetGatheringState()
        localCandidates, _ := agent.GetLocalCandidates()
        
        fmt.Printf("[%s] Status: GatheringState=%s, Candidates=%d\n",
            time.Now().Format("15:04:05"), state, len(localCandidates))
    }
}()

Running the Example

# Gather once (default)
go run examples/continual-gathering/main.go -mode=once

# Gather continually with custom interval
go run examples/continual-gathering/main.go -mode=continually -interval=5s
Try connecting/disconnecting network interfaces while running with -mode=continually to see new candidates discovered automatically.

Automatic Renomination Example

Shows automatic switching to better candidate pairs based on network conditions.

Key Patterns

Enabling automatic renomination:
examples/automatic-renomination/main.go
if isControlling {
    renominationInterval := 3 * time.Second
    iceAgent, err = ice.NewAgentWithOptions(
        ice.WithNetworkTypes([]ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}),
        ice.WithRenomination(ice.DefaultNominationValueGenerator()),
        ice.WithAutomaticRenomination(renominationInterval),
    )
}
Monitoring candidate pair changes:
examples/automatic-renomination/main.go
err = iceAgent.OnSelectedCandidatePairChange(func(local, remote ice.Candidate) {
    fmt.Println(">>> SELECTED CANDIDATE PAIR CHANGED <<<")
    fmt.Printf("    Local:  %s (type: %s)\n", local, local.Type())
    fmt.Printf("    Remote: %s (type: %s)\n", remote, remote.Type())
})
Tracking RTT:
examples/automatic-renomination/main.go
func getRTT() string {
    if selectedLocalCandidateID == "" || selectedRemoteCandidateID == "" {
        return "N/A"
    }
    
    stats := iceAgent.GetCandidatePairsStats()
    for _, stat := range stats {
        if stat.LocalCandidateID == selectedLocalCandidateID && 
           stat.RemoteCandidateID == selectedRemoteCandidateID {
            if stat.CurrentRoundTripTime > 0 {
                return fmt.Sprintf("%.2fms", stat.CurrentRoundTripTime*1000)
            }
        }
    }
    return "N/A"
}
Debugging candidate pairs:
examples/automatic-renomination/main.go
go func() {
    for {
        time.Sleep(5 * time.Second)
        fmt.Println("=== DEBUG: Candidate Pair States ===")
        stats := iceAgent.GetCandidatePairsStats()
        for _, stat := range stats {
            fmt.Printf("  %s <-> %s\n", stat.LocalCandidateID, stat.RemoteCandidateID)
            fmt.Printf("    State: %s, Nominated: %v\n", stat.State, stat.Nominated)
            if stat.CurrentRoundTripTime > 0 {
                fmt.Printf("    RTT: %.2fms\n", stat.CurrentRoundTripTime*1000)
            }
        }
    }
}()

Running the Example

This example requires network namespaces to simulate network changes:
# See README in examples/automatic-renomination for setup instructions
cd examples/automatic-renomination
cat README.md
The example uses tc (traffic control) to add latency to specific interfaces, triggering automatic renomination to better paths.

NAT Rules Example

Demonstrates address rewriting for various NAT scenarios.

Key Patterns

Multi-network host rewriting:
examples/nat-rules/main.go
rules := []ice.AddressRewriteRule{
    // Blue network
    {
        External:        []string{publicBlue},
        Local:           localBlue,
        AsCandidateType: ice.CandidateTypeHost,
        Networks:        []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeTCP4},
    },
    // Green network
    {
        External:        []string{publicGreen},
        Local:           localGreen,
        AsCandidateType: ice.CandidateTypeHost,
        Networks:        []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeTCP4},
    },
    // Global fallback
    {
        External:        []string{globalFallback},
        AsCandidateType: ice.CandidateTypeHost,
    },
}
Interface-scoped rules:
examples/nat-rules/main.go
rules := []ice.AddressRewriteRule{
    {
        External:        []string{publicBlue},
        Local:           localBlue,
        Iface:           "eth0",
        AsCandidateType: ice.CandidateTypeHost,
    },
    {
        External:        []string{publicGreen},
        Local:           localGreen,
        Iface:           "eth1",
        AsCandidateType: ice.CandidateTypeHost,
    },
}
Server reflexive pool:
examples/nat-rules/main.go
rules := []ice.AddressRewriteRule{
    // Primary srflx
    {
        External:        []string{srflxPrimary},
        Local:           "0.0.0.0",
        AsCandidateType: ice.CandidateTypeServerReflexive,
        Networks:        []ice.NetworkType{ice.NetworkTypeUDP4},
    },
    // Secondary srflx catch-all
    {
        External:        []string{srflxSecondary},
        AsCandidateType: ice.CandidateTypeServerReflexive,
        Networks:        []ice.NetworkType{ice.NetworkTypeUDP4},
    },
    // Host override
    {
        External:        []string{hostPublic},
        Local:           hostLocal,
        AsCandidateType: ice.CandidateTypeHost,
    },
}
CIDR-scoped rules:
examples/nat-rules/main.go
rules := []ice.AddressRewriteRule{
    // Scoped to specific CIDR
    {
        External:        []string{scopedPublic},
        Local:           scopedLocal,
        CIDR:            "10.30.0.0/24",
        AsCandidateType: ice.CandidateTypeHost,
    },
    // Catch-all for same CIDR
    {
        External:        []string{catchAll},
        CIDR:            "10.30.0.0/24",
        AsCandidateType: ice.CandidateTypeHost,
    },
}

Running the Example

# List available scenarios
go run examples/nat-rules/main.go -list

# Run specific scenario
go run examples/nat-rules/main.go -scenario=multi-net

# Run all scenarios
go run examples/nat-rules/main.go -scenario=all

# Use with Docker Compose topology
cd examples/nat-rules
docker-compose up
The Docker Compose setup creates a realistic multi-homed environment with multiple networks and NAT gateways.

Common Patterns

Error Handling

All examples demonstrate proper error handling:
if err := agent.GatherCandidates(); err != nil {
    panic(err)
}

Resource Cleanup

Always close the agent when done:
defer func() {
    if closeErr := agent.Close(); closeErr != nil {
        log.Printf("Failed to close agent: %v", closeErr)
    }
}()

Signaling

Examples use HTTP for simplicity, but production should use WebSocket or other real-time protocols:
// Simple HTTP signaling (examples only)
_, err = http.PostForm(fmt.Sprintf("http://localhost:%d/remoteCandidate", port),
    url.Values{"candidate": {c.Marshal()}})

Logging

Enable debug logging to understand behavior:
import "github.com/pion/logging"

loggerFactory := logging.NewDefaultLoggerFactory()
loggerFactory.DefaultLogLevel = logging.LogLevelDebug

agent, err := ice.NewAgentWithOptions(
    ice.WithLoggerFactory(loggerFactory),
)

Building and Running

All examples can be run directly:
# Clone repository
git clone https://github.com/pion/ice.git
cd ice

# Run any example
go run examples/ping-pong/main.go
go run examples/continual-gathering/main.go
go run examples/automatic-renomination/main.go
go run examples/nat-rules/main.go

Troubleshooting Examples

Ping-Pong Connection Fails

  • Ensure both instances are running
  • Check they’re using opposite controlling flags
  • Verify HTTP ports are available (9000, 9001)
  • Enable debug logging to see connection progress

Continual Gathering Shows No New Candidates

  • Try actually changing network interfaces (enable/disable WiFi)
  • Check the monitoring interval isn’t too long
  • Verify interface filters aren’t excluding new interfaces

Automatic Renomination Doesn’t Switch

  • Ensure both agents support renomination
  • Check that network conditions actually changed (use tc to add latency)
  • Verify renomination interval has elapsed
  • Review candidate pair statistics for RTT differences

NAT Rules Not Applied

  • Verify local addresses match exactly
  • Check rule precedence (more specific first)
  • Enable debug logging to see which rules match
  • Ensure external IPs are valid

Next Steps

Configuration

Deep dive into agent configuration options

Gathering

Learn about candidate gathering in detail

Connectivity Checks

Understand connectivity check mechanics

GitHub Repository

Browse the full source code and examples

Build docs developers (and LLMs) love