Skip to content

feat: macOS GUI Phase 9+10 — diagnostics, panic, install/uninstall, VPN config panel#16

Open
Behnam-RK wants to merge 4 commits into
mainfrom
feat/macos-gui-phase-9-10
Open

feat: macOS GUI Phase 9+10 — diagnostics, panic, install/uninstall, VPN config panel#16
Behnam-RK wants to merge 4 commits into
mainfrom
feat/macos-gui-phase-9-10

Conversation

@Behnam-RK

Copy link
Copy Markdown
Owner

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):

  • runPrivileged now captures and returns real stdout/stderr instead of a bare Bool — failures show what actually happened instead of silence
  • Panic (confirmation-gated), Install/Uninstall service (gated on status --json), an About panel, and a View-logs submenu scoped to the unified log (process == "dezhban")
  • One shared OutputPanel window reused by every diagnostic/action

Phase 10 — VPN guard config panel (docs/plans/phase-10-gui-vpn-config.md):

  • Closes the Phase 8 scope note that deferred 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 carefully (other fields first, vpn.enabled last when enabling; reversed when disabling), since config set validates the entire 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, and polls status --json after restart instead of assuming success

Zero Go changesgo build/go vet/go test (93 tests, 11 packages) all pass unmodified. swift build, swift build -c release, and make gui-macos all succeed and produce dist/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 build and swift build -c release
  • make gui-macos produces dist/Dezhban.app

Manual (needs a real macOS host with root — not exercisable in CI/sandbox, see each phase doc's ## Acceptance / verification section):

  • Panic tears down rules; confirm/cancel behavior
  • Install/Uninstall flips the menu and status --json's registered flag correctly
  • Diagnostics output matches hand-run doctor/validate/print-rules/monitor --once
  • VPN panel: seeded values match dezhban 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 simulated start failure is reported as failure, not false success
  • Log show/stream output matches hand-run log commands; no orphaned log stream process after closing the window
  • Auth caching still prompts once per ~5 min burst of privileged actions

🤖 Generated with Claude Code

https://claude.ai/code/session_017o9YaEjft1DgS62KQN4UaS

Behnam-RK and others added 4 commits July 2, 2026 15:21
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant