Chapter 09 · Part III · NAT

NAT traversal: STUN, hole punching & relays

Two laptops, each behind its own router, each invisible to the internet. Neither can accept a call, and yet they manage to talk directly, without either router being configured. This chapter shows the trick, why it sometimes fails, and what happens when it does: every packet takes a detour through a relay.

3478the default UDP and TCP port for STUN, the "what do I look like from outside?" protocol (RFC 5389)
4kinds of ICE candidate: host, server-reflexive, peer-reflexive and relayed (RFC 8445)
41641the UDP port Tailscale listens on for direct WireGuard traffic by default

Common mix-up: "symmetric NAT" and "strict NAT" are not the same thing. A NAT has two separate behaviors: how it maps outgoing traffic to public ports, and how it filters incoming traffic. Hole punching is mostly defeated by the mapping, not the filter. A NAT can filter hard and still be easy to punch through.

Two closed doors

The problem: both sides are behind NAT

Chapter 7 ended on an awkward fact. A NAT drops any packet that arrives at a public port nobody inside has used, because it has no row telling it where to send it. That's fine for browsing, where your laptop always speaks first and the server only ever replies. It's a problem for anything peer-to-peer: a video call, a game, a file transfer between two phones, Remote Desktop to a machine at home, or a mesh VPN like Tailscale.

Picture two laptops, A and B, in two different houses. Each knows only its own private address, something like 192.168.86.20. If A sends to B's private address, the packet never leaves A's house; that address means nothing on the internet. If A somehow learned B's router's public address and sent there, B's router would drop it as an unsolicited stranger. B has the same problem in reverse. Both doors open only from the inside.

There are three ways out. You can open a door by hand, with a port forward, or ask the router to open one with UPnP, NAT-PMP or PCP (Chapter 8). You can have a server both sides can reach pass the packets between them: a relay. Or you can trick both routers into opening a door at the same moment, so that each side's packet arrives looking like a reply. That last one is hole punching, and it works far more often than it has any right to.

Whether it works depends on exactly how each router behaves. So before the trick, the taxonomy.

Mapping and filtering

How a NAT behaves, in two questions

RFC 4787, published in 2007, pinned down NAT behavior for UDP by asking two independent questions. Every NAT answers both.

Question one, mapping: when the same inside address and port sends to a second destination, does it get the same public port as before? There are three possible answers.

  • Endpoint-independent mapping (EIM): yes, always. Laptop port 41641 appears outside as, say, 203.0.113.7:41641 no matter who it talks to.
  • Address-dependent mapping: the same public port for every port on one remote address, but a new one for each new remote address.
  • Address and port-dependent mapping: a new public port for every distinct remote address and port.

Question two, filtering: once a mapping exists, which outside senders may use it to get in?

  • Endpoint-independent filtering: anyone. The mapping is an open door while it lives.
  • Address-dependent filtering: only addresses the inside device has already sent to, from any port.
  • Address and port-dependent filtering: only the exact address and port the inside device has sent to.

RFC 4787 makes endpoint-independent mapping a requirement: a NAT "MUST" behave that way. It is far less firm about filtering. It recommends endpoint-independent filtering when transparency for applications matters most and address and port-dependent filtering when strict filtering matters most, and leaves the choice to the vendor. Many routers predate the RFC, and plenty of current ones ignore it, so all nine combinations still turn up.

The old names

Before RFC 4787, the first STUN specification (RFC 3489, 2003) sorted NATs into four "types" with names you'll still see in game consoles and forum threads. They are combinations of the two answers above.

Old name (RFC 3489)MappingFilteringTailscale's word
Full coneEndpoint-independentEndpoint-independenteasy
Restricted coneEndpoint-independentAddress-dependenteasy
Port-restricted coneEndpoint-independentAddress and port-dependenteasy
SymmetricAddress- or address and port-dependentUsually address and port-dependenthard

The "cone" picture is that one inside port fans out, like a cone, to every destination through a single public port. "Symmetric" means each destination gets its own mapping. Tailscale's engineers, in their long article on the subject, collapse it further: a NAT with endpoint-independent mapping is easy, one with endpoint-dependent mapping is hard, and the filter barely matters. The next sections show why that collapse is fair.

Go deeper: why the four-type labels fell out of favor

