The README is now a landing page: the idea (a mesh VPN with no server, joined
with a network name and a secret), install and platform tables, exit nodes,
and a short build section. Implementation detail stays in docs/.
The flake builds tsng and the tray from source and ships a NixOS module
(services.tsunagi). Dist install notes now show the real `join -n -s` syntax,
and two links that pointed into the old README are redirected.
Co-Authored-By: Claude <noreply@anthropic.com>
The tray icon is dark red with no connection (or a lost exit node), blue when
connected and green while an exit node is in use; the windows get the logo and
an app id that matches the desktop entry, and the exit icon moves to the
bottom left of the tile. Exit-node and broadcast firewall rules fall back to
being recognised by their shape on kernels that have no `comment` match, as
small router builds do. The macOS archive carries Tsunagi.app for the Homebrew
cask, and the daemon plist opens the control socket to the admin group.
Co-Authored-By: Claude <noreply@anthropic.com>
The command line package is now `tsunagi` and builds the `tsng` executable,
which the packages, the unit, the installers and the workflows install and
run. The core library package becomes `tsunagi-core`, keeping the library name
`tsunagi`, so no code changes `use tsunagi::...`. Directories follow
(crates/tsunagi-core, crates/tsunagi). Every command shown to a person is now
`tsng ...`.
Co-Authored-By: Claude <noreply@anthropic.com>
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>