check / check (push) Failing after 2m8s
When the resolver walks from the root servers towards a name, a server that answered SERVFAIL, or referred the query back to its own zone, up or sideways, ended the step, so finding a zone's servers gave up on the zone though its other servers would answer. Such a reply is now passed over for the zone's next server, as a timeout or a refusal already was. To tell a referral that leads closer to the name from one that does not, each walk keeps the zone of the servers it is asking. Other error replies, such as FORMERR, are passed over too. The walk that finds a nameserver's address shares the same server loop, so it changes too. Model: opus-5-5
186 lines
4.4 KiB
Go
186 lines
4.4 KiB
Go
package resolver_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/resolver"
|
|
)
|
|
|
|
// TestCollectIPs_OneAnswerIsEnough checks that one nameserver answering
|
|
// NXDOMAIN says the name has no addresses, though the other timed out.
|
|
func TestCollectIPs_OneAnswerIsEnough(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ips, _, err := resolver.CollectIPs(
|
|
map[string]*resolver.NameserverResponse{
|
|
"ns1.example.": {Status: resolver.StatusTimeout},
|
|
"ns2.example.": {Status: resolver.StatusNXDomain},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, ips)
|
|
}
|
|
|
|
// TestCollectIPs_FailedIsNoAnswer checks that nameservers that all have
|
|
// status error, from a refusal, a server failure, a network error or a
|
|
// referral, are no answer rather than a name with no addresses.
|
|
func TestCollectIPs_FailedIsNoAnswer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ips, _, err := resolver.CollectIPs(
|
|
map[string]*resolver.NameserverResponse{
|
|
"ns1.example.": {Status: resolver.StatusError},
|
|
"ns2.example.": {Status: resolver.StatusError},
|
|
},
|
|
)
|
|
require.ErrorIs(t, err, resolver.ErrNoNameserverAnswered)
|
|
assert.Empty(t, ips)
|
|
}
|
|
|
|
// exampleCom is the zone most cases of TestUsableReply are about.
|
|
const exampleCom = "example.com."
|
|
|
|
// nsRecord builds an NS record that names a server of zone.
|
|
func nsRecord(zone string) *dns.NS {
|
|
return &dns.NS{
|
|
Hdr: dns.RR_Header{
|
|
Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET,
|
|
},
|
|
Ns: "ns1.example.net.",
|
|
}
|
|
}
|
|
|
|
// referralTo builds a reply that refers the query to the servers of
|
|
// zone.
|
|
func referralTo(zone string) *dns.Msg {
|
|
msg := new(dns.Msg)
|
|
msg.Ns = []dns.RR{nsRecord(zone)}
|
|
|
|
return msg
|
|
}
|
|
|
|
// TestUsableReply checks which replies from one of a zone's servers are
|
|
// used. A reply that is not usable moves the query on to the zone's
|
|
// next server.
|
|
func TestUsableReply(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
servfail := new(dns.Msg)
|
|
servfail.Rcode = dns.RcodeServerFailure
|
|
|
|
answer := new(dns.Msg)
|
|
answer.Authoritative = true
|
|
answer.Answer = []dns.RR{nsRecord(exampleCom)}
|
|
|
|
nxdomain := new(dns.Msg)
|
|
nxdomain.Authoritative = true
|
|
nxdomain.Rcode = dns.RcodeNameError
|
|
|
|
tests := []struct {
|
|
name string
|
|
resp *dns.Msg
|
|
zone string
|
|
query string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "SERVFAIL", resp: servfail,
|
|
zone: exampleCom, query: exampleCom, want: false,
|
|
},
|
|
{
|
|
name: "answer", resp: answer,
|
|
zone: exampleCom, query: exampleCom, want: true,
|
|
},
|
|
{
|
|
name: "NXDOMAIN", resp: nxdomain,
|
|
zone: ".", query: exampleCom, want: true,
|
|
},
|
|
{
|
|
name: "root refers to com", resp: referralTo("com."),
|
|
zone: ".", query: exampleCom, want: true,
|
|
},
|
|
{
|
|
name: "com refers to example.com", resp: referralTo(exampleCom),
|
|
zone: "com.", query: "www.example.com.", want: true,
|
|
},
|
|
{
|
|
name: "referral back to the zone", resp: referralTo(exampleCom),
|
|
zone: exampleCom, query: exampleCom, want: false,
|
|
},
|
|
{
|
|
name: "referral up to the root", resp: referralTo("."),
|
|
zone: exampleCom, query: exampleCom, want: false,
|
|
},
|
|
{
|
|
name: "referral sideways", resp: referralTo("net."),
|
|
zone: ".", query: exampleCom, want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.Equal(t, tt.want,
|
|
resolver.UsableReply(tt.resp, tt.zone, tt.query),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractRecordValue_LetterCase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
rr dns.RR
|
|
want string
|
|
}{
|
|
{
|
|
name: "MX target lower-cased",
|
|
rr: &dns.MX{Preference: 1, Mx: "ASPMX.L.GOOGLE.COM."},
|
|
want: "1 aspmx.l.google.com.",
|
|
},
|
|
{
|
|
name: "NS target lower-cased",
|
|
rr: &dns.NS{Ns: "x.ns.joker.COM."},
|
|
want: "x.ns.joker.com.",
|
|
},
|
|
{
|
|
name: "CNAME target lower-cased",
|
|
rr: &dns.CNAME{Target: "WWW.Example.Com."},
|
|
want: "www.example.com.",
|
|
},
|
|
{
|
|
name: "SRV target lower-cased",
|
|
rr: &dns.SRV{
|
|
Priority: 10, Weight: 5, Port: 443,
|
|
Target: "SIP.Example.Com.",
|
|
},
|
|
want: "10 5 443 sip.example.com.",
|
|
},
|
|
{
|
|
name: "TXT value keeps its case",
|
|
rr: &dns.TXT{Txt: []string{"Verify=AbC123"}},
|
|
want: "Verify=AbC123",
|
|
},
|
|
{
|
|
name: "CAA value keeps its case",
|
|
rr: &dns.CAA{Flag: 0, Tag: "issue", Value: "LetsEncrypt.org"},
|
|
want: `0 issue "LetsEncrypt.org"`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.Equal(t, tt.want, resolver.ExtractRecordValue(tt.rr))
|
|
})
|
|
}
|
|
}
|