Skip to content

Configuration ​

Configuration can come from a JSON config file, CLI flags, or both. CLI flags take precedence over JSON file values. Defaults are applied for anything not supplied.

Both binaries accept -c, --config <PATH> to point at a JSON file.

Core fields ​

JSON fieldCLI flagMeaningRequiredDefault
server--listen / --serverserver: UDP bind address; client: remote host:portyes—
password-k, --passwordpre-shared password; master key derived from ityes—
cipher-m, --cipherAEAD cipher namenochacha20-poly1305
tun_name--tun-nameexplicit TUN interface name (e.g. utun7, tun0)noOS picks
tun_ip--tun-iplocal IPv4 address on the TUN interfaceserver: yes; client: omit with peer_ip for auto-assign—
tun_netmask--tun-netmaskIPv4 netmask for the TUN interfaceno255.255.255.0
peer_ip--peer-ippoint-to-point peer IPv4 (server: reserved static client; client: server IP)server: yes; client: omit with tun_ip for auto-assign—
mtu--mtuTUN interface MTUno1400
obfs(config only)carrier obfuscation: none | quic | base64 (both ends must match)nonone

On the server the server field is the UDP bind/listen address; on the client it is the remote server address to connect to.

There are more client-only fields for policy routing — see Policy routing and the full configuration reference.

Example: server config ​

json
{
  "server": "0.0.0.0:8388",
  "password": "correct horse battery staple",
  "cipher": "chacha20-poly1305",
  "tun_name": "utun7",
  "tun_ip": "10.9.0.1",
  "tun_netmask": "255.255.255.0",
  "peer_ip": "10.9.0.2",
  "mtu": 1400
}

Example: client config ​

json
{
  "server": "vpn.example.com:8388",
  "password": "correct horse battery staple",
  "cipher": "chacha20-poly1305",
  "tun_name": "utun7",
  "tun_ip": "10.9.0.2",
  "tun_netmask": "255.255.255.0",
  "peer_ip": "10.9.0.1",
  "mtu": 1400
}

Mirror images

tun_ip and peer_ip swap between the two ends: the server's local tunnel IP is the client's peer, and vice versa. On the client you can omit both and let the server assign a unique address. Joined peers resolve by hostname with Magic DNS (ping laptop, ssh pi.svpn).

Choosing a cipher ​

All ciphers are AEAD, from the RustCrypto project:

Cipher name (config)Key / salt length
aes-128-gcm16 bytes
aes-256-gcm32 bytes
chacha20-poly130532 bytes

The alias chacha20-ietf-poly1305 is accepted and treated as chacha20-poly1305. The default (when none is specified) is chacha20-poly1305 — a good choice everywhere, and the best choice on ARM hardware without a crypto-enabled build. See the ciphers reference.

Carrier obfuscation ​

The optional obfs field shapes the UDP payload so it doesn't read as an opaque random blob — quic wraps each datagram as a QUIC 1-RTT short-header packet, base64 encodes it as printable ASCII. Both ends must agree, and it has no CLI flag:

json
{
  "server": "vpn.example.com:8388",
  "password": "correct horse battery staple",
  "obfs": "quic"
}

This is cosmetic framing only — it adds no security. Details, trade-offs, and wire formats: carrier obfuscation.

Sharing configs between devices ​

  • Omit tun_ip and peer_ip so every device can share one client config and still get a unique, pingable tunnel IP. Magic DNS then lets you ping laptop instead of remembering 10.9.0.37.
  • Export/import a client config as a single shadowvpn:// URI or QR code — see Config URIs & QR codes. The node_id is not in the URI; it lives in <config>.state.
  • Run many clients off one identical placeholder config with the server's NAT mode when you do not need client↔client.

Released under the MIT License.