macOS Spoke
macOS is supported as a spoke that dials an existing Linux/RouterOS hub.
The data path runs natively on utun + poll(2), selected at comptime behind
src/os/. A macOS hub, kqueue, and automatic route mutation are explicitly
out of scope.
Because macOS has no network namespaces and hosted-mac CI runners cannot create a
utun without elevated privileges, the macOS spoke is runbook-certified rather
than CI-gated. The authoritative procedure is
docs/macos-spoke-acceptance.md.
Prerequisites
- A real Mac (Apple Silicon or Intel) with
sudoaccess —utuncreation needs root. - A release macOS binary, or Zig 0.16.0+ to build from source.
- A reachable, already-working Linux/RouterOS hub with a stable underlay endpoint and a per-peer PSK issued for this Mac.
- At least one remote overlay target to ping across the tunnel.
The macOS binary is minimal-dynamic — it links only
libSystem(still zero third-party deps), so it is not a static executable. Do not run the Linux-onlyldd → not a dynamic executablecheck against it.
Install
From a release tarball (subnetra-<version>-macos-arm64.tar.gz or -amd64):
tar -xzf subnetra-<version>-macos-arm64.tar.gz
cd subnetra-<version>-macos-arm64
# Gatekeeper quarantines downloaded binaries — clear it (or build from source):
xattr -d com.apple.quarantine subnetrad subnetra 2>/dev/null || true
Or build locally with zig build (see Installation).
Configure
Write a spoke config.json (see Roles) with the hub
as the single peer and this Mac’s overlay address:
{
"role": "spoke",
"virtual_subnet": "10.0.0.0/24",
"local_id": 4,
"local_tun_ip": "10.0.0.4/24",
"local_routes": ["10.0.0.4/32"],
"peers": [
{ "id": 1, "endpoint": "203.0.113.1:18020", "allowed_src": "10.0.0.0/24", "psk": "…64 hex…" }
]
}
Preview the host plan
On macOS the plan emits ifconfig / route commands (the daemon never applies
them):
./subnetra --print-network-plan --config config.json
Run
sudo ./subnetrad --config config.json
# subnetra v… (… mode=raw_direct …) tun=utun4 sock=/var/run/subnetra.sock [ready]
The utunN interface name is kernel-assigned — read it from the [ready]
banner, then apply the plan with the real name:
sudo ifconfig utun4 inet 10.0.0.4 10.0.0.4 mtu 1400 up
sudo route add -net 10.0.0.0/24 -interface utun4
Run under launchd
For a persistent spoke, install the system daemon plist (it runs as root, restarts
on abnormal exit, and logs to /var/log/subnetrad.log):
sudo install -m 0644 deploy/net.subnetra.subnetrad.plist \
/Library/LaunchDaemons/net.subnetra.subnetrad.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/net.subnetra.subnetrad.plist
sudo launchctl enable system/net.subnetra.subnetrad
Manage it with sudo launchctl kickstart -k system/net.subnetra.subnetrad
(restart) and sudo launchctl bootout system/net.subnetra.subnetrad (stop).
KeepAlivemay restart the daemon onto a differentutunN; re-read the banner and re-apply the plan. Subnetra deliberately leaves routing to you (no automatic route mutation).
Verify
sudo ./subnetra status # peer online, counters climbing
ping 10.0.0.3 # a remote overlay target across the tunnel