Chute Devs

Punching Through NAT — disco, STUN, and the Direct Path

Hello everyone.

In the last post we introduced native Tailscale support in the Chute engine. Today I want to zoom in on the part of Tailscale that is easy to under-appreciate and hard to get right: how two devices behind different home routers actually manage to talk to each other directly, instead of through a relay.

This is the work of two small but consequential protocols — STUN and disco — and a stubborn piece of folklore: that a direct connection is always the best connection. As we found out the hard way, that folklore is wrong on the networks that matter most.

The Problem: Everyone Is Behind NAT

When your laptop at home talks to your server in a datacenter, both sit behind NAT. Neither knows its own public address — the home router rewrites source ports on the way out, and incoming connections are blocked. Two such devices cannot simply “connect” to each other, because neither address is reachable from the outside.

Tailscale has a fallback for exactly this: DERP, a set of relay servers run by Tailscale (or yourself). When two peers cannot talk directly, their encrypted packets bounce through a DERP server, which forwards them by node identity without ever seeing the plaintext. It works everywhere. It is also a relay — every byte crosses an extra hop, adding latency and capping throughput to whatever that relay can carry.

The goal of NAT traversal is to upgrade a DERP-relayed path into a direct UDP connection between the two peers, so traffic skips the relay entirely.

Step One: STUN Discovers Your Public Face

The first thing a peer needs is to learn what its address looks like from the outside. STUN — defined in RFC 5389 — is a tiny protocol for exactly this. The peer sends a Binding Request to a STUN server (Tailscale’s DERP servers also speak STUN), and the server replies with the source address it saw on the packet — the XOR-MAPPED-ADDRESS. That is the peer’s public endpoint: an IP and port that, with some luck, other devices can reach it on.

A device typically gathers a few candidate endpoints this way — its local address, the address its NAT exposes to STUN, and so on. These become the list it will hand to peers later. Chute’s engine runs this discovery on the same UDP socket it later uses for data, so the address it advertises is the address data will actually flow over.

Step Two: disco Negotiates the Hole

Knowing your own public address is not enough — the other peer’s NAT will still drop your packets unless you do something about it. This is where disco comes in. Disco is Tailscale’s NAT-traversal signaling protocol, and it runs over the same UDP socket as WireGuard data, distinguished by a magic prefix so the receiver can tell signaling apart from real traffic.

The handshake is a classic NAT-traversal dance, built from three message types:

  • Ping. Peer A sends a disco Ping to one of peer B’s candidate endpoints. It carries a transaction ID so A can match the reply.
  • Pong. Peer B replies with a Pong, echoing the transaction ID and — critically — including the source address it saw A’s Ping arrive from. That tells A its own effective public address as observed by B, which may differ from what STUN reported.
  • CallMeMaybe. Sent through the DERP relay (the one path guaranteed to work), this message hands peer B a list of endpoints A would like B to try calling back. It is the polite version of “I have punched my NAT for these addresses; please try them.”

The sequence is designed around a property of most NATs called endpoint-dependent mapping: once a device has sent an outbound packet to peer X, the NAT will generally allow inbound replies from X to that same mapping. By having each side Ping the other, both NATs open the pinhole in the right direction, and direct UDP traffic can then flow through the gap.

All of this is encrypted — disco packets are sealed with a NaCl box keyed to each peer’s disco key, so an observer on the wire cannot forge or tamper with the negotiation.

The Upgrade

When the dance succeeds, the engine stops relaying that peer’s traffic through DERP and sends encrypted WireGuard packets directly peer-to-peer. Latency drops by the round-trip to the relay, and throughput is no longer bounded by the relay’s capacity. The relay stays open in the background as a fallback — if the direct path degrades or disappears, traffic falls back to DERP without a reconnect.

Choosing a relay is its own small story: the engine measures latency to each DERP region and picks a “home” DERP, the one it prefers and reports to the control server. Failover between regions is automatic if the home relay becomes unreachable.

The Counterintuitive Part: When Direct Is Worse

Here is where the folklore breaks down. We tested a cross-border link — home broadband in one country to an exit node in another — and measured three paths for a sustained download:

Path Throughput
Raw connection, no tunnel (physical limit of the line) 383 KB/s
Tunnel via DERP relay (TCP 443) 316 KB/s — 82% of raw
Tunnel via disco direct (UDP WireGuard) ~3 KB/s

The direct path “succeeded” — disco negotiated it, the connection upgraded — and then throughput collapsed to near-zero. What happened? The carrier was applying QoS to sustained UDP flows, throttling them by roughly two orders of magnitude. Disco’s tiny probe and pong packets slipped through the throttle unnoticed, so the engine concluded the direct path was good — and then real traffic got crushed.

This is not a bug in our engine. The official Tailscale client selects paths by disco round-trip time, and it does not sense loss or throughput either, so it falls into the exact same trap on these links. The behavior is baked into how Tailscale defines “a good path”: fast to negotiate, blind to capacity. And cross-border links from home broadband and cellular in many regions hit this routinely.

force-derp: A Performance Switch, Not a Downgrade

Because of this, we surface a knob that Tailscale users rarely think about as a performance control: force-derp. On a link where sustained UDP gets throttled, pinning a peer’s traffic to the DERP relay is the faster choice — TCP 443 to the relay evades the UDP throttle and lands at 82% of the raw line speed, versus single-digit kilobytes on the “direct” path.

So the advice for cross-border exit nodes and peers is the opposite of what intuition suggests: do not chase the direct path. Set force-derp = true and let the relay carry the load. On a healthy LAN or same-country link, leave it off and enjoy the direct path.

This is the kind of decision that needs real-world data to get right, and it points to a longer-term improvement we are working toward: loss-aware path selection. Instead of trusting disco RTT alone, the engine would probe a freshly upgraded direct path with a small burst of sequenced pings, measure the delivery rate, and compare it to the DERP baseline. If direct is losing more packets than the relay can carry, it would automatically pin back to DERP and remember the verdict for that network — resetting only on a network change. The official client has no such logic today, which makes this a place where a proxy engine that already understands traffic statistics can actually beat the reference implementation on the networks its users care about.

Why It Matters

NAT traversal is the part of mesh VPNing that looks like magic until you measure it. STUN and disco are elegant — a few hundred bytes of negotiation that turn two unreachable devices into a direct tunnel — and when the underlying network is honest, they deliver everything you want: lower latency, higher throughput, no relay in the middle.

But networks are not always honest. A path that negotiates successfully is not the same as a path that performs well, and the difference only shows up when you watch real traffic. We built the direct-path upgrade, we built the relay fallback, and — just as importantly — we built the knob that lets you override the engine’s optimism when your carrier is lying about UDP.

Tailscale support, including the direct-path upgrade and force-derp, is available now on iOS, macOS, and tvOS. For configuration details, see the Chute Manual.

Thanks.

Chute Devs