Skip to content

Add opt-in, driver-based WAN failover/CGNAT guard#2

Open
deviationist wants to merge 2 commits into
mainfrom
feature/router-cgnat-guard
Open

Add opt-in, driver-based WAN failover/CGNAT guard#2
deviationist wants to merge 2 commits into
mainfrom
feature/router-cgnat-guard

Conversation

@deviationist

Copy link
Copy Markdown
Owner

What

Adds an opt-in, driver-based guard that refuses to publish the detected public IP while the connection is on a WAN failover path / behind CGNAT (e.g. a 4G USB modem used as WAN backup). In that state the public IP is a shared carrier address that can't route back home — publishing it would point the domain at a dead address. The guard skips the update and leaves records on the last known-good IP instead.

Design

  • Agnostic core — only hard couplings remain Node + Cloudflare. All router specifics live behind a driver.
  • src/routers/ — registry + factory. No router block in config → feature is fully inert (opt-in).
  • AsuswrtMerlin driver — authenticates to the router web UI and reads nvram via appGet.cgi (no new deps; built on Node http/https, handles self-signed certs + cookies). Pauses on any of: secondary-WAN failover (wan1_primary), private/CGNAT WAN IP (incl. 100.64.0.0/10), or WAN IP ≠ the router's own realip external-IP probe. Fails open if the router is unreachable, so a transient glitch never freezes DDNS.
  • GuardState — de-dupes notifications so pause/resume emails fire only on a state transition.
  • Router password supplied via an env var named in config (passwordEnv) — never stored in config.json.

Testing

  • Live against a real AsusWRT router: healthy primary WAN → publishable: true.
  • Stubbed branches: failover→pause, CGNAT-primary→pause, realip-mismatch→pause, healthy→publish, login-fail→fail-open. All correct.
  • Opt-in verified inert when no router block is present.

Docs

README section + AGENTS.md. IPv6 / AAAA support noted as a TODO/Roadmap item (separate feature).

deviationist and others added 2 commits June 4, 2026 15:05
When a connection fails over to a CGNAT'd path (e.g. a 4G USB modem as WAN
backup), the public IP seen from the internet becomes a shared carrier address
that can't route back home. Publishing it would point the domain at a dead
address. This adds an optional guard that asks the router whether it's on such
a path and, if so, skips the update and leaves records on the last good IP.

- src/routers/: driver-based abstraction (registry + factory). Core stays
  agnostic; all router specifics live in a driver. No `router` config block ->
  feature is fully inert (opt-in).
- AsuswrtMerlin driver: authenticates to the router web UI and reads nvram via
  appGet.cgi (no extra deps; built on Node http/https, handles self-signed +
  cookies). Pauses updates on secondary-WAN failover, private/CGNAT WAN IP
  (incl. 100.64.0.0/10), or WAN-IP vs router realip-probe mismatch. Fails open
  if the router can't be reached, so a transient glitch never freezes DDNS.
- GuardState: de-dupes notifications so pause/resume emails fire only on a
  state transition, not every cron run.
- Password is supplied via an env var named in config (passwordEnv); never
  stored in config.json.
- Docs: README section + AGENTS.md; IPv6/AAAA noted as a TODO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Logger: create the log directory if missing and swallow write
  failures (one-line stderr warning) instead of throwing. A missing
  log dir was crash-looping the cron job before it reached any update.
- index.js: guard the IP-change notification behind !dryRun so a
  dry-run no longer sends real emails; logs "Would send" instead.
- Rename the --dryRun flag to --dry-run in the docs (yargs camel-cases
  it, so both forms work).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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