8a6216bccb392ee9e9d625bc23a7eaf541506c81
96
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2e7e238459 |
Improve tray VIP monitoring and table layout
The VIP editor put routing and traffic observations in a dense text block, while nested layouts in the devices grid could let text escape its row. Separate the VIP overview from configuration and draw both live tables with explicit cell bounds, font-aware row heights, clipping and horizontal overflow. Measure columns before painting and keep widths stable as live values change. The overview reuses the existing traffic sparklines for service totals and individual listener backends, with TX/RX rates, health, RTT, open flows and counters. Retain bounded histories by service/listener/backend identity and release them when their resources disappear. Recreated listeners start fresh. Use egui 0.31 Scene and Painter for a pannable, zoomable routing diagram with Fit, clickable backend details and linked table selection. Highlight recent counter growth separately from the backend selected for new connections; idle bindings alone do not imply traffic, and stale activity expires. Draw legend markers directly rather than depending on optional font glyphs. The overview only reads saved observations; edits and deletion still require Save. No dependencies, wire formats, IPC or persistent schemas change. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 566 passed, 3 ignored - cargo build --locked -p tsunagi-tray The inherited TSUNAGI_CONTROL_SOCKET was unset for Clippy and tests. Seven new GUI tests cover enlarged fonts, mixed online/offline rows, long names, large counters, narrow/wide layouts, scene clicks, traffic selection, stale samples, counter resets, listener recreation and history cleanup. The first enlarged-font regression exposed an override-font height mismatch; row sizing now resolves the actual selected font and the regression passes. A disposable fixture preview rendered successfully with software OpenGL on an isolated Xvfb display. Initial Xvfb attempts needed explicit xkbcommon and OpenGL runtime paths in the Nix environment. The preview never contacted an agent; its temporary source was removed. macOS, Windows, Wayland and live host TUN traffic were not exercised by this UI change. |
||
|
|
23af1a2d9d |
Add shared TCP and UDP virtual services
Provide named VIPs with independent protocol/port listeners and node/port backends so applications can use one DNS endpoint without a separate load balancer. Every authenticated member may create, edit or delete any service. Client agents select the eligible backend with the lowest measured encrypted data-plane RTT, including relayed paths, and pin existing flows. UDP recovers on another backend after failure; established TCP is never replayed or moved. An unanswered empty TCP SYN may retry another backend before establishment. Keep signed multi-writer field registers in state/vip.rs. Independent backend edits merge, same-field conflicts use Lamport/author/canonical ordering, and listener generations prevent old backend entries returning on port recreation. Permanent service tombstones protect replay while allowing obsolete fields to be compacted. Active-service and deletion-history limits are separate. Commit complete snapshots and the author's durable counter atomically before local routing changes or publication. Restore counters from newer genuine author updates learned from replicas after an old backup is restored. Implement bounded client-side address/port NAT at overlay ingress and reverse translation only after authenticated source/destination admission. Reserve OS source ports to avoid direct-flow collisions and quarantine retired ports for late replies. Handle a backend on the client itself, scoped fragment assembly, checksums, re-fragmentation and quoted ICMP/PMTU headers. Plugins and encrypted transit retain their existing contracts. Data-path probes measure RTT; bounded fair TCP connect checks are enabled by default and configurable per listener. Arbitrary UDP application readiness remains unknown; path checks and explicit port errors drive eligibility without protocol-specific probe payloads. Expose the shared draft API through local IPC, simple vip/listener/backend CLI commands, JSON, editor-based atomic updates and watch output. Add a VIP button on each network tile and a separate tray editor. Edits and removals stay local until Save; stale revisions retain the draft, and closing a dirty window offers explicit discard. Report local routing choices, health reasons, RTT age, active and draining bindings, IP counters, rates, drops and UDP failovers. Extend DNS with service names; keep numeric addresses secondary and preserve existing bindings while VIP addresses or listeners change. This changes control wire ALPN/version to tsunagi/ctrl/8 and local IPC to 22; all members and local clients must upgrade together. Data ALPN remains 5. SQLite schema 10 migrates existing mandatory state without changing identity, secrets, network ids, derivation labels, handshake encoding or node records. Crate versions and release tags are unchanged. No installed agent or tray was replaced. Commands, limits and operational semantics are in docs/vip.md. Checks on Linux in nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 559 passed, 3 ignored - git diff --cached --check The full suite includes real encrypted TCP/UDP VIP traffic, backend-stop UDP failover, real loopback TCP check recovery, source admission, signed merges, stale/deleted state, SQLite migration and rollback, restart/author recovery, network isolation, actual CLI/DNS processes, and headless text-entry/Save-button interaction. Discovery and memory TUNs are the existing test substitutions; authentication, transport, encryption and SQLite remain real. Initial checks outside Nix failed for missing GTK pkg-config metadata, and an initial CLI test inherited TSUNAGI_CONTROL_SOCKET. Final checks use the Nix environment with that variable unset so tests address their temporary sockets. Root/internet-dependent tests remain ignored. No live host TUN, native desktop session, macOS or Windows validation was performed. |
||
|
|
94318e7c16 |
Keep tray device metrics on one line
The device table sized handshake/RTT and packet columns from fixed samples. Its own header exceeded the handshake sample, and six-digit counters wrapped at the slash, making device and total rows taller and misaligning the plots. Measure actual headers, displayed values and totals before drawing, retain the widest values across frames, and disable wrapping inside table cells. When a column grows, request a second egui layout pass so the grid's cached positions settle before larger text is painted over the next column. Keep the plot as the stretch column and horizontal scrolling for narrow windows. This changes only tray presentation. A headless rendering regression checks the actual text shapes for the header, handshake age/control RTT, per-device packet counts and totals as counts grow past five digits, reach billions, shrink again, and the table gets a narrower container. It checks one-line text and non-overlap with the next column; it failed on the original wrapped header. Existing resize and long-relay tests remain in place. Checks on Linux in nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with inherited TSUNAGI_CONTROL_SOCKET unset so tests use their temporary sockets - git diff --cached --check The suite's root/internet-dependent tests remain ignored. No native GUI session or checks on macOS/Windows were run; rendering was checked through egui's headless output. No installed tray binary was replaced. |
||
|
|
2b0c938487 |
Bound iroh path retries to prevent multi-connection OOM
A long-running rc.33 daemon reached OOM with roughly 22 GiB resident and 24 GiB swapped. Live inspection found 40 GiB and 10/20 GiB anonymous container mappings; the active worker stack and tracing metadata identified iroh's scheduled path-open retry loop. In iroh 1.2.0 every failed connection requeues the same path, while each retry visits every connection again. Separate control and data connections multiply the queue geometrically. Vendor the published iroh 1.2.0 source and select it with a workspace Cargo patch. Deduplicate retry admission and cap distinct pending paths at 64, evicting the oldest before inserting a new candidate. Keep the current and next retry batches bounded without combining control/data connections or changing wire protocols, identity derivation, authentication or state. Document source provenance, the live evidence and removal conditions. Upstream PR 4551 merged on October 6, but inspection of the official 1.3.0 crate still shows the unconditional enqueue, so that upgrade alone is not a fix. No version bump, release tag or installed-service change. The default workspace suite includes the exact vendored admission module. Three regressions failed with the original enqueue and pass with the fix: repeated failures across connections, duplicate admission at capacity, and 10,000 distinct candidates with bounded length/capacity and FIFO eviction. Transport integration tests continue using real endpoints and encryption. Checks on Linux in nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with only the inherited TSUNAGI_CONTROL_SOCKET removed from the test environment - git diff --cached --check Initial full-suite runs failed six DNS readiness checks because the inherited system socket path overrode their temporary state directories. Clearing that environment variable resolves those failures without changing application code. Root/network-dependent ignored tests and other operating systems were not run. |
||
|
|
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. |
||
|
|
2631eb6975 |
Protect Linux exit-node connections and tidy the device table
Using an exit node could destroy direct peer connections: the agent's UID bypass protected outbound routing, but reverse-path checks look up replies without that UID and selected the overlay default. A real-host A/B check confirmed that relaxing only strict reverse-path filtering restored direct connections and exit HTTP traffic; the diagnostic host changes were removed. Keep strict filtering and protect the Linux underlay with a dedicated masked mark bit, 0x40000000. Mark the dedicated service UID in OUTPUT, save only that bit in conntrack and restore it before PREROUTING reverse-path checks. Install IPv4/IPv6 mark and UID main-table bypass rules before Tailscale's priorities; leave application-specific and exit-default routing after them. Preserve foreign mark bits, main-table routes and foreign firewall/policy rules. Protect the first inbound TCP/UDP packet using bounded, locally owned socket addresses/ports from /proc and an actual socket match. Refresh socket protection before sending private transport negotiations and on discovery ticks, without firewall writes for an unchanged socket set. Remote announcements never become OS arguments. A failed refresh leaves active exit routing in place. Install protection before routes, remove routes before protection, roll back partial activation, and remove only exact interface tags, including legacy UID rules. Reject strict kernel rp_filter without src_valid_mark with an actionable error; never weaken filtering or change host sysctls. Document mark-aware Netfilter requirements and provide the trusted ip6tables path in the NixOS module. Other platforms retain their existing exit implementation. Peer wire formats, identity derivation, local IPC and persistent storage are unchanged. Make the tray devices grid fill and shrink with the window, constrain the first cell and give remaining width to traffic plots. Keep displayed end-to-end protocol and its short encryption tooltip consistent, independent of carrier protocol. Move handshake/session age into its own column beside the existing iroh control RTT. Use stable per-device hover identities and concise tooltips; missing protocols/measurements remain explicit rather than invented. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 529 passed, 3 ignored; inherited TSUNAGI_CONTROL_SOCKET and TSUNAGI_CONTROL_GROUP were unset - explicit privileged kernel regression: 1 passed, using disposable namespaces, real TCP/UDP IPv4/IPv6, strict kernel and Netfilter RPF, first inbound packets, dynamic UDP binding, application exit routing/IPv6 blocking, repeated apply and exact cleanup with foreign Tailscale-priority rules retained - headless egui grid/scroll-area rendering at multiple sizes and control RTT, mark masks/order, passive socket parsing, partial failure and cleanup tests - git diff --check Earlier iterations caught rtnetlink family API and nonexhaustive initializer errors, Clippy warnings, egui's implicit minimum column width, and kernel-test fixture issues (wildcard echo source, blocked IPv6 neighbour discovery and a closed child stdout pipe). Corrected before final checks; the stdout-only test fix was checked with fmt, Clippy and the explicit kernel regression afterward. Native tray rendering and macOS/Windows checks were not run. The running host agent was not replaced or restarted. Requested release tag: v0.1.0-rc.32. |
||
|
|
90ef5cefec |
Retire offline node claims with equal network-wide cleanup rights
Offline devices reserved names and IPv4 addresses indefinitely, leaving stale members in the device history. Give every authenticated network participant the same right to retire another device's observed signed claim without an owner, quorum, permanent identity ban or secret change. Add a separately signed retirement containing the target's original signed claim as proof. Verify both signatures and network scopes, retain the strongest proved cutoff per target with deterministic ties, and suppress claim versions at or below it in membership, hostname ownership and IPv4 allocation. Keep the old signed records and range agreement, and retain markers after a newer claim returns so old replicas cannot resurrect obsolete reservations. A returning device signs a newer claim, allocates against current holders and resets its previous hostname priority rather than taking a reused name with an old time. Commit local retirement and the requester's author counter atomically before announcing it; receivers persist proofs before applying them. Bound history to 1024 targets by default, reject additional targets without evicting protection, and treat corrupt or over-limit mandatory storage as an explicit error. Repeat bounded marker batches with a per-session rotating cursor so queue pressure cannot indefinitely starve later markers. Exclude retired claims and cached hints from history-derived discovery without preventing a fresh authenticated device from returning. Expose Agent::forget_node through the shared local control API. CLI command tsng network forget-node accepts exact network names, full ids or unique id prefixes, and exact node hostnames, ids, prefixes or overlay IPv4 addresses. Ambiguous selections change nothing. Recheck the observed version and refuse self, online nodes, the selected exit node and absent or changed claims. Add Forget with an identity confirmation to offline tray device rows. Widen Show devices to 1440x640 with an 800x400 minimum to fit its contents. Control ALPN/version advances to tsunagi/ctrl/7 and local IPC to 21; upgrade all peers and restart the agent with matching CLI/tray clients. Data ALPN 5, network identity derivation, handshake encoding and existing record signing bytes remain unchanged. SQLite schema 9 adds retirement storage while retaining identity, secrets, settings and signed claims. Document behavior and limits. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 519 passed, 2 existing ignored tests; inherited TSUNAGI_CONTROL_SOCKET and TSUNAGI_CONTROL_GROUP were unset - git diff --check and the built CLI forget-node help Tests use real iroh authentication and SQLite. Cover restart, stale-replica recovery, equal rights and network isolation, hostname reuse and return, twelve marker batches through a two-message queue, malformed signatures and proofs, invented future cutoffs, deterministic merge, capacity, migration and injected counter-write rollback, selectors and cleanup through two real CLI processes. Earlier iterations caught a private test API, a collapsible tray conditional and a crash-based CLI test waiting for QUIC's idle timeout; corrected before the final successful suite. Manual tray rendering, macOS/Windows, privileged TUNs and public-network traversal were not run. The requested RC tag is v0.1.0-rc.31; no push was requested. |
||
|
|
13bdcaf98d |
Render protocol priority buttons with Phosphor icons
The tray protocol selection used ordinary Unicode up/down arrows for its priority buttons. The default UI fonts lack these glyphs, so both buttons rendered missing-glyph boxes. Use the regular Phosphor ARROW_UP and ARROW_DOWN icons already installed in the application's proportional font fallback. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with inherited control socket and group overrides removed: 509 passed, 2 existing ignored tests No new tests were added for the two glyph substitutions. Manual tray rendering and macOS/Windows checks were not performed. No release tag was requested for this fix. |
||
|
|
d941cdac7a |
Change network protocol selections at runtime
Protocol selection previously belonged to tsng up and therefore to the service command line. A blocked or degraded carrier could only be removed by editing the service and restarting the whole agent. Make the ordered selection a local per-network setting instead. Add Agent::set_protocols and a local control request shared by CLI and tray. Keep tsng protocols without arguments as the protocol catalog; naming a network shows its selection and supplying a comma-separated list replaces it. Accept exact names, full ids and unique prefixes using shared selector rules; ambiguous selectors and invalid lists change nothing. An explicit none selects control-only operation. Offline edits take the directory lock and apply on the next start; live edits never write behind the daemon. Register every built-in plugin and transport in the CLI. Treat up --protocol as ordered initial defaults for networks without an override, allowing a protocol absent from the unit to be enabled later. Retain the targeting of unqualified startup options and allow explicitly scoped options to configure initially disabled protocols. Persist NULL for inherited preferences and an empty selection distinctly in SQLite schema 8; migration preserves existing identities, secrets, signed records and counters. Serialize changes with network lifecycle operations and commit preferences before applying or announcing them. Retire only the affected network's data links and E2E channels, cancel pending opens, and reject obsolete completion results by attempt id. Clear peer data channels when authenticated protocol capabilities change while retaining control sessions and topology. Replay allocations and peer identities after asynchronously prepared plugins become ready. A control-only start defers creation of the agent's single interface until data is enabled; plugins receive a sink that can use it later. Automatic carrier fallback continues to preserve E2E state; explicit policy changes renegotiate it. Identity derivation and peer wire formats are unchanged. Add a collapsible protocols row to each tray network tile, with a catalog provided by the daemon, descriptions, immediately applied checkboxes and preference-order buttons. Keep editing disabled until the worker refreshes status so a second click cannot overwrite an edit using a stale snapshot. Local IPC wire version is 20 for the new request and report fields; upgrade and restart agent, CLI and tray together. Crate release versions remain unchanged; the requested RC tag is v0.1.0-rc.30. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 509 passed, 2 existing ignored tests, with inherited TSUNAGI_CONTROL_SOCKET and TSUNAGI_CONTROL_GROUP removed Tests use real iroh membership, SQLite, UDP, AWG and WireGuard encryption, QUIC carriers and memory TUNs. Cover either endpoint-id role, full-size packet delivery after switching, an unaffected second active network, saved selection after restart, empty/invalid selections, first enablement without restart, rapid changes racing old opens, live and offline CLI changes, startup-absent protocols and preservation of signed state through migration. Earlier compile and focused-test iterations caught initializer mistakes, a short test secret, asynchronous plugin allocation readiness, and an inherited system socket path; corrected before the successful final checks. Plain cargo was unavailable in the host shell, so validation used the Nix toolchain. Manual tray interaction, macOS/Windows, privileged host TUNs and public-network traversal were not run. |
||
|
|
e701986a39 |
Add ephemeral pairwise Amnezia transport and runtime fallback
Plain WG and QUIC can be blocked or severely degraded while the iroh control connection still works. Add an awg carrier with one independent UDP socket, WireGuard engine and randomized masking profile per network/peer attempt. The smaller endpoint id negotiates fresh outer keys, PSK, header protection, header ranges, padding and signature templates through private authenticated control messages. Restart or fallback reentry generates a new profile; ordinary rekeying preserves the current profile. No pair parameters enter signed state, capabilities, persistent storage or public message events. Private payloads and serialized control buffers redact and zeroize themselves. Adapt the bounded packet masking engine from RunaRoo/AmneziaWG-RS at commit 51579a7d6e8882d1d8d6d00822f09e18a9423b73, retaining its Apache license and provenance. Declare WTFPL AND Apache-2.0 in package metadata to cover both new glue and the adapted source. Reuse boringtun instead of upstream's Node/global executors and interface machinery. This is Tsunagi AWG wire version 1, with standard WG headers restored before authentication; external Amnezia server compatibility is not implemented. DNS/SIP signature generators vary fields instead of replaying captures. Each attempt closes independently, bounds allocation, queues, candidates and fragment assemblies, and supports the full 64 KiB carrier contract using UDP packets at most 1200 bytes. Yield between fragment batches so large messages do not starve local socket readers. Keep carrier encryption separate from end-to-end tunnels so multihop relays still forward opaque ciphertext. The system owns the sole interface, signed addresses and admission. Plugins receive neither network secrets nor paths. Expose a bounded opaque control sender at the transport boundary; core control messages remain outside protocol crates. Optional STUN on the pair's actual socket adds IPv4 public candidates in CLI direct/relay mode, with a finite query deadline and an off/alternate-server option. Local mode remains offline. Symmetric NAT, IPv6-only paths and arbitrary UDP blocking can still require fallback; no AWG-over-QUIC or hidden AWG relay is introduced. Default carrier order is wg, awg, wg-quic, tcp-tls. Advance the fallback cycle after setup failures and sustained runtime loss, even after earlier cooldowns expire. Authenticated delivery probes traverse the actual encrypted tunnel, fit the interface MTU and never enter the TUN. Retire after a 20-second warmup and at least 60% loss over at least 15 completed recent observations. Preserve logical tunnels and control sessions while explicitly closing physical links, including TCP reader/writer tasks. Retain the newest private probe settings across independently queued control negotiation and inbound installation. CLI/tray reports keep end-to-end protocol and actual carrier separate. Control ALPN/version becomes tsunagi/ctrl/6 for the new private message; upgrade all members together. Data ALPN/envelopes remain version 5. Network identity derivation, transcript encoding, device identities, signed records, saved network configuration and release versions are unchanged. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with the inherited TSUNAGI_CONTROL_SOCKET override removed: 504 passed, 2 pre-existing ignored Tests use real iroh membership, SQLite, UDP sockets and WireGuard encryption with memory TUNs. Cover three independent pairs, full-size IP and carrier frames, restart freshness/socket release, 80% carrier datagram loss, fallback to real iroh, fresh retries with working delivery checks, unequal small MTUs, malformed negotiations/profiles, role and resource bounds, probe forgery and STUN parsing. Earlier compile/lint iterations and focused tests caught RNG API/initializer mistakes, a retry assertion racing path installation, and a large-fragment burst overflowing loopback buffers; corrected before these successful checks. Normalized upstream license line endings for diff checks. Public NAT/STUN connectivity, actual DPI effectiveness, WAN performance, per-pair memory measurement, macOS/Windows, privileged TUN operations and manual tray interaction were not exercised. |
||
|
|
ed28e86748 |
Derive default network ranges and switch overlapping networks
Every first network previously proposed 10.13.37.0/24. Independently created networks therefore collided when a device joined more than one of them. Use the existing stable NetworkId-derived /24 in 10/8 for every new network, independent of activation order or peer availability. Legacy and explicit signed ranges outrank the derived default; remaining range disagreements still use author-id ordering. The derivation, identity labels, handshake and signed record encoding stay unchanged. There is no creator, timestamp or vote. Reserve ranges before starting network actors and serialise start, stop and switching through the agent lifecycle lock. Refuse equal or nested overlaps without changing a network's subnet locally. A late authenticated range that collides suspends only its network before updating plugins or the interface. Coalesced watch notifications let the agent clean up outside the network actor and reconsider suspended networks when ranges change. An explicit switch stops every blocker before activating the chosen network. Suspended networks retain their startup request and resume whenever their ranges become free, including switching from a wide range to a narrow one. A manual stop cancels that request even for an already suspended network. Persist preference order and enabling together in an SQLite transaction so the user's selection survives restart; no database schema migration is needed. CLI status and network listing show blocker names, ids, ranges and the start command. Network start switches explicitly and reports suspended networks; joining an overlapping network reports it as configured but inactive. The tray adds conflict details, an amber icon, Switch to this network and Keep stopped actions. Local IPC wire version is 19 for the extended reports, so restart the agent and tray together. Release versions and peer wire formats are unchanged. Update the user documentation and compatibility tests. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets with TSUNAGI_CONTROL_SOCKET removed from the test environment: 486 passed, 2 pre-existing ignored tests - final focused range_conflicts integration suite after making discovery fixtures deterministic: 6 passed Earlier validation attempts caught outdated diagnostic assertions, a new test helper scope error, and the inherited system control socket override. Those were corrected before the successful runs. Plain cargo was unavailable in the host shell, so validation used the Nix development toolchain. macOS, Windows, privileged host TUN operations and manual tray interaction were not exercised. |
||
|
|
781e359775 |
Make tunnel and transport layers explicit in status
The tray called the end-to-end tunnel proto and the current route link, then put the tunnel handshake age beside the route. A valid wg tunnel over a tcp-tls transport consequently looked like contradictory state. The terminal status used similarly terse wording.\n\nThe tray now separates end-to-end from data path, renders direct paths as direct over TRANSPORT, keeps handshake or uptime beside the tunnel, and explains both layers in hover text. Terminal status uses the same end-to-end tunnel, data path, transport detail, and control path vocabulary. Routing, protocol selection, IPC, and wire behavior are unchanged.\n\nPresentation tests cover wg over tcp-tls, tcp-tls uptime, relayed and missing paths, a missing handshake, and offline devices. The WireGuard broadcast checksum test uses the current slice-array iterator spelling required by Clippy; its behavior is unchanged.\n\nChecks: cargo fmt --all -- --check; cargo clippy --locked --workspace --all-targets -- -D warnings; cargo test --locked --workspace --all-targets with TSUNAGI_CONTROL_SOCKET removed from the test environment so integration agents use their temporary control sockets. All passed on Linux. |
||
|
|
97377e3703 |
Relay across any mix of links, one online answer for every client, first claim owns a hostname
A tunnel no longer depends on the kind of link under it. The relay hub keeps one forwarding table per network instead of one per protocol, the envelope names the end-to-end protocol, and a neighbour reached over several kinds of transport is sent to over the best open one. A member in the middle needs no protocol in common with either end. Which protocol a pair runs is settled by the lower endpoint id's announced order, so both ends agree without asking. A direct link that comes up later is shorter than the path through others, so traffic moves to it with the same logical link. The agent now answers "is it online" once: a live control session or any data path. Members and peers report how they are reached (direct over a named transport, relayed through a named member, or no data path yet), and `tsng status` and the tray both read that instead of a session list and a WireGuard handshake respectively. A hostname belongs to whoever claimed it first. A claim carries the time its author first claimed the name, signed with the record, and replicas order by (time, id) so every member names the same holder. A later claimant keeps working by address, is not found by the name, and is told so in its status, `tsng status`, the tray and a HostnameTaken event. Wire change: control and data ALPN 5, control PROTOCOL_VERSION 5, envelope version 2, signing domain v3, state schema 7, local control protocol 18. Upgrade every member together. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
0cfc1cbafc |
Tray state icon and window picture; firewall rules without the comment match; macOS app bundle
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> |
||
|
|
9966b50407 |
Rename the CLI package to tsunagi and its executable to tsng
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> |
||
|
|
bb38562b0a |
Exit nodes: block IPv6 while one is in use; exit icon on the network tile
IPv6 has no way through an exit node, so the same policy rules get an unreachable default for IPv6 instead of leaking it. The tray shows a green icon in the tile corner while an exit node is in use, with a one-line confirmation to stop it; confirmations are shortened. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
b2431d7eb1 |
Exit nodes: offer one per network, send all traffic through a member
A member can offer itself as an exit node per network (off by default); it is announced over the control channel (ctrl/4) once its iptables masquerade and forward rules are in place, with a warning when kernel forwarding is off. A device picks one member as its exit node via policy routing, with the agent's own uid exempt from the tunnel; if the exit node disappears the rules stay and traffic is dropped, loudly reported. CLI: `network exit-node`, `exit-node`; tray: tile checkbox, warnings and a per-device icon with confirmation. Linux only. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
c62d29fa14 |
Arch package: start the service and let the GUI reach the agent
The control socket is now opened to the tsunagi group (unit), the Arch package enables and starts the service on install, adds the installing user to the group, restarts a running agent on upgrade, and depends on the libraries the tray loads at run time. The tray looks for the agent's socket again on every poll instead of once at start, and a missing indicator library is reported instead of panicking. Repository URLs unified. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
2f7f9980c7 |
Tray: open the devices window wider and let its table scroll sideways
Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
2e178a6f9d |
Tray: About window, fixed-width device table with totals; WTFPL
Release / tsunagi-linux-aarch64 (push) Successful in 2m19s
Release / tsunagi-linux-x86_64 (push) Successful in 3m56s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Add an About window (client and agent details, protocol versions), pin the device table's column widths and add a total row, show the version in the header and drop the refresh button. Relicense under WTFPL. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
f4dcdec662 |
Tray: separate tray and window processes; device list without self
Release / tsunagi-linux-aarch64 (push) Successful in 2m44s
Release / tsunagi-linux-x86_64 (push) Successful in 4m37s
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 tray process holds only the icon and starts the same binary with --window on Open; the window ends when closed, which works on Wayland where a window cannot be hidden. The devices table now names devices as hostname (id5), leaves out this device and counts the others. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
69310108f4 |
Remember the last hostname each peer announced
Persist announced hostnames per network (state schema 5) and report them for members, so a device that is away is still named in `status` and the tray. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
e7dff34ec2 |
Status: ask the agent for its state directory and its own CAP_NET_ADMIN
The CLI reached a packaged service's socket but read state from its own default directory, so `network` saw nothing next to a running network, and `status` reported the CLI binary's capabilities instead of the agent's. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
a55ea883a1 |
Tray: initialise GTK and run its main loop on Linux
Release / tsunagi-linux-aarch64 (push) Successful in 3m14s
Release / tsunagi-linux-x86_64 (push) Successful in 5m21s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
tray-icon needs gtk::init() and a running GTK loop on Linux, which eframe/winit never provide, so the tray panicked at startup with "GTK has not been initialized". Build the tray on a dedicated GTK thread instead. Co-Authored-By: Claude <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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
533921059a |
Install broadcast route and firewall allowance on the host while broadcast is on
LAN discovery (CS 1.6 and similar) only worked after two manual steps on every host: route 255.255.255.255 through the TUN, and allow inbound traffic on it. Without the route the game's limited broadcast leaves via the physical adapter and never reaches the overlay; without the firewall allowance the server's unicast reply (from a different address than the one the request went to, so never matched by conntrack) is dropped before the game's socket. Verified by hand on Linux: with both in place the server answered A2S_INFO over the overlay. The system level (not a plugin) now installs exactly those two things for the interface it owns, and removes them when broadcast is turned off, the network is left or stopped, or the agent exits. Design: - New boundary overlay::hostrules (BroadcastHostRules: idempotent apply/clear, with Linux, Windows, Mock and Unsupported implementations). TunFactory gains host_rules(); ManagedTunFactory::with_host_rules wires it in the CLI. - Interface::sync_broadcast_rules reconciles the host against the routing table (RoutingTable::broadcast_host_target) and is called after address sync, on live `network broadcast on/off`, on network deactivation and by remove(). Failure is contained: the outcome is kept and logged, never fatal. - One interface carries several networks but the host has one 255.255.255.255 route, so the source is the lowest NetworkId among broadcast-enabled networks with an address (documented in docs/broadcast.md). - Linux: the route is added on the existing netlink thread (preferred source = overlay address, replace semantics). The firewall rule is `iptables -I INPUT -i <if> -p udp -s <overlay range>` tagged with a comment `tsunagi:<if>`; it is UDP-only and range-limited on purpose so TCP services stay unexposed. iptables is located in fixed system directories, never via PATH; stale tagged rules (e.g. from a crash) are replaced. A missing iptables leaves the route in place and is reported. rp_filter is not touched. - Windows: PowerShell (absolute path) New-NetRoute metric 1 and a tagged inbound UDP Defender rule scoped to the interface alias and overlay range. - status shows a `broadcast rules` row (ok, or which half failed and why). IPC control protocol bumped 14 -> 15 because OverlayReport gained a field. - AGENTS.md records the narrow exception to "never touch routing/firewall". Compatibility: restart the agent after upgrading (control protocol 15). No wire or schema change. Checks run (Linux only): cargo fmt --check, cargo clippy --locked --workspace --all-targets -D warnings, cargo test --locked --workspace --all-targets, all passing. Tests use a mock host and a scripted iptables; no OS setting is changed. NOT done: the Windows code was never compiled for or run on Windows (cross-compiling failed for lack of a C toolchain), only its script building is unit-tested; the real netlink route and iptables calls were not exercised against a live kernel in the suite; CS 1.6 was not launched from this change. |
||
|
|
d2778f0b5a |
Prevent WireGuard timer loop failure spam and fix handshake broadcast
When a WireGuard tunnel handshake timed out or a rekey failed,
Inner::on_tunnel_failed closed the transport link and reported the failure
to the agent core. However:
1. The peer remained in WireguardDevice::inner.peers. Once boringtun
declared ConnectionExpired (after 90s), self.tunn.is_expired() remained
permanently true on all subsequent 250ms timer ticks. drive_timers
repeatedly detected failed = true every 250ms, flooding logs with
'WARN WireGuard tunnel failed due to rekey timeout, triggering fallback'
and sending repeated protocol failure events to the core 4 times/sec.
2. In WireguardLink::send, is_handshake_init incorrectly expected an
envelope header offset (msg[74] == 1) when checking for initiation
packets, which failed on raw WireGuard UDP packets. As a result,
handshake initiations were not broadcast to all candidate endpoints.
3. In WireguardTransport::accept_loop, packet source matching contained
dead envelope destination matching inspecting ephemeral key bytes
against local endpoint IDs.
Fixes:
- In crates/tsunagi-wireguard/src/device.rs:
- Add failed: bool to FlowTunnel to latch failure state. Once a failure
is reported, update_timers returns (TunnResult::Done, false) on all
subsequent ticks until a new handshake is started or completed.
- In Inner::on_tunnel_failed, remove the failed peer from inner.peers so
drive_timers ceases ticking on the dead tunnel.
- In crates/tsunagi-wireguard/src/plugin.rs:
- In Worker::sync, retain only active unclosed links in state.links.
- In crates/tsunagi-wireguard/src/transport.rs:
- Check is_handshake_init directly as '!msg.is_empty() && msg[0] == 1',
ensuring handshake initiations are broadcast to all candidate addresses.
- Clean up matched_peer to look up links and peer candidate addresses directly.
- Tests:
- Update test_flow_tunnel_rekey_timeout_triggers_failure to assert failure
latching and verify no repeated failure reports on subsequent ticks.
Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed.
- cargo build --release passed.
|
||
|
|
8cfece9479 |
Filter noisy internal warnings from boringtun and iroh net_report
The CLI's default tracing filter previously allowed WARN level logs from boringtun and iroh, producing two confusing and noisy log messages during routine operation: 1. 'WARN HANDSHAKE(REJECT_AFTER_TIME - KEEPALIVE_TIMEOUT - REKEY_TIMEOUT (on receive))': emitted by boringtun's internal timer tick when an endpoint primarily receives data for 165 seconds (180s - 10s - 5s). In boringtun, this is a routine scheduled rekey timer rather than an error or failure, but was unhelpfully logged as warn! upstream. 2. 'WARN probe timed out': emitted by iroh's net_report module during background HTTP captive portal detection when running in isolated subnets or VMs without direct internet access. This probe has no impact on mesh connectivity. Update the default logging filter to: 'info,iroh=warn,iroh::net_report=error,quinn=warn,rustls=warn,boringtun=error,mainline=off' Real WireGuard handshake and rekey failures continue to be detected and reported cleanly by Tsunagi's own data plane diagnostics and automatic fallback handlers at INFO and WARN levels. Validation on Linux: - cargo fmt --all -- --check passed. - cargo clippy --locked --workspace --all-targets -- -D warnings passed. - cargo test --locked --workspace --all-targets passed. - cargo build --release passed. |
||
|
|
3930dff2dc |
Detect WireGuard tunnel rekey failure and trigger automatic fallback
Previously, because UDP is connectionless, when a pure WireGuard tunnel
encountered handshake or rekey failures (e.g. boringtun logging
`WARN HANDSHAKE(REKEY_TIMEOUT)` and `CONNECTION_EXPIRED`), the
`WireguardLink` never closed. It remained active in `agent.links` and
as a direct hop in `RelayHub.raw`. Consequently, outgoing packets
continued to be blackholed over the dead direct path, preventing
`RelayHub` from using working multihop relay routes and preventing
the agent from falling back to alternative protocols (`tcp-tls` or
`wg-quic`).
Key changes:
- In `crates/tsunagi-wireguard/src/device.rs`:
- In `FlowTunnel`, track active handshake and rekey attempts via
`handshake_started_at`. If an active handshake attempt remains
unanswered beyond `HANDSHAKE_TIMEOUT` (15s) or boringtun reports
`ConnectionExpired`, declare the tunnel dead.
- Reset timeout timers upon successful handshake reception in
`decapsulate`.
- In `drive_timers`, when tunnel failure is detected, trigger
`inner.on_tunnel_failed(peer)`, which closes the direct transport
link and notifies the agent via `PluginContext::report_peer_protocol_failure`.
- In `crates/tsunagi-wireguard/src/transport.rs`:
- Add `WireguardLink::close` to transition `is_closed` to true and
notify `closed_notify`.
- In `WireguardLink::recv`, select on `closed_notify` so readers
unblock and return `None` immediately on close.
- Add `WireguardTransport::close_link` to close active links by
network and peer.
- In `crates/tsunagi/src/dataplane/mod.rs`:
- Add `PluginRequest::ProtocolFailure` and public helper
`PluginContext::report_peer_protocol_failure`.
- In `crates/tsunagi/src/agent/mod.rs` and `crates/tsunagi/src/agent/network.rs`:
- Add `NetCommand::ProtocolFailure` to runtime command handling.
- On protocol failure: remove dead direct link from `self.links`,
clear it from `RelayHub.raw` (so `RelayHub` immediately routes
over available multihop relays), record failure on `wg` for that
peer with exponential backoff cooldown, emit `DataLinkDown`, and
immediately call `ensure_links()` to dial the next mutually supported
protocol (`tcp-tls` or `wg-quic`).
- Tests:
- Add `test_flow_tunnel_rekey_timeout_triggers_failure` and
`test_flow_tunnel_reset_on_handshake` unit tests in `device.rs`.
- Add `test_wireguard_link_close_and_transport_close_link` in
`transport_tests.rs`.
- Add `test_protocol_dial_state_cooldown_and_fallback_selection` in
`network.rs`.
Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed (350+ tests passed).
- Built release binary: target/release/tsunagi.
|
||
|
|
35acc4d231 |
Fix WireGuard relay preemption with UDP probing, allow VM bridges, and categorize status addresses
When remote peers communicate across networks with mixed connectivity (e.g.
a remote Linux node, a local Linux host, and a Windows VM guest under KVM):
- Previously, `WireguardTransport` immediately created and installed
unverified direct links into `RelayHub` in `notify_peer_capability` and
`open()`. When a remote peer had only unroutable private addresses
(e.g. 192.168.122.151 on a KVM NAT bridge), this dead direct link
preempted the working 2-hop relay, causing packet drops, failed handshakes,
and REKEY_TIMEOUT errors.
- Host-guest VM bridges (`virbr*`) were previously filtered out, preventing
the VM guest from discovering the host's bridge IP (192.168.122.1) and
forcing it to attempt cross-interface connections to host Wi-Fi/LAN.
- Concurrent activation of multiple networks sharing one `tsun0` interface
raced in `sync_addresses`, resulting in netlink `File exists (os error 17)`
and `The object already exists` on Windows.
Key fixes:
- In `crates/tsunagi-wireguard/src/transport.rs`:
- Implement bidirectional UDP probing with `TSUNAGI_WG_PROBE_V1` PING/PONG.
- In `open()`, probe candidate endpoints with retransmission up to 1200ms.
If unreachable, return `TransportError::Unreachable`, preventing dead
links from preempting valid relay routes in `RelayHub`.
- In `notify_peer_capability`, send hole-punching probes without prematurely
installing unverified links.
- In `accept_loop`, reply to PING probes with PONG and install confirmed
inbound links targeting the verified source address.
- In `WireguardLink::send`, inspect byte 74 of enveloped packets to detect
handshake initiations, ensuring confirmed links route data packets only
to the active target.
- In `crates/tsunagi-wireguard/src/plugin.rs`:
- Permit VM bridges (`virbr*`) as candidate interfaces (priority 10).
- Add `prioritize_local_subnet` to prioritize candidates sharing a local
IPv4 subnet with the receiver (e.g. 192.168.122.0/24).
- In `crates/tsunagi/src/overlay/interface.rs` & provisioners:
- Add async mutex `sync_lock` to serialize concurrent address syncs across networks.
- Handle netlink EEXIST (os error 17) and Windows already-exists errors
idempotently as success.
- In `crates/tsunagi-cli/src/main.rs`:
- Split `local addresses` in `tsunagi status` into `announced` (candidate
addresses in priority order with interface and rank), `not announced`
(container bridges/internal addresses with interface name), and `loopback`.
- Tests:
- Add `test_wireguard_transport_unreachable_probe_times_out` and
`test_matches_subnet` unit tests.
Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed (350+ tests passed).
|
||
|
|
c791102ad9 |
Fix overlay and virtual interface candidate leakage and WireGuard rekeying
On Windows (Wintun), iface.name in netdev is an adapter GUID, while
iface.friendly_name is 'tsun0'. Checking only iface.name failed to
identify and exclude Wintun overlay interfaces. Consequently, Windows
nodes announced their own overlay IP addresses (e.g. 10.13.37.26) as
candidate endpoints. Remote peers latched onto this overlay IP as the
WireGuard target, routing encrypted packets into tsun0 (recursive loop),
resulting in failed handshakes and subsequent HANDSHAKE(REKEY_TIMEOUT)
and CONNECTION_EXPIRED.
Similarly, Docker bridges (br-*, docker0) and virtual bridges were
announced and alphabetically sorted before physical LAN or Tailscale
interfaces, causing peers to attempt WireGuard handshakes with internal
Docker bridge addresses (e.g. 172.21.0.1).
Key changes:
- In crates/tsunagi-wireguard and crates/tsunagi-tcp-tls:
- Robustly identify overlay interfaces by inspecting iface.name,
iface.friendly_name, and iface.description for 'tsun', 'tsunagi',
and 'wintun'.
- Filter out container and virtual bridge interfaces (docker, br-,
veth, virbr, cni, flannel, cali, kube, dummy, lxc, podman, vethernet,
vboxnet, vmnet).
- Prioritize candidates by physical interface score: interfaces with
a default gateway (physical LAN/WAN) and Tailscale (100.64.0.0/10)
score highest and sort to candidate #0.
- In on_peer_capability, filter out any announced address falling inside
any active overlay IPv4 range (defense in depth on receiver).
- In crates/tsunagi-wireguard/src/transport.rs:
- In WireguardLink::send, broadcast Handshake Initiation packets (type 1)
to all candidate addresses so initial handshakes and rekeys probe all
candidate paths and recover if an endpoint roamed or NAT expired.
- In accept_loop, prioritize exact socket address matches before falling
back to candidate IP matches for NAT port roaming.
- In crates/tsunagi/src/net.rs:
- Update iroh AddrFilter to inspect friendly_name and description,
preventing Wintun overlay IPs from leaking into iroh discovery endpoints.
- Tests:
- Add unit tests for candidate interface filtering, IP validation, and
priority scoring in tsunagi-wireguard and tsunagi-tcp-tls.
Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed (350+ tests passed).
|
||
|
|
f80841da63 |
Fix WireGuard multi-network demux, candidate discovery, and status reporting
When a peer belongs to multiple overlay networks served by one agent (e.g.
a VM participating in both 'hexor' and 'ultradesu'), packets received on
the shared WireGuard UDP socket could not be differentiated by source IP/port
alone. Previously, `WireguardTransport::accept_loop` picked the first
matching link found in iteration order, starving other networks for that peer
and causing rekey timeouts (`WARN HANDSHAKE(REKEY_TIMEOUT)`,
`ERROR CONNECTION_EXPIRED(REKEY_ATTEMPT_TIME)`) and packet loss on the
starved network.
Additionally:
- When a peer had multiple candidate endpoints (such as LAN, VM bridge, and
Tailscale CGNAT addresses), `WireguardLink` sent handshake initiations
only to `addrs[0]`. If the first address was unreachable from the remote
side (e.g. host's Tailscale IP unreachable from inside a local VM), the
handshake never completed.
- In `build_report`, peers known to the `wg-quic` plugin were added with
empty tunnels even if an active `wg` (pure WireGuard) tunnel existed,
causing `tsunagi status` to falsely report 'no WireGuard handshake yet'
for connected and pingable peers.
- `local_ip_candidates` previously included IPv6 addresses which caused
OS error 22 (EINVAL) when passed to the IPv4-bound UDP socket.
Key fixes:
- In `crates/tsunagi-wireguard/src/transport.rs`:
- Store `peer_networks` as `HashSet<NetworkId>` per peer.
- In `WireguardLink`, maintain a list of candidate addresses and a
`confirmed` flag. Before a handshake response confirms the active path,
transmit datagrams (handshake initiations) to all candidate addresses.
Upon receiving an inbound datagram, lock onto the confirmed address.
- In `accept_loop`, deliver inbound datagrams to all active links
associated with the sender's endpoint ID across all networks. Boringtun's
cryptographic index and AEAD verification naturally demultiplexes and
accepts the packet in the correct network while discarding it in others.
- Filter candidate addresses to IPv4 to match the bound socket family.
- In `crates/tsunagi-wireguard/src/plugin.rs`:
- Only collect IPv4 interfaces in `local_ip_candidates`.
- In `crates/tsunagi-cli/src/main.rs`:
- Refactor `build_report` to evaluate all protocols per peer and prioritize
whichever protocol has an active tunnel (`wg` > `tcp-tls` > `wg-quic`).
- In `crates/tsunagi-wireguard/tests/plugin_tests.rs`:
- Add `test_wireguard_two_networks_same_peers` verifying simultaneous
bidirectional packet transmission between two peers sharing two networks
over a single `WireguardTransport`.
Validation on Linux:
- cargo fmt --all -- --check passed.
- cargo clippy --locked --workspace --all-targets -- -D warnings passed.
- cargo test --locked --workspace --all-targets passed (350+ tests passed).
|