feat: zero-config VPN endpoints, instant failover, and test tooling#14
Merged
Conversation
VPN guard previously required a hand-typed server IP in vpn.endpoints — impractical for hostname/rotating-pool VPNs (NordVPN/ProtonVPN/…) and the #1 lockout cause. This makes the guard usable with minimal config and adds a no-root testing path. Endpoints (config.go, netdetect/resolve.go): - vpn.endpoints accepts hostnames or IPs (re-resolved at runtime); validation rejects only malformed entries (classifyTarget). - vpn.autoDiscoverEndpoints learns the live server IP from the active socket (macOS), so a rotating-pool VPN needs no typed endpoint. - EndpointSource.Resolve unions literals + resolved hostnames + discovered IPs, dedups, drops tunnel-internal addresses, and never aborts on partial failure. Failover (netdetect/watch.go, runner.go): - Tunnel Watcher (poll + asymmetric debounce: down debounced, up immediate). - Legacy mode: a tunnel drop blocks immediately (kill switch needing only a tunnel name); tunnel-up does not auto-unblock (recovery stays hysteresis-gated). - Guard mode: watcher is observability-only — the standing pf rule already cuts a drop instantly and leak-free; forcing FULL BLOCK would sever the reconnect handshake and leak during recovery probes. - Endpoint set re-resolved on a refresh ticker; never applied empty and monotonic while blocked (can't drop the endpoint needed to recover). Testing (cmd/dezhban/main.go, monitor/simulate.go): - `dezhban monitor`: read-only live view of IP, country, per-tunnel state, resolved/discovered endpoints, and the would-be verdict. No root. - --simulate-country XX (monitor, run) and --simulate-tunnel-down <dur> (run). Docs: CONFIG.md (new fields + testing recipes), vpn-guard.json sample, detect-vpn hint. All tests pass; all 5 cross-compile targets build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses three findings from the PR #14 review: - runner: a guard-time endpoint refresh that brings nothing new (a loss-only shrink, the signature of a transient DNS/discovery flake) no longer drops a still-needed server endpoint. The monotonic invariant previously only covered the blocked state, so a flake-then-BLOCK could restore a guard that can't reconnect. Genuine rotation (a new address surfaces) still replaces. - config: reject all-numeric top-level labels in isPlausibleHostname so a truncated/malformed IP (e.g. "203.0.113") fails at validate time instead of passing as a "hostname" that never resolves and is silently dropped at run. - cmd: construct the base monitor once on the --simulate-country paths (runDryRun, cmdMonitor) instead of building and discarding a second one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CLAUDE.md / README.md: add the new read-only `monitor` subcommand to the command lists and the no-root diagnose section, plus a note on `--simulate-country` for driving the verdict from anywhere. - README.md: remove the per-phase Status table — all phases are complete and the table no longer carries useful signal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Relocate docs/testing-macos-block.md to docs/plans/ alongside the other plan/test docs (pure rename, contents unchanged). - Ignore .serena/ — local Serena tooling state, not project-tracked. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Why
VPN guard mode previously required a hand-typed server IP in
vpn.endpoints— impractical for hostname/rotating-pool VPNs (NordVPN/ProtonVPN/ExpressVPN) and the documented #1 lockout cause. It was also hard to test without a real sanctioned IP. This makes the guard usable with minimal config and adds a no-root testing path, while keeping the fail-closed leak-prevention guarantees intact.What changed
Zero/low-config endpoints (
internal/config/config.go,internal/netdetect/resolve.go)vpn.endpointsnow accepts hostnames or IPs (re-resolved at runtime); validation rejects only malformed entries (classifyTarget). Backward compatible with existing IP-literal configs.vpn.autoDiscoverEndpointslearns the live server IP from the active socket (macOS), so a rotating-pool VPN needs no typed endpoint.EndpointSource.Resolveunions literals + resolved hostnames + discovered IPs, dedups/sorts, drops tunnel-internal addresses, and never aborts on partial failure.Instant failover where it's actually missing (
internal/netdetect/watch.go,internal/runner/runner.go)Watcher(poll + asymmetric debounce: down debounced, up immediate).Testing tooling (
cmd/dezhban/main.go,internal/monitor/simulate.go)dezhban monitor— read-only live view: IP, country, per-tunnel up/down, resolved+discovered endpoints with source, and the would-be verdict. No root, no firewall change.--simulate-country XX(onmonitorandrun) and--simulate-tunnel-down <dur>(onrun).Config schema: new
vpn.autoDiscoverEndpoints(bool),vpn.endpointRefresh(default 5m),vpn.tunnelWatch(default 1s).Verification
go build,go vet,go test ./...→ 70 pass;-raceclean;make build-all→ all 5 targets compile (darwin/other build-tag seam holds).monitor --once --simulate-country IRflips the verdict to BLOCK;print-rulesrenders the resolved endpoint into the pass-list.Docs updated:
docs/CONFIG.md(new fields + testing recipes),configs/dezhban.vpn-guard.jsonsample,detect-vpnhint.🤖 Generated with Claude Code