1 Commits
Author SHA1 Message Date
sneak a771f51b0b resolver, watcher: a domain's nameservers are only its own delegation (closes #222)
check / check (push) Successful in 1m20s
When a domain's parent zone's servers answer NXDOMAIN, LookupNS returns
ErrNXDomain. The watcher then saves the domain with no nameservers and
nxdomain set, shown on the dashboard and in /api/v1/status, asks for none
of its records and removes those saved, so its old nameservers go in one
NS Change. A domain with no delegation of its own gets an empty set and
its records are still asked at the zone it is in.
FindAuthoritativeNameservers moves to a parent name only on one of those
two answers; when the servers do not answer, it returns the error. After
an upgrade, a domain without its own delegation that was saved with its
parent zone's nameservers gets one NS Change; the README says so.

Model: opus-5-5
2026-10-02 10:24:40 +00:00
3 changed files with 17 additions and 40 deletions
-2
View File
@@ -19,8 +19,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
# Completed Steps # Completed Steps
- 2026-10-02: a nameserver whose query for one record type failed while the
others answered with no records is `ok`, not `nodata` (closes #253).
- 2026-10-02: a domain that does not exist is shown so, with no nameservers; no - 2026-10-02: a domain that does not exist is shown so, with no nameservers; no
name gets a parent's nameservers when its own did not answer (closes #222). name gets a parent's nameservers when its own did not answer (closes #222).
- 2026-10-02: the refused-query test sends one query to four operators' public - 2026-10-02: the refused-query test sends one query to four operators' public
+2 -5
View File
@@ -941,9 +941,7 @@ func isTimeout(err error) bool {
// classifyResponse sets the nameserver's status. One that answered no // classifyResponse sets the nameserver's status. One that answered no
// record type has failed, and Error says why; one that answered some has // record type has failed, and Error says why; one that answered some has
// the status of those answers. It has no data only when every type // the status of those answers.
// answered with no records: a type in FailedTypes may have records, so a
// nameserver with one stays ok.
func classifyResponse(resp *NameserverResponse, state queryState) { func classifyResponse(resp *NameserverResponse, state queryState) {
switch { switch {
case state.gotNXDomain && !state.hasRecords: case state.gotNXDomain && !state.hasRecords:
@@ -963,8 +961,7 @@ func classifyResponse(resp *NameserverResponse, state queryState) {
case state.gotReferral && !state.answered: case state.gotReferral && !state.answered:
resp.Status = StatusError resp.Status = StatusError
resp.Error = "server returned a referral" resp.Error = "server returned a referral"
// An NXDOMAIN reply with no records was taken by the first case. case !state.hasRecords && !state.gotNXDomain:
case !state.hasRecords && len(resp.FailedTypes) == 0:
resp.Status = StatusNoData resp.Status = StatusNoData
} }
} }
+15 -33
View File
@@ -11,77 +11,60 @@ import (
) )
// TestClassifyResponse sets a nameserver's status from the results of // TestClassifyResponse sets a nameserver's status from the results of
// its queries and the record types whose query failed, built here. One // its queries, built here. One that answered some record types, even
// that answered some record types, even with no records, has not failed // with no records, has not failed when its query for another type got
// when its query for another type got no usable reply, whatever the // no usable reply, whatever the reason; one whose every query got none
// reason, and is ok, not nodata: that type may have records. One whose // has.
// every query got none has failed. Only one whose every type answered
// with no records is nodata.
func TestClassifyResponse(t *testing.T) { func TestClassifyResponse(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
name string name string
results queryState results queryState
failedTypes []string wantStatus string
wantStatus string wantError string
wantError string
}{ }{
{
"every type answered with no records",
queryState{answered: true},
nil,
StatusNoData, "",
},
{ {
"some types answered with no records, another timed out", "some types answered with no records, another timed out",
queryState{answered: true, gotTimeout: true}, queryState{answered: true, gotTimeout: true},
[]string{"A"}, StatusNoData, "",
StatusOK, "",
}, },
{ {
"some types answered with no records, another got SERVFAIL", "some types answered with no records, another got SERVFAIL",
queryState{ queryState{
answered: true, gotErrorReply: true, errorReply: "SERVFAIL", answered: true, gotErrorReply: true, errorReply: "SERVFAIL",
}, },
[]string{"A"}, StatusNoData, "",
StatusOK, "",
}, },
{ {
"some types answered with no records, another was refused", "some types answered with no records, another was refused",
queryState{answered: true, gotRefused: true}, queryState{answered: true, gotRefused: true},
[]string{"A"}, StatusNoData, "",
StatusOK, "",
}, },
{ {
"some types answered with no records, another got a network error", "some types answered with no records, another got a network error",
queryState{answered: true, netErr: syscall.ECONNREFUSED}, queryState{answered: true, netErr: syscall.ECONNREFUSED},
[]string{"A"}, StatusNoData, "",
StatusOK, "",
}, },
{ {
"some types answered with no records, another's reply was " + "some types answered with no records, another's reply was " +
"truncated and its retry over TCP failed", "truncated and its retry over TCP failed",
queryState{answered: true, netErr: ErrTruncated}, queryState{answered: true, netErr: ErrTruncated},
[]string{"TXT"}, StatusNoData, "",
StatusOK, "",
}, },
{ {
"some types answered with no records, another got a referral", "some types answered with no records, another got a referral",
queryState{answered: true, gotReferral: true}, queryState{answered: true, gotReferral: true},
[]string{"A"}, StatusNoData, "",
StatusOK, "",
}, },
{ {
"every query timed out", "every query timed out",
queryState{gotTimeout: true}, queryState{gotTimeout: true},
[]string{"A", "AAAA", "CNAME"},
StatusTimeout, "all queries timed out", StatusTimeout, "all queries timed out",
}, },
{ {
"every query got NOTIMP", "every query got NOTIMP",
queryState{gotErrorReply: true, errorReply: "NOTIMP"}, queryState{gotErrorReply: true, errorReply: "NOTIMP"},
[]string{"A", "AAAA", "CNAME"},
StatusError, "server returned NOTIMP", StatusError, "server returned NOTIMP",
}, },
} }
@@ -90,12 +73,11 @@ func TestClassifyResponse(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
resp := &NameserverResponse{Status: StatusOK, FailedTypes: tt.failedTypes} resp := &NameserverResponse{Status: StatusOK}
classifyResponse(resp, tt.results) classifyResponse(resp, tt.results)
assert.Equal(t, tt.wantStatus, resp.Status) assert.Equal(t, tt.wantStatus, resp.Status)
assert.Equal(t, tt.wantError, resp.Error) assert.Equal(t, tt.wantError, resp.Error)
assert.Equal(t, tt.failedTypes, resp.FailedTypes)
}) })
} }
} }