Home › Security › Secure channels & messaging
algorithm low

WireGuard

Problem Operators who link laptops, phones, servers and cluster nodes over untrusted networks need an encrypted tunnel that is small, quiet to scanners, survives address changes, and ties each packet's inner source address to a key.

A UDP tunnel whose peers are public keys, with a 1-RTT Noise handshake, one fixed cipher suite and a key-to-address routing table.

handshake 1 RTT, 2 messages, 4 DH per side; per packet one AEAD seal or open plus O(32) trie steps
vpntunnelnoiseudpcryptokey-routingroaming
Overview

A WireGuard peer is a 32-byte public key plus a list of IP prefixes it may use as source addresses (AllowedIPs). That one table does three jobs: a packet's destination picks the peer to encrypt for, the peer's key authenticates a handshake, and a decrypted packet is accepted only if its inner source address routes back to the key that decrypted it. Handshakes are 2 UDP messages, one cipher suite is fixed, and a server stays silent to anything unauthenticated. Staff laptops, mobile apps, cloud regions, cluster nodes and sensors all use the same tunnel.

Key facts

Use when

  • Remote access: laptops and phones reaching an office or cloud network; the endpoint follows the device across Wi-Fi and mobile networks.
  • Site-to-site and mesh: cloud regions, branch offices, home labs; Tailscale builds its mesh from WireGuard tunnels.
  • Cluster networking: Cilium and Calico encrypt node-to-node pod traffic with WireGuard.
  • IoT and embedded gateways: small per-peer state, no negotiation.

Avoid when

  • You need per-user passwords, certificates or revocation inside the protocol: use mutual TLS or IPsec with IKE.
  • Only a TCP/443 path is open: a UDP tunnel will not pass; use TLS-based tunnels.
  • Traffic must be hidden from observers: the fixed 4-type packet headers are recognisable (see traffic obfuscation).

Deep dive

How it works

In: inner IP packets, local private key, peers' public keys with AllowedIPs, optional 32-byte pre-shared key · Out: UDP datagrams carrying encrypted packets; on receive, authenticated inner packets · State: per peer: static key, AllowedIPs, endpoint, current/previous/next session (keys, 64-bit counters, replay window), latest initiation timestamp, cookie · Cost: handshake 4 DH per side, then one AEAD per packet

  1. Send routing. The destination IP is looked up by longest-prefix match in the cryptokey table; the owner is the peer. No match: drop with "no route to host".
  2. Initiation (148 B). The initiator sends type 1, a sender index, an ephemeral public key, its static key encrypted (48 B), a TAI64N timestamp encrypted (28 B), mac1 and mac2 (16 B each). Noise pattern e, es, s, ss; DH results chain through HMAC-BLAKE2s key derivation and the running hash is the AAD.
  3. Response (92 B). The responder looks up the initiator's static key in its peer list, checks the timestamp is greater than the last one seen from that peer, and answers type 2 with an ephemeral key and an empty AEAD payload (e, ee, se, psk). The pre-shared key defaults to 32 zero bytes.
  4. Keys. Both derive two 32-byte transport keys, one per direction, and zero the handshake state. The responder waits for the first data packet (key confirmation); the initiator sends a keepalive if it has none.
  5. Data (type 4). Header: type, 3 zero bytes, receiver index, 64-bit little-endian counter (16 B); then the inner packet zero-padded to a multiple of 16, sealed with ChaCha20-Poly1305, nonce = 32 zero bits plus the counter, plus a 16-byte tag. An empty payload is a 32-byte keepalive.
  6. Receive. The receiver index finds the session, the replay window is checked, the AEAD is opened, then the window is updated. The inner source IP must match the decrypting peer's AllowedIPs, else drop.
  7. Roaming. After a correctly authenticated packet, the peer's endpoint becomes that packet's outer source IP and port; replies go there.
  8. Timers. Rekey-After-Time 120 s (initiator), Rekey-After-Messages 2^60, Reject-After-Time 180 s, Reject-After-Messages 2^64 − 2^13 − 1, Rekey-Timeout 5 s, Rekey-Attempt-Time 90 s, Keepalive-Timeout 10 s (whitepaper §6.1).

