Real-Time System Design

System Design · Real-Time System Design

NAT Traversal: STUN, TURN & ICE

Almost nobody has a routable public IP anymore — here's how two peers behind separate NATs still manage to find each other and exchange media directly.

8 min read
NAT TraversalSTUNTURNICE

Open two laptops on two different home networks and try to make one connect directly to the other's IP address, and nothing happens. Not because the internet is broken, but because neither laptop actually has a public IP address to give out. Both sit behind a router doing Network Address Translation (NAT), which quietly rewrites every outgoing packet's source address to the router's own public IP, and rewrites it back on the way in. The laptop itself only ever sees its private address — something like 192.168.1.42 — which is meaningless to anyone outside the LAN.

This is a fine arrangement for the client/server web, where every connection is initiated outward and the server has a stable, known address to be reached at. It becomes a real problem the moment two peers both sitting behind NAT need to talk directly to each other, which is exactly the situation WebRTC is built for: a video call, a screen share, a peer-to-peer file transfer. Neither side knows its own public-facing address, and even if it did, most NATs won't let an unsolicited inbound packet through to a port nobody asked to expose. Solving this — reliably, in a few hundred milliseconds, across every consumer router and corporate firewall in the wild — is the job of STUN, TURN, and ICE.

STUN: asking "what do I look like from out there?"

STUN (Session Traversal Utilities for NAT) is deliberately the simplest piece of the puzzle. A STUN server sits on the public internet with nothing to do but answer one question: when this packet arrived, what source IP and port did it come from? Because the client's NAT already rewrote the packet on its way out, the answer tells the client its own public IP and port as the rest of the internet sees it — information it has no way to determine on its own.

Armed with that public IP:port pair, a client can hand it to the other peer, typically over a signaling channel like WebSocket, and the other peer can try sending packets straight to it. If the NAT in between happens to map that same internal port to the same external port consistently — true for most home routers doing simple "endpoint-independent" mapping — a direct peer-to-peer path opens up, and STUN's job is done. No media ever touches a third-party server; it flows straight between the two participants.

Not every NAT cooperates

Some NATs — particularly "symmetric" ones common in corporate networks — assign a different external port for every destination the client talks to. The port a STUN server sees isn't the port the NAT will use when the other peer tries to reach it, so the address STUN discovered is already stale by the time it's shared. In that case, no amount of retrying will produce a working direct connection, and something else has to step in.

TURN: giving up on direct and relaying instead

TURN (Traversal Using Relays around NAT) is the fallback for exactly that situation. A TURN server is a relay with a real public IP that either peer can reach without issue. Instead of trying to connect the two NATs to each other, both peers open a connection to the TURN server, and the server simply copies packets from one side to the other. From the perspective of each peer's own NAT, this looks like an ordinary outbound connection to a server — the one thing NAT has never had a problem with.

The tradeoff is explicit and unavoidable: the moment traffic goes through TURN, the call is no longer peer-to-peer. Every byte of audio and video now flows through infrastructure someone has to run and pay for, doubling the bandwidth cost compared to a direct path (the relay receives the stream once and forwards it again) and adding the relay's round-trip latency on top. TURN buys guaranteed connectivity, not efficiency — it's the connection of last resort, used only when nothing better works.

ICE: trying everything and keeping what works

STUN and TURN are just tools. ICE (Interactive Connectivity Establishment) is the framework that actually decides how to use them. Rather than gambling on one address and hoping it works, each peer gathers every plausible way it might be reachable — its local LAN address, the public address STUN discovered, and a relay address allocated on a TURN server — and packages each one as an ICE candidate.

json
{
  "candidate": "candidate:842163049 1 udp 1677729535 203.0.113.42 54321 typ srflx raddr 10.0.0.7 rport 54321 generation 0",
  "sdpMid": "0",
  "sdpMLineIndex": 0
}

// typ host   -> a local interface address, only useful on the same LAN
// typ srflx  -> "server reflexive": the public IP/port a STUN server observed
// typ relay  -> a TURN-allocated relay address, used as a last resort

Both peers exchange their full candidate lists over the signaling channel, then ICE tries pairing them up: local-to-local first (cheap and fast if both happen to be on the same network), then server-reflexive pairs discovered via STUN, and only falls back to a relayed TURN pair if every direct attempt fails connectivity checks. Whichever pairing actually completes a handshake — and, among several that work, whichever performs best — is the one the session keeps using. All of this typically resolves in well under a second, invisibly, before either side notices anything beyond "the call connected."

Why you provision TURN even though most calls don't need it

It's tempting to treat TURN as an edge case not worth the infrastructure spend — after all, a direct or STUN-assisted path succeeds for the majority of consumer connections. But "most" is doing a lot of work in that sentence. Symmetric NATs behind corporate firewalls, mobile carrier-grade NAT, and locked-down guest networks are common enough in aggregate that some meaningful slice of real users — often cited in the 10-20% range for broad consumer products — simply cannot establish a direct path no matter how well STUN and ICE are implemented.

Ship a WebRTC product with only STUN configured, and every one of those users gets a call that never connects, with a failure mode that's nearly impossible to diagnose from the outside — it just looks like "connecting…" forever. That's why every serious production deployment provisions TURN servers as a mandatory fallback, not an optional extra, and budgets for the relay bandwidth accordingly. The alternative isn't a slightly worse experience; it's a call that silently fails for exactly the users who were already having network trouble.

Key takeaways

  • NAT hides a device's real address from the outside world, so two peers behind separate NATs can't just connect directly without help.
  • STUN tells a client its own public IP:port as seen from the internet, enabling a direct path when the NAT's mapping is predictable.
  • TURN relays media through a public server when no direct path exists — it guarantees connectivity but costs bandwidth and latency, and isn't peer-to-peer anymore.
  • ICE is the orchestration layer: it gathers host, STUN, and TURN candidates from both sides and tries pairs until one actually works.
  • Symmetric NATs and locked-down firewalls make direct connections impossible for a real slice of users, which is why production systems always provision TURN, not just STUN.
2026 — Built by Arpit Mishra