master
33
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4e235cbac6 |
Keep device data paths on one line
The tray used a fixed sample for the data-path column, so longer relay names wrapped and made device rows uneven. Measure each rendered path and retain the widest value, then render path labels without wrapping. Replace direct/via words with embedded Phosphor route icons while preserving transport, relay name, hop count and explanatory tooltips. The app already ships the font; keep its setup in one function and verify registration alongside a long-relay-width regression test. No wire or stored-state changes. Checks: cargo fmt --all -- --check; cargo clippy --locked --workspace --all-targets -- -D warnings; cargo test --locked --workspace --all-targets. |
||
|
|
21328bf818 |
Windows installer: agent service, Wintun, tray and PATH in one setup program
Running on Windows needed wintun.dll by hand, an elevated console and a
manually started agent; without the DLL the agent silently ran with no
interface (and so no exit node or routing at all).
`tsng service` (hidden) runs the same `up` under the Windows service manager:
src/winservice.rs registers with the SCM through the windows-service crate,
reports Running/StopPending/Stopped, and maps a stop request onto the existing
shutdown path (stop_signal). The only unsafe is the extern "system" entry point
that define_windows_service! expands to, allowed in that module alone. A
service has no console, so it logs to %ProgramData%\Tsunagi\logs\tsng.log
(one older file kept past 8 MiB). Its state defaults to
%ProgramData%\Tsunagi\{data,cache} and its control socket to the system-wide
default every client already uses, so an unelevated `tsng status` and the tray
find it.
dist/windows/tsunagi.iss (Inno Setup) installs tsng.exe, the tray and Wintun
0.14.1 (pinned by SHA-256 in CI, licence text shipped), registers the
"Tsunagi" service as LocalSystem with auto start and restart-on-failure, locks
the data/cache/log directories to SYSTEM and Administrators by SID, and by
tasks adds tsng to the PATH, an inbound firewall rule for the agent and a tray
autostart. It stops the service, the tray and any hand-started agent before
replacing files; uninstall removes the service, rule and PATH entry and asks
before deleting the data (never on a silent uninstall). The release workflows
(GitHub and Gitea) build it on the Windows job and publish
tsunagi-setup-<version>-x86_64.exe.
.cargo/config.toml links the MSVC C runtime statically so nothing needs a
Visual C++ redistributable on a clean machine.
Compatibility: an agent run by hand keeps its data in the user profile; the
service has its own, so networks must be joined again once.
Checks: cargo fmt --check, clippy -D warnings, cargo test --workspace passed on
macOS (one unrelated wireguard restart test failed once under load and passed
on three reruns). winservice.rs was cargo-checked for x86_64-pc-windows-msvc in
a scratch crate; the whole tsunagi crate cannot be built for Windows from
macOS (aws-lc needs the Windows SDK). The .iss script and the CI step were NOT
compiled or run: no Inno Setup or Windows here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
||
|
|
7c176e0b74 |
Exit nodes on Windows: use a member as exit node via destination bypass routes
Choosing an exit node on Windows did nothing: the using side was refused, because Windows has no per-user routing and the agent's own traffic would loop into the tunnel it carries. Now the overlay interface gets 0.0.0.0/1 and 128.0.0.0/1 routes (and ::/1, 8000::/1 as an IPv6 blackhole), and every underlay address the agent talks to directly gets a /32 route through the physical default gateway, marked with route metric 7373 so stale ones are found and removed. Bypass routes are written before the halves and removed after them; a failed step removes what was written. All routes are in the active store. The addresses come from the runtime: each network runtime hands the interface the verified direct IPv4 paths of its control connections and the relay hosts in use (Interface::set_underlay, refreshed every dial round); relay names are resolved when the rules are applied. ExitHostRules::needs_bypass (true only on Windows) gates it, so Linux and macOS plans never carry or re-apply on path changes. ExitHostPlan gains `bypass` and `bypass_hosts`; networks leaving forget their entry. Limits: a path that appears after the last apply goes through the tunnel until the next apply; data-plane-only addresses are not collected separately (the data links ride the same iroh paths). Nothing was run on Windows. Checks: cargo fmt --check, cargo clippy --locked --workspace --all-targets -D warnings and cargo test --locked --workspace --all-targets passed on macOS; scripts and ordering are unit-tested with an injected runner and resolver, and Interface::set_underlay with the mock host. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
637915600e |
Exit nodes on Windows: offer through WinNAT; using is refused
Windows reported exit nodes as unsupported. The offering side now creates one WinNAT object per offered range (New-NetNat, named tsunagi-exit:<if>:<range>), prunes this interface's stale ones, reads (never writes) IPv4 forwarding of the overlay interface, and removes everything on clear. All values are single-quoted PowerShell literals. The using side is deliberately not implemented: Windows has no per-user or per-process routing, so the agent's own QUIC/relay traffic cannot be kept out of a default route through the overlay and would loop. It reports that reason and installs no route. The CLI/tray forwarding hint and the kernel-forwarding warning name the Windows command; docs/exit-node.md has a Windows section. Checks: cargo fmt --check, cargo clippy --locked --workspace --all-targets -D warnings and cargo test --locked --workspace --all-targets passed on macOS. The script builders and plan logic are unit-tested with an injected runner on every platform; nothing was run against a real Windows host or cross-compiled (no Windows target installed). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
47c5d38d0c |
Exit nodes on macOS: pf anchor for offering, routes plus route-to for using
The exit-node host rules existed only for Linux (iptables + policy routing); macOS reported "only supported on Linux yet". Offering: a pf anchor com.apple/tsunagi-exit-<if> (evaluated by the stock pf.conf) holds a nat rule per offered range and a pass-in rule. pf is enabled if needed and the pfctl -E reference is released when the rules are removed. net.inet.ip.forwarding is only read, never written, as on Linux. Using: macOS has no uidrange policy routing and the agent is root, so the overlay gets 0.0.0.0/1 and 128.0.0.0/1 routes and a pf route-to rule (matched by the agent's uid) sends the agent's own QUIC/relay traffic back out the physical interface, translated to its address. The rules are loaded before the routes, so a failed load never sends traffic into a tunnel it cannot exempt the agent from. IPv6 is blocked with rejecting ::/1 and 8000::/1 routes. ExitHostPlan gains `overlay` (every network range) for the route-to exemption; Linux ignores it. Known limits: the egress is the default route's interface at apply time; other root processes also bypass the tunnel. Forwarding hints in the CLI and tray name the macOS sysctl. The rule text and command order are unit-tested with a scripted host; nothing was run against a live pf/route (needs root), so the route-to + nat second-pass behaviour is unverified. Checks: cargo fmt --check, cargo clippy --locked --workspace --all-targets -D warnings and cargo test --locked --workspace --all-targets, run on macOS only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
3e066f991f |
CI: install libxdo-dev for the Linux tray link
Release / tsunagi-linux-aarch64 (push) Successful in 3m23s
Release / tsunagi-linux-x86_64 (push) Successful in 6m58s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The x86_64 Linux tray failed at link with "unable to find library -lxdo": tray-icon pulls libxdo, whose .so was not present. Add libxdo-dev to the GUI build's apt install, alongside libgtk-3-dev and libayatana-appindicator3-dev. Everything else (aarch64 agent, macOS, Windows) already built. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3752419075 |
CI: build the Linux tray natively with GTK; drop the aarch64 GUI
Release / tsunagi-linux-aarch64 (push) Successful in 4m14s
Release / tsunagi-linux-x86_64 (push) Failing after 4m29s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The aarch64 GUI cross-build failed: tray-icon links GTK and libappindicator through pkg-config, which zig cross-compilation cannot satisfy without a target sysroot. Rather than set one up, the Linux tray is now built natively on the x86_64 runner with libgtk-3-dev and libayatana-appindicator3-dev installed, so pkg-config just works. The agent is still static musl for both arches. The aarch64 Linux GUI is dropped for now: that archive stays agent-only and no aarch64 tsunagi-gui package is produced. x86_64 gets the tray in its archive and the tsunagi-gui .deb/.pkg.tar.zst. macOS and Windows build the tray natively as before. Guarded on matrix.gnu_target (set only on x86_64 Linux). Note: the native build links the runner's glibc, so the Linux GUI binary needs a reasonably recent glibc; fine for current desktops. A lower baseline would need a GTK sysroot or an older runner image. YAML validated; the Linux GUI build is exercised only by a real CI run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f63343a6a0 |
CI: ship the tray GUI — in the archives and as tsunagi-gui deb/zst
Release / tsunagi-linux-aarch64 (push) Failing after 2m40s
Release / tsunagi-linux-x86_64 (push) Failing after 2m40s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Adds the tray application to the release: - Every target's archive now also contains the tsunagi-tray binary. On Linux it is built against an old glibc baseline (via cargo-zigbuild) rather than static musl, because a GUI must dlopen libGL/X11/Wayland at runtime and a static musl binary cannot; the agent stays static musl. macOS and Windows build the tray natively. - New GUI package variants for Linux: `tsunagi-gui` .deb and .pkg.tar.zst, built by the existing scripts with an extra tray argument. They install the agent (service, polkit, user — same as the headless packages) plus the tray binary, a desktop launcher (dist/linux/tsunagi-tray.desktop) and an icon (dist/linux/tsunagi.svg), so the GUI appears in the applications menu. The GUI package conflicts/replaces/provides the headless tsunagi so the two do not coexist. - The tray finds the system service with no configuration: the library's default control-socket path now prefers /run/tsunagi/agent.sock when present (both the tray and CLI use this), falling back to the per-user path. No control-protocol change. Builds, fmt and clippy clean on macOS; the Linux GUI cross-build and the packages are exercised only by a real CI run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d13bb00623 |
Tray: one device table (online + offline), FQDN copy, green-fade copy feedback
- The devices window now shows a single device table instead of a separate "connected" box and an easily-missed "offline" box below it. Every endpoint known from a live tunnel or the signed membership appears, online first; offline rows show name and address with dashes for the live columns. The table fills the window and scrolls, so nothing hides off the bottom. - Clicking an online peer's name copies hostname.network (the FQDN) so it resolves; a name without a hostname copies its short id. - Copy feedback no longer appends "Copied" text (which reflowed the table for a second). The field just flashes green and fades back over a second, with no change to its text or width. Builds and is clippy-clean (-D warnings) on macOS; not run in this headless session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2a0a8abd60 |
Tray: split rate/plot columns, add unit toggle to the devices window
The connected-peers rate and sparkline shared one grid cell, so the plot still moved right as the rate text grew (e.g. "1.0 kB/s"). They are now two separate grid columns — the plot column starts at the same x on every row, since grid columns align. The devices window also gets its own pkts/bytes toggle, bound to the same unit setting as the main window so the two stay in step. Builds and is clippy-clean (-D warnings) on macOS; not run in this headless session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
db01c58a6f |
Tray: Phosphor icons, click-to-copy fields with feedback, steady plot column
- Bundle egui-phosphor (pinned to egui 0.31) and register its glyphs, so the
icon buttons render real icons instead of tofu: refresh, shuffle (generate
secret), pencil/check/x (rename host), copy (Copy secret).
- Copying is now the field itself, not a separate button: clicking the
shortened device id, the network name, the network id, a range or an address
copies the full value and the field flashes green "Copied" for a second
(tracked in egui temp memory, so it clears itself). The standalone copy
buttons are gone.
- In the devices window the connected-peers "rate / plot" column put the rate
and the inline sparkline in a shared row, so the plot shifted as the rate
text changed width ("0 B/s" vs "1.1 kB/s"). The rate now sits in a
fixed-width, truncating cell, so the plot stays put.
Builds and is clippy-clean (-D warnings) on macOS; the UI has not been run in
this headless session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
de56258eb1 |
Tray: copy a network secret via ipc; use text buttons, not missing glyphs
Two fixes. Secret copy: a new local control request returns a configured network's secret in its tsn1 text form, so the tray's "Copy secret" button can put it on the clipboard. The agent reads it from local state (same source as the CLI's `network secret show`); it crosses only the owner/group socket and is wrapped in a RedactedText type whose Debug prints "<redacted>", so it can never land in a log. In the GUI the worker fetches it and hands it to the UI thread (which owns the clipboard) through a one-shot slot kept out of the status snapshot, so it is copied once and never stored in the cloned snapshot. Control protocol bumped 16 -> 17. Icons: the button glyphs (⧉ ⟳ ✎ ✔ ✕) are not in egui's bundled fonts and showed as tofu squares, so they are now plain text: copy / refresh / random / edit / save / cancel. No icon font to embed. Checks: fmt, clippy --workspace --all-targets -D warnings, and the tsunagi lib, tsunagi-cli and wg-quic local_control tests pass on macOS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8f5ce21ce9 |
Tray: devices window, bytes/packets toggle, copy & edit affordances
Replaces the in-tile collapsing peer list with a "Show devices" button that opens a separate, normal-sized window per network, and fills in the rest of the feedback. Main window: - A global show: pkts/bytes toggle; all rates and sparklines follow it. - Sparklines now reserve their space always (fixed height, "no data yet" placeholder) so the layout no longer jumps when data arrives, and each one overlays its peak (top-left) and current value (bottom-right) with units. - Device id has a copy button; the hostname is editable in place (✎ → edit → ✔/Enter sends SetHostname); network name, range and address copy on click. - The Leave button is a neutral red. Per-tile "Show devices" opens the detail. Devices window (a separate egui viewport per network): - Network summary: id (copy), active/broadcast, range, this device's address, candidates, relay fwd/via/recv, dial/handshake failures. - A larger network traffic sparkline. - Connected peers in a table: hostname when known (else short key), protocol, address, handshake age formatted (s/m/h/d), tx/rx packets, tx/rx bytes, and current rate with an inline sparkline. Names and addresses copy on click. - Offline members listed below with address and failed-dial count. Agent: a SetHostname command (over the existing ipc) backs the in-place rename. Traffic sampler now tracks both packet and byte rates; the toggle selects which. Deferred: a "copy secret" button on the tile — the secret is deliberately not exported to the GUI (secrets never leave the agent), so it needs a small local secret-export ipc, which is the next step. Builds and is clippy-clean (-D warnings) on macOS; the UI has not been run in this headless session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0a344c1b4f |
Report per-peer byte counters, not just packets
The overlay peer report carried packet counts only, so the GUI could show packets/s but no byte throughput. Adds tx_bytes/rx_bytes to OverlayPeerReport and fills them from each data plane: - the wg and wg-quic plugins already counted plaintext bytes per peer; the CLI now maps them into the report; - the generic tunnel (tcp-tls) gains tx_bytes/rx_bytes on its peer state and report, incremented by the plaintext length at the same points it counts packets. The local control protocol version is bumped (TSN 15 -> 16) since the report shape changed; a client and agent on different builds already detect and report the mismatch. Checks: fmt, clippy --workspace --all-targets -D warnings, and the tsunagi lib, tsunagi-cli and wg-quic local_control tests pass on macOS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2cda56e1f8 |
Log inbound handshake rejections at debug, not warn
Creating and leaving networks produced a stream of WARN lines like "Handshake rejected: peer requested an unknown or inactive network network=None". This is not a leave bug: leaving deactivates the network and publishes a release, but remote peers that still consider this device a member keep dialing, and each dial for a network no longer served is correctly rejected by the responder (lookup returns None, so the network stays unidentified — network=None by design, since a pre-handshake claim is unverified). Such a rejection is routine and remotely triggerable — a left network a peer has not caught up on, a stranger, a port scan, an old client — so logging it at WARN let any peer spam the log. It is now debug; enable debug to see rejections when diagnosing. The rejection behaviour itself is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d1de61ba29 |
Tray UI: start hidden, fixed window, grid layout, traffic + peers
Reworks the tray window per feedback and fills it in. - Starts hidden in the tray (no window on launch); the window opens from the menu. On macOS the app now uses the Accessory activation policy, so it lives in the menu bar with no Dock icon (via a winit event-loop hook, pinned to eframe's winit). Fixed, non-resizable 360x600 window; the layout targets it. - Tidied layout: top bar (title + refresh), a bottom panel with the join form and the action banner, and a scrolling list of network tiles between them. Labels and values sit on two-column grids so they align; the join name and secret inputs are the same fixed width. - A ⟳ button beside the secret generates a fresh random secret (NetworkSecret::generate), for creating a network. - Each network tile shows id, range, this device's address, peers (connected/known), the active and broadcast switches, a Leave button, and a collapsing "peers (N)" section with a formatted per-peer table (short key, address, handshake age, tx/rx packets, rate). - Traffic: a new sampler turns the agent's cumulative packet counters into a packets-per-second rate per network and per peer, from the deltas between snapshots (the snapshot now carries a generation and timestamp so each refresh is sampled once). The network tile shows the current rate and a small hand-drawn sparkline of recent rates (no extra dependency). Note: rates are packets/s — the report has no byte counters yet; bytes would need an agent change. Builds and is clippy-clean (-D warnings) on macOS; the UI has not been run in this headless session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
19509dc833 |
Add the tsunagi-tray GUI (egui) — first increment
A native, lightweight tray application to manage a running agent — no web engine. It is a thin client over the agent's local control socket, reusing tsunagi::ipc and the report types directly; it owns its own runtime, logger and event loop, like the CLI. This first increment: - tray icon with an Open/Quit menu (tray-icon); menu events are read on a background thread so Open works even while the window is hidden; - a small window that hides to the tray on close and reopens from the menu; - a background tokio worker that polls status every 2s and runs commands off the UI thread, writing a snapshot the UI renders from and repainting egui; - UI: device header, a per-network card (name, id, range, this device's address, peers connected/known, a Leave button, and the per-network active and broadcast switches), a join form (name + secret), and a transient action/error banner; an "agent unreachable" state that names the socket. Socket resolution: TSUNAGI_CONTROL_SOCKET if set, else the platform default (/var/run/tsunagi/agent.sock on macOS) — the same socket the group-access agent serves. Scope/notes: macOS first (Linux and Windows follow). Not yet done — macOS accessory activation policy (so there is no dock icon), light/dark tray icons from assets (the icon is drawn in code for now), and packaging/autostart. Builds and is clippy-clean (-D warnings) on macOS; the UI itself has not been run in this headless session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
de8e48bee7 |
Let the agent grant the control socket to a group (0660)
For a GUI (or a CLI) running as a different user to reach the agent, the control
socket needs to be group-accessible. This is set by the agent when it binds the
socket — correct from creation, not patched afterwards.
- ipc::ControlSocketAccess { Private, Group(gid) }. Private is the existing
owner-only 0600. Group sets the socket's group to the gid and mode 0660, and
creates the socket's parent directory 0755 so the group can traverse to it; if
the group cannot be set it falls back to owner-only rather than leaving it
wrong. The chown uses std::os::unix::fs::chown — no unsafe, no new lib
dependency.
- ControlSocket::bind takes the access policy (Windows accepts it and keeps its
pipe default for now; a pipe security descriptor is a later follow-up).
- CLI: `tsunagi up --control-group <name>` (env TSUNAGI_CONTROL_GROUP) resolves
the group to its gid via nix (safe wrapper, unix-only dep) and passes
Group(gid); without it the socket stays Private. A name that does not resolve
is reported, and the socket stays owner-only.
- control_socket() now serves the system path (default_control_socket_path) on
macOS too, not just Windows: /var/run/tsunagi/agent.sock, a runtime directory
the GUI can reach, rather than the owner-only state directory. A custom
--state-dir still keeps its own per-user socket.
So the packaged/daemon agent runs e.g. `tsunagi up --control-group tsunagi` and a
user in that group talks to it; a plain per-user agent is unchanged (0600).
Checks: fmt, clippy --workspace --all-targets -D warnings, tsunagi lib tests and
the wg-quic local_control tests pass on macOS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
8c812e6874 |
Bump the GitHub workflow actions off deprecated Node 20
Release / tsunagi-linux-aarch64 (push) Successful in 5m25s
Release / tsunagi-linux-x86_64 (push) Failing after 3h12m37s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
GitHub now forces actions/checkout@v4, actions/upload-artifact@v4 and actions/download-artifact@v4 (all Node 20) onto Node 24 with a deprecation warning. Move them to the current Node 24 majors — checkout@v5, upload-artifact@v7, download-artifact@v8 — which keep the same inputs and the per-artifact subdirectory layout (merge-multiple defaults to false), so the publish step is unchanged. Only the .github workflow is touched. The .gitea workflow stays on checkout@v4 and the @v3 artifact actions, because Gitea does not support the v4+ artifact backend and may not have Node 24 on its runner image; it also does not emit these deprecation warnings. mlugg/setup-zig has no Node 24 release yet (latest v2.2.1 is still Node 20), so that one warning remains until upstream updates — nothing to change on our side. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
63178574ce |
Package as a dedicated tsunagi user; add Arch packages
Release / tsunagi-linux-aarch64 (push) Successful in 2m39s
Release / tsunagi-linux-x86_64 (push) Successful in 2m32s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Reworks the Linux packaging so the service runs as an unprivileged, dedicated "tsunagi" user instead of root — the point of a native package is to set things up properly for that OS's users — and adds Arch Linux packages beside the deb. Both package families are built from the already-compiled static binary; nothing is recompiled. Service model (shared systemd unit, dist/linux/tsunagi.service): - User=tsunagi with AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE, so it creates the TUN and binds low ports without root. - StateDirectory/CacheDirectory/RuntimeDirectory give it /var/lib/tsunagi, /var/cache/tsunagi and /run/tsunagi, and the agent is pointed at them (and the socket at /run/tsunagi/agent.sock) through TSUNAGI_* env in the unit. - The polkit rule now grants the resolve1 actions to the "tsunagi" group; the service user is in it, and admins can add themselves. CLI: when no state dir is chosen, the control commands prefer the system service's socket at /run/tsunagi/agent.sock when it exists, so `sudo tsunagi status`/`join` reach the packaged daemon with no flags. A per-user agent (its socket absent from /run) still uses its own. Packaging: - deb (build-deb.sh): postinst now creates the tsunagi system user+group; unit installed to /lib/systemd/system, binary to /usr/bin. - Arch (new dist/arch/build-pkg.sh): a dependency-free .pkg.tar.zst assembled with bsdtar + zstd (no makepkg, no Arch host) — .PKGINFO, a .INSTALL scriptlet that creates the user and reloads systemd, and a gzipped .MTREE; unit to /usr/lib/systemd/system. - The polkit rule moved to dist/linux (shared by deb, Arch and the tarball), and the tarball install.sh now creates the user and installs the unit + rule. - Both release workflows build the deb and the Arch package on the two Linux matrix entries (deb_arch/pkg_arch), install libarchive-tools+zstd for it, and attach both to the release (Gitea API loop / gh). Not exercised in this session: the .deb and .pkg.tar.zst build and install only on Linux (no dpkg-deb/bsdtar+pacman locally); CLI build, clippy, fmt and tests pass, and all shell/YAML is syntax-checked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a3856eed3b |
Build a dependency-free .deb and attach it to the release
Release / tsunagi-linux-aarch64 (push) Successful in 2m21s
Release / tsunagi-linux-x86_64 (push) Successful in 2m29s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Adds a Debian package to the release for the x86_64 and aarch64 Linux builds, so `apt install ./tsunagi_*.deb` is a convenient path on the Ubuntu hosts. Because the binary is static musl, the package declares no Depends — it is built with plain dpkg-deb on the runner (no cargo-deb or nfpm), from dist/deb/build-deb.sh. The package installs: - the static binary at /usr/bin/tsunagi; - a systemd service at /lib/systemd/system/tsunagi.service that runs the agent as root (ExecStart rewritten to /usr/bin; the source unit targets /usr/local/bin for the tarball installer); - a polkit rule at /usr/share/polkit-1/rules.d granting the resolve1 actions to a "tsunagi" group, for running the agent as a non-root user instead of via the root service. postinst creates that system group and runs daemon-reload; prerm disables the service on removal. Both release workflows gain a "Build the Debian package" step on the two Linux matrix entries (deb/deb_arch), include the .deb in the uploaded run artifacts, and attach it to the release (Gitea via the API loop, GitHub via gh). macOS and Windows are unchanged. Service runs as root by default; the polkit rule/group is the documented opt-in for the non-root path — say if you'd rather the service itself run as the tsunagi user. YAML and shell syntax validated; the .deb build itself runs only on the Linux runner (no dpkg-deb locally) and has not been exercised in this session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
eac54f8c79 |
Revert the GitHub workflow guard to server_url
Release / tsunagi-linux-aarch64 (push) Canceled after 15s
Release / tsunagi-linux-x86_64 (push) Canceled after 0s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Misdiagnosed the earlier report: Gitea was correctly running its own .gitea/workflows copy, not this .github one, so the server_url guard was fine. The switch to a GITHUB_RELEASE repo variable then made the workflow skip on GitHub itself, since that variable is not set there. Put both job guards back to `github.server_url == 'https://github.com'`, which runs on GitHub with no manual setup and skips on Gitea (its .gitea copy is the one that runs). No repository variable is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a8dfd7017b |
Gate the GitHub release workflow on a repo variable, not server_url
Release / tsunagi-linux-aarch64 (push) Canceled after 35s
Release / tsunagi-linux-x86_64 (push) Canceled after 0s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The github.server_url guard did not keep this workflow from running on Gitea: Gitea reports github.server_url as https://github.com, so the condition was true there too and the jobs ran (and would fail on @v4/gh). Switch both job guards to `vars.GITHUB_RELEASE == 'true'`, a repository variable set only in the GitHub repo's settings. It is deterministic: absent on Gitea, so the jobs skip there and the .gitea workflow is the one that runs; present on GitHub once configured, so it runs there. The header documents how to set it (Settings → Secrets and variables → Actions → Variables → GITHUB_RELEASE=true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
99f4cf0220 |
Add a GitHub release workflow alongside the Gitea one
Release / tsunagi-linux-aarch64 (push) Canceled after 36s
Release / tsunagi-linux-x86_64 (push) Canceled after 0s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Gitea and GitHub need different release plumbing, so each gets its own file: the Gitea workflow stays in .gitea/workflows, and this GitHub one lives in .github/workflows. The build half is identical (static musl Linux via cargo-zigbuild, native macOS aarch64 and Windows x86_64); only publishing differs — GitHub uses the v4 artifact backend and `gh release create`/`upload`, which the Gitea file cannot use. Because Gitea reads both .gitea/workflows and .github/workflows, every job here is guarded with `if: github.server_url == 'https://github.com'`, so on Gitea this file's jobs are skipped (no v4/gh failures) and the .gitea file is the one that runs. GitHub never reads .gitea, so there it runs only this one. Publish is re-run safe: it creates the release on the first run and uploads with --clobber onto an existing release afterwards; a pre-release suffix on the tag marks it prerelease. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
69263b2dd9 |
Move the release workflow to .gitea/workflows
Release / tsunagi-linux-aarch64 (push) Successful in 3m39s
Release / tsunagi-linux-x86_64 (push) Successful in 3m27s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The release pipeline is Gitea-specific (v3 artifacts, Gitea release API), so it must not run on github.com, where it would fail. GitHub only reads .github/workflows, while Gitea reads both, so moving the file to .gitea/workflows keeps it running on Gitea and invisible to GitHub. A separate GitHub-flavoured release workflow can be added under .github later. The portable .github/workflows/ci.yml (fmt/clippy/test) is left in place; it works on both and is not what this change is about. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
20444013c0 |
Split release into build artifacts + a publish job (Gitea)
The direct-upload-from-each-build approach failed ("Upload the archive to the
release" exit 1) and left no artifacts to inspect. Restructured into the
familiar shape: build jobs now leave their archive as a run artifact, and a
single publish job — gated on all targets succeeding — collects them and fills
the release.
- Build jobs use actions/upload-artifact@v3 (Gitea has no v4 backend; v3 is
what shows under the run), so the archive is visible and handed on.
- publish (needs: build) downloads everything with download-artifact@v3 and
uploads each archive to the Gitea release with curl, once, with no matrix
race. It prints the API base and the HTTP status/body of every call, so a
failure (e.g. a read-only Actions token → 403, or a bad API URL) is now
visible in the log instead of a bare exit 1.
- Release API base prefers GITHUB_API_URL (Gitea sets it to its /api/v1 root)
and falls back to GITHUB_SERVER_URL/api/v1. Asset uploads run in the main
shell via process substitution so a failed upload fails the job.
Build, package, musl/zig setup and the target matrix are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2196f6635b |
Quiet the cosmetic zig linker_messages warning on the musl build
The musl release build now links cleanly through cargo-zigbuild, but rustc's new linker_messages lint echoed zig's "ignoring deprecated linker optimization setting '1'" note — zig's linker declining the -O1 GNU-ld flag Rust passes. It is cosmetic and the binary is correct, so allow that one lint, scoped to the zig build step only. macOS and Windows keep full linker diagnostics, and a real link error still fails the build. No behaviour change to the produced binaries; takes effect on the next tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6438982d45 |
Upload release assets via the Gitea API instead of artifacts + gh
Release / tsunagi-linux-aarch64 (push) Failing after 2m57s
Release / tsunagi-linux-x86_64 (push) Failing after 2m53s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
The release ran on Gitea Actions, where it failed at actions/upload-artifact@v4: the v4 artifact backend (and the GitHub REST API that `gh release create` uses) are not available on Gitea/GHES, so both the artifact handoff and the publish job were dead ends there. Reworked so each build job uploads its archive straight to the release through Gitea's native API (`/api/v1/repos/<repo>/releases`) with curl: create the release for the tag if absent (races between matrix jobs are resolved by reading the id back by tag), then POST the archive as an asset. The separate publish job, download-artifact and `gh` are gone. The id is parsed with grep, so the runner needs only curl and coreutils — no jq, no third-party release action. Uses the Gitea-injected GITHUB_TOKEN, GITHUB_SERVER_URL and GITHUB_REPOSITORY; a tag with a pre-release suffix (e.g. -rc.2) is marked prerelease. This targets Gitea by design; it no longer runs as-is on github.com (different API), which is fine — the GitHub build is being retired. Build and package steps, the musl/zig setup and the target matrix are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
21b619388c |
Pin Zig to a real release so the musl release build fetches reliably
Release / tsunagi-linux-aarch64 (push) Failing after 3m49s
Release / tsunagi-linux-x86_64 (push) Failing after 3m26s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The release run failed at "Install zig": mlugg/setup-zig, left unpinned, resolved to the newest version it knew of — an unreleased 0.16.0 whose tarball 404s on ziglang.org — and the community mirrors returned 502/503 around it, so the step errored out before cargo-zigbuild ever ran. Pin the action to Zig 0.14.1, an archived release with a permanent official download URL. The official source then succeeds even when the mirrors are flaky, and the version stays compatible with cargo-zigbuild. Also moves the action to @v2. Only the Linux musl jobs use zig; macOS and Windows are unaffected. Not run on a real tag yet in this session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d1ab993dd2 |
Add a tagged-release CI workflow and Linux/Windows packaging
Release / tsunagi-linux-aarch64 (push) Failing after 3m12s
Release / tsunagi-linux-x86_64 (push) Failing after 2m21s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Pushing a `v*` tag now builds the agent for every supported target and attaches the archives to a GitHub release. Modelled on the furumi release workflow (a build matrix, then a publish job that runs `gh release create`), adapted to this project's two differences: it needs system integration (capabilities/polkit on Linux, elevation+wintun on Windows, a root daemon on macOS), and its users run varied distros, not just Ubuntu. Linux is built as a fully **static musl** binary with cargo-zigbuild, so one artifact runs on Arch, Alpine, NixOS and old-glibc systems alike, and can be repackaged into whatever a distro prefers later. musl has no prebuilt aws-lc-sys bindings, so the Linux jobs install cmake + libclang (bindgen) and zig (the cross C compiler zigbuild drives). macOS (aarch64) and Windows (x86_64) build natively; Linux covers x86_64 and aarch64. Targets: x86_64/aarch64 linux-musl, aarch64 macOS, x86_64 Windows. Each archive carries the binary, the top-level README, and the platform's install notes plus integration files: - dist/linux/: a static-friendly README, a systemd unit (runs as root, so no capability grant or polkit rule is needed; a commented non-root variant is included), and an install.sh that drops the binary and the unit or prints the setcap/polkit alternative. - dist/windows/: install notes covering wintun.dll and Run-as-administrator. - dist/macos/: the existing LaunchDaemon plist and notes, now also shipped in the archive. Not exercised yet: the workflow only runs on a real tag push, and the aws-lc-sys-under-musl build has not been run locally in this session (no zig toolchain here). YAML and install.sh syntax are validated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
da7ce1dca1 |
Drop link-local addresses from the status "not announced" list
A host with several interfaces has a link-local (fe80::/10, and 169.254/16 on IPv4) on each, and none of them is ever a peer candidate. Listing them under "not announced" padded the section with one noise line per interface — most of the list on a machine with several VPN utuns. They are now filtered out; loopback and real unannounced addresses are unaffected. Checks: cargo fmt --all --check and cargo clippy -p tsunagi-cli --all-targets -D warnings pass; verified by eye against a live `tsunagi status`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4d2f8a54ec |
Compact the status address list to one line per address
`tsunagi status` printed every local address as two lines: the address, then an indented note holding the interface (and, for candidates, a priority rank). On a host with many interfaces — common on macOS, where each VPN adds a utun and every interface carries a link-local — that made the "local addresses" section very long and hard to scan. Each address is now a single line: the announce status stays the label (announced / not announced / loopback) and the address and its interface share the detail, as `<addr> · <iface>`. The per-row note and the candidate-priority rank are dropped; ordering still reflects candidate priority. No other section changes, and nothing but the presentation does. Checks: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -D warnings, and the tsunagi-cli tests all pass on macOS; verified by eye against a live `tsunagi status`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c3a6201ee2 |
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> |