check / check (push) Canceled after 0s
When the caller's context ends, the next query returns ErrContextCanceled before anything is sent. querySingleType recorded that as a network error, which outranks a timeout, so a nameserver that had only timed out could be reported as "network error: context canceled". It now records no reason for it. Model: opus-5-5
167 lines
5.8 KiB
Go
167 lines
5.8 KiB
Go
package resolver
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"strconv"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// 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, and its error says all queries timed
|
|
// out only when every one did: a reply such as a refusal, or a network
|
|
// error, at one of its addresses is given instead. Only one whose every
|
|
// type answered with no records is nodata.
|
|
func TestClassifyResponse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
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},
|
|
[]string{"A"}, StatusOK, ""},
|
|
{"some types answered with no records, another got SERVFAIL",
|
|
queryState{answered: true, gotErrorReply: true, errorReply: "SERVFAIL"},
|
|
[]string{"A"}, StatusOK, ""},
|
|
{"some types answered with no records, another was refused",
|
|
queryState{answered: true, gotRefused: true},
|
|
[]string{"A"}, StatusOK, ""},
|
|
{"some types answered with no records, another got a network error",
|
|
queryState{answered: true, netErr: syscall.ECONNREFUSED},
|
|
[]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},
|
|
[]string{"TXT"}, StatusOK, ""},
|
|
{"some types answered with no records, another got a referral",
|
|
queryState{answered: true, gotReferral: true},
|
|
[]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"},
|
|
{"every query got SERVFAIL at one address, timed out at another",
|
|
queryState{gotErrorReply: true, errorReply: "SERVFAIL", gotTimeout: true},
|
|
[]string{"A"}, StatusError, "server returned SERVFAIL"},
|
|
{"every query was refused at one address, timed out at another",
|
|
queryState{gotRefused: true, gotTimeout: true},
|
|
[]string{"A"}, StatusError, "server returned REFUSED"},
|
|
{"every query got a network error at one address, timed out at another",
|
|
queryState{netErr: syscall.ECONNREFUSED, gotTimeout: true},
|
|
[]string{"A"}, StatusError, "network error: connection refused"},
|
|
{"every query got a referral at one address, timed out at another",
|
|
queryState{gotReferral: true, gotTimeout: true},
|
|
[]string{"A"}, StatusError, "server returned a referral"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestReadReply checks which replies to a query about one record type,
|
|
// built here, are an answer: one with the code NOERROR or NXDOMAIN. A
|
|
// reply with any other code is not, and the type's query has failed; a
|
|
// nameserver whose only reply it is has failed, and Error gives the
|
|
// code, or its number when the code has no name.
|
|
func TestReadReply(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
rcode int
|
|
wantStatus string
|
|
wantError string
|
|
}{
|
|
{dns.RcodeSuccess, StatusNoData, ""},
|
|
{dns.RcodeNameError, StatusNXDomain, ""},
|
|
{dns.RcodeServerFailure, StatusError, "server returned SERVFAIL"},
|
|
{dns.RcodeNotImplemented, StatusError, "server returned NOTIMP"},
|
|
{dns.RcodeFormatError, StatusError, "server returned FORMERR"},
|
|
{12, StatusError, "server returned 12"}, // unassigned, no name
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(strconv.Itoa(tt.rcode), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msg := new(dns.Msg)
|
|
msg.Authoritative = true
|
|
msg.Rcode = tt.rcode
|
|
|
|
resp := &NameserverResponse{Records: map[string][]string{}}
|
|
|
|
var state queryState
|
|
|
|
err := readReply(msg, resp, &state)
|
|
classifyResponse(resp, state)
|
|
|
|
if tt.wantStatus == StatusError {
|
|
require.ErrorIs(t, err, ErrUnusableReply)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
assert.Equal(t, tt.wantStatus, resp.Status)
|
|
assert.Equal(t, tt.wantError, resp.Error)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestQuerySingleType_ContextEnded asks a nameserver about one record
|
|
// type with a context that has already ended, so no query is sent. A
|
|
// query the context cut short says nothing about the nameserver and
|
|
// records no reason: a nameserver whose earlier query timed out is
|
|
// still reported as timed out, not as a network error.
|
|
func TestQuerySingleType_ContextEnded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := NewFromLogger(slog.New(slog.DiscardHandler))
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
resp := &NameserverResponse{Records: map[string][]string{}, Status: StatusOK}
|
|
|
|
// The query for an earlier record type timed out.
|
|
state := queryState{gotTimeout: true}
|
|
|
|
_, err := r.querySingleType(
|
|
ctx, []string{"192.0.2.1"}, "example.com.", dns.TypeA, resp, &state,
|
|
)
|
|
require.ErrorIs(t, err, ErrContextCanceled)
|
|
assert.Equal(t, queryState{gotTimeout: true}, state)
|
|
|
|
classifyResponse(resp, state)
|
|
|
|
assert.Equal(t, StatusTimeout, resp.Status)
|
|
assert.Equal(t, "all queries timed out", resp.Error)
|
|
}
|