Files
dnswatcher/TODO.md
sneak 9cb2c2b7e0
All checks were successful
check / check (push) Successful in 1m18s
test: make live DNS tests robust instead of gated (closes #93)
The resolver's live-DNS tests failed nondeterministically, a different
subset each run. Three structural causes, all test-side:

- Burst fan-out. Every test in the package is parallel and the build
  hosts have many cores, so all ~35 iterative resolutions started at
  the same instant and, because queryServers walks rootServerList() in
  fixed order, hit the same root server within milliseconds. Root
  servers rate-limit that.
- No retry anywhere. One dropped UDP packet in a delegation chain
  failed a test outright.
- Unanimity assertions. TestQueryAllNameservers_AllReturnOK and
  _NXDomainFromAllNS required every one of a domain's nameservers to
  answer, with no tolerance for one being slow.

New internal/resolver/livedns_test.go addresses each: a package-wide
gate bounds how many live resolutions are in flight at once, every
live operation gets three attempts with exponential backoff and its
own deadline, and multi-nameserver assertions now need a strict
majority rather than unanimity. The retry predicate is deliberately
transport-level -- "did a nameserver answer at all" -- never the
assertion under test, so a resolver that answers incorrectly still
fails on the first attempt. A nameserver that stays silent is
tolerated; one that answers wrongly is not.

livedns_harness_test.go tests that machinery directly: quorum
arithmetic, status counting, the gate's concurrency bound, per-attempt
deadlines, and recovery from a transient failure. It touches no DNS.

Nothing is mocked, faked, stubbed, recorded, skipped or build-tagged,
and production resolver behaviour is unchanged.

Test caps move to the new org-wide values ruled at prompts issue 41:
60s hard cap, 20s target, 90s -timeout backstop. REPO_POLICIES.md is
re-vendored byte-identical from sneak/prompts rather than hand-edited,
which also picks up the golangci-lint paragraph this copy had drifted
behind on. TESTING.md's stale 30-second target follows to 60.

#93
2026-08-10 13:09:17 +00:00

8.3 KiB

Workflow

  • branch (from main)
  • do the work in Next Step
  • move Next Step to the top of Completed Steps
  • move the top item of Future Steps into Next Step
  • commit (TODO.md changes in the same commit as the work)
  • merge to main if the branch is not protected, otherwise open a PR
  • push

Status

pre-1.0. No git tags. Core resolver work in flight on feature/resolver (dirty: internal/resolver/resolver_test.go). Local checkout has diverged from origin: origin/main is 8 commits ahead (watcher orchestrator, unified TARGETS) and origin/feature/resolver already contains the full iterative resolver implementation with hermetic mocked tests.

Next Step

Policy scaffold commit: add LICENSE, REPO_POLICIES.md, .editorconfig, .dockerignore, and .gitea/workflows/check.yml, and add the missing fmt-check, docker, and hooks targets to the Makefile. One commit, then confirm make check still passes.

Completed Steps

  • 2026-08-10: live-DNS test flakiness addressed by robustness rather than gating, per the owner's ruling on #93: new internal/resolver/livedns_test.go adds a package-wide concurrency gate (so parallel tests stop bursting at the first root server), retry with exponential backoff on transport failures only, and quorum instead of unanimity for multi-nameserver assertions. The make test cap moved to the new org-wide 60s hard cap / 20s target with a 90s -timeout backstop; REPO_POLICIES.md re-vendored byte-identical from sneak/prompts. No mocks, no -short, no build tags, no skips, and no change to production resolver behaviour
  • 2026-08-10: all linting moved into Docker: new root Dockerfile.lint on the digest-pinned golangci/golangci-lint:v2.12.2 image, script/lint reduced to a thin wrapper that builds it with --no-cache-filter=lint so the linter actually executes every run, golangci-lint install dropped from script/bootstrap (goimports stays, script/fmt needs it on the host), and the root Dockerfile given its own lint stage so its build no longer recurses through make check into script/lint. golangci-lint config verify is deliberately omitted: it fetches its schema over an unpinned live HTTPS call
  • 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs in Dockerfile and script/bootstrap); .golangci.yml set to the org-standard v2-schema config used across the org's repos (owner-authorized; same file is being landed as canonical via prompts PR #24), with settings under linters.settings so the lll/funlen/cyclop/dupl thresholds apply; fixed the resulting goconst, dupl, and lll findings; the informational gomodguard deprecation warning under this config is accepted
  • 2026-07-07 Adopted scripts-to-rule-them-all: script/ entrypoints, Makefile shims, README Entrypoints section
  • 2026-02-20: iterative DNS resolver implemented; tests made hermetic with mocked DNS (origin/feature/resolver, unmerged)
  • 2026-02-20: CI actions and go install refs pinned to commit SHAs; Gitea Actions workflow for make check (origin/ci/make-check, unmerged)
  • 2026-02-20: watcher monitoring orchestrator merged to main (#8)
  • 2026-02-20: DOMAINS/HOSTNAMES unified into single TARGETS config (#11)
  • 2026-02-19: TCP port connectivity checker, made concurrent with port validation; gosec G704 SSRF findings fixed without suppression (feature branches, unmerged)
  • 2026-02-19: TLS certificate inspector with no-peer-certificates error path and IP SANs (feature branch, unmerged)
  • 2026-02-19: gosec SSRF and formatting fixes on main
  • 2026-02-19: initial scaffold with per-nameserver DNS monitoring model

Future Steps

Compliance:

  • Add README sections required by policy (Description, Getting Started, Rationale, Design, TODO, License, Author) if any are missing
  • Pin Dockerfile base images by sha256 and ensure the Docker build runs make check

Branch reconciliation:

  • Sync local checkout with origin: local main is 8 commits behind origin/main; local feature/resolver has diverged from origin/feature/resolver, which already implements the resolver
  • Merge in-flight branches to main once green: feature/resolver, ci/make-check, feature/portcheck-implementation, feature/tlscheck-implementation

Resolver (plan from untracked TODO.md; largely implemented on origin/feature/resolver, verify each item before closing):

  • Add github.com/miekg/dns dependency
  • roots.go: hardcoded IANA root server list (a through m, IPv4/IPv6), rootServers() returning ip:53 strings
  • query.go: low-level query(ctx, server, name, qtype): UDP with TCP fallback on truncation, RD=0, context respected, 5s per-query timeout, returns raw *dns.Msg
  • trace.go: iterative delegation chasing from roots: referral detection (NOERROR, empty answer, NS in authority), glue extraction with bailiwick check, out-of-bailiwick NS resolved with recursion guard, delegation depth limit (20), retry across nameservers on failure, do not chase CNAMEs inside trace
  • FindAuthoritativeNameservers: NS set via trace, sorted, FQDN normalized, trailing dot handled; must pass its 9 tests
  • QueryNameserver: resolve NS host to IPs, query A/AAAA/CNAME/MX/TXT/ SRV/CAA/NS, build NameserverResponse with status mapping (OK, NXDomain, NoData, Error), documented record formatting, sorted values, lame delegation detection; must pass its 16 tests
  • QueryAllNameservers: find NS set for parent domain (public suffix list), query all NS in parallel with bounded concurrency, return map even when all fail, context cancellation; must pass its 4 tests
  • LookupNS: thin wrapper over FindAuthoritativeNameservers, sorted, identical results; must pass its 3 tests
  • ResolveIPAddresses: collect A/AAAA from all NS, follow CNAME chains with MaxCNAMEDepth, dedupe, sort, NXDOMAIN returns empty slice with nil error; must pass its 9 tests
  • All 39 resolver tests pass, make check green, merge to main

Watcher (internal/watcher/watcher.go):

  • Scheduling loop in Run(ctx): initial check on startup, separate tickers for DNS/port and TLS intervals, persist state via state.Save() after each cycle, clean shutdown on context cancel
  • Domain check: LookupNS, compare to stored state, store silently on first run, notify with old/new NS lists on change
  • Hostname check: QueryAllNameservers, compare per-NS records; notify on record changes, NS failure, NS recovery, inconsistency detected, inconsistency resolved, empty response; store silently on first run
  • Port check: ResolveIPAddresses, check ports 80 and 443 per IP, notify on open/closed transitions, handle new and disappeared IPs
  • TLS check: for each open IP:443, CheckCertificate; notify on expiry warning, certificate change (CN/issuer/SANs), TLS failure/recovery

Port checker (internal/portcheck/portcheck.go):

  • Tests against known-open ports and RFC documentation IPs
  • CheckPort: net.DialTimeout (5s), context respected; (true, nil) open, (false, nil) closed/timeout/refused, error only for unexpected failures

TLS checker (internal/tlscheck/tlscheck.go):

  • Tests against known public HTTPS servers, verify fields populated
  • CheckCertificate: tls.Dial to specific IP:443 with hostname as SNI; extract subject CN, issuer CN and org, NotAfter, SANs; error on handshake failure

Notification service (internal/notify/notify.go, Slack/Mattermost/ntfy backends exist):

  • Structured notification types: DNS change, port change, TLS expiry, TLS change, NS failure, NS recovery, NS inconsistency
  • Per-backend formatting: Slack/Mattermost attachment colors (red failures/expiry, yellow warnings, green recoveries, blue info); ntfy priorities (urgent failures, high warnings, default changes, low recoveries); include hostname, nameserver, old/new values, timestamps

HTTP API handlers:

  • Wire *state.State and *watcher.Watcher into handler params
  • GET /api/v1/status: full state snapshot as JSON
  • GET /api/v1/domains: domain states with NS records and last-checked
  • GET /api/v1/hostnames: hostname states with per-NS record data

Infrastructure notes (from untracked TODO.md):

  • Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de remote intentionally; do not "fix" it
  • Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix
  • Resolver tests originally used live DNS against *.dns.sneak.cloud (required records documented in the test file header); origin now has mocked hermetic tests, keep them hermetic