NAME
vpn-switch — POSIX-shell VPN connection manager for FreeBSD and
Linux
SYNOPSIS
vpn-switch [command]
[arguments…]
vpn-switch help
[subcommand]
vpn-switch bootstrap path
profile
vpn-switch start config
vpn-switch stop
vpn-switch sync
vpn-switch version
vpn-switch validate
vpn-switch inspect
vpn-switch session subcommand
[name | PID]
vpn-switch
wireguard|openvpn subcommand
[arguments…]
DESCRIPTION
vpn-switch is a POSIX-shell-based connection manager
for WireGuard and OpenVPN. It organises VPN configurations as a
filesystem database (directories group configurations by purpose,
symlinks express defaults and named sessions) and automates firewall and
DNS integration via composable phase scripts.
The tool follows a combinator architecture: every command is one of
three function types (terminal, combinator, batch combinator), each
outputs the next rewrite step rather than executing side effects
directly, and a small set of interpreters controls execution. The result
is dry-run-by-default behaviour for terminals and a deeply inspectable
command pipeline. See ARCHITECTURE.md for the full
design rationale.
After installation (see gmake install,
INSTALL.md), initialise a per-user database with
vpn-switch bootstrap, then import,
add, and start VPN configurations
through the commands documented below.
Concepts
- Database
-
A per-user directory (default $HOME/.vpn-switch/db) holding
imported configurations, environment overrides, active and saved
sessions, and logs. Created once via bootstrap, kept in
sync with the installed source via sync.
- Configuration
-
A static .conf (WireGuard) or .ovpn (OpenVPN) file
imported into the database. Lives under wireguard/ or
openvpn/, optionally grouped in subdirectories via
add and link.
- Patch
-
Provider configuration files are typically written for Linux — a
WireGuard .conf, for example, embeds
PostUp/PostDown hooks that call Linux’s
resolvconf. vpn-switch never runs the imported file as-is: at
start it patches a session copy — stripping those
Linux-specific DNS hooks (DNS is handled by the dns phase
instead) and, for OpenVPN, injecting the up/down scripts, daemon mode
and interface — so one config works on FreeBSD and Linux alike. Preview
the result with <proto> patch config.
- Session
-
A runtime instance of a started VPN — one Unix process plus its metadata
directory under .session/<PID>/ (interface, protocol,
cached connect/disconnect scripts). Sessions can be saved by
name for later resumption via session start
<name>.
- Phase
-
A composable step in the connect/disconnect script (typically
firewall, vpn, dns), run in order on connect
and in reverse on disconnect. Each phase is generated from a swappable
backend (firewall:
pf/ipfw/iptables/none; dns:
resolvconf/unbound/djbdns/dnsmasq/none),
selected per phase via a phase:backend syntax in
VPN_SWITCH_PHASES_CONNECT /
VPN_SWITCH_PHASES_DISCONNECT. Custom backends can be
added from a template, so sophisticated setups can trigger whatever
extra actions they need. See vpn-switch help phases.
- Profile
-
A predefined set of environment defaults installed by
bootstrap. minimal covers the essential
interpreters and safe display defaults; all installs the full
interpreter set for broader compatibility.
Command syntax
The canonical form is vpn-switch
<object> <verb> [args], where
object is wireguard, openvpn,
session, logs,
phases, env,
environment, database, etc.:
vpn-switch wireguard import ~/server.conf
vpn-switch session save work
vpn-switch logs clean
A small set of top-level <verb>-only shortcuts is
provided for the most common operations. Each delegates to one or more
object-verb forms:
vpn-switch start <config> -> wireguard start | openvpn start
vpn-switch stop -> wireguard stop + openvpn stop (batch)
vpn-switch sync -> phases sync + env sync + version sync
vpn-switch validate -> all <object> validate (batch)
vpn-switch inspect -> all <object> inspect (batch)
Use the shortcut when the object is obvious or you want the
bundled behaviour; use the explicit object verb form when you
need protocol- or subsystem-specific control.
COMMANDS
Without a database
The only commands that run without an existing database. Start here
on a fresh install.
- bootstrap <path> <profile>
-
Create a new database at <path> with the named profile
- help [<subcommand>]
-
Show top-level help, or help for a command group or topic
- complete [<words>]
-
Print completion candidates for a partially typed command line
(bash-completion back-end)
Connection
Bring tunnels up and down, and keep the database in sync with the
source.
- stop
-
Stop every active VPN session (idempotent)
- sync
-
Refresh the database from installed source templates
- version
-
Report the database and source SHAs (drift means run ‘sync’)
- start [<config>]
-
Connect using a config; with no argument, resume the default session or
pick at random
Sessions
A session is one started VPN instance: a process plus its
.session/<PID>/ metadata. Save, resume, inspect and reconcile
them.
- session disconnect <PID>
-
Lower-level disconnect of a session by PID (no name resolution)
- session stop [<name>|<PID>]
-
Stop an active session; with no argument, stop all active sessions
- session list
-
Show all active and saved sessions
- session show [<name>|<PID>]
-
Show a session’s cached connect script and metadata
- session remove <name>|<PID>
-
Remove a stopped session from the database
- session start [<name>|<PID>]
-
Resume a saved session by replaying its cached connect script
- session switch <name>|<PID>
[<from>]
-
Stop active sessions (all, or only <from>), then resume the target
session
- session save [<name>]
-
Save the most-recent session under a name for later resumption
- session clean
-
Remove stale session symlinks and orphaned sessions
- session refresh
-
Reconcile interface ownership with the loaded peers
- session help
-
Show session-management command help
- Reserved Session Keywords
-
latest - system-managed ownership pointer, updated on each start default
- user-managed, set via ‘session save’ with no name
Protocol-specific
(wireguard, openvpn)
Manage configurations, groups and tunnels for both
wireguard and openvpn. The two
subcommands share the same shape; substitute either where
<proto> appears.
- <proto> start [<category>]
-
Start a WireGuard tunnel; with no argument, use the default or a random
config
- <proto> stop [<interface>]
-
Stop WireGuard sessions, or one specific interface
- <proto> import <file>
-
Import a WireGuard .conf file into the database
- <proto> list [<category>]
-
List WireGuard configs and categories
- <proto> add <category> [<config>
[<alias>]]
-
Create a category, or link a config into one
- <proto> link <alias>
<target>
-
Create a protocol-level alias to a config
- <proto> remove <name>
-
Remove a config, link or category (with safety checks)
- <proto> validate
-
Check WireGuard symlink integrity in the database
- <proto> clean [<category>]
-
Generate cleanup commands for broken links
- <proto> patch <config>
-
Preview the patched config that would be used to connect
- <proto> info <category>
-
Show category details, including config expiry dates
- <proto> dump
-
Generate restorable shell commands for all WireGuard configs
- <proto> help
-
Show WireGuard command help
Database lifecycle
Back up, restore and refresh the database itself.
- phases sync [<phase>]
-
Refresh phase scripts (firewall, vpn, dns) from source templates
- env sync
-
Refresh environment defaults in .env/default/ from source templates
- version sync
-
Stamp the database’s .version with the source’s current SHA
- dump
-
Export the database as an executable shell script
- batch <file>
-
Execute a file of vpn-switch commands line by line
- restore <file>
-
Restore the database from a previously-generated dump file
Configuration
Read and change vpn-switch environment variables.
- environment cache [on|off|status]
-
Manage the cached-environment optimisation
- setenv <VAR> <VALUE>
-
Set a vpn-switch environment variable (writes to .env/local/)
- getenv <VAR>
-
Print the effective value of a variable with its source
- unsetenv <VAR>
-
Remove a variable from .env/local/
- printenv [<VAR>]
-
Show all effective environment variables, or just one
- helpenv [<name> [<location>]]
-
Show env-var documentation (value + docs); with no argument, list all
- setintp <fn> <value>
-
Set an interpreter (shortcut for setenv of the resolved interpreter var)
- getintp <fn>
-
Show an interpreter (shortcut for getenv of the resolved interpreter
var)
- helpintp <fn> [<location>]
-
Show docs for an interpreter variable (class default or per-function)
- environment inspect
-
Show the environment resolution chain (default -> local -> cache)
Diagnostics
Inspect current state and check health.
- status
-
Show a brief status of the active VPN and its interfaces
- inspect
-
Full descriptive state dump (sessions, configs, phases, system)
- logs validate
-
Check for old log files beyond the retention period
- logs clean
-
Generate rm commands for old logs (review, then pipe to sh)
- validate
-
Health check: run all sub-checks; non-zero exit on critical issues
- Individual validate sub-checks
-
Each check of ‘validate’ is also runnable alone for focused output,
e.g.: install validate, version validate, sudo validate, network
validate, database validate, binaries validate, permissions validate,
environment validate, phases validate.
ENVIRONMENT
- VPN_SWITCH_BASE
-
Path to the database directory. Default: $HOME/.vpn-switch/db.
Each user typically has their own database.
- VPN_SWITCH_LIBDIR
-
Path to the installed library directory (include/, template/). Set by
the installer (sed-patched into the script). Default if unset:
/usr/local/lib/vpn-switch.
- VPN_SWITCH_TERMINAL_INTERPRETER
-
How terminal functions’ output is processed. Default:
cat (display without executing — safe by default). Set
to sh to auto-execute.
- VPN_SWITCH_COMBINATOR_INTERPRETER,
VPN_SWITCH_BATCH_COMBINATOR_INTERPRETER
-
Interpreters for combinator and batch-combinator output. Defaults
recurse through the dispatcher; rarely need overriding.
- VPN_SWITCH_INTERPRETER_<function>
-
Per-function interpreter overrides. Highest priority in the resolution
chain. Used for dry-run inspection (set to cat),
logging, or privilege escalation (set to sudo sh). See
TUTORIAL_SUDO.md.
- VPN_SWITCH_PHASES_CONNECT,
VPN_SWITCH_PHASES_DISCONNECT
-
Ordered list of phases to run on connect/disconnect. Defaults: “firewall
vpn dns” and “dns vpn firewall” respectively. Set to omit phases
(e.g. “vpn” alone skips firewall and DNS integration).
- VPN_SWITCH_INTERFACE_wireguard,
VPN_SWITCH_INTERFACE_openvpn
-
Interface names for each protocol. Defaults: wg0 and
tun0.
- VPN_SWITCH_KEEPALIVE_wireguard
-
PersistentKeepalive (seconds) injected into patched WireGuard configs
that do not set one themselves; an explicit value in the config always
wins. Keeps tunnels alive across NAT rebinds and link flaps. Default:
25. Set to 0 to disable injection.
- VPN_SWITCH_RETENTION_DAYS_LOG,
VPN_SWITCH_RETENTION_DAYS_TRACE
-
Days to retain log/trace files. Default: 1. Set to 0 to disable logging.
Higher values useful during debugging.
- VPN_SWITCH_FUNCTION_OVERRIDE
-
Path to a shell file sourced after core modules. Allows replacing any
function (test mocks, local customisations). See
ARCHITECTURE.md.
FILES
- /usr/local/bin/vpn-switch
-
The main script (location varies with install PREFIX).
- /usr/local/sbin/{wg,ovpn}-resolvconf-{up,down}
-
DNS helper scripts (FreeBSD-specific; path hardcoded by phase scripts).
- /usr/local/lib/vpn-switch/include/*.sh
-
Shell module library (sourced at runtime via
VPN_SWITCH_LIBDIR).
- /usr/local/lib/vpn-switch/template/
-
Templates: environment defaults, phase scripts, platform variables,
VERSION marker.
- /usr/local/share/doc/vpn-switch/
-
Installed documentation (README.md, LICENSE,
QUICK_REFERENCE.md).
- $VPN_SWITCH_BASE/
-
Per-user database (sessions, configurations, environment overrides,
logs). Default: $HOME/.vpn-switch/db.
- $VPN_SWITCH_BASE/.env/local/
-
Per-user environment overrides. Highest priority. Edit via
setenv rather than directly.
- $VPN_SWITCH_BASE/.session/<PID>/
-
Active VPN session metadata (interface, protocol, original config,
cached connect/disconnect scripts).
- $VPN_SWITCH_BASE/session/<name>
-
Symlinks naming saved sessions for resumption.
- $VPN_SWITCH_BASE/.log/YYYY-MM-DD/
-
Per-day log and trace files. Subject to retention via the
*RETENTION_DAYS_** variables.
EXIT STATUS
- 0
-
Success, no findings.
- non-zero
-
For validate: critical findings detected (count of
issues, capped). For other commands: command-specific failure.
EXAMPLES
Bootstrap a per-user database and verify the install:
vpn-switch bootstrap ~/.vpn-switch/db minimal
vpn-switch validate
Import a config, group it, and connect:
vpn-switch wireguard import ~/Downloads/wg-CH-12.conf
vpn-switch wireguard add privacy wg-CH-12 switzerland
vpn-switch start switzerland
Save the live session by name, then resume later:
vpn-switch session save work
vpn-switch stop
vpn-switch session start work
Inspect what a command would do, without executing:
vpn-switch setenv VPN_SWITCH_INTERPRETER_start cat
vpn-switch start switzerland
# Output: the next-rewrite-step command, not its result.
After upgrading vpn-switch source, refresh the database:
sudo gmake install
vpn-switch sync
vpn-switch version
SEE ALSO
INSTALL.md(7),
TUTORIAL_QUICKSTART.md(7),
TUTORIAL_SESSIONS.md(7),
TUTORIAL_SUDO.md(7),
TUTORIAL_MIGRATION.md(7),
TUTORIAL_TROUBLESHOOTING.md(7),
ARCHITECTURE.md(7),
DEBUGGING_GUIDE.md(7)
wg-quick(8), openvpn(8),
pf.conf(5), resolvconf(8)
REPOSITORY
Source, issues and releases: https://github.com/enk-ode/vpn-switch
LICENSE
BSD 2-Clause License. See LICENSE in the source
distribution.
AUTHOR
Dr. Johannes Brügmann, with assistance from Claude (Anthropic),
2024–2026.