RFC 3489 tried to let a device discover its NAT's type with a fixed sequence of STUN tests, then pick a strategy. In practice the labels were too coarse. Real NATs mix behaviors: endpoint-independent for a while and then not, once a port is already taken; one behavior for some ports and another for others; different behavior for TCP and UDP. Two layers of NAT stacked together behave as whichever layer is harder, and a test can't always tell which layer it saw.

When STUN was rewritten as RFC 5389 in 2008, the type-detection algorithm was removed. STUN became a tool that other protocols use, not a classifier. The discovery tests survived separately, as the experimental RFC 5780, "NAT Behavior Discovery Using STUN," which is careful to say its results are a hint, not a guarantee. The approach that won is to stop predicting and just try every path, which is ICE (below).

A mirror on the internet

STUN: learning your public address

The first obstacle is that a device behind NAT doesn't know its own public address and port. Its operating system only sees the private one. The router knows, but it isn't telling.

STUN, Session Traversal Utilities for NAT, solves this with a server on the public internet that acts as a mirror. The device sends a small UDP packet called a Binding request to the STUN server, usually on port 3478. The packet crosses the NAT, which creates a mapping and rewrites the source. The server looks at the source address and port the packet arrived with, and sends them back in the reply, in an attribute called XOR-MAPPED-ADDRESS. Now the laptop knows: "from out there, I look like 203.0.113.7:41641."

That address is called the server-reflexive address, because a server reflected it back. It is only useful if it's the same address other people will see. With endpoint-independent mapping, it is: the router reuses the mapping for every destination, so a peer who sends to 203.0.113.7:41641 is aiming at the right door. With endpoint-dependent mapping, the address is true only for the STUN server. The moment the laptop sends to anyone else, the router opens a different port, and the address the laptop advertised is wrong.

A device can find out which case it is in by asking two STUN servers at different addresses and comparing the answers. If the port changes, the mapping depends on the destination. That's exactly what Tailscale's tailscale netcheck reports as MappingVariesByDestIP.

X-Port = 41641 ⊕ 0x2112 · X-Address = 203.0.113.7 ⊕ 0x2112A442
XOR-MAPPED-ADDRESS: the port is XORed with the top 16 bits of STUN's fixed "magic cookie," and an IPv4 address with the whole cookie. The receiver XORs again to get the real values.
Go deeper: why the address is XORed

Every STUN message has a 20-byte header: a message type, a length, the fixed 32-bit magic cookie 0x2112A442 and a 96-bit transaction ID that pairs a response with its request. The original RFC 3489 returned the address in plain form, as MAPPED-ADDRESS.

That turned out to be a problem because of the ALGs from Chapter 7. Some NATs scanned every packet's payload for anything that looked like their own public address and "helpfully" rewrote it to the private one, so the STUN client got its private address back and learned nothing. XORing the address with a constant makes it unrecognizable to such a scanner while costing nothing to undo. RFC 5389 made XOR-MAPPED-ADDRESS the standard. The cookie also gives a quick way to tell STUN packets apart from other protocols sharing the same port. In 2020 RFC 8489 replaced RFC 5389 with a revision that keeps the same wire format.

The trick

Hole punching and simultaneous open

Here is the whole idea. A and B each ask a STUN server for their public address. They tell each other those addresses through some third party they can both already reach: a signaling server, a game's lobby, Tailscale's coordination and relay servers. Then, at roughly the same moment, each sends a UDP packet straight at the other's public address and port.

Follow it with two port-restricted cone routers, the most common home setup. A's packet leaves first. Crossing A's router, it creates a mapping and, just as importantly, a filter entry: "replies from B's public address and port are welcome." It reaches B's router, which checks its own filter. B hasn't sent anything to A yet, so the filter says no, and the packet is dropped. That first packet looks wasted. It isn't. It was never meant to arrive; its job was to open A's door from the inside.

Now B's packet leaves. Crossing B's router, it opens B's door toward A. It reaches A's router, which checks its filter and finds the entry A's first packet just made: a packet from B's public address and port is expected. It's let in. A answers, and since B's door is now open too, the answer gets through. Both sides are talking directly, router to router, and neither router was configured. Each believes it is carrying an ordinary outbound conversation and its replies.

The name for both sides sending at once is simultaneous open. Timing doesn't need to be precise: each side keeps retrying every so often, and the first packet to arrive after the other side has sent wins. The hole stays open as long as traffic keeps the mapping alive, which is why traversal-aware software sends keepalives (Chapter 7 covers the timers).

Why two symmetric NATs defeat it

