Skip to content

feat: zero-config VPN endpoints, instant failover, and test tooling#14

Merged
Behnam-RK merged 4 commits into
mainfrom
feat/zero-config-vpn-endpoints
Jun 29, 2026
Merged

feat: zero-config VPN endpoints, instant failover, and test tooling#14
Behnam-RK merged 4 commits into
mainfrom
feat/zero-config-vpn-endpoints

Conversation

@Behnam-RK

Copy link
Copy Markdown
Owner

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.endpoints now accepts hostnames or IPs (re-resolved at runtime); validation rejects only malformed entries (classifyTarget). Backward compatible with existing IP-literal configs.
  • New 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/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)

  • Tunnel Watcher (poll + asymmetric debounce: down debounced, up immediate).
  • Legacy/direct mode: a tunnel drop blocks immediately — a kill switch needing only a tunnel name, no endpoints. 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, with a safety invariant: never applied empty, and monotonic while blocked (can't drop the endpoint needed to recover).

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 (on monitor and run) and --simulate-tunnel-down <dur> (on run).

Config schema: new vpn.autoDiscoverEndpoints (bool), vpn.endpointRefresh (default 5m), vpn.tunnelWatch (default 1s).

Verification

  • go build, go vet, go test ./...70 pass; -race clean; make build-all → all 5 targets compile (darwin/other build-tag seam holds).
  • Live no-root checks: validate a hostname config; monitor --once --simulate-country IR flips the verdict to BLOCK; print-rules renders the resolved endpoint into the pass-list.

Docs updated: docs/CONFIG.md (new fields + testing recipes), configs/dezhban.vpn-guard.json sample, detect-vpn hint.

🤖 Generated with Claude Code

Behnam-RK and others added 4 commits June 29, 2026 12:42
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>
@Behnam-RK Behnam-RK merged commit fb2feb3 into main Jun 29, 2026
5 checks passed
@Behnam-RK Behnam-RK deleted the feat/zero-config-vpn-endpoints branch June 29, 2026 15:33
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