testing

How VPN Works 0.4.0 Was Tested: 121 Tests, a Real WireGuard Peer and 14 Ways Out Blocked

Every number on vpnw.com comes from one script in the code repository, run on one Linux machine with 2 CPUs. Its output is committed next to the code, so anyone can check a figure against the run that produced it.

The script is tools/record-results.sh, and it writes to test/results. It builds the plugins and the binaries, runs every test, runs them again under Go’s race detector, measures coverage, runs nine fuzz targets, times the plugins and the WireGuard path, and tries every known way out of the sealed sandbox.

The tests aim at what VPN Works promises, more than at line counts. Here’s what that means for each promise.

How 0.4.0 was tested: 121 of 121 tests passed, 14 of 14 ways out blocked, 82.5% of statements covered, 9 fuzz targets clean, 70 MB/s and 0.26 ms through a WireGuard tunnel, 0.15 ms for a Guard decision, 0 race failures

A Sealed Program Can’t Get Out

A sealed program tries 16 ways around vpnw: straight to a public address, a private one and cloud metadata; UDP; a DNS lookup; a raw socket; a child process connecting on its own; two kinds of Unix socket; io_uring, which can make sockets without calling socket(); and system calls through a second table. Each is tried from inside the sandbox and, where it makes sense, from outside as a control, so a blocked result means the sandbox blocked it. 14 ran and all 14 were blocked. The 2 over IPv6 couldn’t run, because the test machine has no IPv6. That’s the next gap to close. The bypass matrix

Plugins Can’t Take the Run Down

The plugin tests use small plugins built to misbehave. One panics on its second event. One spins in an endless loop. One writes to the console without asking for the permission. One falls behind by sleeping on every event. One exports a hook with the wrong signature. One refuses connections with a reason full of terminal escape codes.

Each is checked for what should happen. The one that writes without permission is refused before any of its code runs. The panic and the endless loop stop the plugin and nothing else, and after the loop, a check makes sure Go’s garbage collector can still run, because that’s how an earlier design failed. The slow one loses events but never slows the program. The escape codes come out as harmless question marks. A Guard that breaks refuses every connection after it.

WireGuard Works, and Fails Safely

The tests run a real WireGuard peer, in user space, the same way vpnw runs its end. Inside its tunnel it has a web server that says who’s asking, and a DNS server that knows a few names. The real vpnw binary reaches the web server by name through the tunnel, and the peer sees the tunnel’s address, not the machine’s. The DNS server sees the lookup, so names went through the tunnel.

Then the failures. A key the peer doesn’t know: the run stops before the program starts. An address outside the tunnel’s AllowedIPs: refused with that reason. deny_private against the tunnel’s own private addresses: refused, and nothing reaches the peer. A config with a PostUp shell hook: refused. A config whose DNS server the tunnel would drop: refused at the start.

The Numbers

What Result
Test functions 121 of 121 passed
Under the race detector no failures
Statements covered, unit tests and the real binary together 82.5%
Fuzz targets: config, policy rules and decisions, broker, proxy URLs, exit answers, WireGuard configs, plugin manifests, signatures 9, all clean
A connection opened through a WireGuard tunnel 0.26 ms
Data through a WireGuard tunnel, both ends on the same machine 70 MB/s
A Guard plugin’s decision 0.15 ms
One event through the Trace plugin 0.44 ms
Starting a plugin, compiled code from the cache 63 ms

What It Doesn’t Show

One machine is one machine. The figures come from a 2-CPU Linux VM, and the WireGuard peer ran on it too, so the throughput is what two ends of the tunnel manage on two CPUs, not what a real network will give. No IPv6. And a test suite, however pointed, isn’t an outside security review, which the project still wants before it calls anything 1.0.

Before each release, an independent review read the code that changed, looking for ways to break it. For 0.3.0 it found a plugin that could crash vpnw with a malformed export, a sleep that ran past a Guard’s time budget, and a burst of connections that could switch a Guard off; for 0.4.0, a handshake timeout too short to survive one lost packet, and a quiet fallback that could have sent DNS outside the tunnel. All were fixed, with tests, before the release went out.