The trick quietly relies on one thing: the public address each side learned from STUN is the same one the other side will see. Endpoint-independent mapping guarantees that. Take it away from one side and watch what happens.

Say A is behind a symmetric NAT and B behind an endpoint-independent one. A learned 203.0.113.7:41641 from STUN, but when A sends to B, its router opens a new port, say 52117. B's router sees a packet from :52117. If B's router filters by address only, that's fine: B had already sent to A's address, any port, so it lets it in, B learns the real port from the packet, and replies to :52117. Connection made. But if B filters by address and port, it was expecting :41641, so :52117 is dropped. Meanwhile B is faithfully sending to :41641, a mapping that A's router opened only for the STUN server, so A's router drops that too. Neither side ever learns the right port.

Put symmetric NATs on both sides and it's hopeless by plain means. Each side's packets come from a port the other can't predict, aimed at a port that was opened for someone else. In the old vocabulary, the result is this table.

A \ BFull coneRestrictedPort-restrictedSymmetric
Full conedirectdirectdirectdirect
Restricteddirectdirectdirectdirect
Port-restricteddirectdirectdirectrelay
Symmetricdirectdirectrelayrelay

Hole punching outcome with plain STUN and simultaneous sends, no port prediction. The simulator below derives every cell of this table from the mapping and filtering rules, so you can check it.

Go deeper: port prediction and the birthday paradox

A symmetric NAT's new ports aren't always random. Some routers hand them out in sequence: if STUN saw :41641, the next mapping is likely :41642. A side that knows this can guess. That's port prediction, and against sequential allocators it often works. Randomized allocation, which RFC 6056 recommends for security, kills it.

Against a random allocator there's still a statistical trick. If the hard side opens many ports at once, by sending to many guessed ports on the easy side, and the easy side probes many random ports on the hard side, some probe will probably land on an open one. It's the birthday paradox again. Tailscale's article works the numbers: with 256 ports open on the hard side, 174 random probes give an even chance, 1,024 give 98% and 2,048 give 99.9%. With hard NATs on both sides the odds collapse to almost nothing, which is why a relay is unavoidable there.

TCP can be hole-punched too, using TCP's own simultaneous-open, where both sides send a SYN at once. It's fussier: the timing matters more, and many NATs and firewalls handle the crossed SYNs badly. That's one reason peer-to-peer systems overwhelmingly prefer UDP. RFC 5128 surveys both techniques, building on the 2005 paper by Ford, Srisuresh and Kegel that made UDP hole punching widely understood.

Instrument 1

A hole-punching simulator

Pick each router's mapping and filtering behavior, then press Run. Both laptops ask two STUN servers for their public address, swap candidates through the coordination server, then fire packets at each other in rounds, learning any new source port they see (an ICE "peer-reflexive" candidate). Every NAT keeps a real mapping table and applies its filter to every arriving packet. If no packet gets through in both directions after four rounds, the laptops fall back to the relay. Non-endpoint-independent mappings pick random ports, so run it twice and the numbers change.

Router A

Router B

Router A, old name–
Router B, old name–
A as STUN sees it–
B as STUN sees it–
Outcome–
  1. Press Run.

Addresses: A is 192.168.86.20 behind 203.0.113.7; B is 10.0.0.8 behind 198.51.100.44; STUN servers at 192.0.2.10 and 192.0.2.20, port 3478. Both laptops use UDP port 41641. Each router keeps the laptop's own port for the first mapping if it can.

When punching fails

TURN: a relay that both sides can reach

When no direct path can be made, the fallback is the oldest idea in networking: a middleman. Both A and B can make outbound connections to a server on the public internet, and the server can pass packets between them. NAT is no obstacle, because both sides spoke first.

TURN, Traversal Using Relays around NAT, is the standard version, now defined in RFC 8656 (2020). A client asks a TURN server for an allocation: a public address and port on the server, the relayed transport address, that will stand in for the client. The client then installs permissions naming the peer addresses allowed to send to it, a deliberate echo of address-dependent filtering. From then on, anything the peer sends to the relayed address is forwarded to the client, and anything the client sends through the server comes out of the relayed address toward the peer.

A relay always works, which makes it the safety net under every peer-to-peer system. It also costs real things. Every byte crosses the internet twice, out to the relay and back, so latency is the sum of two trips instead of one direct one; a relay in another city can add tens of milliseconds even when the two devices sit on the same desk. Someone has to pay for the relay's bandwidth, so public relays are usually shared and often rate-limited. And the relay sees the traffic go by, so the payload had better be encrypted end to end, which every serious system does.

