Step 1 of 5 · Reading · ~4 min
Read
Tunneling & Routing
NAT Traversal and Roaming Endpoints
After this lesson your program tracks where each peer can currently be reached, follows a peer that changes address, and refuses to be steered by a forged or replayed packet.
Why endpoints are learned, not configured
Most clients sit behind NAT: the home router owns one public address and rewrites every outgoing UDP datagram so it looks like it came from that address and a port the router chose. It remembers the pair in a mapping, and only datagrams that arrive from the address the client contacted are translated back inside. When the mapping has been idle for a while (about 30 seconds is common for UDP) the router forgets it.
That is why WireGuard needs only one side to be reachable. The client sends first. The server sees the datagram arrive from some public ip:port, and that source address, not the config file, is where the server replies. A server with a thousand laptops has no endpoint configured for any of them. When the laptop moves from Wi-Fi to a mobile network the source address changes with the next datagram, and the server follows it. This is roaming.
Roaming is a security decision
The source address of a UDP datagram is trivial to forge, so the server may not move a peer's endpoint just because a datagram claims to be from that peer. WireGuard updates the endpoint only after a packet has authenticated (the tag verified) and passed the replay check (its counter is newer than anything accepted before). Without the second rule, an attacker who recorded one valid packet from your phone could replay it from their own address and steal your replies.
A worked sequence, with the state after each line:
| Packet | Authentic | Counter | Source | Result | Endpoint kept |
|---|---|---|---|---|---|
| 1 | yes | 0 | 1.2.3.4:1000 | first contact, LEARNED | 1.2.3.4:1000 |
| 2 | yes | 1 | 1.2.3.4:1000 | nothing changed, OK | 1.2.3.4:1000 |
| 3 | yes | 2 | 5.6.7.8:2000 | roamed, ROAMED | 5.6.7.8:2000 |
| 4 | yes | 1 | 1.2.3.4:1000 | old counter, replayed: dropped | 5.6.7.8:2000 |
| 5 | no | 9 | 9.9.9.9:9 | bad tag: dropped | 5.6.7.8:2000 |
Notice that packet 5 does not even use up counter 9. Only an accepted packet moves the state, and each peer keeps its own counter and endpoint. In this stage a counter must be strictly greater than the highest accepted so far; the full sliding window from the earlier lesson would also admit late packets, but would not change the idea.
Sending is the mirror image: a packet for a peer goes to the last endpoint learned for it, and if none has been learned there is nowhere to send until the peer contacts you.
Keeping the mapping open
If nobody sends, the NAT forgets the mapping and the server cannot reach the client any more, even though the client has not moved. PersistentKeepalive = 25 makes the client send an empty authenticated packet every 25 seconds, which is less than the typical 30 second timeout, so the mapping never goes idle. The rule is strict: a keepalive interval equal to the timeout can lose the race. A value of 0 disables keepalives, which is right for a server that is not behind NAT and wastes battery on a phone that does not need it.
Peers behind NAT on both sides
Two laptops, each behind its own NAT, cannot start the conversation alone. Tailscale adds a coordination server where each side learns the other's public address, then tries UDP hole punching: both send at the same moment, and each outgoing datagram opens the mapping that lets the other's in. Symmetric and carrier-grade NATs pick a new mapping per destination and defeat the trick, so Tailscale falls back to relaying through DERP servers. Plain WireGuard expects at least one end to have a stable address.
Common mistakes
- Roaming on an unauthenticated packet. Anyone can spray forged datagrams and steal the endpoint.
- Roaming on a replayed packet. A recorded old packet from the old address moves the endpoint backwards.
- Counting a rejected packet's counter. Only accepted packets advance the counter.
- Sharing state between peers. One peer's endpoint or counter must not touch another's.
- A keepalive equal to the NAT timeout. It must be strictly smaller.
Your exercise
The starter reads the commands. You keep the per-peer endpoint and counter and decide each answer. The most likely mistake is moving the endpoint before checking the replay rule.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…