Per-nameserver query status is discarded: NS failure and NS recovery notifications never fire #104
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two of the README's headline notification types — NS query failure and NS recovery — are advertised prominently but cannot fire. The resolver computes the information correctly and the watcher then throws it away.
This is the largest functional gap standing between this repo and a defensible 1.0: a nameserver going dark is exactly the event a DNS monitoring daemon exists to report, and today it is silently invisible.
Current state (audited against
origin/main, commit9347a28)The data exists and is correct at the resolver layer.
internal/resolver/iterative.go:596-609classifies each per-nameserver outcome intoStatusOK,StatusNXDomain,StatusNoData,StatusTimeout, orStatusError(constants atinternal/resolver/resolver.go:16-20), andqueryEachNSpopulates the error path atiterative.go:686-696.It is then discarded in two places:
LookupAllRecords(internal/resolver/iterative.go:714-729) returns onlyNameserverResponse.Records, dropping.Statusand.Errorentirely (iterative.go:723-726).watcher.buildHostnameState(internal/watcher/watcher.go:339-359) then hardcodesStatus: statusOKfor every nameserver key present in the result (watcher.go:353).The consequences cascade:
status: "ok"with an empty record set — indistinguishable from a nameserver that legitimately holds no records for that name.detectNSDisappearances(internal/watcher/watcher.go:428-445) is gated onprevNS.Status != statusError. SinceStatusis never anything butstatusOK, that branch is unreachable dead code.resolverinternals thatwatchernever reads.errorfield onNameserverRecordState(internal/state/state.go:44-49) is never populated for nameserver entries, so the state file cannot show why a nameserver is unhealthy either.The README claims (
README.md:47-56):> NS query failure: A nameserver that previously responded becomes unreachable (timeout, SERVFAIL, REFUSED, network error). This is distinct from "responded with no records."
> NS recovery: A previously-unreachable nameserver starts responding again.
Neither is true today. This also makes the blanket claim at
README.md:106-107("Every observable state change produces a notification") false.Definition of done
LookupAllRecords(or a sibling method) preserves each nameserver'sStatusandErrorinstead of flattening to records-only. Changing the existing signature is acceptable — update all call sites. Do not add a second parallel code path that can drift from the first.buildHostnameStatestores the real per-nameserver status rather than hardcodingstatusOK, and populatesNameserverRecordState.Errorwith the failure detail when the query failed.watcher.go:428-445becomes genuinely reachable — verify that with a test rather than by inspection.StatusNXDomain/StatusNoData/StatusTimeoutmap onto persisted state. The README's status table (README.md:368-371) currently lists onlyokanderror. Either map the richer resolver statuses down to that pair, or widen the table — but code and README must agree when you are done, and the README table must be updated in the same commit.internal/watcher/interfaces.go— the watcher tests already work this way.make checkis green and totalmake testwall time stays well under the 20-second policy ceiling.TODO.mdis updated in the same commit as the work.The finishing commit's title must end with
(closes #N)referencing this issue.Hard constraints
.golangci.yml, and do not change the golangci-lint version pin.Out of scope
Glue / NS-IP change detection is a separate gap tracked in its own issue. Do not fold it in.
[manager] CORRECTION to item 8 of the definition of done — read this before starting.
I wrote item 8 saying to "use the existing mocked resolver/portcheck/tlscheck interfaces in
internal/watcher/interfaces.go— the watcher tests already work this way." That was accurate formainand is wrong for the state this issue will actually be implemented against. Following it would undo work already in the merge queue.What changed
PR #97 is
merge-readyand awaiting @sneak. It deletesmockResolverentirely. Verified directly:origin/main:internal/watcher/watcher_test.gocontains 6 references tomockResolver.a535ae8): zero references. The watcher tests now construct the real iterative resolver withresolver.NewFromLogger(log)and query live DNS.PR #97 also removes
resolver.NewFromLoggerWithClient— the constructor that existed solely to inject a fake DNS client — and deletes theTESTING.mdcarve-out that previously permitted DNS mocks in packages consuming the resolver. The live-DNS policy now applies to every package, with no exceptions.So the seam I pointed at no longer exists, and re-creating it would directly reverse #97.
The pattern to follow instead
PR #97 establishes how to test change-detection without mocking DNS: seed the state store with a synthetic previous observation that live DNS cannot possibly match, then run against live DNS and assert on the resulting transition. It uses reserved
.invalidnameserver names and RFC 5737 documentation addresses (192.0.2.0/24,203.0.113.0/24) as the synthetic prior state.That maps cleanly onto this issue:
Note PR #97 keeps the port checker, TLS checker, and notifier as test doubles, and deliberately generalised their assertions ("all ports open", "one cert for any address") so they hold for whatever live DNS returns — including multi-IP and AAAA answers. Those are not DNS and remain legitimate test doubles. Only the DNS/resolver layer is off limits.
Also note #97 explicitly dropped exact
StatusTimeoutassertions, because a real unreachable address may fail fast via ICMP unreachable rather than timing out, depending on network path. Do not assert a specific failure classification where the network can legitimately produce a different one — item 3 of the DoD asks the notification to name the failure kind, which is about what the code reports, not about pinning live DNS to one outcome. Assert tolerantly.Consequences for whoever picks this up
mockResolver,NewFromLoggerWithClient, or any fake/stub DNS client, resolver, transport, or nameserver. DNS is never mocked in this repository. This is the single issue in the 1.0 backlog where that temptation is strongest, because the thing being tested is nameserver failure.main. Branching offmaintoday means writing tests against amockResolverthat is about to be deleted, then having them destroyed in the rebase.make testwall time against the 20-second policy ceiling. This issue adds failure-path coverage, and #97 already moved the watcher suite onto live DNS — the two compound. Measure, do not assume.This issue stays blocked until #97 merges. I am not dispatching an implementer for it before then. Same correction applies to #105.