Go deeper: allocations, permissions and channels

TURN is built as an extension of STUN, using the same message format. Allocations have a lifetime, 10 minutes by default, which the client must refresh with Refresh requests. Permissions last 5 minutes and are refreshed by sending to the peer or by renewing them explicitly. Permissions are per IP address, not per port, so a peer behind a NAT can change ports without breaking anything.

Data travels in one of two ways. A Send or Data indication wraps each packet in a full STUN message with the peer's address attached, which costs at least 36 bytes per packet. For busy flows, a client can bind a channel: a 16-bit number standing for one peer. ChannelData messages then carry just a 4-byte header, which matters for the many small packets of voice and video. A client reaches its TURN server over UDP, TCP or TLS; the TCP and TLS options exist precisely for networks that block UDP, where a relay on port 443 may be the only thing that gets through.

Try everything

ICE: gather, pair, check, choose

STUN tells you an address. TURN gives you a relay. Something has to decide which path to use, and the honest answer is that nobody can know in advance. ICE, Interactive Connectivity Establishment (RFC 8445, 2018), stops guessing and tests.

Gather. Each side collects every address it might be reachable at. These are its candidates. A host candidate is an address on one of its own interfaces, like 192.168.86.20:41641. A server-reflexive candidate is what STUN reported. A relayed candidate is a TURN allocation. A fourth kind, peer-reflexive, is discovered during checks: an address a peer's packet turned out to come from that nobody had listed, which is exactly the "new port" a symmetric NAT invents.

Exchange. The two sides swap candidate lists over a signaling channel, which ICE deliberately leaves unspecified. WebRTC applications use whatever their app already has; Tailscale uses its own servers.

Pair and check. Each local candidate is paired with each remote one and the pairs are sorted by priority. Then each side sends a STUN Binding request down each pair, paced a few tens of milliseconds apart, and answers the other side's requests. A check succeeds when a request gets a response. These checks are the hole punching: both sides fire at each other's candidates at about the same time.

Choose. One side, the controlling agent, nominates the best pair that worked, and media flows there. Host pairs are preferred over server-reflexive, and both over relayed, so a LAN path wins if one exists and the relay is used only when nothing else answered.

priority = 2²⁴ × type + 2⁸ × local + (256 − component)
RFC 8445's candidate priority. Recommended type preferences: host 126, peer-reflexive 110, server-reflexive 100, relayed 0. So any host candidate outranks every relayed one, whatever the other terms say.
Go deeper: pair priority and why both sides agree

Both agents need to sort pairs the same way, or they'd test them in different orders and waste the simultaneous-open timing. RFC 8445 defines the pair priority from the controlling side's candidate priority G and the controlled side's D as 2³² × min(G, D) + 2 × max(G, D) + (1 if G > D, else 0). Because it uses the minimum first, a pair is only as good as its worse half, and both sides compute identical numbers.

Connectivity checks carry a shared secret exchanged during signaling (the ICE username fragment and password), so a stray STUN packet from some other session can't be mistaken for success. Checks also double as consent: RFC 7675 has ICE agents keep sending checks during the session, so traffic stops if the far side stops agreeing to receive it.

A real system

How Tailscale does it: direct or DERP

Tailscale builds a mesh VPN out of WireGuard tunnels between every pair of devices, and it faces the problem of this chapter thousands of times a day: two machines, each behind some NAT, that need a direct UDP path. Its approach is ICE-like in spirit but its own in detail, and its engineers wrote it up at length in "How NAT traversal works" (David Anderson, 2020), the most readable account of the subject anywhere.

The relay is called DERP, Designated Encrypted Relay for Packets. Tailscale runs DERP servers in cities around the world. Each device picks the one with the lowest latency as its home, and data to DERP travels over HTTPS on TCP port 443, which gets through almost any network that allows web browsing. DERP forwards packets by the destination's public key. It can't read them: they are already WireGuard-encrypted, and the keys never leave the devices.

The order of events is the reverse of what you might expect. According to Tailscale, every connection starts on DERP. Packets flow through the relay from the first moment, so a connection works immediately. Meanwhile the two devices use DERP as a side channel to swap candidate addresses and run their own discovery pings over UDP, hole punching in the background. When a direct path answers, traffic moves onto it; if nothing ever answers, it stays on DERP. That's why a session can begin slow and then get fast a few seconds later.

