Vivian Voss

Seven Lines of WireGuard

technical beauty freebsd wireguard networking

Somewhere around 2015 a security researcher was writing a kernel rootkit, and he had a problem that had nothing to do with the rootkit itself. He needed to get data out. Whatever carried it had to be quiet enough that a machine running it would not announce itself to anyone scanning the network, and small enough that he could hold the whole thing in his head. He was also, separately, rather tired of OpenVPN and IPsec, having spent time finding bugs in both. The plan involved an exit point on his parents' computer, and installing a rootkit on his parents' computer struck even him as a poor idea. So, as he put it later, he started taking out the nasty code.

What remained is now in the FreeBSD base system, in OpenBSD, in NetBSD, in the Linux kernel and on a few hundred million telephones. The stealth requirement never left. It is why a WireGuard endpoint says nothing at all when an unauthenticated packet arrives, which is a pleasant thing to have on a machine facing the open internet.

On the machine where I write this, it belongs to nobody:

# pkg which /usr/bin/wg
/usr/bin/wg was not found in the database
# pkg which /boot/kernel/if_wg.ko
/boot/kernel/if_wg.ko was not found in the database

Neither file is in any package, because both arrived with the system, along with their manual page in section eight. There is no port to install, no repository to trust and no version to reconcile with the kernel's. On a Linux machine WireGuard is a module the distribution happens to ship; here it is part of the same tree that produced the kernel and the userland, released as one tested thing. That difference is the whole argument of this publication, and it is unusual to find it demonstrated quite so plainly.

I wrote about WireGuard last December, and that piece did what love letters do: it asserted things. Four thousand lines. Modern cryptography. Ten gigabits against OpenVPN's hundred megabits. Every one of those was borrowed from somewhere else, and not one was checked. This series exists to go back and check, so let us start with the part a reader can actually do something with.

What it is like to use

Setting up a tunnel begins with a key pair, and the command that makes one has no options worth mentioning:

wg genkey | tee private.key | wg pubkey > public.key

Thirty-two bytes each. No certificate authority, no signing request, no expiry date, no chain to validate at three in the morning. You send the public half to whoever is on the other end, by any means at all, and they send you theirs.

Then the configuration, and this is the whole of it for a working point-to-point tunnel:

[Interface]
PrivateKey = <your 32 bytes, base64>
ListenPort = 51820

[Peer]
PublicKey = <their 32 bytes, base64>
AllowedIPs = 10.7.0.2/32
Endpoint = far.example:51820

Seven lines. wg-quick up wg0 and the interface exists, behaving like any other interface on the machine: it shows up in ifconfig, you route over it, your firewall sees it, and nothing about it needs special handling.

The line worth pausing on is AllowedIPs, because it does two jobs that every other VPN keeps in separate systems. Outbound, it is the routing table: traffic for 10.7.0.2 goes to this peer. Inbound, it is the access rule: a packet claiming to come from 10.7.0.2 is only accepted if it arrived, correctly authenticated, from this peer's key. Donenfeld calls the arrangement cryptokey routing, and the practical upshot is that identity and route are the same statement. There is no situation in which a peer is authorised but misrouted, because there is only one place where either is written down.

One tunnel, one statement per peer peer A own private key public key of B peer B own private key public key of A encrypted tunnel ChaCha20-Poly1305, keys rotated every 120 s AllowedIPs = 10.7.0.2/32 route out, accept in AllowedIPs = 10.7.0.1/32 route out, accept in anyone without a key receives no answer at all

Two more things a person notices in the first week.

There is no connection. Nothing to establish, nothing to tear down, no session that can be up while the tunnel is down. The interface either exists or it does not, and packets either authenticate or they are dropped. A handshake happens when there is traffic and the current keys are older than two minutes, and it happens invisibly.

And roaming works without anyone arranging it. The endpoint address for a peer is learned from the most recent correctly authenticated packet, so when a telephone leaves the office wireless and picks up a mobile connection, the tunnel simply continues from the new address. An SSH session survives it. Nobody has ever thanked a VPN for that, which is roughly the intended effect.

Two teams, one specification

Now the claim. The famous figure has exactly one primary source, and it carries a qualifier that tends to fall off in transmission: the whitepaper says fewer than four thousand lines, excluding cryptographic primitives, and it means the Linux kernel module. Not the tools, not the Go implementation, not any other operating system.

