Skip to content

Multiple clients (NAT mode) ​

There are three multi-client stories. They solve different problems:

  1. Static learning (default) — each client picks a distinct tun_ip. Clients can ping each other; you hand-edit every config.
  2. Automatic assignment — omit tun_ip / peer_ip. The server pushes a unique address. Same shared config, and clients can still ping each other. Pair with Magic DNS to reach peers by hostname.
  3. --nat (this page) — every client shares one placeholder tun_ip. Zero handshake, but clients cannot address each other.

With --nat the server tells clients apart by their UDP endpoint and maps each onto a distinct internal IP drawn from the TUN subnet, rewriting inner addresses as packets pass through. Every client can then run the same static config (same placeholder tun_ip) — no per-client setup, and no IP-assignment handshake (0-RTT: a client just starts sending). Assignment and --nat are mutually exclusive.

Setup ​

Server — add "nat": true (or pass --nat):

json
{
  "server": "0.0.0.0:8388",
  "password": "correct horse battery staple",
  "tun_ip": "10.9.0.1",
  "tun_netmask": "255.255.255.0",
  "peer_ip": "10.9.0.2",
  "nat": true
}

Clients — all run the ordinary static config, identical on every device:

json
{
  "server": "vpn.example.com:8388",
  "password": "correct horse battery staple",
  "tun_ip": "10.9.0.2",
  "tun_netmask": "255.255.255.0",
  "peer_ip": "10.9.0.1"
}

This pairs well with config URIs / QR codes: export one client config once and every device can import it unchanged.

How it works ​

The server keys a mapping by the client's UDP 4-tuple, allocates a free internal IP from the subnet (network/broadcast/server excluded), and rewrites the inner source on ingress (placeholder → internal) and destination on egress (internal → placeholder), fixing IPv4/TCP/UDP checksums incrementally.

Mappings are refreshed by traffic (data or the client keepalive, 15 s by default) and reclaimed after lease_ttl_secs / --lease-ttl-secs idle (default 120).

Trade-offs ​

  • Clients cannot address each other — they share one placeholder, so the topology is hub-and-spoke to the server and beyond.
  • ICMP error payloads that embed the original header aren't rewritten, so tunnelled PMTU discovery may suffer.
  • The per-packet cost is a couple of checksum deltas, negligible next to the AEAD.

Without NAT mode: distinct tunnel IPs ​

If you prefer clients to be individually addressable (or want to avoid the trade-offs above), skip --nat. Either:

  • give each client its own tun_ip (10.9.0.2, 10.9.0.3, …) in the server's TUN subnet, or
  • omit tun_ip / peer_ip and let the server assign a unique address to each device.

In the default learning mode the server maps (and re-maps, after a NAT rebind) each client's UDP address from its keepalive (or AssignRequest) announcements alone. Assigned clients can then ping each other through the hub relay; --nat clients cannot.

Released under the MIT License.