Seeing which path you got

Two commands tell you what is going on. tailscale netcheck reports on your own network: whether UDP gets out at all, your public address as STUN sees it, whether the mapping varies by destination (a hard NAT), which port-mapping protocols your router offers, and the latency to each DERP region. tailscale ping pings a peer over Tailscale itself and says which path each reply took.

$ tailscale ping desk-pc
pong from desk-pc (100.101.102.103) via DERP(nyc) in 23ms
pong from desk-pc (100.101.102.103) via DERP(nyc) in 19ms
pong from desk-pc (100.101.102.103) via 192.168.86.31:41641 in 2ms

The first two replies went through the New York relay. The third went straight across the LAN, to the peer's host candidate on port 41641. That switch is the upgrade happening in front of you. If the output keeps saying via DERP(nyc) and ends with "direct connection not established," discovery failed and every packet is being relayed. tailscale status shows the same thing in one word per peer: direct followed by an address and port, or relay followed by a region.

Go deeper: the case that prompted this site

J's laptop showed exactly the failure this chapter describes. Remote Desktop to a machine on the same desk was sluggish, and tailscale ping said via DERP. Two machines a meter apart were sending every screen update to a relay in another building and back.

The laptop sat behind two NATs, a LAN on 192.168.86.x behind a router that was itself on 192.168.1.x (Chapter 8), and it had a dozen network adapters: a Hyper-V virtual switch, a network bridge and leftover VPN adapters. Every adapter is a potential host candidate, and every candidate means more pairs to test. A double NAT behaves as its harder layer, so the server-reflexive route is fragile; and with stale or bridged adapters, the LAN candidate that should win outright can be missing or unreachable. With no pair answering, the connection never left DERP.

The fixes follow from the mechanism: remove the adapters that shouldn't exist so the real LAN address is a clean candidate, make sure UDP 41641 isn't blocked between the two machines, and fix the double NAT so the outer router isn't fighting the inner one. Then tailscale ping should show the switch to a 192.168.86.x address. Chapter 13 walks through the diagnosis step by step.

Instrument 2

What a relay costs

Remote Desktop sends you a stream of screen updates: each time something on the far screen changes, the changed region is encoded and shipped. How fast that feels depends on two things, round-trip time and throughput, and a relay can hurt both. Set the direct path and the relay detour, and see how long a click takes to come back as pixels, and how many updates per second fit.

Direct: click → screen–
Relay: click → screen–
Direct: updates / s–
Relay: updates / s–
Relay penalty–

Side by side

The tools in this chapter

ToolStandardWhat it doesCost
STUNRFC 5389 (now 8489)Tells a device its public address and portOne tiny round trip
Hole punchingRFC 5128 (survey)Opens both NATs from the inside at onceFree, when it works
TURNRFC 8656Relays packets through a public serverExtra latency and someone's bandwidth
ICERFC 8445Gathers all candidates, tests every pair, picks the bestA second or two of checks
DERPTailscaleRelay over HTTPS, plus the side channel for discoverySame as TURN, works through nearly any firewall
NAT-PMP / PCPRFC 6886 / 6887Asks the router to open a port outrightOnly if the router allows it (Chapter 8)
Cheat sheet

Terms from this chapter

Mapping behavior
Whether a NAT reuses one public port for every destination (endpoint-independent) or opens a new one per destination (endpoint-dependent).
Filtering behavior
Which outside senders may use an open mapping: anyone, addresses already contacted, or exact addresses and ports already contacted.
Easy / hard NAT
Tailscale's shorthand: endpoint-independent mapping is easy, endpoint-dependent is hard.
Cone / symmetric
The older RFC 3489 names. Cone NATs keep one mapping for all destinations; symmetric NATs don't.
STUN
A protocol for asking a public server what your address and port look like from outside.
Server-reflexive address
Your public address and port as reported by a STUN server.
Hole punching
Both sides sending to each other's public address at once, so each NAT sees the other's packets as replies.
TURN
A standard relay: a public server that forwards packets between two clients that can't reach each other.
ICE
The procedure that gathers every candidate address, tests every pair and picks the best one that works.
Candidate
An address a device might be reached at: host, server-reflexive, peer-reflexive or relayed.
DERP
Tailscale's relay, Designated Encrypted Relay for Packets, reached over HTTPS; also its side channel for path discovery.
Where this comes from

Sources