diff --git a/TODO.md b/TODO.md index c0c6c2a..0a5c99f 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ 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 diff --git a/internal/resolver/iterative.go b/internal/resolver/iterative.go index b275a2f..35df296 100644 --- a/internal/resolver/iterative.go +++ b/internal/resolver/iterative.go @@ -941,7 +941,9 @@ 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. +// 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) { switch { case state.gotNXDomain && !state.hasRecords: @@ -961,7 +963,8 @@ func classifyResponse(resp *NameserverResponse, state queryState) { case state.gotReferral && !state.answered: resp.Status = StatusError 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 } } diff --git a/internal/resolver/iterative_internal_test.go b/internal/resolver/iterative_internal_test.go index f36097d..c756bb0 100644 --- a/internal/resolver/iterative_internal_test.go +++ b/internal/resolver/iterative_internal_test.go @@ -11,60 +11,77 @@ import ( ) // TestClassifyResponse sets a nameserver's status from the results of -// 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. +// 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. func TestClassifyResponse(t *testing.T) { t.Parallel() tests := []struct { - name string - results queryState - wantStatus string - wantError string + name string + results queryState + failedTypes []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", queryState{answered: true, gotTimeout: true}, - StatusNoData, "", + []string{"A"}, + StatusOK, "", }, { "some types answered with no records, another got SERVFAIL", queryState{ answered: true, gotErrorReply: true, errorReply: "SERVFAIL", }, - StatusNoData, "", + []string{"A"}, + StatusOK, "", }, { "some types answered with no records, another was refused", queryState{answered: true, gotRefused: true}, - StatusNoData, "", + []string{"A"}, + StatusOK, "", }, { "some types answered with no records, another got a network error", queryState{answered: true, netErr: syscall.ECONNREFUSED}, - StatusNoData, "", + []string{"A"}, + StatusOK, "", }, { "some types answered with no records, another's reply was " + "truncated and its retry over TCP failed", queryState{answered: true, netErr: ErrTruncated}, - StatusNoData, "", + []string{"TXT"}, + StatusOK, "", }, { "some types answered with no records, another got a referral", queryState{answered: true, gotReferral: true}, - StatusNoData, "", + []string{"A"}, + StatusOK, "", }, { "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", }, } @@ -73,11 +90,12 @@ func TestClassifyResponse(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - resp := &NameserverResponse{Status: StatusOK} + resp := &NameserverResponse{Status: StatusOK, FailedTypes: tt.failedTypes} 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) }) } }