feat: macOS GUI Phase 9+10 — diagnostics, panic, install/uninstall, VPN config panel#16
Open
Behnam-RK wants to merge 4 commits into
Open
feat: macOS GUI Phase 9+10 — diagnostics, panic, install/uninstall, VPN config panel#16Behnam-RK wants to merge 4 commits into
Behnam-RK wants to merge 4 commits into
Conversation
…dge cases install-local.sh installed the system config 0600, so the unprivileged inspect commands (e.g. the `dezhban status` the script itself runs) could not read the root-owned config and failed with "permission denied". Install it 0644 to match config.Save's convention — the file holds no secrets and the root daemon and unprivileged readers resolve the same path. Rework uninstall-local.sh to survive a half-registered or already-gone service: panic-flush the firewall rules first (so a wedged service can't leave the host blocked when the legacy `launchctl unload` fails with I/O error 5), tolerate stop/unregister failures, remove a leftover launchd plist, and delete the installed /etc/dezhban config by default (KEEP_CONFIG=1 to keep). Both scripts now flag a `go install` copy of dezhban left on $PATH that would shadow the local build. Update the Makefile help and docs/development.md to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In VPN guard mode, the geoTick loop kept running country lookups even after the watcher reported the tunnel down. With egress pinned to the (now-down) tunnel, every lookup times out, and fail-closed then drives the Decider GUARD -> FULL BLOCK after the hysteresis window. FULL BLOCK renders no passes, so it closes the very endpoints the guard holds open for reconnect -- directly fighting the "guard holds the line, endpoints open for reconnect" intent the watcher path is careful to preserve. Track the tunnel up/down state from the watcher edges and skip the geo step while the tunnel is down and still guarding (Watcher != nil && !tunnelUp && !blocked). While blocked we still probe -- the bounded recovery probe is the only path back out of FULL BLOCK. With no watcher, tunnelUp stays true and the loop behaves exactly as before. This also removes the WARN spam and the stacked lookup-timeout budget burned each tick while the tunnel is down. Tests: add TestVPNTunnelDownSkipsGeoStep (a failing monitor must not full-block while the tunnel is down); rewrite TestVPNWatcherObservabilityOnly to terminate on a timeout instead of monitor exhaustion, since the new skip stops the geo ticks that used to drain it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FxggCfcVjXMNujNWoVgYBY
…n reconnect
Reproduced: cut the VPN by hand, and it could not reconnect until dezhban
was stopped. Root cause is a reconnect livelock, not a config fault.
VPN FULL BLOCK rendered "no accepts beyond loopback" — it dropped the
endpoint passes too. So once fail-closed escalated GUARD -> FULL BLOCK on
failed lookups, the encrypted handshake could no longer reach the server:
the tunnel could not re-establish. Recovery is only the time-windowed probe,
which lifts GUARD for one lookup then re-cuts to FULL BLOCK — tearing the
tunnel back down every cycle. The block lifts only after `hysteresis`
consecutive *allowed* lookups, but the tunnel is cut each cycle so lookups
keep failing: the block can never lift on its own. Only stopping the daemon
breaks it. (The earlier tunnel-down skip did not catch this: FULL BLOCK
fired while the interface still looked up, before the watcher's down edge.)
The endpoints are the VPN server's physical IPs; they carry only encrypted
handshake packets, never a plaintext leak. The forbidden-exit leak flows
through the tunnel interface, which stays cut. So FULL BLOCK now renders
GUARD minus the tunnel-interface pass: tunnel-egress cut (no leak) but the
endpoint passes kept open (reconnect works). The tunnel transport now
survives the probe re-cut, so a genuinely-down tunnel comes back and a later
probe observes an allowed exit — no livelock, no leak.
Applied across all three backends (nft/pf/wfp) plus the shared Mode doc;
render tests rewritten to assert endpoint kept + tunnel-iface dropped +
allowlist still omitted. runner probe/watcher docstrings and docs/modes.{md,html}
updated to match. Cross-builds linux/windows/darwin; full suite green.
Follow-up (not in this change): scope the recovery lookup to the provider
IPs on the tunnel interface to shrink the probe's bounded leak window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxggCfcVjXMNujNWoVgYBY
Closes the CLI↔GUI gap identified after Phase 8: the menubar app only exposed Start/Stop/Block/Unblock, leaving Panic, install/uninstall, diagnostics, and VPN-guard config editing CLI-only. Phase 9 (diagnostics, output capture, safety controls): - runPrivileged now returns captured stdout/stderr instead of a bare Bool, so failures show real output instead of silence - Panic (confirmation-gated), Install/Uninstall service, About panel, and a View logs submenu filtered to the unified log - One shared OutputPanel window reused by every diagnostic/action Phase 10 (VPN guard config panel): - Closes the Phase 8 scope note deferring in-app VPN-mode toggling - Drives the CLI's existing config get/set/validate per dotted key — no new Go command, no second config schema mirrored in Swift - Orders field writes (other fields before enabling, vpn.enabled first when disabling) since config set validates the whole config on every write, not just the touched field - Discloses the stop/start restart's fail-open window explicitly via a warning modal rather than hiding it; polls status --json after restart instead of assuming success Zero Go changes; go build/vet/test and swift build (debug + release) all pass. Planned via independent draft + adversarial review before implementation (see docs/plans/phase-9-gui-diagnostics.md and phase-10-gui-vpn-config.md for full design rationale and the still-open manual/root verification checklist). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017o9YaEjft1DgS62KQN4UaS
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
Closes the CLI↔GUI gap identified after Phase 8: the menubar app previously only exposed Start/Stop/Block/Unblock, leaving several essential CLI features reachable only from the terminal.
Phase 9 — diagnostics, output capture, safety controls (docs/plans/phase-9-gui-diagnostics.md):
runPrivilegednow captures and returns real stdout/stderr instead of a bareBool— failures show what actually happened instead of silencestatus --json), an About panel, and a View-logs submenu scoped to the unified log (process == "dezhban")OutputPanelwindow reused by every diagnostic/actionPhase 10 — VPN guard config panel (docs/plans/phase-10-gui-vpn-config.md):
config get/set/validateper dotted key — no new Go command, no second config schema mirrored in Swiftvpn.enabledlast when enabling; reversed when disabling), sinceconfig setvalidates the entire config on every write, not just the touched fieldstop/startrestart's fail-open window explicitly via a warning modal rather than hiding it, and pollsstatus --jsonafter restart instead of assuming successZero Go changes —
go build/go vet/go test(93 tests, 11 packages) all pass unmodified.swift build,swift build -c release, andmake gui-macosall succeed and producedist/Dezhban.app.This was planned via an independent draft + adversarial review pass before implementation (see the two phase docs for full design rationale, rejected alternatives, and the risks/open-questions sections).
Test plan
Automated (done):
go build ./... && go vet ./... && go test ./...swift buildandswift build -c releasemake gui-macosproducesdist/Dezhban.appManual (needs a real macOS host with root — not exercisable in CI/sandbox, see each phase doc's
## Acceptance / verificationsection):status --json's registered flag correctlydoctor/validate/print-rules/monitor --oncedezhban config show; enabling with valid endpoints round-trips through restart (icon ⚪ during the gap, then 🟢/🔴); a deliberately-bad endpoint is refused before any restart; a simulatedstartfailure is reported as failure, not false successlogcommands; no orphanedlog streamprocess after closing the window🤖 Generated with Claude Code
https://claude.ai/code/session_017o9YaEjft1DgS62KQN4UaS