1 Commits
Author SHA1 Message Date
sneak a771f51b0b resolver, watcher: a domain's nameservers are only its own delegation (closes #222)
check / check (push) Waiting to run
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
- 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
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
+2 -5
View File
@@ -941,9 +941,7 @@ func isTimeout(err error) bool {
// classifyResponse sets the nameserver's status. One that answered no
// 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
// answered with no records: a type in FailedTypes may have records, so a
// nameserver with one stays ok.
// the status of those answers.
func classifyResponse(resp *NameserverResponse, state queryState) {
switch {
case state.gotNXDomain && !state.hasRecords:
@@ -963,8 +961,7 @@ func classifyResponse(resp *NameserverResponse, state queryState) {
case state.gotReferral && !state.answered:
resp.Status = StatusError
resp.Error = "server returned a referral"
// An NXDOMAIN reply with no records was taken by the first case.
case !state.hasRecords && len(resp.FailedTypes) == 0:
case !state.hasRecords && !state.gotNXDomain:
resp.Status = StatusNoData
}
}
+15 -33
View File
@@ -11,77 +11,60 @@ import (
)
// TestClassifyResponse sets a nameserver's status from the results of
// its queries and the record types whose query failed, built here. One
// that answered some record types, even with no records, has not failed
// when its query for another type got no usable reply, whatever the
// reason, and is ok, not nodata: that type may have records. One whose
// every query got none has failed. Only one whose every type answered
// with no records is nodata.
// its queries, built here. One that answered some record types, even
// with no records, has not failed when its query for another type got
// no usable reply, whatever the reason; one whose every query got none
// has.
func TestClassifyResponse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
results queryState
failedTypes []string
wantStatus string
wantError string
name string
results queryState
wantStatus string
wantError string
}{
{
"every type answered with no records",
queryState{answered: true},
nil,
StatusNoData, "",
},
{
"some types answered with no records, another timed out",
queryState{answered: true, gotTimeout: true},
[]string{"A"},
StatusOK, "",
StatusNoData, "",
},
{
"some types answered with no records, another got SERVFAIL",
queryState{
answered: true, gotErrorReply: true, errorReply: "SERVFAIL",
},
[]string{"A"},
StatusOK, "",
StatusNoData, "",
},
{
"some types answered with no records, another was refused",
queryState{answered: true, gotRefused: true},
[]string{"A"},
StatusOK, "",
StatusNoData, "",
},
{
"some types answered with no records, another got a network error",
queryState{answered: true, netErr: syscall.ECONNREFUSED},
[]string{"A"},
StatusOK, "",
StatusNoData, "",
},
{
"some types answered with no records, another's reply was " +
"truncated and its retry over TCP failed",
queryState{answered: true, netErr: ErrTruncated},
[]string{"TXT"},
StatusOK, "",
StatusNoData, "",
},
{
"some types answered with no records, another got a referral",
queryState{answered: true, gotReferral: true},
[]string{"A"},
StatusOK, "",
StatusNoData, "",
},
{
"every query timed out",
queryState{gotTimeout: true},
[]string{"A", "AAAA", "CNAME"},
StatusTimeout, "all queries timed out",
},
{
"every query got NOTIMP",
queryState{gotErrorReply: true, errorReply: "NOTIMP"},
[]string{"A", "AAAA", "CNAME"},
StatusError, "server returned NOTIMP",
},
}
@@ -90,12 +73,11 @@ func TestClassifyResponse(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &NameserverResponse{Status: StatusOK, FailedTypes: tt.failedTypes}
resp := &NameserverResponse{Status: StatusOK}
classifyResponse(resp, tt.results)
assert.Equal(t, tt.wantStatus, resp.Status)
assert.Equal(t, tt.wantError, resp.Error)
assert.Equal(t, tt.failedTypes, resp.FailedTypes)
})
}
}