Run a VPN connection in an isolated Linux network namespace + mount namespace, and launch only selected applications through it — without touching your host routing table or breaking existing connections.
airlock is designed for the “I want one browser / one command to go through the VPN, not my whole machine” workflow.
- Per-application VPN routing using Linux network namespaces
- DNS isolation using a persistent mount namespace with an overlay on
/etc airlock up/airlock run -- …/airlock downlifecycle- Works with OpenConnect (AnyConnect-compatible VPNs)
- Linux with:
ip(iproute2),nsenter,unshare,sudo - VPN driver:
openconnect - Firewall backend:
nftoriptables - Optional but common:
systemd-resolved(handled)
From the repo:
make
sudo make installUninstall:
sudo make uninstallConfigs can live in:
$XDG_CONFIG_HOME/airlock/<name>.conf(usually~/.config/airlock/<name>.conf)/etc/airlock/<name>.conf
See section Configuration overview for details.
airlock --profile <name> upairlock --profile <name> run -- curl -sS https://api.ipify.orgairlock --profile <name> downIf you create:
~/.config/airlock/default.conf
then you can omit --profile default from all commands, and it will be used by default:
airlock up
airlock run -- curl -sS https://api.ipify.org
airlock downYou can also set one of these environment variables to specify a different default profile:
AIRLOCK_DEFAULT_PROFILE=<name>AIRLOCK_DEFAULT_CONFIG=<path/to/config>
airlock-firefox is a simple wrapper around airlock run -- firefox that passes some extra arguments to Firefox to
- ensure the VPN is up (runs
airlock upif needed) - launches Firefox with a custom profile by default
By default, airlock-firefox uses a dedicated Firefox profile and forces private browsing.
- Dedicated profile avoids cross-contamination with your main profile
-no-remoteallows running alongside your normal Firefox instance
Create a fresh profile directory and delete it after Firefox exits:
airlock-firefox --temp-profileDelete the configured profile directory (refuses if it appears in use):
airlock-firefox --clean-profilecurl -sS https://api.ipify.org; echo
airlock run -- curl -sS https://api.ipify.org; echoairlock run -- ip route
airlock run -- ip route get 1.1.1.1If the VPN is active, default routing should go via the VPN interface inside the namespace.
A config file is just a shell script that sets variables and defines one auth function.
Minimal required variables for OpenConnect:
OPENCONNECT_SERVEROPENCONNECT_USERAIRLOCK_AUTH_FUNCTION(name of a function you define)
Example config:
AIRLOCK_CONFIG_NAME='myvpn'
AIRLOCK_NAMESPACE='myvpn'
OPENCONNECT_SERVER='vpn.example.com'
OPENCONNECT_USER='alice'
OPENCONNECT_USERGROUP='mygroup' # optional, only if your VPN uses group-based auth
OPENCONNECT_EXTRA_ARGS=(--no-xmlpost) # optional, any extra args to pass to openconnect
AIRLOCK_AUTH_FUNCTION='my_auth_payload'
my_auth_payload() {
local otp pw
read -r -s -p "OTP: " otp < /dev/tty
echo >&2
pw="your-password-source-here"
printf '%s\n%s\n' "$pw" "$otp"
}Notes:
- The auth function must write the payload to stdout
- It should read prompts from
/dev/tty, because stdout may be piped/redirected - You can fetch the password from anywhere (prompt, keyring,
pass, etc)
At a high level:
airlock up- creates a network namespace
- creates a veth pair + NAT so the namespace can reach the internet
- starts a persistent helper inside:
- the network namespace, and
- a new mount namespace where
/etcis overlaid
- runs
openconnectinside that namespace so routes/DNS changes stay contained
airlock run -- <cmd>- enters the helper’s network + mount namespace via
nsenter - drops privileges to the calling user
- executes the command in the isolated environment
- enters the helper’s network + mount namespace via
airlock down- stops
openconnect - kills remaining processes in the namespace
- removes NAT rules and restores sysctl state
- deletes the namespace
- stops
airlockrefuses to run as root. It usessudointernally only where required.- Auth payload is handled in-memory (no temp files by default).
- The mount namespace design prevents common “DNS leakage” pitfalls on systemd-resolved systems.
This project is heavily inspired by:
nsdo- a practical tool for running commands in namespaces, and especially its approach to dealing with/etc/resolv.confvia mount namespace tricks: https://github.com/ausbin/nsdo- socketbox gist - a concise reference implementation of netns + veth + NAT + VPN patterns: https://gist.github.com/socketbox/929378a16b43ed9026a226eb25fabe18
Thanks to those projects for documenting and demonstrating these patterns.
- This project was almost entirely vibe-coded using ChatGPT 5.2 Thinking Mode, with some manual cleanup and testing. It’s possible there are security issues or bugs that I’m not aware of.
- Use at your own risk