Files
dnswatcher/TODO.md
sneak ff66ecc0c9
All checks were successful
check / check (push) Successful in 43s
ci: re-run make check on every cibuild instead of serving it from the layer cache (closes #115)
`script/cibuild` was plain `docker build .`. The Dockerfile does
`COPY . .` and then `RUN make check`, and Docker invalidates `COPY . .`
only on a content change, so on a byte-identical tree the check layer
was reused and the suite never ran. The script's header comment claimed
that a successful build implies all checks pass, which was false
whenever the cache was warm. Reproduced on this branch's parent: a
second consecutive run returned success in 283 ms with
`#13 [builder 9/10] RUN make check` reported `CACHED`.

That matters more here than in a typical repo. DNS is never mocked in
this repository, so the suite queries live DNS and its outcome varies
with real-world conditions; caching the verdict of a non-deterministic
check replays a stale result in exactly the case where re-running is
most valuable. It is also the gate every PR is verified through.

Fix: declare `ARG CHECK_EPOCH` immediately above the check step and
expand it into the command, with `script/cibuild` passing a fresh
`$(date +%s%N)` per invocation. A build argument's value participates in
the cache key of later instructions in the stage even when they do not
reference it, so a fresh value busts this layer either way; the value is
expanded into the command deliberately, which makes the invalidation a
property of the command string itself rather than of how a given builder
treats unreferenced args, and surfaces the epoch in the build log as a
diagnostic. Placing the ARG here and no earlier keeps the pinned
toolchain installs and `go mod download` above the invalidation line, so
only the check and the steps after it re-run. The epoch is nanosecond
granular so that two concurrent invocations starting in the same second
cannot share a value.

A plain `docker build` without the argument caches as before; nothing
outside the CI entrypoint changes behaviour.

Verified by experiment, not inspection:

- Two consecutive runs on an unchanged tree: 55.2 s and 42.2 s, both
  exit 0, with distinct epochs. The second run shows
  `RUN echo "check epoch: ..." && make check` executing for 36.0 s and
  216 passing tests across all eight packages, while `apk add`, both
  pinned `go install` steps, `go mod download`, `COPY go.mod go.sum` and
  `COPY . .` all report `CACHED`.
- Negative control: planted `internal/config/zz_negative_control_test.go`
  calling `t.Fatal("NEGATIVE-CONTROL-115: planted failure, cache did not
  serve this layer")`. The build failed in 24.7 s with exit 1, printing
  that exact message and `--- FAIL: TestNegativeControlIssue115`, and the
  check step exited with code 2. A cached layer cannot produce a failure
  predicted in advance, so this establishes the suite ran. The file was
  then removed, `git status` confirmed clean, and the tree built green
  again in 48.1 s.
- Total build time 42-55 s against the policy's 5-minute ceiling.
- `make check` green. No pin touched: the `golang` and `alpine` sha256
  digests, golangci-lint `c0d3ddc9`, and goimports `009367f5` are
  unchanged, and `.golangci.yml` still hashes to `021cc83f4e6f...`.
2026-08-09 06:22:28 +00:00

164 lines
7.7 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: `script/cibuild` can no longer report a green it did not
earn. The Dockerfile declares `ARG CHECK_EPOCH` immediately above the
check step and expands it into the `RUN` command, and `script/cibuild`
passes a fresh `$(date +%s%N)` per invocation, so the `make check`
layer is always re-executed while the pinned toolchain install and
`go mod download` stay cached. Verified by experiment: before the fix
a second run on an unchanged tree returned in 283 ms with the check
layer `CACHED`; after it the check runs every time, and a deliberately
planted always-failing test made the build fail with exactly that
test's message
- 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