
Picture this: you're at a coffee shop and want to connect back to your office dev machine to fix one line of code. You open a terminal and type ssh dev-machine.corp.com.
Of course it won't connect. That dev machine lives inside your company's private network, holding only a 192.168.x.x address. Your laptop sits behind the coffee shop's LAN, also holding a private address. Both devices are stuck behind their own NATs, and neither can see the other.
Almost reflexively, you start brainstorming: assign the dev machine a public IP? Ask ops to set up port forwarding on the router? Stand up a jump host? Dust off that ZeroTier account you haven't touched in months?
All of these work, but every one of them requires extra effort on your part — configuring routes, editing firewall rules, requesting IPs, maintaining a relay server. What you actually want is dead simple: to communicate as if both devices were on the same LAN, without any network configuration, without a public IP, and ideally with the secure connection handled automatically too.
That's exactly what Tailscale does. This article dissects the core technical components behind it, step by step: NAT traversal, WireGuard encryption, and control-plane coordination. Of these, NAT traversal is the hardest and most fascinating part of the whole problem, and it's where we'll spend the most ink.
Rather than diving straight into Tailscale's implementation, let's begin with the problem itself: how do you get two devices behind NATs to talk to each other? If you've ever deployed a service that needed to punch through a private network, you've almost certainly run into this.
Your laptop sits behind the coffee shop's WiFi:
Laptop (private 10.0.1.5)
↓
Coffee shop router (NAT)
↓
Public IP: 203.0.113.42Your company's dev machine:
Dev machine (private 192.168.1.100)
↓
Company router (NAT)
↓
Public IP: 198.51.100.77Your goal: send a packet from the laptop to the dev machine.
Where's the catch? When your laptop sends a packet destined for 198.51.100.77, it can reach the company router. But once the router receives it, it asks: who is this packet for? What port? There's no rule on the company router saying "forward packets on this port to some machine inside the network."
This is NAT's behavior in a nutshell: it lets internal devices initiate outbound connections, but rejects any unsolicited inbound connection.
The traditional "solutions" are all familiar:
192.168.1.100. You need admin access to the router. Every new device or service means manually adding another rule.These three share one thing in common: they all require a "middleman" with a public IP. Either the middleman is the router (in port forwarding) or a jump host.
Tailscale's goal is to eliminate the middleman — to let two devices talk directly. To do that, it needs to solve two problems:
We covered the basics of public and private addresses in the IP addresses article. Here we'll revisit NAT from the angle of its behavior — not to pass a networking exam, but to understand what "traversing NAT" is actually traversing.
When your laptop accesses google.com from inside the network, the data flows like this:
Laptop (10.0.1.5:54321)
↓ Send TCP SYN, srcIP=10.0.1.5, srcPort=54321, dst=google.com:443
Router (NAT)
├── Record mapping: internal (10.0.1.5:54321) ↔ public (203.0.113.42:12345)
├── Rewrite source IP and port: 10.0.1.5:54321 → 203.0.113.42:12345
└── Forward to GoogleThe NAT router performs three key operations in this flow:
1. Creating a mapping: it records "this internal IP + port" and which port of its public IP it corresponds to. This mapping is created dynamically — until you send that first packet, no such record exists on the NAT.
2. Address translation: it rewrites the packet's source IP to the public IP and the source port to the mapped port. Google sees the connection originating from 203.0.113.42:12345, with no idea that 10.0.1.5 exists.
3. Reverse translation + forwarding: Google's reply is destined for 203.0.113.42:12345. The NAT looks up its mapping table, finds that 12345 corresponds to the internal 10.0.1.5:54321, rewrites the destination to 10.0.1.5:54321, and forwards it back.
This mapping table is NAT's "state." Every outbound connection has a mapping record. When you open 30 tabs in Chrome, the NAT table holds at least dozens of mappings.
The key point: this mapping table is created passively. Only after an internal device sends an outbound packet does the NAT create a mapping. If Google suddenly sends a packet to 203.0.113.42:12345 and there's no record for 12345 in the table (because the mapping has expired), the NAT simply drops it.
This is why internal devices are "invisible" to the outside world — outsiders can't see you. Only after you "knock on the door going out" does the NAT leave a temporary "return address."
Think of NAT as the security desk in an office building:
At this point you might think: so if I get two devices to "step out looking for each other at the same time," the guards on both sides will let them through?
Exactly — that's the core idea behind hole punching. But before we get to hole punching, we need to understand just how strict the NAT "guard" is. Different types of NAT run different levels of security.
NAT behavior isn't uniform. Different implementations follow different mapping and filtering rules. Understanding NAT types is a prerequisite for understanding "why some scenarios can hole-punch while others must relay."
NAT behavior can be classified along two dimensions: Mapping (how mappings are assigned) and Filtering (which inbound packets are allowed through).
When the internal device 10.0.1.5:54321 sends its first UDP packet out, the NAT assigns it the public port 12345. The question is: the next time 10.0.1.5:54321 sends a packet to a different destination, does the NAT assign the same 12345 or a fresh port?
Two behaviors:
Endpoint-Independent Mapping (EIM):
internal (10.0.1.5:54321) → regardless of the destination, NAT uses the same public port 12345
Endpoint-Dependent Mapping (EDM):
internal (10.0.1.5:54321) → to A → NAT assigns 12345
internal (10.0.1.5:54321) → to B → NAT assigns 12346 (different!)EIM is friendly to hole punching: your public identity is fixed. Once someone knows your public IP:Port, they can reach you (assuming filtering also allows it).
EDM is unfriendly to hole punching: your public identity varies by destination. The port the peer you want to punch to sees, and the port the STUN server sees, may differ. That means the address STUN returns may be invalid for the peer.
Filtering rules decide whether the NAT lets an inbound packet through. The criterion is typically "does this packet's source address match the remote address of an existing mapping."
Three levels:
1. Endpoint-Independent Filtering (EIF, doesn't check the remote):
Once the internal device has sent a packet anywhere → anyone can send to this public port and the NAT lets it through.
The most permissive. Once a mapping exists, anyone can "come in."
2. Address-Dependent Filtering:
Internal device sent a packet to IP-A → only inbound packets from IP-A are allowed.
A packet from IP-B (even with the correct destination port) is dropped.
3. Address and Port-Dependent Filtering:
Internal device sent a packet to IP-A:Port-P → only inbound packets from IP-A:Port-P are allowed.
The strictest. Even a packet from a different port of the same IP is rejected.Combining the two dimensions yields four NAT types:
| Traditional name | Mapping | Filtering | Impact on hole punching |
|---|---|---|---|
| Full Cone NAT | EIM | EIF | Easiest to punch — as long as a mapping exists, anyone can send in |
| Restricted Cone NAT | EIM | Address-Dependent | You must first send a packet to the peer's IP before they can reply |
| Port Restricted Cone NAT | EIM | Address and Port-Dependent | You must first send a packet to the peer's IP:Port |
| Symmetric NAT | EDM | Address and Port-Dependent | Hardest to punch — the public port the peer sees isn't the one STUN returned |
In practice, though, we care more about a more useful split: Easy NAT vs. Hard NAT.
Most home routers run NAT at the Easy NAT level. Corporate networks and mobile hotspots are more likely to use Hard NAT. Tailscale adopts different strategies depending on the situation.
In one sentence: the difficulty of hole punching depends on the NAT's mapping and filtering policies. Mapping affects "whether your public identity is stable," and filtering affects "whose messages you can receive." EIM + loose filtering = easy to punch. EDM + strict filtering = hard to punch.
Before attempting to hole-punch, each device needs to answer one question: "Who am I on the public internet?"
A laptop hiding behind a NAT only knows its private address is 10.0.1.5. It has no idea the NAT assigned it the public port 203.0.113.42:12345. But it needs to tell the peer this — otherwise, where would the peer send packets?
STUN (Session Traversal Utilities for NAT) does exactly this. The protocol itself is extremely simple:
Step 1: The laptop sends a UDP packet to a public STUN server
Source: (private) 10.0.1.5:54321
Dst: (public) stun.example.com:3478
Step 2: As the packet passes through the NAT, the NAT creates a mapping:
10.0.1.5:54321 → 203.0.113.42:12345
Step 3: The STUN server receives the packet, observes the source is 203.0.113.42:12345,
and wraps this address into its response and sends it back
Step 4: The NAT receives the reply, looks up its table, finds 12345 maps to 10.0.1.5:54321,
and forwards it to the laptop
Step 5: The laptop reads the STUN response and learns its public identity is 203.0.113.42:12345STUN is essentially an echo protocol: "Can you tell me what you see me as?" The NAT "anonymizes" your identity in the middle, and STUN helps you recover that anonymized identity.
Using the office building analogy: as you leave, the guard hands you a temporary number. You walk to the STUN bulletin board in the middle of the plaza and read that "your temporary number is 203.0.113.42:12345." You tell your friend that number, and they can find you through it — provided the guard lets your friend in.
For STUN to work correctly, it makes an implicit assumption: the packet sent to the STUN server and the packet sent to the peer use the same mapping after passing through the NAT.
This assumption holds under Easy NAT (EIM) but fails under Symmetric NAT. Because Symmetric NAT assigns different ports to different destinations, the port the STUN server sees ≠ the port the peer sees. That's exactly why Symmetric NAT is "hard" — STUN doesn't directly work.
Now that we have STUN, devices A and B have each learned their public identity through a STUN server. Next: how do we get A to send data directly to B?
Let's walk through the flow with concrete addresses.
Assume two devices:
10.0.1.5, public 203.0.113.42:12345 (Easy NAT)192.168.2.10, public 198.51.100.77:54321 (Easy NAT)Both devices know the other exists (this "knowing the other exists" is handled by Tailscale's control plane, which we'll cover later) and know the other's public address (each discovered via STUN and exchanged through the control plane).
Now A wants to send data to B. The most direct attempt:
A → B@198.51.100.77:54321 (UDP)What happens to this packet?
10.0.1.5:40001 → 203.0.113.42:40001 (assuming A used a new source port), then forwards it out.198.51.100.77:54321 ← A. This inbound packet is dropped.Where's the problem? B's NAT filtered A's inbound packet, because B hasn't yet "gone outbound" to A.
The key step in hole punching: both sides "go out at the same time."
If A sends its first packet while B also sends a packet to A's public address:
Timeline:
t1: A → B@198.51.100.77:54321
A's NAT: creates mapping A→B
arrives at B's NAT: B hasn't sent to A yet → dropped ❌
t2: B → A@203.0.113.42:12345
B's NAT: creates mapping B→A, allows B's outbound packet
arrives at A's NAT: looks up table, mapping A→B exists → allowed! ✅
t3: A retries: A → B@198.51.100.77:54321
arrives at B's NAT: looks up table, B sent to A at t2 → allowed! ✅This is the core principle of UDP hole punching. In essence, hole punching exploits NAT's statefulness: as long as both sides have "gone outbound," the NAT treats the reply as legitimate. The key is coordinating the timing — getting both sides to go out at nearly the same time, or retrying with a short delay.
Using the access-control analogy:
If a third party (the control plane) tells A and B in advance which building the other is in and what time to go, they can agree on a time to knock on the door together.
As mentioned earlier, the difficulty with Symmetric NAT is that the public address STUN returns is invalid for the peer (different port number). Tailscale's strategy for this problem is port guessing.
Symmetric NAT assigns different ports to different destinations, but the assignment usually follows a pattern — sequential increments (+1 each time) or random but predictable (algorithm-based). Tailscale's client will:
If port guessing also fails, it falls back to DERP relaying. Even in scenarios with Symmetric NAT on both ends, according to Tailscale's official data, the direct-connection success rate can still reach over 90%, relying on port guessing and retries.
Under some network topologies, hole punching simply cannot succeed:
This is where a fallback is needed: a relay. Tailscale's relay network is called DERP (Designated Encrypted Relay for Packets).
DERP is essentially a set of servers distributed around the globe, each with a public IP. If two devices can't connect directly, they each connect to the nearest/fastest DERP node, which forwards traffic between them:
A ←──→ DERP ←──→ BAn obvious question: how is this fundamentally different from a traditional VPN relay server?
The difference lies in three layers:
1. End-to-end encryption: DERP is a "transit station," not an "unpacking station." Packets A sends to B are encrypted with WireGuard, and DERP can only see the ciphertext. Even if DERP is compromised, the keys are in A's and B's hands; an attacker holding the ciphertext can't decrypt it. This differs from a traditional VPN — a VPN server's admin theoretically has the ability to decrypt traffic passing through the server.
2. Best-effort direct connection: DERP is a fallback, not the default. The connection starts through DERP to guarantee immediate availability, while the background continuously attempts hole punching. Once hole punching succeeds, it automatically switches to a direct connection.
3. Global distribution: Tailscale deploys DERP nodes across many regions worldwide, and clients automatically pick the lowest-latency node. This means that even when relaying, latency is usually acceptable.
In Tailscale, you can use tailscale netcheck to view the latency and reachability of DERP nodes:
$ tailscale netcheck
Report:
* UDP: true
* IPv4: yes, 203.0.113.42:12345
* IPv6: no, but OS has support
* MappingVariesByDestIP:
* HairPinning: false
* PortMapping:
* Nearest DERP: Singapore
* DERP latency:
- tok: 45 ms (Tokyo)
- sin: 28 ms (Singapore)
- hkg: 22 ms (Hong Kong)You can see the latency and distance of each DERP node, and Tailscale automatically picks the lowest-latency one.
Now let's assemble the pieces and walk through Tailscale's complete networking flow.
Tailscale's architecture has two layers:
| Layer | Role | Component |
|---|---|---|
| Control plane | Coordinates everything — key distribution, IP allocation, hole-punching info exchange | Coordination server (Tailscale-hosted) |
| Data plane | The actual encrypted data transfer | WireGuard tunnel (direct or relayed via DERP) |
When you run tailscale up on a device:
1. The client generates a WireGuard key pair (public + private key)
2. The private key is stored permanently on the local device and never leaves it
3. The client contacts the coordination server, announcing its existence
── sends: public key, device name, operating system
4. The coordination server verifies identity (via SSO/pre-shared key)
5. The coordination server assigns the device a 100.x.y.z Tailscale IP
6. The device is added to the ACL list of your Tailnet (your private network)When device A tries to connect to device B:
1. A queries the coordination server: "What's the info for device B in my Tailnet?"
2. The coordination server returns B's:
- WireGuard public key
- List of known endpoint candidates (last successfully punched IP:Port, etc.)
- Tailscale IP (100.x.y.z)
3. A begins running concurrently in the background:
a. Start STUN probing to update its own endpoint info
b. Submit the latest endpoint to the coordination server, syncing to other peers
c. Attempt to connect directly to B using known endpoints
4. The coordination server pushes A's endpoint changes to BNote here: the coordination server is a state-synchronization hub, not a traffic-relay hub. It only helps exchange keys and address info during the connection-establishment phase. Once the connection is up, data traffic never passes through the coordination server.
Tailscale takes a "connect first, then optimize" approach:
Phase 1: DERP relay (immediately available, O(ms) scale)
A → DERP → B
Ensures the connection is usable from its very first second.
All traffic transits DERP, encrypted with WireGuard.
Phase 2: NAT hole punching (parallel in the background, completes in seconds)
After A and B exchange endpoints via the coordination server,
each discovers its public identity via STUN.
Both sides attempt UDP hole punching on multiple ports simultaneously:
- The default port discovered by STUN
- Predicted Symmetric NAT ports (if Hard NAT is detected)
Phase 3: Switching to direct connection (on successful hole punch)
Once any UDP connection succeeds, the data plane switches to direct.
The DERP connection is kept alive but carries no data.
If the direct connection drops (network switch, etc.), it automatically falls back to DERP.This "DERP first + asynchronous hole punching" design is where Tailscale's greatest engineering value lies. Traditional UDP hole-punching approaches (like STUN + TURN) leave the connection unusable until hole punching completes, and the user experiences it as "occasionally can't connect, occasionally have to wait a few seconds." By "relaying first and hole-punching in the background," Tailscale cuts that wait to zero.
We've repeatedly mentioned "sending UDP packets" and "exchanging public keys," but haven't detailed what those packets actually look like. Tailscale's data plane is built on WireGuard — a VPN protocol renowned for its simplicity.
Why choose WireGuard over the more "classic" IPsec or OpenVPN? The core reason is that WireGuard's design philosophy aligns closely with Tailscale's needs:
1. Connectionless
A WireGuard "tunnel" requires no connection setup. As soon as any peer has another peer's public key and endpoint (IP:Port), it can immediately send encrypted UDP packets to that address. No handshake, no negotiation, no state.
This is crucial for NAT traversal: you don't need to "establish a WireGuard connection before hole punching." Hole punching itself is sending WireGuard packets, and a WireGuard packet is a hole-punching packet. The two are one and the same.
2. Silent
WireGuard never sends packets on its own initiative. If two devices establish a WireGuard tunnel but don't communicate for 30 minutes, there's no traffic on the link at all. By contrast, IPsec and OpenVPN send keepalive packets.
3. Minimal Key Exchange: The Noise Protocol
WireGuard uses the Noise_IK mode of the Noise Protocol Framework. The key exchange needs just one and a half round trips (1.5 RTT) and only public keys (no certificates, no pre-shared keys). The specific cryptographic details:
These all fall under the basics of symmetric and asymmetric encryption. Put simply: you only need the peer's public key (a 32-byte blob), with no other credentials, to establish an exclusive, secure, encrypted communication tunnel.
On top of WireGuard, Tailscale handles the most essential management-layer work:
1. Each device generates its own WireGuard key pair
2. Public keys are distributed to other devices in the Tailnet via the coordination server
3. Each device's WireGuard config is automatically generated and updated by the Tailscale client
4. When a new device joins/leaves the Tailnet, ACL policy updates sync automatically
5. Trust between devices is based on public keys, not IP addresses or shared passwordsThis is also an expression of "Zero Trust": even devices within the same Tailnet can't trust each other just because they're "on the same network." Each device's identity is proven by its public key, and ACL policies decide who can talk to whom.
Tailscale's approach might puzzle you: if the goal is decentralized end-to-end communication, why is there a centralized coordination server? It seems self-contradictory.
The answer is: centralized control plane + decentralized data plane = the best of both worlds.
| Control plane | Data plane | |
|---|---|---|
| Responsible for | Key distribution, endpoint discovery, ACL enforcement | Actual data transfer |
| Traffic profile | Low-frequency, lightweight, globally consistent | High-frequency, high-volume, path-optimized |
| Topology | Centralized | Direct / relayed |
| Bottleneck | None — control info is tiny | None — doesn't pass through the coordination server |
The engineering benefits of a centralized control plane:
1. Vastly simplified key and trust management
Each device only needs to trust the coordination server (trust established via SSO). Devices don't need to authenticate each other — they exchange public keys through the coordination server, and encrypting with each other's public keys is itself natural proof of identity.
Don't underestimate this. ZeroTier also needs a centralized controller to do something similar.
2. Centralized management and enforcement of ACLs
Admins configure ACL rules in the Tailscale Admin Console, and the coordination server pushes them to each device. Devices enforce ACLs locally — rejecting unauthorized connections directly at the data plane.
No need to individually modify firewall rules on each device. No need to manage the secure distribution of config files. Just write a few lines of JSON in a web UI.
3. Simple signaling
The coordination server tells you "the peer's public address is X," and you can then connect directly. It doesn't need to handle any business traffic, so Tailscale can serve millions of users worldwide while the coordination server carries almost no load.
This architecture mirrors the signaling servers of instant-messaging tools like WeChat/WhatsApp (Signal Server) — "I know where the other person is, but I don't handle your content."
4. Balancing flexibility and security
The coordination server's existence lets Tailscale offer more complex network topologies (Subnet Router, Exit Node, MagicDNS) while exercising finer-grained control over security (roles, tags, conditional ACLs, etc.). If these features were fully decentralized, both implementation complexity and security risk would spike.
You'll need two devices (a laptop + VPS, or a laptop + phone works). Make sure Tailscale is installed (you've run tailscale up) and both devices are in the same Tailnet (same account or shared).
tailscale statusExample output:
100.72.34.56 my-laptop user@ linux -
100.65.78.90 dev-server user@ linux active; direct 203.0.113.42:12345, tx 1234 rx 5678Note the direct marker after dev-server — this indicates the connection to that device has successfully hole-punched and is using a direct connection.
ping 100.65.78.90tailscale ping 100.65.78.90Example output:
pong from dev-server (100.65.78.90) via 198.51.100.77:54321 in 8msvia 198.51.100.77:54321 indicates a direct connection (sending packets straight to the peer's public IP). If it were relaying, it would show something like via DERP(hkg).
tailscale netcheckYou'll see the DERP node nearest to you and its latency. Note the MappingVariesByDestIP field — if it's true, you're behind a Symmetric NAT, which affects the hole-punching success rate.
In the output of tailscale status, the last field of each line contains the connection method and traffic stats. You can observe how the connection method changes across different network environments (WiFi, mobile hotspot, corporate network, etc.) — which scenarios allow direct connection and which require DERP relaying. This is a direct reflection of the principles covered in this article.
Keywords: NAT · NAT traversal · STUN · UDP hole punching · DERP · WireGuard · coordination server · Endpoint-Dependent Mapping · end-to-end encryption · Zero Trust
By now you've gained a deep understanding of the hardest and most complex part behind Tailscale — the complete implementation of NAT traversal over UDP. Across the previous articles in the networking series, we've built a complete knowledge stack from the bottom up: LANs, IP addresses, routing, UDP, TCP, HTTP, DNS, TLS, WebSocket. Tailscale isn't a new protocol layer — it's a clever orchestration of existing infrastructure, using end-to-end encryption + NAT traversal + a centralized control plane to solve precisely the biggest pain point in the traditional network model: NAT turned the internet's end-to-end model into a client/server model.
If you're interested in WireGuard's cryptographic details, I recommend reading the WireGuard whitepaper (very short, only ~30 pages, worth reading for any developer interested in information security).
If you want to go further, a natural next stop is the core transport layer of HTTP/3 — QUIC. QUIC rebuilds reliability and congestion control on top of UDP while shedding a whole series of TCP's historical baggage. You'll see many ideas there that echo this article: self-built reliability over UDP, 0-RTT connections, connection migration, and why HTTP/3 is encrypted by default.