Silence and load. mac1 is a keyed BLAKE2s over the message, keyed with a hash of the receiver's public key: a sender without it gets no reply and no state is allocated. Under load a valid mac1 without a valid mac2 gets a 64-byte cookie reply (smaller than an initiation, so no amplification): a MAC of the sender's outer IP and port under a secret changed every 2 minutes. A retry with mac2 proves address ownership and can be rate limited, like a SYN cookie.

Code

Rust std only, no crypto: cryptokey routing (IPv4 trie, longest prefix wins), the inbound source check and a per-peer receive window (RFC 6479 ring, W = 8128 as in WireGuard).

#[derive(Default)]
struct Node { kid: [Option<usize>; 2], peer: Option<usize> } // binary trie over IPv4 bits
#[derive(Default)]
struct Routing { nodes: Vec<Node> }
impl Routing {
    /// `net/len` belongs to `peer` (re-inserting a prefix moves it).
    fn insert(&mut self, net: u32, len: u32, peer: usize) {
        if self.nodes.is_empty() { self.nodes.push(Node::default()); }
        let mut at = 0;
        for i in 0..len {
            let b = (net >> (31 - i)) as usize & 1;
            at = self.nodes[at].kid[b].unwrap_or_else(|| {
                self.nodes.push(Node::default());
                self.nodes[at].kid[b] = Some(self.nodes.len() - 1);
                self.nodes.len() - 1
            });
        }
        self.nodes[at].peer = Some(peer);
    }
    /// Longest-prefix match: keep the last peer seen on the walk down.
    fn lookup(&self, ip: u32) -> Option<usize> {
        let (mut at, mut best) = (0, self.nodes.first()?.peer);
        for i in 0..32 {
            let Some(n) = self.nodes[at].kid[(ip >> (31 - i)) as usize & 1] else { break };
            at = n;
            best = self.nodes[at].peer.or(best);
        }
        best
    }
}
const M: usize = 128; // ring words, window W = (M-1)*64 = 8128 counters
const W: u64 = (M as u64 - 1) * 64;
/// Window over n = counter + 1 (RFC 6479 ring, no bit shifting).
struct Window { top: u64, ring: [u64; M] }
impl Window {
    fn check(&self, c: u64) -> bool {
        let n = c + 1;
        n > self.top || (self.top - n < W && self.ring[(n / 64) as usize % M] >> (n % 64) & 1 == 0)
    }
    /// Call only after the AEAD tag verified; false = duplicate.
    fn update(&mut self, c: u64) -> bool {
        let n = c + 1;
        if n > self.top {
            for i in 1..=(n / 64 - self.top / 64).min(M as u64) {
                self.ring[(self.top / 64 + i) as usize % M] = 0; // clear words the window enters
            }
            self.top = n;
        }
        let (w, bit) = ((n / 64) as usize % M, 1u64 << (n % 64));
        let fresh = self.ring[w] & bit == 0;
        self.ring[w] |= bit;
        fresh
    }
}
/// `peer` owns the key that opened the packet; `src` is the inner source IP.
fn receive(r: &Routing, w: &mut Window, peer: usize, counter: u64, src: u32) -> Result<(), &'static str> {
    if !w.check(counter) { return Err("replay"); }
    // ... AEAD open with the peer's receiving key goes here; on failure drop silently ...
    if !w.update(counter) { return Err("replay"); }
    if r.lookup(src) != Some(peer) { return Err("wrong source"); }
    Ok(())
}
fn ip(a: u8, b: u8, c: u8, d: u8) -> u32 { u32::from_be_bytes([a, b, c, d]) }
fn main() {
    let mut r = Routing::default();
    for (peer, net, len) in [(1, ip(10, 192, 122, 3), 32), (1, ip(10, 192, 124, 0), 24),
        (2, ip(192, 168, 0, 0), 16), (3, ip(0, 0, 0, 0), 0), (3, ip(192, 168, 7, 0), 24)] {
        r.insert(net, len, peer);
    }
    assert_eq!(r.lookup(ip(192, 168, 87, 21)), Some(2));
    assert_eq!(r.lookup(ip(192, 168, 7, 9)), Some(3)); // /24 beats /16
    assert_eq!(r.lookup(ip(8, 8, 8, 8)), Some(3)); // 0.0.0.0/0 catches the rest
    let mut w = Window { top: 0, ring: [0; M] }; // peer 1's receive window
    assert_eq!(receive(&r, &mut w, 1, 0, ip(10, 192, 124, 5)), Ok(()));
    assert_eq!(receive(&r, &mut w, 1, 1, ip(192, 168, 1, 1)), Err("wrong source")); // peer 2's range
    assert_eq!(receive(&r, &mut w, 1, 0, ip(10, 192, 124, 5)), Err("replay"));
    assert_eq!(receive(&r, &mut w, 1, 9000, ip(10, 192, 122, 3)), Ok(()));
    assert_eq!(receive(&r, &mut w, 1, 100, ip(10, 192, 122, 3)), Err("replay")); // 9001-101 >= W
    assert_eq!(receive(&r, &mut w, 1, 8999, ip(10, 192, 122, 3)), Ok(())); // reordered, in window
}

