NAT Traversal: from NAT Types to Complete Mastery of Hole Punching
We take the STUN/TURN/hole-punching story from the home page all the way down: NAT type classification, the packet-by-packet sequence, symmetric-NAT workarounds, and the TCP variant. This is the grittiest, and most important, territory in P2P engineering.
- Register: both send to S (creating NAT mappings toward S)
- S shares each side's public/private endpoints
- Simultaneous send: first packets dropped, but each opens its own hole
- Both holes open → direct connection established (S no longer needed)
The four stages of UDP hole punching. The key: by sending first, you create the passage for the peer in your own NAT.
The four NAT types: what determines traversability
NATs are classically divided into four types by how they create mappings (internal IP:port ⇔ external IP:port) and how strictly they filter inbound packets (RFC 3489; the modern RFC 4787 separates mapping and filtering behavior, but the four-type view remains the most intuitive).
- Full Cone NAT: assigns one fixed external port per internal socket and, once the mapping exists, passes packets from anyone. Easiest to traverse.
- Restricted Cone NAT: fixed mapping, but only passes packets from destination IPs the inside host has previously sent to.
- Port Restricted Cone NAT: stricter still, allowing only a previously contacted IP and port. Up to here, standard hole punching works.
- Symmetric NAT: assigns a different external port per destination. The port observed by the STUN server differs from the one used toward the peer, so textbook hole punching fundamentally fails. Common in corporate networks and mobile carriers (CGNAT, Carrier-Grade NAT).
UDP hole punching: the complete sequence, packet by packet
The essence of hole punching is exploiting one property: a NAT passes, for a limited time, return traffic along the same path as an outbound packet. By each side "sending first," both NATs are pre-loaded with a passage (mapping, the hole) addressed to the other peer. Let's walk through it step by step. The cast: peers A and B behind their NATs, and the rendezvous server S (the go-between, also called a signaling server) that both are initially connected to.
- Registration: A and B each send a UDP packet to S. This creates a mapping toward S in each NAT, and S observes each peer's public endpoint (the IP:port visible from outside the NAT).
- Endpoint exchange: S tells A both B's public and private endpoints, and vice versa. Exchanging the private endpoints too is essential (see below).
- Simultaneous send (the punch): A starts sending UDP packets to B's endpoints while B does the same toward A, nearly simultaneously. The first few packets are dropped by the other side's NAT as "unknown source", but each send creates the mapping (the hole) toward the peer in the sender's own NAT.
- Breakthrough: packets arriving after both holes exist look to each NAT like replies to outbound traffic, and pass. Direct two-way UDP flows from here on; the rendezvous server is no longer needed and can disconnect.
Why also try the private endpoints? Because if A and B sit behind the same NAT (same home or office), traffic between their public endpoints depends on the NAT supporting hairpinning and often fails. The standard practice is to try direct delivery to the private address at the same time and adopt whichever connects first. ICE's (Interactive Connectivity Establishment) testing of all candidate pairs is the generalization of exactly this.
Nor does "simultaneous" mean the same instant. What matters is that each side has sent its own packet before the other side's first packet arrives. Implementations simply retry every few tens of milliseconds for several seconds; even if one side's first shots are discarded, later ones pass through the hole.
Success matrix by NAT type pairing
The outcome of standard UDP hole punching (without port prediction) is essentially determined by the pairing of NAT types at the two ends.
| A \ B | Full Cone | Restricted | Port Restricted | Symmetric |
|---|---|---|---|---|
| Full Cone | ○ | ○ | ○ | ○ |
| Restricted | ○ | ○ | ○ | ○ |
| Port Restricted | ○ | ○ | ○ | × |
| Symmetric | ○ | ○ | × | × |
A symmetric NAT uses a fresh external port toward the peer, so it gets rejected whenever the peer requires a port-exact match (Port Restricted or stricter); that's why the table shows ×. If the peer is Full Cone or Restricted (which ignore the port), a symmetric side still gets through. In short, only "Symmetric × Port Restricted" and "Symmetric × Symmetric" are beyond the standard technique.
Beating symmetric NATs: port prediction and the birthday paradox
- Port prediction: many symmetric NATs allocate ports sequentially (+1 and so on) rather than at random. Query STUN twice, infer the allocation pattern, and you can aim at the port that will be used next.
- Massively parallel attempts (the birthday paradox): even against random allocation you can win probabilistically. If the symmetric side opens holes from a few hundred source sockets while the other side probes a few hundred ports, the collision probability soars by the birthday paradox: with roughly 400 ports tried on each side of a 65,535-port space, success exceeds 90%. Mesh VPNs such as Tailscale use exactly this technique.
- And if all else fails: multi-layer CGNAT or truly random allocation can still win. Then you fall back gracefully to a TURN relay. The design rule of thumb: the whole cascade, fallback included, should settle automatically within a few seconds.
TCP hole punching: the simultaneous-open trick
Hole punching is possible over TCP too. The key is simultaneous open, a transition defined in the TCP state machine since the beginning: if both sides send SYNs at nearly the same time, the connection is established from the two SYNs without a SYN+ACK handshake, a scenario that practically never happens in normal use. By repeatedly connecting to each other from the same local port (using SO_REUSEADDR), both sides can open holes just as with UDP.
Success rates are considerably lower than UDP, though: (1) many NATs track TCP state (the direction of the SYN) and answer an unexpected inbound SYN with RST; (2) the timing window for the two SYNs is tighter than for UDP; (3) OS TCP-stack behavior varies widely. In practice the usual answer is to punch with UDP and run a reliability layer on top (QUIC or SCTP). WebRTC data channels are exactly this construction (SCTP over UDP).
Keep-alives and mapping lifetimes: holes close if left alone
NAT mappings time out. For UDP, many devices expire mappings after 30 seconds to a few minutes of silence (RFC 4787 recommends at least 2 minutes; 30-second boxes are not rare), and for established TCP the recommendation is 2 hours or more, though again it depends on the device. Keeping a P2P connection alive therefore requires periodic keep-alive packets even when idle; small UDP packets every 15-25 seconds are the norm. On mobile devices keep-alive frequency translates directly into battery drain, so sophisticated implementations measure the actual timeout and adapt the interval. A robust P2P app also needs a re-punch procedure for when the hole has closed.
How it is used in the real world
- WebRTC / ICE: hole punching runs billions of times a day in the standardized form of ICE connectivity checks. Within the flow of candidate gathering (host / STUN reflexive / TURN relay) → SDP (Session Description Protocol) exchange → priority-ordered checks of all candidate pairs → best-path selection, the mutual STUN binding requests function as the punch itself.
- Online games: console matchmaking classically has the matchmaking server double as the rendezvous server and connects opponents directly by hole punching. The familiar "NAT Type A/B/C" indicator is a pre-diagnosis of whether this will succeed.
- Mesh VPNs (e.g. Tailscale): each node exchanges keys and endpoints via a coordination server and connects WireGuard tunnels by hole punching. Against symmetric NATs it applies port prediction and birthday-paradox probing, and for the remainder guarantees connectivity through relay servers called DERP (the TURN equivalent), a textbook example of the layered strategy.
ICE: from candidate gathering to an established connection
- Candidate gathering: each peer collects its local address (host candidate), its STUN-discovered external address (server-reflexive candidate), and a TURN relay address (relay candidate).
- Candidate exchange: carried in SDP over the signaling channel (for WebRTC, typically via a WebSocket server).
- Connectivity checks: all candidate pairs are prioritized and probed with mutual STUN binding requests. These mutual checks are what performs hole punching.
- Selection: among the pairs that connected, the highest-priority path (direct > reflexive > relay) is chosen for the session; on network changes, checks rerun (ICE restart).
Real-world pitfalls
On mobile networks the carrier's NAT (CGNAT) stacks on top of the home router, making double NAT common. Firewalled environments that block UDP require fallback to TCP or TURN over TLS (port 443). The fundamental fix is IPv6: when devices have global addresses, the NAT traversal problem itself disappears. But for now, the gritty world of ICE endures. For related foundational research such as ad-hoc routing and churn measurement, see the references.