Files
dnswatcher/TODO.md
sneak 02b63a4e65
All checks were successful
check / check (push) Successful in 37s
server: set ReadTimeout, WriteTimeout, and IdleTimeout (closes #99)
The http.Server literal only set ReadHeaderTimeout. The other three
timeouts defaulted to zero, which in net/http means no limit: past the
header phase a peer could hold a connection open forever, responses had
no write deadline, and keep-alive connections were never reaped.
REPO_POLICIES.md requires all four before 1.0.

All four are now named constants in internal/server/server.go:

  ReadHeaderTimeout  10s  (unchanged)
  ReadTimeout        15s  whole request; every route is a bodyless GET
  WriteTimeout       75s  handler execution plus response flush
  IdleTimeout       120s  keep-alive reaping

WriteTimeout must exceed the 60s chimw.Timeout handler budget in
routes.go. net/http arms the write deadline once the request headers
have been read, so it covers handler execution as well as the response
write; a smaller value would sever the connection before a handler that
legitimately used its full budget could respond, making that budget
unreachable. The 15s difference is the response-flush allowance. The
comment on the const block states this relationship.

IdleTimeout sits above the common Prometheus scrape intervals so the
scraper reuses its connection instead of reconnecting each cycle, while
an abandoned connection is still reaped within two minutes.

The http.Server literal moved into newHTTPServer so the configuration
is testable without binding a socket. Tests assert all four fields are
non-zero, that WriteTimeout exceeds the handler budget, and that
ReadTimeout covers ReadHeaderTimeout; they compare configured values
only and measure no elapsed time, so they cannot flake.
2026-08-09 05:40:31 +00:00

162 lines
7.5 KiB
Markdown

# 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-09: `http.Server` now sets all four socket-level timeouts
(`ReadTimeout` 15s, `ReadHeaderTimeout` 10s, `WriteTimeout` 75s,
`IdleTimeout` 120s) as named constants in `internal/server/server.go`,
closing the slowloris / unreaped-keep-alive exposure required by
`REPO_POLICIES.md` before 1.0; `WriteTimeout` is deliberately greater
than the 60s `chimw.Timeout` handler budget so that budget stays
reachable, and tests in `internal/server` pin both the non-zero
values and that relationship (#99)
- 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