WireGuard Explained: Keys, AllowedIPs, and NAT Traversal
How WireGuard really works: the static key handshake, what AllowedIPs controls, PersistentKeepalive and NAT, MTU math, and how it compares to OpenVPN.
WireGuard is small enough to hold in your head, and that is the design goal. No certificate authority, no TLS negotiation, no user database, no daemon pushing configuration to clients. Just one Curve25519 keypair per interface, a UDP socket, and one list per peer that doubles as a routing table and an access control list. Most production problems trace back to misreading that list, or to MTU.
Keys: no PKI, no accounts, two public keys
Each interface holds exactly one private key, and the public key is derived from it:
wg genkey | tee private.key | wg pubkey > public.key
A private key is 32 raw bytes, base64 encoded to 44 characters. Each side needs the other’s public key, distributed out of band. There is no in-protocol introduction: an interface completes a handshake only with a key already listed in a [Peer] block. Consequences:
- No expiry. Keys never age out. Rotation is a manual change at both ends.
- No revocation list. Removing access means deleting the
[Peer]block on every interface that trusted it. - No identity beyond the key. Answering “which person is this” requires a layer on top of WireGuard, not inside it.
- Nothing central to steal. No server holds client private keys, because none are ever transmitted.
Two peers can also share a PresharedKey, a 32 byte symmetric secret mixed into the handshake alongside the public key authentication. Its documented purpose is post-quantum mitigation: if Curve25519 falls years from now to an adversary who recorded today’s traffic, sessions that also used a preshared key stay confidential. Generate it with wg genpsk and set the same value on both sides.
The handshake and the timers around it
WireGuard uses the Noise_IKpsk2 pattern from the Noise Protocol Framework: Curve25519 for the exchange, ChaCha20-Poly1305 for authenticated encryption, BLAKE2s for hashing, HKDF for derivation. The set is fixed, so there is no negotiation and no downgrade surface.
The exchange is a single round trip: initiation, response, data. The timers that matter operationally:
- A session rekeys after roughly 120 seconds of use, and a key is refused outright after 180 seconds.
- A failed handshake retries roughly every 5 seconds while traffic is queued to send.
- With nothing to send and nothing arriving, the interface goes completely silent. There is no periodic traffic by default.
Silence is deliberate. An interface never answers a packet it cannot authenticate, so a UDP scan of the listening port returns nothing. Under a handshake flood a peer falls back to cookie replies, so expensive cryptography is not spent on unverified senders.
One asymmetry catches people out: only a peer with an Endpoint configured can initiate. The client knows where the server is, so it starts; the server learns the client’s address from the first authenticated packet and keeps updating it, which is what makes Wi-Fi to cellular roaming seamless.
Reading a config: what wg understands, what wg-quick adds
[Interface]
PrivateKey = <server private key>
Address = 10.7.0.1/24
ListenPort = 51820
MTU = 1420
PostUp = iptables -A FORWARD -i %i -j ACCEPT
[Peer]
PublicKey = <client public key>
PresharedKey = <optional shared secret>
AllowedIPs = 10.7.0.2/32
PersistentKeepalive = 25
Only a subset of those keys reaches the kernel. PrivateKey, ListenPort, FwMark, PublicKey, PresharedKey, AllowedIPs, Endpoint, and PersistentKeepalive are protocol configuration. Address, DNS, MTU, Table, SaveConfig, and the PreUp/PostUp/PreDown/PostDown hooks belong to the wg-quick wrapper. That is why wg setconf wg0 wg0.conf rejects a file wg-quick up wg0 accepts. For a working pair of files built from scratch, see the WireGuard setup on Ubuntu for server and client.
AllowedIPs does two jobs at once
This field causes most of the confusion, because it behaves differently by direction.
Outbound it is a routing table. WireGuard keeps a longest-prefix-match trie across all peers on the interface. A packet entering the tunnel is matched against it to pick whose key encrypts it. If no peer covers the destination, it is dropped.
Inbound it is an access control list. Once a packet is decrypted, the inner source address must fall inside that same peer’s AllowedIPs or it is discarded. A peer cannot spoof traffic for a subnet it was not granted. What follows:
- A prefix belongs to one peer per interface. Overlaps resolve by longest prefix; an identical entry added to a second peer silently moves from the first.
- Full tunnel is
0.0.0.0/0, ::/0. Split tunnel lists only the subnets you want carried, such as10.7.0.0/24, 192.168.20.0/24. - Hub and spoke: the hub lists each spoke as a
/32; each spoke lists the tunnel subnet plus any LAN routed behind the hub. The mesh designer generates both this shape and a full mesh, with the tunnel count for each. - Site to site: add the remote LAN prefix, then handle the two things WireGuard will not do for you: IP forwarding on the gateway and a return route for the remote LAN hosts.
wg-quick installs a route per AllowedIPs entry. A default route gets special handling: instead of the old 0.0.0.0/1 plus 128.0.0.0/1 split, it fwmarks tunnel packets and adds a policy rule using suppress_prefixlength, so encrypted output does not loop back into the tunnel. Set Table = off to own routing yourself.
NAT traversal, honestly
WireGuard has no STUN, no ICE, no hole punching, no relay fallback. It is a plain UDP socket. What it does have is endpoint roaming: a peer’s stored endpoint updates to the source address of the most recent correctly authenticated packet. Behind NAT, the mapping has to be kept alive from the inside:
PersistentKeepalive = 25
Twenty-five seconds is the interval the WireGuard documentation calls sensible, short enough to stay under the idle UDP timeouts many consumer NAT and firewall tables apply. An empty keepalive is 60 bytes on the wire over IPv4 and fires only after 25 seconds of silence.
Set it on the NAT’d side, pointing at the stable side. Adding it on a public server is mostly wasted traffic, since the server can only aim keepalives at the peer’s last known endpoint.
If both ends sit behind NAT or carrier-grade NAT, plain WireGuard has no mechanism to open a direct path by itself. The fixes are a small VPS as a public hub, a forwarded UDP port on one side, native IPv6, or an overlay with a coordination service.
MTU, the cause of “it connects but pages hang”
Overhead is fixed. A data message carries a 16 byte WireGuard header plus a 16 byte Poly1305 tag, so 32 bytes, plus 8 bytes of UDP, plus the outer IP header: 20 bytes for IPv4, 40 for IPv6. That is 60 bytes over IPv4, 80 over IPv6.
wg-quick does not hardcode a number. It looks up the route to the peer endpoint, falls back to the default route, and subtracts the worst case 80, so an ordinary 1500 byte path produces the familiar 1420. A PPPoE line with a 1492 byte MTU produces 1412. Stacked over another tunnel, subtract again.
The symptom is distinctive: handshake succeeds, ping works, SSH is fine, then large HTTPS responses stall. Small packets pass and large ones vanish, because a router drops them and the ICMP fragmentation-needed message never gets back.
Test the real ceiling from inside the tunnel with the do-not-fragment bit set, remembering ICMP adds 28 bytes:
ping -M do -s 1392 10.7.0.1
If that fails, step down by 20 until it passes, add 28 to the working payload, and set the result as MTU. The floor worth trying is 1280, the IPv6 minimum. On a router terminating the tunnel, TCP MSS clamping beats guessing an MTU per client. The full MTU calculation, a lookup table, and the clamping rules go through both methods and where each one stops working.
WireGuard against OpenVPN and Tailscale
OpenVPN is a different class of tool: TLS-based, TCP or UDP, with a certificate PKI and revocation, username and password plugins, and server-pushed routes and DNS. The old line about it being slow because it runs in userspace no longer holds. Its data channel offload module ovpn landed in mainline Linux 6.16 in 2025 and OpenVPN 2.7 added support in February 2026, closing much of the historic throughput gap. The size gap remains: the WireGuard whitepaper puts its Linux implementation at under 4,000 lines, against a far larger OpenVPN plus its TLS library. Choose OpenVPN when TCP 443 must carry the tunnel on a hostile network, or when certificate revocation and per-user auth are contractual requirements.
Tailscale, Netbird, and similar overlays use WireGuard as the data plane and add what the protocol deliberately omits: key distribution, NAT hole punching, encrypted relays when a direct path fails, SSO identity, policy ACLs. The trade-off is a control plane and an identity provider you now depend on, exactly what self-hosted WireGuard avoids. WireGuard against Tailscale, compared feature by feature, covers what that control plane buys and what it costs.
Rule of thumb: fixed sites with one stable public endpoint are plain WireGuard territory, many roaming devices behind unpredictable NAT are overlay territory, and TCP fallback or certificate revocation is OpenVPN.
A troubleshooting order that saves time
- Run
wg show. The latest handshake line is the diagnosis. No value means it never completed. - No handshake: wrong key pairing, wrong or unresolvable
Endpoint, UDP blocked upstream, or skewed clocks. Confirm the server is listening withss -ulnp. - Handshake present, no traffic: almost always
AllowedIPs, missing IP forwarding, or a gateway firewall rule. - Works small, stalls large: MTU. See the ping test above.
- Dies after a few minutes idle: a NAT mapping expired. Add
PersistentKeepalive = 25on the NAT’d side.
When that is not enough, the kernel module logs handshake decisions via echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control. Turn it off afterwards; it is verbose and logs peer public keys.
See also
Sources
Related
WireGuard vs Tailscale: Which One to Run
Tailscale runs on WireGuard, so the real choice is about the control plane: key distribution, NAT traversal, relays, identity, and what fails without them.
WireGuard MTU Calculator and Fragmentation Fix
Work out the right WireGuard MTU from your path MTU, understand the 60 and 80 byte overhead, measure the real ceiling, and clamp TCP MSS correctly.
WireGuard Setup on Ubuntu: Server and Client
A config-first WireGuard install on Ubuntu: key generation, wg0.conf for server and client, forwarding, firewall rules, and adding peers without downtime.