A machine behind NAT cannot accept an incoming connection. That’s not a misconfiguration you can fix in the router UI — it’s the definition of the thing. NAT keeps a table of the outbound connections your devices started, and it uses that table to send replies back to the right one. A packet that arrives out of the blue, matching no row in the table, has nowhere to go. The box drops it. That’s the entire reason your laptop isn’t getting port-scanned to death right now, and it’s also, on paper, a reason two laptops behind two different NATs should never be able to talk to each other directly.
And yet, right now, millions of video calls are running with audio and video flowing straight between two such machines — no server in the middle forwarding a single byte. Both endpoints are, by the rules above, unreachable. They’re talking anyway. The way they pull this off is one of the great practical hacks of the modern internet, and once you see it you can’t unsee it in every call you make.
The move: two packets, one instant
Here’s the trick, and it’s almost dumb enough to be insulting. Call the two peers A and B. Neither can receive an unsolicited packet. But both can send.
When A sends a UDP packet toward B’s public address, A’s NAT does what NAT always does: it creates a table entry — “I sent something to B, so I’ll accept a reply from B” — and forwards the packet out. The packet reaches B’s NAT, which has no matching entry, and gets dropped. Wasted. Except it wasn’t wasted, because it left a hole open on A’s side: A’s NAT is now, briefly, willing to let a packet from B back in.
Now do the same thing from B, at the same time. B fires a packet at A. B’s NAT opens its own hole. And this time, when B’s packet arrives at A’s NAT, there is a matching entry — the one A’s earlier packet created. It sails through. A’s reply likewise finds B’s freshly-opened hole. Within a round or two, both NATs have entries pointing at each other, and a two-way UDP path exists between two machines that each, individually, cannot be connected to.
That’s UDP hole punching. The endpoints don’t open a hole for the other side; they open it by reaching out, and the other side’s simultaneous reach-out arrives just in time to use it. It was documented and measured in a 2005 USENIX paper by Bryan Ford, Pyda Srisuresh, and Dan Kegel, “Peer-to-Peer Communication Across Network Address Translators.” They tested it against a pile of real, deployed home routers and found roughly 82% supported it for UDP. Not a lab curiosity — the load-bearing mechanism under WebRTC, most VoIP, and a lot of online gaming.
But read the setup again and you’ll spot the problem it hand-waves: A has to send its first packet toward B’s public address. How does A know B’s public address? B is behind NAT. B doesn’t know its own public address. Neither does A.
Why a stranger has to tell you your own address
This is the part people find genuinely strange the first time. A machine behind NAT does not know the address the outside world sees it as. Your laptop knows its private 192.168.x.x address, which is useless to anyone outside your house. The public IP and — critically — the specific port your NAT assigned to this particular flow are decided by the NAT, on the fly, and never reported back inward.
So you ask. You send a packet to a server on the public internet whose only job is to look at where your packet appears to come from and read that address back to you. That server is a STUN server — Session Traversal Utilities for NAT, currently RFC 8489, originally RFC 3489 back in 2003. You send a STUN Binding request; it replies “I see you as 203.0.113.7:54321.” That reflected address — the public IP and port your NAT happened to assign — is your server-reflexive candidate, and it’s the thing you hand to the other peer so they know where to aim.
STUN is deliberately tiny. It doesn’t relay your call, it doesn’t touch your media, it just holds up a mirror. A STUN exchange is a couple of packets and then it’s out of the loop entirely, which is why public STUN servers can serve enormous populations for almost nothing. Both peers do this, trade their reflexive addresses through whatever signaling channel set up the call (the web server, the app backend — some already-connected path), and now each knows where to punch. The hole punching from the previous section can begin.
Unless, that is, the mirror lied. Which, for one whole category of NAT, it does.
The NAT that refuses to be punched
The seductive assumption in “ask STUN for your public address” is that you have a public address — one stable IP:port the outside world sees you at. For most home routers, true enough. For a meaningful minority, false in a way that breaks everything.
The original STUN spec tried to taxonomize NATs into four species: full cone, restricted cone, port-restricted cone, and symmetric. That vocabulary caused years of confusion and was eventually retired — RFC 4787 replaced it with language about behaviors, because real NATs didn’t fit the four neat boxes. But one distinction from that old taxonomy survives because it’s the one that actually decides whether your call connects: does the NAT assign the same external port regardless of who you’re talking to, or a different one per destination?
The nice case, in the new terms, is endpoint-independent mapping. You send to STUN, you get port 54321. You send to your peer, you still go out on 54321. So the address STUN reflected back is the address your peer will see. The mirror told the truth.
The nightmare case is what the old spec called symmetric NAT, now address-and-port-dependent mapping. Here the NAT allocates a fresh external port for every distinct destination. You talk to STUN and get 54321. You talk to your peer and go out on 61000 instead. The reflexive address STUN handed you — 54321 — is now a lie: it was only ever valid for talking to the STUN server. Your peer aims at 54321, the NAT has no entry for it, and the punch hits a wall. You did everything right and it doesn’t work, because the address you advertised was true for exactly one conversation and you’re now having a different one.
Symmetric NAT is why NAT traversal can never be “solved.” Two symmetric NATs facing each other, in the general case, cannot be hole-punched at all — there’s no port to predict, because the port that matters doesn’t exist until the flow it belongs to is created, and creating it is the thing you’re trying to do. Carrier-grade NAT, the thing your mobile carrier runs, is frequently symmetric. So is a lot of enterprise gear. When people say a call “fell back to relay,” this is almost always why.
When punching fails, you pay rent
If the endpoints can’t reach each other directly, there’s exactly one honest option left: put a machine in the middle that both can reach — because both can make outbound connections — and have it forward everything. That machine runs TURN, Traversal Using Relays around NAT, RFC 8656.
Each peer opens an outbound connection to the TURN server (outbound always works, that’s the one thing NAT never blocks) and asks it to allocate a relay address. Now the TURN server has a public address that stands in for each peer. A sends media to the relay, the relay forwards it to B; B sends to the relay, the relay forwards to A. The two endpoints never touch. They’re pen pals writing through a post office that reads every letter.
TURN works essentially always, which is why production WebRTC deployments treat it as mandatory infrastructure, not a nice-to-have. It’s also the option nobody wants to use, for a reason that shows up on a bill. A direct hole-punched call costs the operator nothing — the bytes go peer to peer. A relayed call routes every frame of video, both directions, through your server, on your bandwidth. At scale, the fraction of calls that fall back to TURN is a line item, and it’s the line item that makes “just use WebRTC, it’s peer-to-peer” a phrase that makes infrastructure engineers laugh. It’s peer-to-peer until it isn’t, and how often it isn’t depends on NAT types you don’t control and can’t see.
ICE: try everything, trust nothing
The last piece is the one that ties the mess together, because in practice you don’t know in advance which of these paths will work. You might have a direct route on the local network, a hole-punched route over the public internet, or a doomed pair of symmetric NATs that needs a relay — and you can’t tell by inspection. So you don’t try to be clever. You gather every option and race them.
That’s ICE, Interactive Connectivity Establishment, RFC 8445. Each peer collects candidates: its local address (host), its STUN-reflected public address (server-reflexive), and a TURN relay address (relay), maybe several of each. It sends the whole list to the other side. Then both peers systematically fire connectivity checks — STUN packets, this time between each other — across every pairing of “my candidates” × “your candidates,” and watch which pairs actually get a response. Direct paths win if they exist; relays win if nothing else does. ICE picks the best pair that works and promotes it to carry the call, keeping others warm in case the winner dies.
It’s brute force, and it’s the right design, because the network won’t tell you the truth about itself. You cannot ask a NAT what kind it is and believe the answer; you can only send a packet and see if something comes back. ICE is the institutionalization of that humility. Every video call you make quietly runs this pageant — dozens of little probes firing into the dark — in the second between “connecting…” and the other person’s face.
What this is really about
Step back and the whole apparatus is faintly absurd. STUN exists so a machine can learn its own address. Hole punching exists so two machines can trick their own gatekeepers into cooperating. TURN exists to pay for the cases where the trick fails. ICE exists to run all of it as a horse race because none of it is reliable enough to trust on its own. That is a lot of protocol to accomplish the single most basic thing a network is supposed to do: let two computers send each other packets.
We built all of it because NAT quietly deleted the assumption everything else was resting on — that any host can reach any host. That deletion was the real cost of NAT, paid not at the router but here, in the permanent tax of traversal machinery that every real-time app has to carry. None of this is in the WebRTC demo. The demo shows two boxes and an arrow. The arrow is a lie, or at least a hope — behind it is STUN, TURN, ICE, and the small daily miracle that most of the time, against NATs nobody designed to cooperate, the two packets arrive at the same instant and the hole holds.