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.