VPN Works 0.7.0 Adds Exit With Fixed Addresses and a Second Policy Check at the Far End
VPN Works 0.7.0 brings Exit back, inside vpnw itself. vpnw exit runs on a server with fixed addresses, takes each client’s requests over TLS, decides every one again with that client’s own policy, and connects from the client’s own address.
Partners and internal systems often let traffic in by address, so agents and CI jobs need fixed addresses to leave from. The usual answer is a proxy with a fixed address, and a plain proxy trusts whatever reaches it with the right password. A hijacked client, or anyone who copied its token, can send anything through it.
An exit checks every request again, with the same policy file the client uses and the same code that decided it on the client’s machine. A client that skips its own check gets nothing that policy refuses.

How It Works
# a private CA and the exit's certificate
vpnw exit cert --host 198.51.100.2 --name exit-de
# a client's token
vpnw exit token --out agent-1.token
vpnw exit serve --config exit-de.toml
The exit’s file lists its clients: each with the hash of its token, its own address or a pool it shares, and its policy, usually the same file the client runs with. On the client’s machine, vpnw guard --proxy https://198.51.100.2:8443 --ca ca.pem --token-file agent-1.token sends the program’s connections there. Give --proxy twice and the client moves to the second exit when the first stops answering.
Names are looked up at the exit, so deny_private sees where a name really points. Whatever a client’s policy allows, the exit doesn’t connect to its own machine unless its file says that client may, and never to a cloud’s metadata service. Each connection in the exit’s record carries the client’s run and connection IDs, so the two records line up. And plugins run at the exit as they do in a run: Ledger can seal its record, and a Guard plugin gets a say, told which client asked.
What the Tests Showed
The tests run the real vpnw exit serve with the real vpnw as its client, and 21 of 21 checks passed. A client sent 11 forbidden requests straight to the exit with its valid token: its own policy refused all 11 at the exit, and none reached a server. With one policy file at both ends, the 3 requests the client refused never reached the exit, and the exit allowed the other 2 by the same rules. When the exit in use was killed after 3 of 10 fetches, the other 7 went through the second exit, from its own address, and none failed. Given the same policy and the same DNS answers, a run’s broker and an exit decided 513 of 513 requests the same way.
What the Review Found
An independent review went at the exit as an attacker would, in three passes, and found real problems before release:
- Anyone could take it offline. A few thousand connections that never logged in filled every slot. Now a connection gets 10 seconds to show its token, one address holds at most 32 waiting, and when all are taken the address holding the most gives way.
- A wildcard source.
source = "0.0.0.0"let the machine pick any address, another client’s included. It’s refused now, and each connection is checked for where it really left from. - The exit’s own machine. A client whose policy allowed
localhostreached every service on the exit. Now it’s out of reach unless the exit’s file says so. - DNS answers in refusals. A refusal named the address a name resolved to at the exit, so a token could be used to read the exit’s DNS. Now the client hears the rule, and the record keeps the rest.
Each is a case in the tests now. The review also tightened tests that would have passed without proving their claim.
Measured
| What | 0.7.0 |
|---|---|
| Exit checks, run with the real binaries | 21 of 21 passed |
| Forbidden requests sent straight to an exit with a valid token | 11, all refused |
| Requests decided the same way by a run and by an exit | 513 of 513 |
| Opening a connection through an exit, TLS and token included | 2.0 ms |
| Data through an exit, both ends on one machine | 511 MB/s |
| Test functions | 188 of 188 passed |
| Ways out of the sealed sandbox | 17 of 17 blocked, IPv6 included |
| Faults thrown at the WireGuard path | 18, all handled |
| Statements covered | 83.5% |
| Fuzz targets | 14, all clean |
How it was measured: test/results
What It Doesn’t Do Yet
An exit reads its file when it starts, so a new client means a restart. Its copy of a client’s policy is its own; when the client’s changes, copy it over. Over SOCKS5 a refusal carries no reason, where HTTP CONNECT carries the rule and why.
Next is 0.8.0, Tunnel plugins: plugins that hand WireGuard configs to the kernel and rotate them. Exit · Roadmap · Download