check / check (push) Failing after 2m2s
A domain's NS set was taken from whichever of its own servers answered first, so when they disagree (during a move between DNS providers, or with a stale secondary) the set could change between checks and send an NS change notification with nothing changed. The walk now stops at the referral to the domain from its parent zone's servers and returns that delegation, which those servers all hold alike; the domain's own servers are no longer asked for it. The NS records in an answer are still used where no such referral comes first, as from a server that holds both the parent zone and the domain. Hostnames get their zone's servers the same way. Model: opus-5-5
238 lines
5.7 KiB
Go
238 lines
5.7 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)
|
|
}
|
|
|
|
const (
|
|
// exampleCom is the zone most cases of TestUsableReply and
|
|
// TestNSSetFrom are about, and wwwExampleCom a name in it.
|
|
exampleCom = "example.com."
|
|
wwwExampleCom = "www.example.com."
|
|
|
|
// exampleNS is the server the NS records nsRecord builds name.
|
|
exampleNS = "ns1.example.net."
|
|
)
|
|
|
|
// 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: exampleNS,
|
|
}
|
|
}
|
|
|
|
// 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: wwwExampleCom, 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),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNSSetFrom checks which NS set a reply gives for a domain; a set
|
|
// that is not empty ends the walk. The referral to example.com that
|
|
// com's servers all send alike gives its delegation, so the set is the
|
|
// same whichever of them answered, and example.com's own servers, which
|
|
// can disagree, are not asked.
|
|
func TestNSSetFrom(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
answer := new(dns.Msg)
|
|
answer.Authoritative = true
|
|
answer.Answer = []dns.RR{nsRecord(exampleCom)}
|
|
|
|
tests := []struct {
|
|
name string
|
|
resp *dns.Msg
|
|
domain string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "com refers to example.com", resp: referralTo(exampleCom),
|
|
domain: exampleCom, want: []string{exampleNS},
|
|
},
|
|
{
|
|
name: "com refers on, for www.example.com",
|
|
resp: referralTo(exampleCom), domain: wwwExampleCom,
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "answer from a server that holds example.com",
|
|
resp: answer, domain: exampleCom,
|
|
want: []string{exampleNS},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.ElementsMatch(t, tt.want,
|
|
resolver.NSSetFrom(tt.resp, tt.domain),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
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))
|
|
})
|
|
}
|
|
}
|