Part XIV — NAT traversal and real-time media
46. NAT behaviors, STUN, TURN, and ICE
46.1 The peer-to-peer problem
Two clients (a laptop on home Wi-Fi, a phone on cellular) both behind NAT want a
direct connection (video call, game, file transfer). Neither has a reachable
public IP:port. Neither can initiate to the other. You need NAT traversal.
46.2 NAT mapping and filtering behavior (RFC 4787 terms)
Two independent properties of a NAT, per internal socket:
Mapping behavior — does the same internal IP:port get the same external
IP:port for different destinations?
- Endpoint-Independent Mapping (EIM): yes — one external port regardless of destination. Traversal-friendly. (Most home routers today.)
- Address-Dependent / Address-and-Port-Dependent Mapping: a new external port per destination IP (or per IP+port). This is "symmetric NAT". Hostile to traversal — the port the peer must target isn't the port anyone else observed.
Filtering behavior — who is allowed to send in to an existing mapping?
- Endpoint-Independent Filtering: anyone (once the mapping exists) — "full cone".
- Address-Dependent Filtering: only hosts the internal endpoint has sent to (any port).
- Address-and-Port-Dependent Filtering: only the exact
IP:portthe internal endpoint sent to — "port-restricted cone".
Also matters: hairpinning (can two internal hosts reach each other via the external IP?) and port preservation / port parity.
46.3 STUN — discover your public mapping
STUN (Session Traversal Utilities for NAT, RFC 8489): the client sends a
Binding Request to a public STUN server (UDP/3478, or over TLS/TCP). The server
replies with the source IP:port it saw — i.e. your external mapping for
that socket. Now the client knows its "server-reflexive" candidate.
- If your NAT has EIM, that reflexive
IP:portis the same one your peer can send to → direct connection works after a hole punch. - If your NAT is symmetric, the port STUN saw is only valid toward the STUN server; your peer will hit a different port → STUN alone fails → you need TURN.
STUN also does keepalives (refresh the mapping before it times out) and, in ICE, connectivity checks.
46.4 Hole punching
A (behind NAT-A) Signaling server B (behind NAT-B)
|-- STUN -> learns A_pub:pa --------| |
| |------ STUN -> B_pub:pb ------|
|------ offer (A_pub:pa) ---------> relayed <---- answer (B_pub:pb)-|
| |
|== A sends UDP to B_pub:pb (opens NAT-A mapping; NAT-B drops it) ==>|
|<= B sends UDP to A_pub:pa (opens NAT-B mapping; now NAT-A allows) =|
|== subsequent packets both ways pass - direct P2P path established ==|
Both sides must fire near-simultaneously (coordinated by the signaling channel) so each NAT sees an outbound packet first and then permits the inbound reply. Works when at least the mapping is endpoint-independent on both sides; often works with one symmetric NAT via port prediction; fails with symmetric-on-both.
46.5 TURN — relay when direct fails
TURN (Traversal Using Relays around NAT, RFC 8656): a public relay server allocates a relayed transport address on itself; both peers send to the relay, which forwards. Always works (it's just a well-connected middleman) but costs bandwidth/latency and server capacity. ~10–20% of real-world WebRTC calls end up relayed. TURN runs over UDP/3478, TCP/3478, or TLS/5443/443 (to punch through restrictive firewalls that only allow "HTTPS").
46.6 ICE — try everything, pick the best
ICE (Interactive Connectivity Establishment, RFC 8445) is the algorithm that ties it together. Each peer gathers candidates:
- host — local
IP:port(LAN, works if peers are on the same network / have IPv6). - server-reflexive (srflx) — from STUN (your public mapping).
- relayed — from TURN.
- (peer-reflexive — discovered during checks.)
Peers exchange candidate lists via signaling (SDP), form candidate pairs, prioritize them (host > srflx > relay; IPv6 often preferred), and run connectivity checks (STUN binding requests) on every pair simultaneously. The highest-priority pair that succeeds becomes the path; ICE can switch if it degrades (ICE restart). Trickle ICE sends candidates as they're found instead of waiting, cutting call setup time.
46.7 Why IPv6 quietly fixes most of this
With end-to-end IPv6, both peers have globally routable addresses; ICE's host candidates connect directly (subject to each side's stateful firewall allowing the hole-punched flow). No STUN mapping games, no TURN relay. This is a real, measurable reason to deploy IPv6 for real-time apps.
46.8 Keepalives and timeouts
UDP NAT mappings expire fast — RFC 4787 says ≥ 2 min, but real routers use 30–60 s, some as low as 20 s. Any P2P/media session must send a keepalive (STUN binding indication, or an RTP no-op, or a DTLS heartbeat) every 15–25 s or inbound media stops when the mapping is reaped. TCP mappings live longer (minutes to hours) but still need keepalive for long idle gaps.
46.9 War story
A WebRTC app worked for 80% of calls; the rest had one-way or no audio. Logs showed those clients were behind symmetric NAT (corporate firewalls) and the deployment had no TURN server ("STUN is enough"). ICE found only srflx candidates that didn't pair. Standing up a TURN server (with TLS/443 transport for the strictest networks) fixed connectivity for the remaining 20%. Lesson: STUN handles ~80–90%; you must run TURN for the rest, and expose it on 443/TLS for locked-down networks.
Try it yourself
# Classify your NAT / see your reflexive address:
stunclient stun.l.google.com 19302 # from 'stuntman'
# or:
turnutils_stunclient stun.l.google.com # from coturn
# Watch NAT mapping lifetime: send one UDP packet out, then see how long return works
sudo conntrack -L -p udp # on a Linux NAT box: see mappings + timeouts
# WebRTC internals in Chrome: chrome://webrtc-internals (ICE candidate pairs, selected pair)