resolver: a nameserver with a failed record type is not nodata (closes #253) #254

Merged
clawbot merged 1 commits from issue-253-nodata-only-when-all-answered into next 2026-10-02 13:16:43 +02:00
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)
}) })
} }
} }