Compiled with stable rustc; all asserts pass. A timing harness with the same algorithms (1000 peers, 4 random /16-/32 prefixes each, 4096 random addresses, 20 million lookups) gave 21 ns per lookup and 1.1 ns per window check + update on an Apple M4 Pro (two runs).

Complexity and constants

  • Lookup O(32) trie steps for IPv4 (O(128) for IPv6), independent of peer count. Measured: order of 20 ns on a warm 4,000-prefix trie; a cold trie costs up to one cache miss per level.
  • Window check + update O(1), 1 KiB of ring per peer (measured 1.1 ns).
  • Wire sizes: initiation 148 B, response 92 B, cookie reply 64 B, data 32 B + padded payload; with IPv4 and UDP, a keepalive is 60 B on the wire.
  • Cost drivers: one ChaCha20-Poly1305 pass and one UDP syscall per datagram unless batched; one handshake per ~120 s per peer pair.

Efficiency

  • Cost: per data packet one AEAD plus a ~20 ns lookup and ~1 ns window test (measured here); per session 4 Curve25519 DH per side plus one ephemeral key pair; the bound is AEAD throughput and per-packet syscall and cache cost, not the handshake.
  • Levers:
    • GSO "super packets" — encrypt and send a group, route cached; whitepaper reports 35% faster sending from fewer cache misses (§7.1) — Linux kernel path
    • parallel crypto — kernel padata spreads AEAD over cores, order kept (whitepaper §7.2) — high-bandwidth links
    • 16-byte aligned header and padding — in-place decryption with natural alignment (§5.4.6) — any CPU
    • radix trie with lockless RCU reads — no lock on the per-packet routing lookup (§7.4) — many cores
  • Naive → standard → state of the art:
    • userspace tunnel, one read/write syscall per packet through a TUN device · one syscall pair per packet
    • in-kernel virtual interface, per-peer queues · no TUN copy, one core per flow
    • GSO batching and multi-core crypto in the kernel (whitepaper §7.1-7.2) · fewer syscalls and cache misses per byte

Implementation notes

  • Cryptokey table: one prefix belongs to one peer, so moving 10.0.0.0/24 from A to B is an insert; 0.0.0.0/0 makes a peer the default gateway. Keep IPv4 and IPv6 in separate tries.
  • Index tables (32-bit session indexes, like IPsec SPIs) use a keyed hash against collision attacks; the kernel uses SipHash-2-4 (whitepaper §7.4).
  • Update the window only after the AEAD opens; a forged counter must not move top. Counters start at 0.
  • Without the inner-source check any peer could inject packets claiming another peer's address.
  • Endpoints are learned automatically; set PersistentKeepalive (the quickstart suggests 25 s) on peers behind NAT that must stay reachable.
  • Compare MACs in constant time (constant-time code).

Pitfalls

  • Overlapping AllowedIPs are legal; a forgotten /32 silently takes traffic from a /16.
  • 0.0.0.0/0 on two peers: the prefix has one owner, the last one inserted.
  • A peer behind NAT becomes unreachable once its mapping expires, unless keepalives run.
  • A stuck handshake retries only every 5 s (plus up to 333 ms jitter) for 90 s, then waits for new traffic.
  • MTU: tunnel overhead (at least 60 bytes over IPv4, 80 over IPv6) shrinks the inner MTU; black holes stall large transfers.
  • No key distribution, expiry or revocation inside the protocol.

Variants and alternatives

IPsec ESP with IKEv2standard in routers and appliances, certificates and negotiable suites; heavier state machine.
OpenVPN / TLS tunnelspass TCP/443, use certificates; more overhead.
SSH forwardingone-off application tunnels.Noise transportsthe same handshake family inside applications, no routing layer.
Pre-shared keymixes a symmetric secret into the handshake, an optional layer such as for post-quantum resistance (protocol page).