Start with the implementation on this machine, since that is the one I can hold up. FreeBSD's wg driver is a separate piece of work from Donenfeld's Linux code, written against the specification rather than ported from it. With comments and blank lines removed, and the bundled cryptography counted separately, its protocol part is 4,364 lines in nine files.

Linux, measured the same way, is 4,703 lines in twenty-seven.

Two teams, two operating systems, no shared code, and a difference of seven per cent in the line count. A specification that lands two independent implementations that close together has said what it means, and that is a harder thing to achieve than any line count.

The file counts are where the two traditions part company. FreeBSD's driver holds the protocol in nine files: the interface, the Noise handshake, the cookie machinery, the allowed-IP table, and the cryptography beside them. Linux spreads the same work across twenty-seven. Neither arrangement is wrong, and the smaller one is easier to read end to end in an afternoon, which is the claim the whitepaper actually made and the one the base system happens to deliver.

The Linux side also carries the longer paper trail, which is worth having: at its kernel debut in version 5.6 the module was 4,569 lines across 27 files, and today it is 4,661 across the same 27. Six years and five months of maintenance in the busiest kernel subsystem there is, and the thing grew by ninety-two lines. The file count did not move at all. Kernel subsystems do not usually behave that way.

The comparison everyone reaches for has quietly stopped working, though, and it is worth knowing why before using it. OpenVPN has a kernel module now too, ovpn, merged in Linux 6.16 last July, and it measures 4,692 lines. Within thirty-one lines of WireGuard.

Source lines, logarithmic scale FreeBSD wg driver 4,364 Linux wireguard module 4,703 OpenVPN ovpn module 4,692 OpenVPN daemon 86,225 strongSwan 395,558 OpenSSL, parts in use 456,972 The top three bars carry the same job in three separate code bases. Only the first two contain a complete protocol.

The difference lies in what fits inside the size. WireGuard's lines are the entire protocol: handshake, key derivation, transport, rekeying, the lot. OpenVPN's 4,692 are the data path alone; its control channel, TLS session, certificate handling and configuration parser live in a userspace daemon of 86,225 lines, which leans in turn on OpenSSL, where the parts OpenVPN actually uses come to 456,972 lines. strongSwan, doing the same job by way of IPsec, is 395,558 lines in its own tree. So the fair statement is that WireGuard's module is the whole protocol, while OpenVPN's is one component of a considerably larger machine, and the seven lines of configuration above are what that architectural difference feels like from the outside.

Where the work lives WireGuard wg0.conf, seven lines kernel module handshake · key derivation transport · rekeying 9 files, 4,364 lines OpenVPN PKI: CA, certificates, keys config file, 297 options available userspace daemon control channel · TLS · certificates · 86,225 lines OpenSSL, 456,972 lines in use kernel module, data path only, 4,692 lines Both columns deliver an encrypted tunnel. One of them fits in a single box.

The binaries tell the same story inside one system, which is the only place a size comparison is ever fair. if_wg.ko is 147,008 bytes; ipsec.ko, built from the same source tree with the same flags in the same release, is 207,360. The tool wg is 91,112 bytes stripped and links against three libraries, none of them OpenSSL. No PAM either, and no plugin loader. The tunnel that ships with the system is smaller than the tunnel that has shipped with every Unix since the nineties.

The decisions you are not allowed to make

There is a second kind of smallness that a line count misses, and it is the one an operator feels.

A working WireGuard tunnel between two machines is seven lines of configuration and two key pairs, thirty-two bytes each. The equivalent OpenVPN setup, using the sample configuration files the project itself ships, requires a certificate authority, a server certificate, a server key, a client certificate and a client key, before anybody has typed a line about routing. wg-quick recognises fifteen configuration keys in total, and four of those are hooks for running shell commands. OpenVPN's manual documents 297 options.

Then there are the numbers nobody gets to touch. The file messages.h holds an enum of thirteen constants that govern the protocol's behaviour in time: rekey after 120 seconds, reject after 180, retry the handshake every 5, send a keepalive after 10 seconds of silence, accept at most 50 handshake initiations per second. None of them is exposed. There is no tunable, no sysctl, no configuration directive. The only timing value an operator may set is PersistentKeepalive, and that one exists for punching through NAT rather than for changing how the cryptography behaves.

An OpenVPN deployment that goes wrong at three in the morning usually goes wrong because somebody once set an option that made sense at the time. WireGuard removes that failure mode by removing the options, and thirteen constants in a header file are what the removal looks like in practice.

Security: a very short list, and one uncomfortable entry

