Zero-Leak OpenVPN App Sandbox: Proving an App Never Leaks Your Real IP When the VPN Drops
RobOS VPN Servers and RobOS VPN Sandbox let you bind individual Linux desktop applications (such as Firefox, Transmission, or CLI tools) to an OpenVPN provider (with 3,598 built-in IPVanish server profiles) inside a dedicated Linux network namespace (ip netns). Even if the OpenVPN daemon crashes or is force-killed (SIGKILL) mid-session while the app is actively transmitting, three independent kernel barriers prevent the app from ever falling back to your normal public IP.
216.131.74.203) while the normal public IP (105.42.88.19) is blocked, and the top banner reports the live 5-step mid-session OpenVPN drop kill-switch proof.
PUSH_REPLY, paired with 3,598 bundled IPVanish server profiles filterable by country and city.
robos_vpn_<id> namespace, verifies external exit IP over tun0 before starting the binary, and isolates browser profiles and D-Bus sessions.Why Traditional OS-Wide VPNs Leak When OpenVPN Stops
On a standard desktop VPN setup, OpenVPN installs split default routes (0.0.0.0/1 and 128.0.0.0/1) over tun0 on top of the host's primary default route (0.0.0.0/0 via 192.168.1.1 dev enp2s0). If the OpenVPN process crashes or drops the tunnel interface, the kernel automatically removes the /1 routes on tun0 and immediately routes in-flight sockets over the underlying host interface—exposing the user's real public IP.
The 3-Barrier Kernel Architecture
Instead of sharing the host network stack, RobOS VPN Sandbox (packages/robos-lib/vpn-sandbox-engine.js) constructs a per-application network namespace (robos_vpn_<id>) where physical interfaces do not exist and enforces three independent kernel firewall barriers:
robos_vpn_<id>.
flowchart LR
subgraph NS["Isolated Linux Network Namespace (robos_vpn_25db)"]
APP["Sandboxed App (Firefox / Transmission)
uid=1000, CAP_NET_ADMIN stripped"]
DNS["Namespace DNS & NSS
/etc/netns/robos_vpn_25db/resolv.conf
nsswitch.conf: hosts: files dns"]
IPT_NS["Barrier 1: In-Namespace iptables OUTPUT
Policy: DROP
1. -o lo ACCEPT
2. -o tun+ ACCEPT
3. -o veth_n_25db -d 216.131.74.202:443/udp -m owner --uid-owner 0 ACCEPT
4. -o veth_n_25db DROP (blocks uid 1000 -> EPERM)"]
OVPN["OpenVPN 2.7 Daemon
uid=0 (--dns-updown disable)"]
TUN0["tun0 (172.21.23.221)"]
VETH_N["veth_n_25db (10.203.41.2/30)"]
APP --> DNS
APP -->|Allowed when tun0 UP| TUN0
TUN0 --> OVPN
OVPN -->|uid=0 encrypted UDP:443| IPT_NS
APP -.->|When tun0 DOWN: falls back to default route| IPT_NS
IPT_NS -->|uid=0 ONLY to 216.131.74.202:443| VETH_N
end
subgraph HOST["Host Kernel (enp2s0 — Normal Public IP 105.42.88.19)"]
VETH_H["veth_h_25db (10.203.41.1/30)"]
IPT_FWD["Barrier 2: Host FORWARD Chain (ROBOS_VPN_FWD_25db)
ACCEPT only 10.203.41.2 -> 216.131.74.202:443/udp
DROP all other packets from veth_h_25db"]
IPT_NAT["Barrier 3: Host NAT POSTROUTING
MASQUERADE only -s 10.203.41.2 -d 216.131.74.202:443/udp
No NAT rule exists for any other destination"]
VETH_N --> VETH_H
VETH_H --> IPT_FWD
IPT_FWD --> IPT_NAT
end
subgraph EXT["External Internet"]
VPNSRV["IPVanish Server (atl-a01)
216.131.74.202:443/udp
Exit IP: 216.131.74.203"]
REALIP["Direct Host Egress (105.42.88.19)
UNREACHABLE FROM SANDBOX"]
IPT_NAT -->|Encrypted OpenVPN Carrier| VPNSRV
IPT_FWD -.->|0 packets allowed| REALIP
end
- Barrier 1 — In-Namespace
iptablesDefault-DROP + UID Owner Lock: The target application is spawned as the unprivileged desktop user (uid 1000) withsetpriv --inh-caps=-net_admin,-net_rawso it can never alter routes or firewall rules. Insiderobos_vpn_<id>,iptables -P OUTPUT DROPandip6tables -P OUTPUT DROPdrop all traffic by default. Only-o tun+is open touid 1000. On the carrier linkveth_n_<id>, only-m owner --uid-owner 0(the root OpenVPN daemon) destined to the exact VPN server IP and port (216.131.74.202:443/udp) is accepted, followed immediately by-A OUTPUT -o veth_n_<id> -j DROP. Whenopenvpnstops andtun0vanishes, any packet sent by the app is routed towardveth_n_<id>and immediately rejected by the kernel withEPERM. - Barrier 2 — Host Kernel
FORWARDChain Lock (ROBOS_VPN_FWD_<id>): On the host side of the virtual Ethernet cable (veth_h_<id>), a dedicated chain inserted atFORWARD 1only permits packets from10.203.X.2/32to<vpn_server_ip>/32:<vpn_port>and unconditionallyDROPs all other traffic fromveth_h_<id>before NAT. - Barrier 3 — Destination-Scoped Host
POSTROUTINGNAT: The hostMASQUERADErule is scoped strictly to-s 10.203.X.2/32 -d <vpn_server_ip>/32 -p udp --dport 443. Even ifFORWARDdid not exist, the host kernel has no NAT translation rule to masquerade10.203.X.2onto the normal public IP for any other destination.
Live 5-Phase End-to-End Kill-Switch Proof
To prove that stopping OpenVPN mid-session never exposes the host's real IP, the automated E2E test suite (tests/e2e/vpn-sandbox-killswitch-proof.test.cjs) executes proveKillSwitchOnVpnDrop() against the live Linux kernel:
- Phase 1 — Host Baseline: Queries
https://api.ipify.orgon the host outside the sandbox and records the real public IP (105.42.88.19). - Phase 2 — In-Namespace Egress While OpenVPN (
tun0) is UP: Starts an isolated namespace (robos_vpn_25db) connected to IPVanish Atlanta (atl-a01.ipvanish.com). Running insiderobos_vpn_25dbas unprivileged useruid 1000withCAP_NET_ADMINstripped, the test querieshttps://api.ipify.organd verifies the returned IP is the VPN exit IP (216.131.74.203, tunnel IP172.21.23.221) and never105.42.88.19. - Phase 3 — Force-Kill OpenVPN Mid-Flight (
kill -9): SendsSIGKILLto the OpenVPN daemon insiderobos_vpn_25dbwhile leaving the namespace, the running app, and the fallback default route (default via 10.203.41.1 dev veth_n_25db) up. Verifiestun0is destroyed. - Phase 4 — 4 Post-Kill Egress Probes from the Running App: With
tun0gone and theveth_n_25dbdefault route active, the unprivileged app insiderobos_vpn_25dbattempts four distinct egress connections:- Probe A (HTTPS IP Check):
GET https://api.ipify.org→leaked: false(blocked:EAI_AGAIN, UDP DNS packets dropped). - Probe B (Direct TCP to Public IP):
net.connect('1.1.1.1', 80)bypassing DNS →leaked: false(blocked:ETIMEDOUT, TCP SYN packets dropped). - Probe C (Direct UDP DNS Packet):
dgram.send('8.8.8.8', 53)→leaked: false(blocked immediately by kernel:EPERM). - Probe D (Direct UDP to Whitelisted VPN Server IP as App UID):
dgram.send('216.131.74.202', 443)asuid 1000→leaked: false(blocked immediately by kernel:EPERMbecause-m owner --uid-owner 0only allowsuid 0).
- Probe A (HTTPS IP Check):
- Phase 5 — Kernel
iptablesPacket Counter Proof: Inspectsip netns exec robos_vpn_25db iptables -L OUTPUT -v -n -xand verifies that14packets (1,070bytes) were intercepted and dropped by-A OUTPUT -o veth_n_25db -j DROP, with0packets leaking to105.42.88.19.