Bring the macOS stack up: utun interface, resolver files, system paths
macOS was TUN-less: the agent ran only with `--no-tun`, so tunnels formed but nothing reached the operating system. The provisioner, host rules and DNS publisher had Linux and Windows implementations and fell through to the unsupported/erroring paths on macOS. This adds the macOS half so `tsunagi up` carries real traffic there. The shared decision logic (plan_changes, rules_summary, Published) is untouched — only the per-platform mechanics are new. What it does on macOS (all root-only; macOS has no per-capability grant): - Interface: a `utun` device opened over the in-kernel utun control socket via the existing `tun` dependency (no kext, no Network Extension). The kernel assigns the `utunN` name, so a name derived from the network id is not a legal utun name; `open_tun` leaves the name unset on macOS and reads the real one back, and `Interface::start` adopts `device.name()` as the name everything downstream configures (host rules, reconfigure). A utun is torn down with the process, so a crash never leaks one. Addresses, MTU, up and the overlay subnet route are applied with `/sbin/ifconfig` and `/sbin/route` (absolute paths, args built only from locally-derived values), the same role netlink plays on Linux and netsh on Windows. Observation is `netdev` (getifaddrs). - DNS: a resolver-dir publisher writes `/etc/resolver/<network>` files pointing the system resolver at the overlay DNS server for each zone. The files carry a `port`, so the server needs no privileged socket, and a marker line, so the publisher only ever replaces or removes files it wrote and never clobbers a user's own. Removed on revert. - Broadcast host rules: the `255.255.255.255` route is installed with `route`. The inbound-UDP firewall allowance (a pf anchor) is deferred and reported as the incomplete half, so `status` says what to allow by hand; the route still goes in. Failure stays contained, exactly as on the other platforms. - Privilege: probe is optimistic (like Windows); the honest answer comes from the utun open, whose permission error now says to run with sudo or as a daemon. Deployment: - Default paths on macOS are fixed system locations rather than per-user Library dirs, so they do not depend on whose shell ran sudo: state `/var/db/tsunagi`, cache `/var/db/tsunagi/cache` (StoragePaths::user_default), control socket `/var/run/tsunagi/agent.sock` (default_control_socket_path, mirroring the Windows %ProgramData% branch). The local control socket already uses the shared `unix` adapter, so no IPC changes were needed — macOS is Unix. - dist/macos/ carries a root LaunchDaemon plist and install notes; README's deployment section now documents the macOS path instead of "not implemented". Decisions (agreed up front): utun auto-assign with the real name adopted; ifconfig/route rather than raw PF_ROUTE/ioctl (which would need unsafe); /etc/resolver files rather than scutil; route-now/firewall-deferred; root via sudo or a LaunchDaemon rather than Network Extension (which would require a signed sandboxed app bundle, not a Rust CLI). Checks: on macOS (Darwin 27), `cargo fmt --all --check`, `cargo clippy --locked --workspace --all-targets -D warnings`, and `cargo test --locked --workspace --all-targets` all pass (0 failures); the new macos-gated unit tests run under the standard workspace command via feature unification. Not yet exercised live: actual utun creation, ifconfig/route application and /etc/resolver writes need a root run, which has not been done in this session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
533921059a
commit
c3a6201ee2
16 files changed
+1349
-22
No files matched your search
@@ -478,6 +478,7 @@ The original IP packet, including its DF bit, is preserved. See
|
||||
| `setcap cap_net_admin+p` | ordinary user, one capability | recommended: nothing to prepare, nothing left behind. Lost on every rebuild or copy of the binary. |
|
||||
| systemd service | `User=`, `AmbientCapabilities=CAP_NET_ADMIN` | the same, for an installed service |
|
||||
| `sudo tsunagi up` | root | everything works, nothing is isolated |
|
||||
| macOS root LaunchDaemon | root | the native macOS deployment; see below |
|
||||
| `--no-tun` | ordinary user, no capabilities | tunnels run and handshake, traffic never reaches the OS |
|
||||
|
||||
On Windows, put `wintun.dll` beside the executable and start `tsunagi up`
|
||||
@@ -489,9 +490,24 @@ of claiming the agent is absent or trying to edit its locked state.
|
||||
Creating a TUN without sufficient privileges also explains how to restart
|
||||
the agent with the required permissions.
|
||||
|
||||
**Not implemented yet.** macOS has no provisioner; `--no-tun` is the way to
|
||||
run it. The control plane and tunnels are unaffected. The decision logic
|
||||
that says *what* to change is shared and tested on every platform.
|
||||
On macOS the agent creates a `utun` interface, configures it with `ifconfig`
|
||||
and `route`, and points the system resolver at the overlay by writing
|
||||
`/etc/resolver/<network>` files (which carry a port, so the DNS server needs no
|
||||
privileged socket). All of this needs **root**: macOS has no per-capability
|
||||
grant like Linux, and a `utun` plus `/etc/resolver` are root-only. The kernel
|
||||
assigns the interface its `utunN` name, so the name derived from the network id
|
||||
is only a request and the real one is adopted once the interface is up.
|
||||
|
||||
Run it as root — `sudo tsunagi up` for a quick start, or install it as a root
|
||||
**LaunchDaemon** for the native always-on deployment. As a daemon the agent runs
|
||||
unprivileged commands (`join`, `status`, `dns`, `network`) over its control
|
||||
socket like any other platform; the socket lives at `/var/run/tsunagi/agent.sock`
|
||||
and the stores at `/var/db/tsunagi`, both fixed system paths so they do not
|
||||
depend on whose shell started `sudo`. A ready-to-edit plist and install notes
|
||||
are in [dist/macos/](dist/macos/). The broadcast route is installed; the
|
||||
inbound-UDP firewall allowance (a `pf` anchor) is not yet, so `status` reports
|
||||
that half as incomplete and names what to allow by hand. The decision logic that
|
||||
says *what* to change is shared and tested on every platform.
|
||||
|
||||
## Checks
|
||||
|
||||
|
||||
Reference in new issue
Block a user