Linux support hardening + cross-platform audit (0.5.0) - #39
Merged
Conversation
…der perf (Refs #35) - Restore file-drop overlay: Svelte 5 prunes child-combinator rules with runtime-added ancestor classes; use descendant combinator - Remove dead CSS (import-modal remnants in App.svelte, use-current-network remnants in AutomationEditor) - Remove 4 never-imported components (MiniMode, SplitTunnelUI, ScriptWarning, KeyGenerator) and unused tPlain translator - Prune 100 unused i18n keys per locale (en/ko/ja), all statically verified - Gate store notifications in stores/tunnels.js: Svelte object stores re-notify unconditionally, causing full re-sorts and reactive recompute at 1 Hz while idle - Key LogViewer rows by monotonic id (ring-buffer wrap rewrote all 1000 rows per record) and TunnelList rows by name - Settings: carry trusted_ssids from fresh fetch (lost-update with CLI) - Drop frontend/frontend/bindings stubs committed by accident in bootstrap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… for unreachable hosts (Fixes #32) Router-sourced 'Destination host unreachable' replies carry no RTT token yet ping.exe can exit 0; report unreachable instead of a made-up number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mer race (Refs #35) - Linux: don't register tray.OnClick — Wails StatusNotifier fires it on the dbusmenu 'opened' event too, raising the window on every right-click - Linux: serialize showDock; concurrent SetFrameless toggles could map the window permanently undecorated - helper: shutdown AfterFunc re-checks it is still the current timer under h.mu (Stop() can't cancel an already-running callback); armShutdownTimer also stops a stale pending timer when a tunnel is active - macOS: showDock retry goroutine bails on quit via package appQuitting — Show() on a destroyed window re-runs window creation - macOS: track SSID warm-up retry goroutine in healthWg - Linux: reap pkexec to avoid one zombie per spawn attempt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s cross-check; validate automation rules on save (Refs #35) Removed (never reachable from any platform, GUI binding, or cgo export): notify package, diag CIDR/speedtest, config keygen, storage.atomicRename, tunnel.ClearAllActiveStates, update.BrewUpgradeCommand, network.OriginalNetworkState, windows VerifyIpForwardRoute (superseded by batch variant), ipc.MultiStatusResponse, wifi.DefaultRules. Wired wifi.ValidateRule into SaveAutomationRules — saves silently no-op'd malformed rules. Derive ipc.ProtocolVersion from ProtocolMajor/Minor so a constant bump can't leave the wire string behind. Also fix TunnelDetail autoLatency frozen at first computation: the dep-less $: statement never re-ran, so the latency-target label/tooltip kept showing the first-selected tunnel's endpoint forever (pre-existing, found in audit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- darwin: reapply() verified DNS drift before rewriting — every route event (roam, DHCP renew, sleep/wake) was spawning 10-20 networksetup writes and HUP'ing mDNSResponder, wiping the machine-wide DNS cache - darwin: loop watchdog reads interface counters via IFMIB sysctl instead of exec'ing netstat every 5s per full-tunnel connection (~17k spawns/day). NET_RT_IFLIST2 was rejected empirically: its if_msghdr2 ifi_ibytes wraps at 32 bits; only IFMIB carries true 64-bit counters. netstat kept as fallback; live cross-check test added - helper: latency probes drop to 5min cadence with no GUI subscribed — the headless helper (wg-quick semantics) pinged every 30s forever - wifi: SSID poll relaxes to 60s when an event watcher (NM DBus/wlanapi) is attached; Linux burned ~17k nmcli spawns/day alongside a working DBus watcher. Watchers now report attachment - helper: reuse the diff's JSON bytes in the 1Hz status broadcast instead of marshalling the same struct twice - gui: config watcher 1s → 3s (runs for the GUI lifetime, also hidden) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on services, failed-verify and ForceShutdown paths (Fixes #34) Four gaps left tunnel DNS state behind after disconnect: - RestoreDNSFromSnapshot only wrote -setdnsservers; search domains from DNS= hostnames persisted on every service forever. The snapshot now carries servers AND search domains (DNSSnapshot), and the restore iterates the union of snapshot, per-manager captures, and live services — so services that appeared mid-session (Ethernet plug, iPhone USB) are cleaned with their own captured originals - SetDNS commits dnsActive BEFORE verification: a verify failure used to return with dnsActive=false, so rollback and crash recovery both no-op'd and the user was stuck on tunnel DNS unrecoverably - ForceShutdown (helper-upgrade path) restores DNS best-effort before os.Exit — utun dies with the process but networksetup overrides don't - Crash-recovery journal gains pre_mod_search (additive; old journals restore search domains to Empty, which is correct for the common case) Also: darwin ping -W is milliseconds, not seconds — "-W 3" meant 3ms; now 3000. Regression test injects run() and asserts both servers and search domains are restored per service. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(Refs #35) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e right network (Fixes #22) - Default-route selection now requires RTF_UP + /0 netmask, picks the lowest metric, and skips tunnel/virtual interfaces — previously the first 00000000-destination row won, so a wg0 default route or a downed secondary uplink could supply the gateway - ARP lookup is scoped to the route's device and requires ATF_COM — previously any row with a matching IP won, so a docker bridge sharing 192.168.x.1 with the LAN (or a stale incomplete entry) could supply the MAC. Gateway-MAC is a trust signal for Automation rules - Physical-interface classification consults /sys/class/net/<if>/device on Linux: docker0, virbr0, tailscale0, veth*, vmnet*, CNI bridges have no backing device and no longer satisfy subnet rules for networks the machine isn't on. Name denylist kept for the other platforms - Parsers extracted into an untagged file with fixture tests that run on every platform; Linux CI exercises the full path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ID verification (Fixes #20) The pipe SDDL granted GRGW to Interactive Users (S-1-5-4) — every logged-on account on a multi-user machine could drive the SYSTEM helper (disconnect tunnels, disable kill switch, forge SSIDs into automation, force shutdown). And verifyPeerUID was a stub Windows always skipped: --uid was never passed there and os.Getuid() is -1 on Windows anyway. - GUI captures its token user SID and forwards it via --owner-sid (validated against a strict SID pattern before touching the PowerShell argv or the security descriptor) - Pipe SDDL now grants GRGW to that SID only; falls back to the old IU grant when no SID was supplied (helper spawned by an older GUI) - Per-connection verifyPeer: GetNamedPipeClientProcessId → process token → EqualSid against the owner (SYSTEM also allowed). Defense in depth on top of the ACL; the owner's own elevated processes keep the same user SID so admin terminals still work - Unix path unchanged (SO_PEERCRED/Getpeereid UID check as before) Verified: darwin+windows builds, vet, full test suite, SID-injection unit tests. Behavioral multi-user validation still needs a Windows machine — noted on the issue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Linux-only test fixture bug (Refs #25, #35) - CI now runs on pull_request + workflow_dispatch only — a bare push (including to main) triggers nothing; releases remain tag-only - macOS and Windows join Ubuntu: frontend build + go test + vet each (Windows without -race: needs cgo/mingw; Linux -race covers shared code) - TestMatchAsset_FindsPlatformAsset used a linux-amd64 decoy asset that legitimately out-matched the expected one the first time the suite ran on Linux — platform-neutral decoy now Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it sweep (Refs #35) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The macOS LaunchDaemon shipped RunAtLoad=true and installed itself
unconditionally on first launch, so a root helper started at every boot
regardless of the auto_start setting — which only ever governed the GUI
LaunchAgent. The result was a root process with no window and no tray
icon on a machine where "Launch at startup" was switched off, and it was
not merely idle: the helper evaluates the user's Wi-Fi automation rules
itself, so it could bring a tunnel up while the user believed the app
was closed.
A running GUI is the user's statement of intent that WireGuide should be
active. Make that the whole rule:
- RunAtLoad=false, so launchd registers the job but never starts it.
installAndLoadDaemon now kickstarts explicitly after bootstrap,
which bootstrap no longer does on its own.
- Drop the isDaemon() guard around the shutdown grace window. It
applies to every launch mode now, so a helper that loses its GUI
(or never gets one) exits instead of lingering. The active-tunnel
check in armShutdownTimer still keeps a live tunnel alive.
Users who want WireGuide from login enable auto_start, which installs
the GUI LaunchAgent; the GUI then spawns the helper on the normal path.
The trade is an admin prompt on any launch that finds no live helper
socket, which is the intended cost of leaving no invisible root process
behind. A live socket still short-circuits, so relaunching the GUI while
a tunnel is up does not re-prompt.
Existing installs migrate themselves: the plist content changed, so
PlistNeedsReinstall detects the drift and forces a reinstall.
Tests pin both halves of the rule (RunAtLoad read back via plutil, not
string-matched, and the kickstart that RunAtLoad=false now depends on).
Also removes build/darwin/com.wireguide.helper.plist: no build task
referenced it and its contents had drifted from what the code generates,
so it read as policy while being dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tying the helper's lifetime to the GUI left the CLI with no way to bring the stack up: dialHelper deliberately never spawns or elevates a helper, so with the app closed `ctl connect` failed permanently. It also exposed a second problem — a CLI command was indistinguishable from a GUI. Request.Transient (protocol 1.0 -> 1.1, additive) marks short-lived clients so the server never counts them as control connections. Control connections drive the shutdown grace window: attaching cancels it, detaching re-arms it. A `ctl` invocation connects, pings and exits within milliseconds, so without the flag a single `ctl status` cut a GUI-less helper's remaining life from 60s to 10s — the CLI killed the helper it had just queried. Older helpers ignore the field and behave as before. `ctl start` launches the app and waits for the helper socket. It is the only command that starts anything; connect/status still fail with "is the app running?", matching the contract docker has with dockerd. Starting stays explicit because it costs an admin prompt on macOS and because a running WireGuide is what the helper treats as consent to apply automation rules. Launch is per-OS (open -b by bundle ID, falling back to the enclosing .app, then to re-execing ourselves; Linux and Windows re-exec directly, the GUI being this same binary). `ctl stop` needs no per-OS teardown. It calls Helper.RequestQuit, and the helper broadcasts EventQuit so a connected GUI runs its normal quit path — which disconnects tunnels and stops the helper on the way out. Shutting the helper down directly would be wrong: the GUI's health monitor would respawn it and prompt for a password seconds after the user asked everything to stop. With no GUI attached the helper shuts itself down instead. The tray's Quit body moves to trayManager.quitApp so both routes leave identical state behind. Both commands are idempotent and report the end state, so they are safe to script: start on a running app and stop on a stopped one exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
launchd appends the helper's StandardOutPath forever with no rotation, and this line fires per connected tunnel every 30 seconds. On a machine running since April it was 33,720 of 35,240 lines — 95.7% of a 7.7 MB log. Nothing is lost by demoting it: the same value is already broadcast in the status event, rendered in the UI and readable via `ctl status`. The helper's log level is runtime-mutable, so anyone debugging a latency problem can turn it back on live with `wireguide ctl set loglevel debug` or the Settings UI. This is the fix rather than log rotation. Rotation would cap a file that is 96% one redundant line; removing the line drops four months of logs to roughly 330 KB, and the helper now only runs alongside the GUI. That leaves no case for adding newsyslog wiring to the privileged install script. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by running `ctl start` for real: it launched /Applications/WireGuide.app — the installed release — from a dev build's CLI. Both bundles claim com.korjwl1.wireguide, and `open -b` leaves the choice to LaunchServices. The consequence is not cosmetic. The two builds generate different LaunchDaemon plists, so whichever app starts sees the other's plist as drift and reinstalls its own, putting an admin-password prompt in front of the user on every single launch. Observed exactly that: the release app opened an authorization dialog to roll the plist back to RunAtLoad=true, undoing the migration the dev build had just done. Try the .app enclosing our own executable first, and only ask LaunchServices to resolve the bundle ID when there is no such bundle (the CLI is a bare binary on $PATH, where no "our own" app exists and the bundle ID is the right question). Symlinks are resolved before the walk so a linked CLI still finds the bundle its real binary lives in. The path walk moves to bundleFromExePath so it can be tested without an os.Executable that happens to sit in the right place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oming up Any launch that finds no live helper shows a macOS admin-password dialog, and the helper socket only appears once it is answered. osascript gives that dialog no deadline, so the CLI's own two-minute cap cancelled nothing — it just made `ctl start` lie. Observed twice while testing: the command printed "the helper did not come up" and exited nonzero while the app was up and the prompt was still on screen; answering it afterwards brought everything up correctly, by which point a script had already taken the failure branch. Raise the cap to 10 minutes, which is a human answering a password prompt rather than a machine timing out, and say something after 15 seconds so a CLI blocked on a prompt hidden behind a full-screen window doesn't read as hung. The timeout message now points at the still-open prompt instead of implying the launch failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… connection (Refs #6) The helper now ties its lifetime to the GUI (self-exits after the 60s startup grace; ctl calls are Transient and don't touch the timer), so the headless automation matrix — whose rules disconnect every tunnel for long stretches — lost its helper mid-run: status calls returned nothing and the matrix failed. Hold one non-transient control connection (python stand-in speaking the length-prefixed frame) for the test duration. Verified on the Pi: automation matrix PASS with the patch; healthcheck rerun PASS unmodified (its one failure was a transient curl timeout, not a product regression — helper journal shows no activity in the window). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…op (Fixes #38) Older Homebrew skips auto_updates casks even when named explicitly, and the skip exits 0 — RunUpdate reported success while doing nothing, stranding brew installs on old versions (observed: 0.3.1 pinned for three months). --greedy forces the upgrade on every brew version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d no GUI Transient CLI clients (ctl, wifi-rule evaluation) intentionally never fire the IPC server's OnConnect/OnDisconnect, so after the GUI quits with a tunnel up, a `ctl disconnect` of the last tunnel left the elevated helper with no GUI, no tunnel, and no shutdown timer — alive until reboot or `ctl stop`. Regression vs 0.4.2, where the CLI's non-transient disconnect re-armed the grace window. Arm the grace window from every teardown path that can drop the active count to zero (Disconnect handler, rule-driven disconnect, connect rollback) when no control connection is attached. armShutdownTimer's active-tunnel guard keeps this a no-op while any tunnel survives. Verified live on Windows: helper self-exits 10s after the sequence GUI-kill → ctl disconnect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…levate tests The 3-OS CI matrix runs `go test ./...` on windows-latest, where two tests added on this branch fail on path-separator rules alone: - TestBundleFromExePath pins darwin-only .app bundle lookup with Unix fixtures that filepath mangles under Windows; skip it there. - TestValidateArgsSocketSID used /var/run paths that fail the new absolute-path check on Windows; use the production pipe address (\\.\pipe\wireguide) so the SID rules stay exercised on all three OSes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Linux support hardening + cross-platform audit round, verified on all 3 OSes:
Also:
ctl start/stop,--jsonoutput, 3-OS CI matrix, pipe scoped to spawning user SID (#20), Linux gateway fingerprinting (#22), locale-agnostic ping parsing (#32), dead-code purge (keygen / CIDR calc / speed test / mini mode).Releasing as 0.5.0 after merge.
🤖 Generated with Claude Code