resolver: a nameserver with a failed record type is not nodata (closes #253)
check / check (push) Successful in 1m10s

classifyResponse set nodata when every record type that answered had
no records, even when another type's query got no usable reply. That
type is listed in FailedTypes and its records are unknown, so the
nameserver has not said it has none. It now stays ok, the status
README describes for a nameserver with a failed type, and nodata is
set only when no type failed. The watcher saved nodata as ok already,
so saved state is unchanged; the live test that rejects nodata no
longer fails when one of a nameserver's queries is lost.

Model: opus-5-5
This commit is contained in:
2026-10-02 11:04:44 +00:00
parent 67b67b8475
commit 62eee5731e
3 changed files with 40 additions and 17 deletions
+2
View File
@@ -19,6 +19,8 @@ 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
+5 -2
View File
@@ -941,7 +941,9 @@ 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. // 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.
func classifyResponse(resp *NameserverResponse, state queryState) { func classifyResponse(resp *NameserverResponse, state queryState) {
switch { switch {
case state.gotNXDomain && !state.hasRecords: case state.gotNXDomain && !state.hasRecords:
@@ -961,7 +963,8 @@ 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"
case !state.hasRecords && !state.gotNXDomain: // An NXDOMAIN reply with no records was taken by the first case.
case !state.hasRecords && len(resp.FailedTypes) == 0:
resp.Status = StatusNoData resp.Status = StatusNoData
} }
} }
+33 -15
View File
@@ -11,60 +11,77 @@ import (
) )
// TestClassifyResponse sets a nameserver's status from the results of // TestClassifyResponse sets a nameserver's status from the results of
// its queries, built here. One that answered some record types, even // its queries and the record types whose query failed, built here. One
// with no records, has not failed when its query for another type got // that answered some record types, even with no records, has not failed
// no usable reply, whatever the reason; one whose every query got none // when its query for another type got no usable reply, whatever the
// has. // 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.
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
wantStatus string failedTypes []string
wantError string wantStatus 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},
StatusNoData, "", []string{"A"},
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",
}, },
StatusNoData, "", []string{"A"},
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},
StatusNoData, "", []string{"A"},
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},
StatusNoData, "", []string{"A"},
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},
StatusNoData, "", []string{"TXT"},
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},
StatusNoData, "", []string{"A"},
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",
}, },
} }
@@ -73,11 +90,12 @@ 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} resp := &NameserverResponse{Status: StatusOK, FailedTypes: tt.failedTypes}
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)
}) })
} }
} }