case studies

Sealed Sandbox Case Study: A Script That Ignores Proxy Settings Tries Three Ways Out and Finds None

A script that ignores proxy settings tries three ways out with a stolen deploy token: HTTPS by name with the proxy switched off, a direct connection to the attacker’s address, and a local service on a Unix socket that connects for whoever asks. Inside vpnw’s sealed sandbox none of them works.

The case was recorded with VPN Works 0.2.0’s demo kit; the screenshot is from that recording. The sandbox is the same code in 0.4.0, and its tests now try 16 ways out, not three.

A program under vpnw normally goes out through the proxy settings vpnw gives it. Most tools honor them. A script told to leak something has every reason not to. Without vpnw, the first way out worked. Under vpnw each failed at a different wall: the name couldn’t be looked up, the address was unreachable, and the socket wasn’t permitted. vpnw itself never saw a connection.

The 0.2.0 demo: a script that ignores proxy settings tries a direct connection, a DNS lookup and a child process, and all three fail inside the sealed sandbox

How It’s Sealed

On Linux, vpnw starts the program in a network namespace of its own. The only interface in it is loopback, and the only things listening there are vpnw’s two proxy ports. There’s no default route, so a direct connection fails with “network is unreachable” at once, and a DNS lookup has nowhere to go.

A seccomp filter closes what a namespace leaves open: Unix sockets, so the program can’t ask a local service like Docker to connect for it; io_uring, which can make sockets without calling socket(); and system calls through a second system call table.

The third way is the one people forget. A Unix socket lives in the file system, outside any network namespace, and some local services act on whatever their socket tells them. The Docker socket is the classic case: a program that can reach it can ask Docker to start a container on the host’s own network. The filter makes the script’s socket call fail before it gets that far.

Child processes inherit all of it. A script that starts another program to do its connecting gets nowhere either.

Tested Way by Way

The tests try each way out from inside the sandbox, and from outside as a control where that makes sense, so “blocked” means the sandbox blocked it:

Way out Result
TCP to loopback, this machine’s address, a public address, a private address, cloud metadata blocked
UDP to this machine and to a public DNS resolver blocked
A DNS lookup through the system resolver blocked
A raw IP socket blocked
A child process connecting on its own blocked
An abstract Unix socket, and one in the file system like Docker’s blocked
io_uring blocked
A system call through the second table blocked
TCP over IPv6, to loopback and to a public address blocked
ping blocked

17 of 17 were blocked, in VPN Works 0.4.1, on a GitHub Actions Linux runner. The full matrix

When a Program Needs a Socket

Some programs legitimately need a local socket: ssh-agent, a D-Bus session. --allow-unix-sockets lifts the socket filter for that run. The network stays sealed.