Reference

Every command, option, file, rule and event in vpnw 0.4.0. The same reference ships with the code as docs/reference.md; plugin authors want docs/plugins.md.

Commands

Command What it does
vpnw run [options] -- CMD [ARGS] runs a program through a chosen path
vpnw trace [options] -- CMD [ARGS] the same, printing every connection
vpnw guard [options] -- CMD [ARGS] the same, under a policy, enforced
vpnw learn [--from last|FILE] [--name NAME] [--ports] [--wildcards] [-o FILE] drafts a policy from traces, with the built-in Learn plugin
vpnw advise PLUGIN [--from FILE] [--set KEY=VALUE] [-o FILE] runs an Advisor plugin on traces
vpnw plugin list|info|add|remove|keygen|sign|trust|keys manages plugins
vpnw doctor checks what this machine supports
vpnw version

Options for run, trace and guard

Option Meaning
--policy FILE a file with a [policy] table, and optionally paths
--config FILE a file with [network] and [paths.NAME] tables
--via NAME a named path from --config or --policy
--direct connect from this machine (the default)
--wireguard FILE through a WireGuard tunnel, from a wg-quick file; vpnw runs the tunnel itself
--proxy URL socks5://, socks5h://, http://, https:// or socks5+tls://; given more than once, a list of exits with failover
--ca FILE, --token-file FILE for proxies over TLS
--dns MODE local or remote for --proxy; tunnel or local for --wireguard
--allow RULE, --deny RULE rules on the command line
--deny-private deny loopback, private and link-local addresses, cloud metadata included
--default allow|deny when no rule matches
--plugin NAME an Observer or Guard plugin alongside the run
--set NAME.KEY=VALUE a setting for a plugin
--out FILE, --json events to a file, or JSON on the console
-v, -q, --no-save more, less, and don’t keep this trace as last
--backend sealed|env sealed (Linux, enforced) or env (proxy settings only)
--allow-unix-sockets let a sealed program use Unix sockets
--no-deny-exit keep the program’s exit code when something was denied

A policy or a Guard plugin needs the sealed backend: the env backend only sets proxy variables, so it can’t stop a program that ignores them.

Exit Codes

Code Meaning
the program’s own it ran and nothing was denied
120 something was denied and the program exited with 0
121 usage or configuration error
122 no sealed backend on this machine
123 the path can’t be used, so the program never started
124 vpnw itself failed
126, 127 the program can’t be run, or wasn’t found

Files

version = 1
name = "agent"

[network]                  # the path used unless --via names another
type = "wireguard"
config = "office.conf"     # a wg-quick file, next to this one

[paths.home]
type = "proxy"
url = "socks5h://127.0.0.1:1080"

[policy]
default = "deny"
deny_private = true
allow = ["api.github.com", "*.githubusercontent.com", "pypi.org:443"]
deny = ["telemetry.example"]

A strict subset of TOML. Anything vpnw doesn’t know is an error with its line number, never skipped.

WireGuard Config Files

The usual wg-quick format. [Interface] takes PrivateKey, Address, DNS, MTU and ListenPort; Table, SaveConfig and FwMark are accepted and do nothing, since the tunnel has no routing table or interface. Each [Peer] takes PublicKey, Endpoint, AllowedIPs, PresharedKey and PersistentKeepalive. IPv6 endpoints are written [address]:port.

  • Shell hooks are refused. PreUp, PostUp, PreDown and PostDown stop the run: vpnw runs no commands from a config file.
  • DNS goes through the tunnel, to the config’s DNS servers. A config with no DNS server needs --dns local said out loud, and one whose DNS server is outside every peer’s AllowedIPs is refused.
  • The tunnel must be up before the program starts. vpnw waits up to 12 seconds for a handshake with every peer (WireGuard retries after 5) and stops with exit code 123 if one doesn’t answer.
  • AllowedIPs hold. A connection to an address outside every peer’s AllowedIPs is refused with that reason.

Policy Rules

A rule is a host (api.github.com), every subdomain of one (*.github.com, not github.com itself), an address (10.0.0.7) or a range (10.0.0.0/8), each with an optional port. Rules never match more than they say.

Every connection is decided in the same order: deny rules on the name, deny rules on every address it resolved to, deny_private on every address, allow rules, then the default. With an allow list and no default, the default is deny. A name is looked up only when an address could change the answer. If a name resolves to several addresses, all must pass. Guard plugins come after the policy, and can only refuse.

Events

One JSON object per line. Every event has v, ts, type and run; most have pid, path and conn.

Type Fields
run.start mode, backend, path_kind, path, policy, plugins
connection.attempt host or ip, port, proto
dns.query, dns.result host, then ips and ms, or error
policy.allow, policy.deny rule, reason; a Guard plugin’s rule is plugin:NAME
path.switch from, to, error, ms
connection.open, connection.close ip, exit, ms; then bytes_up, bytes_down
connection.error error, exit
plugin.error plugin, type, error
process.start, process.exit, run.end the program, its exit code, the totals
plugin.NAME.TYPE whatever plugin NAME emitted

Where Things Are Kept

What Where
the last trace ~/.local/state/vpnw/last.jsonl
installed plugins ~/.local/share/vpnw/plugins/
trusted signing keys ~/.config/vpnw/trusted-keys
compiled plugins ~/.cache/vpnw/wasm/, safe to delete

Each follows $XDG_STATE_HOME, $XDG_DATA_HOME, $XDG_CONFIG_HOME and $XDG_CACHE_HOME when set.