October 14, 2026
In the post on UDP hole punching I wrote that WireGuard has no way to discover a peer’s public endpoint, and that tools like Tailscale add a coordination layer on top of it. This post goes through what that layer does, based on Tailscale’s own write-up of their NAT traversal and their documentation on DERP servers. I have not measured any of this myself, so the numbers below are the ones they publish.
Coordination, not data
The first thing to notice is that Tailscale’s servers are not in the data path in the normal case. The coordination server distributes public keys and candidate endpoints between the nodes of a tailnet, and the actual traffic is plain WireGuard between peers. The side channel used to exchange endpoints can tolerate a few seconds of latency and carries only a few thousand bytes per node, which is why a single small machine can serve a very large number of nodes.
Finding the candidates
Each node builds a list of addresses where it might be reachable:
- IPv6 addresses, when available
- LAN IPv4 addresses, for peers on the same network
- the public
ip:portseen by a STUN server - ports obtained from the router through UPnP IGD, NAT-PMP or PCP
- endpoints set manually by an administrator
The STUN request has to be sent from the same UDP socket that WireGuard uses. A mapping created by a different socket would be a different mapping, and the address the STUN server reports would be useless for the actual traffic.
Port mapping protocols ask the router directly for a forward, which makes the NAT effectively transparent for that port. They stop helping when there are two NATs in a row, because the request only reaches the nearest one.
Probing
Once two peers have exchanged their candidate lists, each one sends probes to all of the other’s candidates. These probes are the simultaneous packets that open the mapping on each side, exactly as in the basic hole punching described in the previous post. They are also ping and pong messages, so the same exchange measures whether a path works and how fast it is. The best working path wins, and the preference is based on measured round-trip time. In practice that usually means LAN before WAN, and WAN before WAN through a NAT.
Stateful firewalls forget a mapping after roughly 30 seconds without traffic, so the connection needs periodic keepalives to stay open.
Hard NATs and the birthday paradox
The case that breaks simple hole punching is the NAT that picks a new external port for every destination (what the article calls endpoint-dependent mapping). The port that STUN reported is valid only for the STUN server, so the peer cannot know which port to aim at.
When only one side is behind such a NAT, the other side can still succeed by guessing. The “easy” side opens many sockets, so that the hard side’s NAT has many mappings that could be hit, and the hard side then sends probes to random ports. This is the birthday paradox: collisions become likely long before the numbers get large. The published figures are:
- one hard NAT: 1024 probes give about 98% success, and at 100 packets per second half of the attempts succeed in under 2 seconds
- two hard NATs: 99.9% success needs around 170,000 probes per side, which is about 28 minutes at 100 packets per second, with a median of about 9 minutes
The second case is slow enough that it is not a practical answer on its own, which is where the relay comes in.
DERP, the fallback that starts first
Tailscale’s relay is called DERP (Designated Encrypted Relay for Packets). It is an HTTP-based relay that forwards packets to a destination identified by its public key. The traffic it carries is already encrypted by WireGuard, with keys that never leave the devices, so the server only sees ciphertext.
The ordering is the interesting design choice. A connection does not wait for the punch to succeed or fail. It starts on DERP so that packets flow immediately, while path discovery runs in parallel, and when a better path is found the traffic moves to it without the application noticing. DERP servers also relay the discovery messages (disco) that the peers use to negotiate the direct path. Each client measures latency to the available regions, picks a home DERP server, and reports it to the coordination server so that other nodes know where to reach it.
The documentation also mentions peer relays: when no direct path works, Tailscale tries relaying through another device in the tailnet that has been configured for it, and falls back to DERP only if none is available. tailscale netcheck shows which DERP regions a machine can reach and the latency to each, and tailscale status shows for each peer whether the current path is direct or relayed.
IPv6 and security
IPv6 removes the need for STUN, port mapping, birthday probing and hairpinning, but stateful firewalls still apply, so the probing logic stays. At the time of the article about a third of traffic was IPv6, unevenly distributed, so it adds a path and does not replace the others.
Since paths change at runtime, the source IP of a packet carries no trust. Authentication rests on the WireGuard keys, so a spoofed probe can at worst cause a bad path to be tried.
What this means for my setup
The previous post ended with the plan to start with plain WireGuard and see whether the laptop side needed anything more. After reading this, the part I would be missing is clear: endpoint exchange, the relay fallback, and the keepalive handling are the work that Tailscale does, and plain WireGuard leaves it to me. If the home router behaves as a well-behaved NAT, a static endpoint and a keepalive may still be enough. If the laptop ends up behind a hard NAT on someone else’s network, a relay path is the thing I would have to build myself.
Sources: Tailscale, “How NAT traversal works”, and the DERP servers page in the Tailscale documentation.