Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quick Start

This walkthrough brings up the smallest useful mesh: one hub and two spokes, building a virtual 10.0.0.0/24 overlay. Two spokes is what makes the hub earn its name — it relays traffic between the spokes, which never talk to each other directly. It assumes you have the subnetrad daemon and subnetra control tool installed (see Installation).

Throughout: the hub is at the public address 203.0.113.1:18020, spoke A (overlay 10.0.0.2) at 203.0.113.2, and spoke B (overlay 10.0.0.3) at 203.0.113.3.

Every link needs its own 32-byte pre-shared key (64 hex chars). Never reuse one key across peers — so this two-link mesh needs two keys:

openssl rand -hex 32   # → KEY_A, for the hub ↔ spoke-A link
openssl rand -hex 32   # → KEY_B, for the hub ↔ spoke-B link

2. Write the configs

The simplest way is to set a role and let the daemon derive the forwarding policy at boot.

Hub (config.json on 203.0.113.1) — lists both spokes:

{
  "role": "hub",
  "virtual_subnet": "10.0.0.0/24",
  "local_id": 1,
  "listen_ports": [18020, 18023, 18026],
  "peers": [
    { "id": 2, "endpoint": "203.0.113.2:18020", "allowed_src": "10.0.0.2/32", "psk": "…KEY_A…" },
    { "id": 3, "endpoint": "203.0.113.3:18020", "allowed_src": "10.0.0.3/32", "psk": "…KEY_B…" }
  ]
}

Spoke A (config.json on 203.0.113.2):

{
  "role": "spoke",
  "virtual_subnet": "10.0.0.0/24",
  "local_id": 2,
  "local_tun_ip": "10.0.0.2/24",
  "local_routes": ["10.0.0.2/32"],
  "peers": [
    { "id": 1, "endpoint": "203.0.113.1:18020", "allowed_src": "10.0.0.0/24", "psk": "…KEY_A…" }
  ]
}

Spoke B (config.json on 203.0.113.3) — same shape, its own id and address:

{
  "role": "spoke",
  "virtual_subnet": "10.0.0.0/24",
  "local_id": 3,
  "local_tun_ip": "10.0.0.3/24",
  "local_routes": ["10.0.0.3/32"],
  "peers": [
    { "id": 1, "endpoint": "203.0.113.1:18020", "allowed_src": "10.0.0.0/24", "psk": "…KEY_B…" }
  ]
}

Each link carries its own PSK: KEY_A is shared by the hub’s peer 2 and spoke A; KEY_B by the hub’s peer 3 and spoke B — and the two keys differ. The spokes need no listen_ports (a spoke binds the single default port). See the Configuration Reference for every field.

3. Validate before running

--check parses the config, runs every sanity rule, and exits without touching the network:

subnetrad --check --config config.json
# spoke A: subnetra v… (mtu=1452, udp_ports={ 18020 }, mode=raw_direct, local_id=2, peers=1) [config ok]
# hub:     subnetra v… (mtu=1452, udp_ports={ 18020, 18023, 18026 }, mode=raw_direct, local_id=1, peers=2) [config ok]

4. Print and apply the host network plan

The daemon creates the TUN device but does not configure host addressing, routes, or MTU (that would break the zero-dependency guarantee). Ask it to print the exact commands instead:

subnetrad --print-network-plan --config config.json

Review the emitted ip link / ip addr / ip route commands and run them (on macOS the plan emits ifconfig / route). See Host Network Plan for details, including how the safe MTU is computed.

Run this on each spoke. The hub has no local_tun_ip, so its plan only creates the bare TUN device — it is a pure relay with no overlay address.

5. Start the daemons

# On the hub and both spokes (TUN creation needs NET_ADMIN / root):
sudo subnetrad --config config.json

For a real deployment, run it under systemd or launchd instead — see Production Deployment.

6. Verify connectivity

From spoke A, ping spoke B — the packet goes A → hub → B and back, exercising the hub relay:

ping 10.0.0.3

Then check the live counters on any node:

subnetra status

On the spokes you should see udp_tx / udp_rx climbing and the peer online; on the hub the relay_* counters increment as it forwards between the spokes. If traffic is not flowing, the drop counters tell you why — read Observability & Troubleshooting.

The hub here is a pure relay with no overlay address, so there is nothing at 10.0.0.1 to ping. To make the hub itself reachable, see Roles → Reaching the hub itself.

7. Add Site-to-Site routes (optional)

To reach a LAN behind spoke B (e.g. 192.168.3.0/24), the hub needs a relay rule for that prefix. Inject it at runtime — it hot-updates over the control socket with no restart:

# On the hub:
subnetra policy add --src 0.0.0.0/0 --dst 192.168.3.0/24 --action forward --target 3
subnetra policy show
subnetra save              # persist the active policy back to config.json

Spoke B must also deliver that prefix locally (add it to local_routes) and be allowed to source it (widen peer 3’s allowed_src on the hub to cover it). The full Site-to-Site walkthrough is in Production Deployment.

Where to go next