Fixed exit addresses, with the policy checked again
Two AI agents reach a partner's API through exits in two countries, each from a fixed address the partner allows. Every request is checked by the agent's own policy, then again at the exit, and both ends keep a record that joins up. Partway through, one exit fails, and a client with a stolen token tries the exit directly. Everything below was recorded in a real run in a private test network; the decisions you try yourself run the exit's own code in this page.
Loading Exit's engine
Clients
Exits
Destinations
Step 1 of 6
Two agents, two exits
A partner lets in traffic only from addresses it knows. Two agents need to reach it: a research agent and a CI job. Each runs under vpnw guard, sealed, so its only way out is the Agent, and the Agent's only way out is a list of two exits: exit-de first, exit-nl if exit-de stops answering. The agent's policy is a file. The exits read the same file for that client.
| Client | Policy (the same file at the Agent and at the exits) | From exit-de | From exit-nl |
|---|
The exits know each client by a token and keep only its SHA-256. The Agent reaches them over TLS and checks their certificates, and it sends its run's ID and each connection's number with every request.
Step 2 of 6
Each agent leaves from its own address
Both agents fetch from the partner through exit-de. The Agent allows each request, the exit decides it again with the same policy, and connects from the client's fixed address. The partner's servers report the address they saw.
The recorded run
The partner's allowlist names each agent's own address, not one address shared by everyone on the exit.
Step 3 of 6
The policy, checked at both ends
agent-1's policy allows the partner's names and nothing else, and refuses private addresses. Its first request below is refused by the Agent before anything leaves the machine. The second is allowed by name: names are resolved at the exit, so the Agent cannot see where intranet.partner.test points. The exit can, and refuses it.
The recorded run
The exit runs the Agent's own policy engine and request plan. In the tests, 110 requests under 5 policies went to the Agent and straight to an exit, with one DNS server: the two decided every one the same way, with the same rule and reason.
Step 4 of 6
A tampered client asks the exit directly
Someone copies agent-2's token into a script that skips the Agent and its policy, and asks exit-de for the cloud metadata address, the attacker's server and the intranet. The exit does not trust the client to have checked anything. It decides with agent-2's policy before it dials.
The recorded run
agent-2.toml the file exit-de reads for agent-2; you can edit it
Ask exit-de as agent-2
The answer comes from the exit's own Go code, running in this page, with the DNS answers of the recorded run. Nothing is sent anywhere.
A token gets a client in; it does not get it anything its policy refuses. Nothing the tampered client asked for reached a server.
Step 5 of 6
exit-de fails, and traffic moves to exit-nl
exit-de's process is killed. The next request of each agent tries exit-de, is refused a connection, and moves to exit-nl, the next exit on the Agent's list. Every request after that goes straight to exit-nl. The partner allows the Dutch addresses too, so the work goes on.
The recorded run
An exit that dies without a word, so that nothing comes back at all, takes longer to give up on: the Agent waits up to 3 seconds for an exit in a list to accept a connection and finish TLS. The tests measure both kinds of failure.
Step 6 of 6
One record, joined from both ends
The Agent kept a record of every connection, and so did each exit, tagged with the client's run ID and connection number. The exit's code in this page joins the four recorded files: each connection that reached an exit is matched with the exit's record of it, across the failover. Pick a row to see both records.
| Run / connection | Destination | At the Agent | At the exit |
|---|
What is real here
Recorded
- The run: vpnw 0.2.0 for both agents and vpnw-exit 0.1.0 for both exits, the real binaries with real TLS, on October 5, 2026. Steps 2 to 6 replay it from the four records they wrote and the test's own log. The replay is slower than the run, which took a few seconds.
- The private test network: Linux network namespaces on one machine. The two "countries" are two namespaces with addresses from documentation ranges.
Running in this page
- Exit's own Go code, compiled to WebAssembly: it reads the exits' configuration in step 1, decides your requests in step 4, decides the recorded exit requests again and joins the records in step 6.
Stand-ins
- The partner's servers and the DNS server are small programs written for the test, and the tampered client is a test script holding a copy of agent-2's token. The tokens were made for the run.
- No network: this page makes no requests.