Across six years and seven platforms, WireGuard's own components account for ten CVEs. Seven of them are in the Linux module, and their character is uniform: a memory leak when IPv6 is disabled, a data race fixed by a READ_ONCE annotation, a null-pointer dereference in the netlink code, an unaligned 64-bit access on PA-RISC hardware. Local fault classes, every one. None touches the cryptography, none permits remote code execution, and none has ever appeared in the exploited-vulnerabilities catalogue.

For scale, and using the same query against the same database over the same period: OpenVPN has 28, of which 22 score 7.0 or higher and five sit between 9.1 and 9.8. strongSwan has 8, of which 7 are 7.0 or higher. The usual caveat applies with force here, because attention manufactures CVEs and a tool nobody examines looks spotless. WireGuard is examined rather thoroughly, which brings us to the part that is genuinely uncomfortable.

The single serious failure in the whole record is one month old, and it is ours. FreeBSD advisory SA-26:52, published on 29 July 2026: the if_wg driver dispatched packets to the OpenCrypto framework and then failed to check whether the authentication tag had actually verified. Forged transport data was accepted. CVSS 7.5, reported by Reo Shiseki, fixed. It is the only case in six years where a WireGuard implementation lost a cryptographic guarantee, and it happened in FreeBSD's own reimplementation of the protocol rather than in Donenfeld's code. That belongs in this piece precisely because the rest of the sheet reads so well. What followed is also worth recording: a reporter with a name, an advisory with a number, a patch, and a public description of exactly what went wrong. A base system earns its keep in that sequence, since faults turn up everywhere and only the quality of the answer varies.

The four questions this series asks about attack surface get honest answers. It runs in kernel address space, which is more than root. It accepts network data from strangers by design. It gives up no privileges, having none to give up. The fourth answer is the one that carries the weight: it does not parse a complex format. There are four message types, a four-byte header consisting of one type byte and three reserved zeroes, a handshake initiation of exactly 148 bytes and a response of exactly 92. No variable-length fields, no extension list, no version negotiation, no cipher suite identifier. There is very little in that wire format for an attacker to work with.

The absence is deliberate and the whitepaper is blunt about the reasoning: cipher agility, it argues, increased complexity monumentally, and the continuing torrent of TLS vulnerabilities is the evidence. The handshake follows the IKpsk2 pattern of the Noise Protocol Framework, and the primitives are fixed at ChaCha20-Poly1305 for transport, Curve25519 for key exchange, BLAKE2s for hashing and HKDF for derivation, with XChaCha20-Poly1305 for cookies. Should one of them break, the plan is replacement rather than negotiation: the protocol identifier is hashed into every handshake, so a version two would simply be a different protocol that cannot accidentally speak to version one.

The handshake itself has been through four independent proof tools, at ACNS 2018, EuroS&P 2019, USENIX Security 2020 and NDSS 2024. Secrecy, authentication, forward secrecy and key-compromise-impersonation resistance all hold. Identity hiding does not, and both the researchers and the project say so. Every handshake message carries a MAC computed over the recipient's public key, which means anybody holding a candidate key can test it by recomputation. Since VPN providers publish their server keys to subscribers, the practical consequence is that WireGuard does not hide who is talking to whom. The 2024 paper went further and found something no proof had modelled: implementations precompute the static-static Diffie-Hellman product when an interface is configured and keep it in memory, which makes an attacker with access to that memory as powerful as one holding the private key.

Longevity: where it lives, and who keeps it

FreeBSD in the base system since 13.2. OpenBSD since 6.8, NetBSD since 10.0, Linux mainline since 5.6 in March 2020. The gap in that first sentence covers an almighty palaver.

Netgate commissioned a FreeBSD port that landed in the tree in November 2020. Two weeks before 13.0 shipped, Donenfeld and two others rewrote it in a week, and his account of what they found was unsparing: sleeps added to paper over race conditions, a validation function that returned true, printf statements deep in cryptographic code, and buffer overflows he called spectacular. Kyle Evans, who had done much of the rewriting alongside him, went through the accusations line by line on the same mailing list and disputed the plural in several of them. His verdict on the tone was that it was a highly unnecessary jab, and his verdict on the emergency rewrite was that three developers beating on code for a week is not a higher bar.

Then the core team did the thing that deserves the attention here: two weeks before a release, with the code written, reviewed in haste and technically working, they took it out. FreeBSD 13.0 shipped without WireGuard rather than with a driver nobody was prepared to stand behind, and development moved outside the base system until it was ready. It returned in 13.2, quietly and properly reviewed, which is the version most people now use without knowing any of this happened.