In production

  • Linux kernel, ports for Windows, macOS/iOS and Android; wireguard-go and Rust userspace implementations.
  • Tailscale: a mesh of point-to-point WireGuard tunnels, with its clients shipping the wireguard-go package.
  • Cilium and Calico: encryption of pod traffic between Kubernetes nodes (Cilium uses UDP port 51871).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
WireGuard (Linux)CWireGuard/wireguard-linuxkernel built-in (ip link add wg0 type wireguard)————reference implementation
wireguard-toolsCWireGuard/wireguard-toolsbrew install wireguard-tools————wg and wg-quick configuration tools
wireguard-goGoWireGuard/wireguard-gobrew install wireguard-go————userspace; used by macOS/iOS/Android apps and Tailscale
BoringTunRustcloudflare/boringtuncargo add boringtun————userspace library and CLI
GotaTunRustmullvad/gotatuncargo add gotatun————userspace, portability and speed
TailscaleGotailscale/tailscalebrew install tailscale————mesh and key distribution on top of WireGuard

Sources

  • Donenfeld, WireGuard: Next Generation Kernel Network Tunnel, NDSS 2017. paper · DOI · code
  • Donenfeld, WireGuard: Protocol & Cryptography. wireguard.com/protocol
  • Perrin, The Noise Protocol Framework. spec
  • Zhang, Tsou, IPsec Anti-Replay Algorithm without Bit Shifting, RFC 6479, 2012. RFC
  • Cilium docs, WireGuard Transparent Encryption. docs
  • Tigera, Encrypt in-cluster pod traffic (Calico). docs
  • Tailscale, How Tailscale works. blog

Used by

Systems (4):

Alternatives and related

ipsec esp · plannedIPsec ESP tunnels with IKEv2 key exchange and anti-replay
noise protocol · plannedNoise framework handshakes (IK, XX): DH patterns, chaining keys, transport ciphers
SSH protocol (transport, auth, channels)Administrators, developers and automation that run commands, move files or reach private networks on remote machines over an untrusted network need an authenticated, encrypted connection that carries many independent streams and proves which machine they reached.Replay protection windowReceivers of authenticated messages (VPN packets, voice frames, sensor readings, signed API calls) must reject copies that an attacker or the network re-sends, though every copy carries a valid tag and messages may arrive out of order.TLS 1.3Any network service must keep traffic between clients, or between internal services, private, tamper-proof and authenticated to the right server, without hand-building a secure channel.Mutual TLSA service, device or database that accepts connections from many callers must know which caller is on the other end before it reads a request, without sending a password or token on every connection.AEAD (AES-GCM, ChaCha20-Poly1305)Developers encrypting data at rest, tokens, cookies or messages need encryption that also detects tampering, so attackers cannot flip bits, truncate or splice ciphertexts unnoticed.ECDH key exchange (X25519, P-256)Two parties on an open network who share no secret need the same fresh symmetric key at the start of a session, without any eavesdropper being able to compute it, whether they are browsers, phones, tunnels or devices pairing.HKDF key derivation (extract-and-expand)A system holds one shared secret, such as a key-agreement result or a master key, but needs many independent keys from it (per direction, purpose, file, device or epoch), and the secret itself is not uniformly random.Message authentication codes (HMAC, CMAC, Poly1305, KMAC)A receiver gets bytes over a channel an attacker can modify (webhook, cookie, radio frame, download link) and must tell whether a holder of a shared secret produced them unchanged.
longest prefix match · plannedIP prefix lookup structures (DIR-24-8, Poptrie, LC-trie) for routes, ACLs and geo-IP
radix trie · plannedCompressed prefix tree for path routing, domain matching and prefix lookups
syn cookies · plannedStateless SYN handling that encodes connection state in the initial sequence number
NAT traversal (STUN, ICE, TURN)Two programs on different private networks, each behind a router that rewrites addresses and drops unsolicited packets, need to exchange data directly. Neither can reach the other by address, and a relay costs bandwidth.
key rotation · plannedRotating keys without downtime: key ids, overlap windows, re-wrapping, JWKS publication, revocation
Containers and KubernetesA team running several services must choose how to package and schedule them across machines. This guide answers whether Kubernetes is worth its operational weight or a simpler way to run containers suffices.