NAT traversal enables peer-to-peer connectivity through Network Address Translation (NAT) devices. This guide covers STUN for server reflexive candidates, TURN for relay candidates, and address rewrite rules for 1:1 NAT scenarios.
NAT Traversal Techniques
ICE uses three main techniques to traverse NAT:
STUN - Server Reflexive Candidates
Discover your public IP and port by querying a STUN server. Works through most NATs but fails with symmetric NAT.
TURN - Relay Candidates
Relay all traffic through a TURN server. Works in all NAT scenarios but adds latency and server costs.
Address Rewriting
Map known private addresses to public addresses for 1:1 NAT scenarios like AWS EC2 or static NAT.
STUN for Server Reflexive Candidates
STUN (Session Traversal Utilities for NAT) helps discover your public address:
import " github.com/pion/stun/v3 "
stunURLs := [] * stun . URI {
{ Scheme : stun . SchemeTypeSTUN , Host : "stun.l.google.com" , Port : 19302 },
{ Scheme : stun . SchemeTypeSTUN , Host : "stun1.l.google.com" , Port : 19302 },
}
agent , err := ice . NewAgentWithOptions (
ice . WithUrls ( stunURLs ),
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
ice . CandidateTypeServerReflexive ,
}),
)
How STUN Works
From gather.go:734:
func ( a * Agent ) gatherCandidatesSrflx ( ctx context . Context , urls [] * stun . URI , networkTypes [] NetworkType ) {
for _ , networkType := range networkTypes {
for i := range urls {
// Bind local UDP socket
conn , err := listenUDPInPortRange ( ... )
// Send STUN binding request
xorAddr , err := stunx . GetXORMappedAddr ( conn , serverAddr , a . stunGatherTimeout )
// Create srflx candidate with public address
srflxConfig := CandidateServerReflexiveConfig {
Network : network ,
Address : xorAddr . IP . String (),
Port : xorAddr . Port ,
Component : ComponentRTP ,
RelAddr : lAddr . IP . String (),
RelPort : lAddr . Port ,
}
c , err := NewCandidateServerReflexive ( & srflxConfig )
}
}
}
STUN Gather Timeout
Configure how long to wait for STUN responses:
agent , err := ice . NewAgentWithOptions (
ice . WithUrls ( stunURLs ),
ice . WithSTUNGatherTimeout ( 5 * time . Second ), // default: 5s
)
Increase the timeout if STUN servers are slow to respond or network conditions are poor.
TURN for Relay Candidates
TURN (Traversal Using Relays around NAT) provides relay candidates that work through any NAT:
turnURLs := [] * stun . URI {
{
Scheme : stun . SchemeTypeTURN ,
Host : "turn.example.com" ,
Port : 3478 ,
Username : "username" ,
Password : "password" ,
},
}
agent , err := ice . NewAgentWithOptions (
ice . WithUrls ( turnURLs ),
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
ice . CandidateTypeServerReflexive ,
ice . CandidateTypeRelay ,
}),
)
TURN Transport Protocols
TURN supports multiple transport protocols:
{ Scheme : stun . SchemeTypeTURN , Proto : stun . ProtoTypeUDP ,
Host : "turn.example.com" , Port : 3478 ,
Username : "user" , Password : "pass" }
Standard TURN over UDP. Most efficient but may be blocked by firewalls. { Scheme : stun . SchemeTypeTURN , Proto : stun . ProtoTypeTCP ,
Host : "turn.example.com" , Port : 3478 ,
Username : "user" , Password : "pass" }
TURN over TCP. Better firewall traversal but higher latency. { Scheme : stun . SchemeTypeTURNS , Proto : stun . ProtoTypeTCP ,
Host : "turn.example.com" , Port : 5349 ,
Username : "user" , Password : "pass" }
TURN over TLS. Encrypted and works through most corporate firewalls. { Scheme : stun . SchemeTypeTURNS , Proto : stun . ProtoTypeUDP ,
Host : "turn.example.com" , Port : 5349 ,
Username : "user" , Password : "pass" }
TURN over DTLS. Encrypted with lower latency than TLS.
TURN Allocation Process
From gather.go:826:
func ( a * Agent ) gatherCandidatesRelay ( ctx context . Context , urls [] * stun . URI ) {
for _ , url := range urls {
// Create TURN client
client , err := turn . NewClient ( & turn . ClientConfig {
TURNServerAddr : turnServerAddr ,
Conn : locConn ,
Username : url . Username ,
Password : url . Password ,
})
// Listen for allocation
client . Listen ()
// Allocate relay address
relayConn , err := client . Allocate ()
// Create relay candidate
relayConfig := CandidateRelayConfig {
Network : network ,
Address : relayConn . LocalAddr (). IP . String (),
Port : relayConn . LocalAddr (). Port ,
RelAddr : localAddr ,
RelPort : localPort ,
}
}
}
TLS Certificate Verification
Skip certificate verification for self-signed certificates (development only):
// NOT RECOMMENDED FOR PRODUCTION
agent , err := ice . NewAgentWithOptions (
ice . WithUrls ( turnURLs ),
ice . WithInsecureSkipVerify ( true ),
)
Only use InsecureSkipVerify in development. In production, use properly signed certificates.
Address Rewrite Rules
Address rewrite rules map local private addresses to known public addresses for 1:1 NAT scenarios:
Basic Host Rewriting
rules := [] ice . AddressRewriteRule {
{
External : [] string { "203.0.113.10" },
Local : "10.0.1.100" ,
AsCandidateType : ice . CandidateTypeHost ,
},
}
agent , err := ice . NewAgentWithOptions (
ice . WithAddressRewriteRules ( rules ... ),
)
This replaces host candidate 10.0.1.100 with 203.0.113.10.
Server Reflexive Rewriting
rules := [] ice . AddressRewriteRule {
{
External : [] string { "198.51.100.50" , "198.51.100.60" },
AsCandidateType : ice . CandidateTypeServerReflexive ,
Mode : ice . AddressRewriteAppend ,
},
}
agent , err := ice . NewAgentWithOptions (
ice . WithAddressRewriteRules ( rules ... ),
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
ice . CandidateTypeServerReflexive ,
}),
)
This creates additional srflx candidates without STUN servers.
Rewrite Modes
Replaces the original candidate with the external address(es). {
External : [] string { "203.0.113.10" },
Local : "10.0.1.100" ,
Mode : ice . AddressRewriteReplace ,
}
Default for host candidates.
Keeps the original candidate and adds the external address(es). {
External : [] string { "203.0.113.10" },
Local : "10.0.1.100" ,
Mode : ice . AddressRewriteAppend ,
}
Default for srflx and relay candidates.
Rule Scoping
Limit rules to specific interfaces, CIDR blocks, or network types:
rules := [] ice . AddressRewriteRule {
// Interface-scoped rule
{
External : [] string { "203.0.113.10" },
Iface : "eth0" ,
AsCandidateType : ice . CandidateTypeHost ,
},
// CIDR-scoped rule
{
External : [] string { "203.0.113.20" },
CIDR : "10.10.0.0/24" ,
AsCandidateType : ice . CandidateTypeHost ,
},
// Network type scoped rule
{
External : [] string { "203.0.113.30" },
Networks : [] ice . NetworkType { ice . NetworkTypeUDP4 },
AsCandidateType : ice . CandidateTypeHost ,
},
}
Rule Precedence
From agent_options.go:36:
Rules are evaluated in order with the following precedence:
Explicit local match - Rules with Local field matching the candidate address
Most specific catch-all - Rules without Local but with the most specific scope:
Interface + CIDR (highest)
Interface only
CIDR only
Global (lowest)
rules := [] ice . AddressRewriteRule {
// Most specific: iface + CIDR
{ External : [] string { "203.0.113.10" }, Iface : "eth0" , CIDR : "10.10.0.0/24" },
// Interface only
{ External : [] string { "203.0.113.20" }, Iface : "eth0" },
// CIDR only
{ External : [] string { "203.0.113.30" }, CIDR : "10.0.0.0/8" },
// Global catch-all
{ External : [] string { "203.0.113.100" }},
}
Complete NAT Traversal Examples
AWS EC2 Instance
Map EC2 private IP to public IP:
import (
" net "
" github.com/pion/ice/v4 "
)
func createEC2Agent ( privateIP , publicIP string ) ( * ice . Agent , error ) {
rules := [] ice . AddressRewriteRule {
{
External : [] string { publicIP },
Local : privateIP ,
AsCandidateType : ice . CandidateTypeHost ,
Mode : ice . AddressRewriteReplace ,
},
}
return ice . NewAgentWithOptions (
ice . WithAddressRewriteRules ( rules ... ),
ice . WithNetworkTypes ([] ice . NetworkType {
ice . NetworkTypeUDP4 ,
}),
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
}),
)
}
Multi-Homed Server
Map multiple network interfaces:
func createMultiHomedAgent () ( * ice . Agent , error ) {
rules := [] ice . AddressRewriteRule {
// Blue network
{
External : [] string { "203.0.113.10" },
Local : "10.10.0.20" ,
Iface : "eth0" ,
AsCandidateType : ice . CandidateTypeHost ,
},
// Green network
{
External : [] string { "203.0.113.20" },
Local : "10.20.0.20" ,
Iface : "eth1" ,
AsCandidateType : ice . CandidateTypeHost ,
},
// Fallback for other interfaces
{
External : [] string { "198.51.100.200" },
AsCandidateType : ice . CandidateTypeHost ,
},
}
return ice . NewAgentWithOptions (
ice . WithAddressRewriteRules ( rules ... ),
)
}
STUN + Address Rewriting
Combine STUN with rewrite rules:
func createHybridAgent () ( * ice . Agent , error ) {
stunURLs := [] * stun . URI {
{ Scheme : stun . SchemeTypeSTUN , Host : "stun.l.google.com" , Port : 19302 },
}
rules := [] ice . AddressRewriteRule {
// Known mapping for primary interface
{
External : [] string { "203.0.113.10" },
Local : "10.0.1.100" ,
Iface : "eth0" ,
AsCandidateType : ice . CandidateTypeHost ,
},
// STUN for other interfaces
}
return ice . NewAgentWithOptions (
ice . WithUrls ( stunURLs ),
ice . WithAddressRewriteRules ( rules ... ),
ice . WithCandidateTypes ([] ice . CandidateType {
ice . CandidateTypeHost ,
ice . CandidateTypeServerReflexive ,
}),
)
}
NAT Type Detection
While Pion ICE doesn’t include built-in NAT type detection, you can infer NAT behavior:
func detectNATBehavior ( agent * ice . Agent ) {
candidates , _ := agent . GetLocalCandidates ()
hasHost := false
hasSrflx := false
for _ , c := range candidates {
switch c . Type () {
case ice . CandidateTypeHost :
hasHost = true
case ice . CandidateTypeServerReflexive :
hasSrflx = true
}
}
if ! hasHost && hasSrflx {
fmt . Println ( "Behind NAT (no direct connectivity)" )
} else if hasHost && hasSrflx {
fmt . Println ( "Behind NAT with host candidates (may have public IPs)" )
} else if hasHost && ! hasSrflx {
fmt . Println ( "No NAT or STUN failed" )
}
}
Troubleshooting
No Server Reflexive Candidates
Verify STUN server URLs are correct
Check firewall allows UDP to STUN port (usually 3478 or 19302)
Increase STUN gather timeout
Try multiple STUN servers
TURN Allocation Failures
Verify TURN credentials are correct
Check TURN server allows your source IP
Ensure required transport protocol is supported
Check firewall allows connections to TURN port
Address Rewriting Not Working
Verify local address matches exactly
Check rule precedence (more specific rules first)
Enable debug logging to see which rules match
Verify external IPs are valid and reachable
Symmetric NAT Issues
Use TURN relay candidates
Try multiple STUN servers to detect symmetric NAT
Consider deploying TURN servers closer to users
Next Steps
Configuration Configure ICE agent options
Gathering Learn about candidate gathering
Multiplexing Share ports with UDPMux and TCPMux
Examples See NAT traversal examples