That is what the base system guarantee costs to keep, and it is the reason the guarantee is worth anything. A release train that cannot leave a feature on the platform will eventually ship one it should not have.

Maintenance today is one person. The MAINTAINERS entry for WireGuard lists Donenfeld and nobody else. The tools went nearly four years between releases at one point, the Go implementation has not carried a tag since 2020 while receiving commits into this year, and the Apple client has not been touched since August 2024. Read charitably, that is a finished protocol in careful maintenance, which is exactly what the release history of a completed thing looks like. Read carefully, it is a bus factor of one on software that terminates a good deal of the world's remote access.

The same author, for what it is worth on the question of whether the judgement behind those thirteen constants is sound, is co-maintainer of the Linux random number generator alongside Theodore Ts'o, and rewrote its entropy extractor from SHA-1 to BLAKE2s in version 5.17.

Stability: the interface that did not move

A wg0.conf written in 2016 works today, and the same seven lines run unchanged on all four operating systems. The protocol has carried the same identifier string since the beginning, and that string is hashed into the handshake, so compatibility follows from the design rather than from anybody's discipline about it.

The performance claim is where my own previous piece has to be withdrawn rather than confirmed. Ten gigabits, a hundred megabits, fifty-seven per cent faster: none of those numbers exists on wireguard.com, and none is in the whitepaper. I searched both. What the project actually publishes is a benchmark table from Linux 4.6.1 on gigabit hardware, showing WireGuard at 1,011 Mbit/s against OpenVPN's 258, which is a factor of 3.9 rather than a hundred, and which the project itself now heads with a warning that these benchmarks are old, crusty and not super well conducted. The WireGuard figure is simply line rate; the tooltip notes that the CPU was not maxed out.

Independent work is more interesting than the marketing would be. A Tübingen group measured on ten-gigabit links in 2020 and found that IPsec with AES-NI beats WireGuard when threads float freely, while WireGuard wins by about thirty per cent once CPUs are pinned. An Amsterdam study the same year gave the throughput crown to strongSwan with AES-GCM, and gave WireGuard connection setup by a distance that is hard to argue with: 6.9 milliseconds against strongSwan's 33.6 and OpenVPN's 1,152.7. Tailscale, working on the userspace Go implementation, reached 13 Gbit/s on a 25-gigabit card in 2023 by fixing segmentation offload.

The defensible version, then: WireGuard beats OpenVPN comfortably in every independent measurement, ties with well-tuned IPsec on throughput, and establishes a tunnel roughly a hundred and sixty times faster than OpenVPN does. Less quotable than what I published in December, and rather harder to argue with.

A working tunnel is seven lines of configuration and two keys of thirty-two bytes each. Six years of kernel maintenance added ninety-two lines to the thing underneath, and not one file.

The limit

The list of things WireGuard declines to do is longer than the list of things it does, and the refusals are the design.

It does not distribute keys, and the whitepaper says plainly that this is the wrong layer for that problem. It has no concept of a user, a group, a role or a directory: there is a public key and a list of permitted addresses, and that is the entire authorisation model. It does not assign addresses, and the project's own note on the subject observes that in some cases insane people want dynamic configuration. Everything an organisation actually needs therefore sits in the layer above, which is why Tailscale, NetBird, Headscale and Firezone exist, and why each ships a coordination server, an identity integration, a relay network and a policy engine that together dwarf the protocol underneath. Anyone deploying WireGuard for two hundred laptops is really deploying one of those, with WireGuard as the part that moves the packets.

It is also easy to spot on the wire. Fixed message types, a reserved field of zeroes, handshake lengths of exactly 148 and 92 bytes, and no padding anywhere: a middlebox needs nothing more, and does not need to decrypt a thing. Russian providers began blocking it in 2021, the Great Firewall followed in 2022, and by 2023 the rule in use was to drop a flow once more than two data packets follow a handshake. The AmneziaWG fork exists to add junk packets and randomised headers, leaving the cryptography untouched. Being quiet towards a scanner does nothing against a censor who is looking at the shape of the packets.

Add the identity-hiding gap, add the precomputation finding, add a maintainer count of one, and add the kernel address space, where FreeBSD's own reimplementation demonstrated last month what an unchecked return value costs there.

The whitepaper only ever claimed the tunnel and refused the rest on purpose, and the refusal is what kept the thing at 27 files.

Ninety-two lines in six years is what finished looks like, and almost nothing in this industry is allowed to get there.