STUN, TURN & ICE
The protocols that punch through NATs
STUN (Session Traversal Utilities for NAT)
Defined in RFC 8489 (supersedes RFC 5389 and the original RFC 3489). STUN's core purpose is simple: tell a client what its public IP:port looks like from the outside. It's a lightweight, stateless protocol — just a few UDP packets.
How STUN Works
- Client sends a Binding Request to a STUN server (typically on port 3478)
- The STUN server sees the source
IP:portof the incoming packet — this is the NAT's external mapping - Server replies with a Binding Response containing
XOR-MAPPED-ADDRESS— the client's publicIP:port - Client now knows its own public address as seen by the outside world
STUN Binding Request/Response
192.168.1.5:12345198.51.100.1:3478Note
Why XOR-MAPPED-ADDRESS instead of plain MAPPED-ADDRESS? Some middleboxes (ALGs, overzealous firewalls) inspect packet payloads for IP addresses and rewrite them. XOR-ing with the magic cookie prevents these devices from interfering with the address in the response.
STUN Limitations
What STUN Cannot Do
- Useless with Symmetric NAT (APDM): The address STUN reports is only valid for that specific STUN server. A different peer will see a different mapping.
- No relay capability: STUN only discovers addresses — if direct connectivity fails, STUN can't help.
- Blocked by strict firewalls: If outbound UDP is blocked entirely, STUN requests never reach the server.
- No authentication by default: Basic Binding requests are unauthenticated (though STUN supports MESSAGE-INTEGRITY for authenticated uses in ICE).
UDP Hole Punching — Step by Step
The fundamental technique for establishing direct UDP connectivity between two hosts behind NAT. Requires both NATs to use Endpoint-Independent Mapping (EIM) for reliable operation.
Prerequisites
STUN Discovery
Both peers learn their public IP:port
Signaling Exchange
Peers share addresses via a server
Simultaneous Send
Both send to each other's public addr
Direct Connection
NAT mappings now allow bidirectional flow
Detailed Sequence
10.0.0.5:8000203.0.113.1198.51.100.110.0.0.9:8000Why It Works (with EIM NATs)
- EIM guarantees stable mappings: Peer A's STUN-discovered address
203.0.113.1:54321is the same address used when A sends to Peer B. The NAT reuses the mapping. - Outbound creates the "hole": When A sends to B's public address, NAT A creates an outbound mapping that also allows inbound from B's IP (ADF) or B's IP:port (APDF).
- Timing matters with APDF: With Address+Port-Dependent Filtering, B's NAT will only accept packets from an IP:port that B has sent to. So B must also send to A's public address, creating the reciprocal hole.
- Simultaneous = both holes open: Once both peers have sent packets, both NATs have outbound mappings and will accept the other's packets.
Warning
Why it fails with APDM (Symmetric) NAT: When Peer A sends to Peer B, the Symmetric NAT creates a new mapping (different from what STUN reported). B is sending to A's STUN-reported address, which is now the wrong port. Neither peer's packets reach the other.
TCP Hole Punching (Simultaneous Open)
TCP's three-way handshake (SYN, SYN-ACK, ACK) doesn't seem compatible with hole-punching. But TCP has a rarely-used feature: simultaneous open — both sides send SYN at the same time, and the connection establishes via SYN, SYN+ACK, ACK on both sides.
- Stateless — just send packets
- Timing is forgiving (seconds window)
- Well-understood, high success rate
- Works with most EIM NATs
- Requires simultaneous SYN (tight timing)
- Many NATs don't handle TCP simultaneous open correctly
- OS-level quirks: some stacks reset on unexpected SYN
- Firewalls often statefully track TCP and block this
Tip
Practical reality: TCP hole punching works maybe 60% of the time vs 90%+ for UDP. It's used as a fallback when UDP is blocked (e.g., some corporate firewalls block all UDP except DNS). Tailscale primarily uses UDP (WireGuard) but can fall back to TCP via DERP relay over HTTPS.
TURN (Traversal Using Relays around NAT)
Defined in RFC 8656. TURN is the "if all else fails" protocol. When direct connectivity is impossible (both sides behind Symmetric NAT, UDP blocked, etc.), traffic is relayed through a TURN server. Every packet goes: Client A → TURN server → Client B.
How TURN Works
- Allocate: Client sends an Allocate request to the TURN server. Server allocates a relay address (a public IP:port on the TURN server) and returns it to the client.
- Permission: Client creates permissions on the TURN server specifying which peers can send to its relay address.
- Channel Bind: Optionally, client binds a channel number (2 bytes) to a specific peer for reduced overhead (4-byte channel header vs 36-byte TURN header).
- Relay: Data from the peer arrives at the relay address, and the TURN server forwards it to the client. Bidirectional.
TURN Relay Topology
TURN Costs & Trade-offs
| Aspect | Impact |
|---|---|
| Bandwidth | TURN server carries ALL data — doubles bandwidth cost (ingress + egress). Expensive at scale. |
| Latency | Extra hop through the relay. Path: A → TURN → B instead of A → B. Can add 10-100ms+ RTT. |
| Reliability | TURN server is a single point of failure and a bottleneck. Must be highly available. |
| Privacy | The relay sees unencrypted metadata (source/dest IPs). Payload can be encrypted (DTLS/SRTP), but the relay knows who's talking to whom. |
Note
TURN transport options: The client-to-TURN-server connection can use UDP, TCP, or TLS-over-TCP. The TLS option is important because some networks block all UDP — TLS over TCP on port 443 looks like HTTPS and passes through most firewalls. The TURN-server-to-peer leg is typically UDP.
ICE (Interactive Connectivity Establishment)
Defined in RFC 8445. ICE is not a single protocol — it's a framework that orchestrates STUN, TURN, and direct connectivity into a systematic candidate gathering and connectivity checking process. If STUN is "discover your address" and TURN is "relay traffic," ICE is "try everything and pick the best working path."
Candidate Types
| Candidate Type | Source | Priority | Description |
|---|---|---|---|
| Host | Local interface | Highest | Direct local IP addresses (e.g., 192.168.1.5:8000). Works if peers are on the same LAN. |
| Server Reflexive (srflx) | STUN | Medium | Public IP:port discovered via STUN. Enables hole-punching for NAT traversal. |
| Peer Reflexive (prflx) | Connectivity check | Medium | Discovered during ICE checks — a mapping that wasn't known during gathering. Happens with some NATs. |
| Relay | TURN | Lowest | TURN relay address. Last resort — guaranteed to work but adds latency and cost. |
ICE Process Flow
Gather Candidates
Host, STUN (srflx), TURN (relay)
Exchange via Signaling
SDP offer/answer carries candidates
Pair Candidates
Every local candidate paired with every remote
Connectivity Checks
STUN Binding on each pair, ordered by priority
Nominate Best Pair
Highest-priority working pair wins
Candidate Pairing & Priority
How ICE Selects the Best Path
ICE creates a checklist of candidate pairs, ordered by priority. Priority formula favors:
- Host-to-Host — both peers on the same LAN (direct, zero NAT traversal)
- Host-to-srflx / srflx-to-Host — one peer on public IP, other behind NAT
- srflx-to-srflx — both behind NAT, hole-punching
- Any-to-relay / relay-to-Any — TURN relay, last resort
Connectivity checks use STUN Binding requests on each pair. The check succeeds if both peers can exchange STUN messages on that candidate pair. Failed checks are pruned, and the highest-priority surviving pair is nominated for data transfer.
- Gathering phase: Agent collects all candidates (host IPs, STUN-derived reflexive addresses, TURN allocations). This happens in parallel — STUN and TURN requests fire simultaneously.
- Exchange phase: Candidates are sent to the remote peer via the signaling channel (WebRTC uses SDP in offer/answer; Tailscale uses the coordination server).
- Checking phase: Both agents perform STUN connectivity checks on each candidate pair in priority order. Checks happen from both sides simultaneously (ICE is symmetric).
- Nomination: The controlling agent picks the best valid pair. In regular nomination, it re-checks with a USE-CANDIDATE flag. In aggressive nomination, every check includes USE-CANDIDATE and the first success wins.
- Completed: Data flows over the nominated pair. ICE can perform periodic keep-alive checks and switch paths if the nominated pair fails (ICE restart).
Tip
ICE-Lite: Servers with public IPs don't need the full ICE state machine. ICE-Lite is a minimal implementation for hosts that aren't behind NAT — they only respond to checks, never initiate them. Common for SFUs (Selective Forwarding Units) and media servers.
Trickle ICE
- Gather ALL candidates first
- Then exchange them
- Then start checking
- Slow: must wait for TURN allocation (can take seconds)
- Start exchanging candidates as they're discovered
- Connectivity checks begin immediately
- New candidates added to the checklist incrementally
- Much faster: host candidates checked while STUN/TURN still gathering
How It All Fits Together
Decision Tree
Same LAN?
Use host candidate
EIM NAT?
STUN + hole punch
Symmetric NAT?
Port prediction (maybe)
All else fails
TURN relay
Warning
The cost of TURN at scale: WebRTC services (video calls, etc.) pay heavily for TURN bandwidth. A 1:1 video call at 2 Mbps through TURN costs the provider 2 Mbps ingress + 2 Mbps egress per stream direction. This is why optimizing NAT traversal to avoid relay is critical — and why Tailscale invested so heavily in DERP (their TURN equivalent) optimization.