What's this all about, then?

WireGuard in one paragraph

WireGuard is a modern VPN that lives inside the Linux kernel (and other OS kernels). It is deliberately minimal — the reference implementation is around 4,000 lines of code, compared to OpenVPN's ~400,000. That simplicity is a security feature: less code means a smaller attack surface and an implementation you can actually audit. WireGuard uses state-of-the-art cryptography (Curve25519 key exchange, ChaCha20-Poly1305 encryption, BLAKE2 hashing) and a handshake protocol called Noise_IKpsk2.

The core idea: route by identity, not address

Traditional VPNs route packets by IP address, with authentication bolted on top. WireGuard flips this. Every peer is identified by a Curve25519 public key, and routing decisions are made based on that cryptographic identity. The IP addresses a peer is allowed to claim are declared up front in the configuration — they can't be forged, because the keys can't be forged.

This is the Cryptokey Routing Table: a mapping from public key to a set of allowed inner IP ranges (CIDRs). It is the foundational data structure of WireGuard, described in Section 2 of the whitepaper.

Whitepaper reference: Jason A. Donenfeld, "WireGuard: Next Generation Kernel Network Tunnel," NDSS 2017. Section 2 introduces Cryptokey Routing; Section 3 covers the send/receive flows in detail.

Your interface's own identity: a Curve25519 keypair

It's not just peers that have keys — the WireGuard interface itself (wg0) has its own static Curve25519 public/private keypair. The public key shown in the header is how this interface proves its identity to peers during the handshake. Each peer must know this public key in advance and configure it as the counterpart they expect to talk to; only the holder of the matching private key can respond correctly.

The private key never leaves the device and is never transmitted. In wg show wg0 output you only see the public key — a 32-byte Curve25519 point encoded in base64. This asymmetry (peers exchange public keys out of band; private keys stay local) is what gives WireGuard its authentication guarantee without any PKI or certificate authority.

The Noise handshake and the session key

Before any data packets flow, WireGuard runs a two-message handshake defined by the Noise_IKpsk2 protocol pattern. Each party uses its static Curve25519 keypair and the peer's known public key to perform a series of Diffie-Hellman exchanges. The results are mixed with an optional pre-shared symmetric key (the "psk2" in the name) through BLAKE2s into a shared secret, from which a pair of symmetric session keys are derived — one for each direction of traffic.

The "Session Key" column in the routing table shows the current symmetric key for each peer. It reads until you hit Send or Recv for the first time, at which point a ⚡ HANDSHAKE event appears in the log and the column fills in with the (simulated) 256-bit key. In real WireGuard, session keys are rotated every 3 minutes or 260 packets, whichever comes first, by silently running a new handshake in the background.

ChaCha20-Poly1305 and the nonce

Data packets are encrypted with ChaCha20-Poly1305, an authenticated encryption with associated data (AEAD) cipher. It simultaneously encrypts the payload and produces a 128-bit authentication tag — so a single decryption step both decrypts and verifies integrity. Any tampering with the ciphertext causes decryption to fail outright.

The cipher requires a nonce — a 64-bit counter that must never repeat under the same session key. WireGuard uses a simple incrementing counter starting at zero, incremented for every packet sent to a given peer. The nonce travels in the packet header so the receiver can decrypt; because it is also part of the authenticated data, replaying a captured packet with a previously-seen nonce is rejected. The Nonce column in the demo shows the current outbound counter for each peer — watch it increment each time you hit Send.

The routing table: what the demo shows

In the demo, wg0 is a WireGuard interface with an inner address of 10.0.0.1/24. Each row in the table is a peer: a remote machine identified by its public key. Each peer has:

  allowed_ips — the inner IP addresses this peer is permitted to use.
  endpoint — the real outer IP:port where packets for this peer are sent.
  session key — the symmetric key derived from the Noise handshake with this peer.
  nonce — the outbound ChaCha20-Poly1305 counter for this peer.

The allowed_ips field serves double duty: it is used outbound to decide which peer to send a packet to (longest-prefix match, like a routing table), and inbound to validate that the decrypted packet's source IP is one the peer is actually authorized to use.

Sending a packet

When wg0 needs to send a packet to an inner destination (say 10.0.0.2), it walks the cryptokey routing table looking for a peer whose allowed_ips contains that address. Once found, if no session exists yet, it runs the Noise handshake to derive a session key. Then it encrypts the entire IP packet using ChaCha20-Poly1305 with that session key and the current nonce, increments the nonce, wraps it in a new UDP packet addressed to the peer's outer endpoint, and sends it on its way.

The inner packet — source address, destination address, payload — becomes completely opaque to anyone on the internet. Only the outer IP:port is visible. Hit Send on any row in the demo to see this lookup in action.

Receiving a packet

When a UDP packet arrives at wg0's listen port, WireGuard identifies which peer's session key can decrypt it (the handshake establishes this association). After decryption and authentication-tag verification, the inner source IP is revealed. WireGuard then checks: is this IP in that peer's allowed_ips? If not, the packet is silently dropped — even though it decrypted correctly. A peer can't claim an IP it wasn't configured for.

This is the security guarantee: cryptographic identity and routing policy are unified. You cannot receive a packet that claims to come from 10.0.0.2 unless it was encrypted by the key associated with that address. Hit Recv in the demo to see a successful receive, and try adding a peer without an allowed IP that matches to observe the drop.

Endpoints & Roaming

The endpoint field — the outer IP:port — is treated as mutable hint, not authoritative configuration. Every time WireGuard successfully decrypts and validates a packet from a peer, if that packet arrived from a different outer IP than the one on record, it silently updates the endpoint.

No signaling, no reconnect, no re-authentication. A phone can move from WiFi to LTE, change its public IP entirely, and the tunnel just keeps working — the next valid packet updates the table. Hit Roam in the demo to watch the endpoint column update live as the "phone" moves to a new IP.

Why TailScale is built on this

TailScale is, at its core, WireGuard plus two things WireGuard deliberately leaves out: key distribution and NAT traversal.

WireGuard assumes you have already exchanged public keys out-of-band and configured each peer's allowed_ips. TailScale's coordination server (hosted by TailScale, or self-hosted as Headscale) handles this automatically — when you add a device to your tailnet, it gets a public key, that key is distributed to all other devices, and each device's WireGuard cryptokey routing table is updated.

NAT traversal (getting two devices behind separate home routers to talk directly) is handled via DERP relay servers and ICE-style hole-punching — again, completely transparent to WireGuard itself, which just sees packets arriving from (possibly changing) outer endpoints and updates its table accordingly.

The elegant result: TailScale gives every device in your network a stable inner IP (100.x.x.x in the CGNAT range), backed by exactly the cryptokey routing model this demo illustrates.