Files
coredns/plugin/backend_lookup_test.go
Ville Vesilehto 4ec856f347 Merge commit from fork
EndpointSlice FQDN addresses were copied into endpoint records as if
they were IP addresses. CoreDNS then treated each value as a CNAME
target. A tenant could build a branching chain of headless Services that
made one query perform exponential work and exhaust CoreDNS memory.

Publish endpoints only from IPv4 and IPv6 slices and validate every
address against its declared family. Deduplicate CNAME targets for the
whole query so repeated or converging paths are chased once.

Protect internal self-lookups with an independent depth limit. This
stops direct and indirect k8s_external CNAME loops without consuming the
loop counter already maintained by the file and rewrite plugins.

Add regression coverage for address types, recursive convergence,
repeated k8s_external targets, and direct and indirect self-reference.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2026-10-24 19:45:58 -07:00

805 lines
27 KiB
Go

package plugin
import (
"context"
"errors"
"fmt"
"net"
"testing"
"time"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// mockBackend implements ServiceBackend interface for testing
var _ ServiceBackend = &mockBackend{}
type mockBackend struct {
mockServices func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error)
mockReverse func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error)
mockLookup func(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error)
mockRecords func(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error)
mockNameError func(err error) bool
minTTL uint32
serial uint32
}
func (m *mockBackend) Serial(_state request.Request) uint32 {
if m.serial == 0 {
return uint32(time.Now().Unix())
}
return m.serial
}
func (m *mockBackend) MinTTL(_state request.Request) uint32 {
if m.minTTL == 0 {
return 30
}
return m.minTTL
}
func (m *mockBackend) Services(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) {
return m.mockServices(ctx, state, exact, opt)
}
func (m *mockBackend) Reverse(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) {
return m.mockReverse(ctx, state, exact, opt)
}
func (m *mockBackend) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) {
return m.mockLookup(ctx, state, name, typ)
}
func (m *mockBackend) IsNameError(err error) bool {
if m.mockNameError != nil {
return m.mockNameError(err)
}
return false
}
func (m *mockBackend) Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) {
return m.mockRecords(ctx, state, exact)
}
func TestNSStateReset(t *testing.T) {
// Create a mock backend that always returns error
mock := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return nil, fmt.Errorf("mock error")
},
}
// Create a test request
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeNS)
state := request.Request{
Req: req,
W: &test.ResponseWriter{},
}
originalName := state.QName()
ctx := context.TODO()
// Call NS function which should fail due to mock error
records, extra, err := NS(ctx, mock, "example.org.", state, Options{})
// Verify error is returned
if err == nil {
t.Error("Expected error from mock backend, got nil")
}
// Verify query name is reset even when an error occurs
if state.QName() != originalName {
t.Errorf("Query name not properly reset after error. Expected %s, got %s", originalName, state.QName())
}
// Verify no records are returned
if len(records) != 0 || len(extra) != 0 {
t.Error("Expected no records returned on error")
}
}
func TestARecords_Dedup(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "1.2.3.4", TTL: 60},
{Host: "1.2.3.4", TTL: 60},
{Host: "::1", TTL: 60},
}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("a.example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, tc, err := A(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc {
t.Fatal("unexpected truncation")
}
if len(recs) != 1 {
t.Fatalf("expected 1 A record, got %d", len(recs))
}
if _, ok := recs[0].(*dns.A); !ok {
t.Fatalf("expected A record, got %T", recs[0])
}
}
func TestAAAARecords_Dedup(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "::1", TTL: 60},
{Host: "::1", TTL: 60},
{Host: "1.2.3.4", TTL: 60},
}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("aaaa.example.org.", dns.TypeAAAA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, tc, err := AAAA(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc {
t.Fatal("unexpected truncation")
}
if len(recs) != 1 {
t.Fatalf("expected 1 AAAA record, got %d", len(recs))
}
if _, ok := recs[0].(*dns.AAAA); !ok {
t.Fatalf("expected AAAA record, got %T", recs[0])
}
}
func TestTXTWithInternalCNAME(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
switch state.QName() {
case "txt.example.org.":
return []msg.Service{{Host: "target.example.org.", TTL: 50}}, nil
case "target.example.org.":
return []msg.Service{{Text: "v=txt1", TTL: 40}}, nil
default:
return nil, nil
}
},
}
req := new(dns.Msg)
req.SetQuestion("txt.example.org.", dns.TypeTXT)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, tc, err := TXT(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc {
t.Fatal("unexpected truncation")
}
if len(recs) != 2 {
t.Fatalf("expected 2 records (CNAME+TXT), got %d", len(recs))
}
if _, ok := recs[0].(*dns.CNAME); !ok {
t.Fatalf("expected first record CNAME, got %T", recs[0])
}
if _, ok := recs[1].(*dns.TXT); !ok {
t.Fatalf("expected second record TXT, got %T", recs[1])
}
}
func TestCNAMEHostIsNameAndIpIgnored(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "target.example.org.", TTL: 50},
{Host: "1.2.3.4", TTL: 50},
}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("name.example.org.", dns.TypeCNAME)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, err := CNAME(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 CNAME, got %d", len(recs))
}
if _, ok := recs[0].(*dns.CNAME); !ok {
t.Fatalf("expected CNAME, got %T", recs[0])
}
}
func TestCNAMEChainLimitAndLoop(t *testing.T) {
// Construct internal CNAME chain longer than maxCnameChainLength and ensure truncation of chain
chainLength := maxCnameChainLength + 2
names := make([]string, 0, chainLength)
for i := range chainLength {
names = append(names, fmt.Sprintf("c%d.example.org.", i))
}
chain := map[string]string{}
for i := range len(names) - 1 {
chain[names[i]] = names[i+1]
}
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
if nxt, ok := chain[state.QName()]; ok {
return []msg.Service{{Host: nxt, TTL: 10}}, nil
}
return []msg.Service{}, nil
},
}
// A query should follow the chain only until limit; since no terminal A, result is 0 or just below limit
req := new(dns.Msg)
req.SetQuestion(names[0], dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, _, err := A(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// We cannot exceed limit; if any records exist they must be <= maxCnameChainLength
if len(recs) > maxCnameChainLength {
t.Fatalf("CNAME chain exceeded limit: %d > %d", len(recs), maxCnameChainLength)
}
// Now create a direct loop: qname CNAME qname should be ignored
b2 := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{{Host: state.QName(), TTL: 10}}, nil
},
}
req2 := new(dns.Msg)
req2.SetQuestion("loop.example.org.", dns.TypeA)
state2 := request.Request{Req: req2, W: &test.ResponseWriter{}}
recs2, _, err := A(context.Background(), b2, "example.org.", state2, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs2) != 0 {
t.Fatalf("expected 0 records due to CNAME self-loop, got %d", len(recs2))
}
}
func TestAWithExternalCNAMELookupTruncated(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{{Host: "alias.external."}}, nil
},
mockLookup: func(_ctx context.Context, _state request.Request, name string, typ uint16) (*dns.Msg, error) {
if name != "alias.external." || typ != dns.TypeA {
t.Fatalf("unexpected mockLookup: %s %d", name, typ)
}
m := new(dns.Msg)
m.Truncated = true
a := &dns.A{Hdr: dns.RR_Header{Name: "alias.external.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, A: net.ParseIP("1.2.3.4")}
m.Answer = []dns.RR{a}
return m, nil
},
}
req := new(dns.Msg)
req.SetQuestion("cname.example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, tc, err := A(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !tc {
t.Fatal("expected truncation true")
}
if len(recs) != 2 {
t.Fatalf("expected 2 records (CNAME+A), got %d", len(recs))
}
if _, ok := recs[0].(*dns.CNAME); !ok {
t.Fatalf("expected first record CNAME, got %T", recs[0])
}
if _, ok := recs[1].(*dns.A); !ok {
t.Fatalf("expected second record A, got %T", recs[1])
}
}
func TestAAAAWithInternalCNAMEChain(t *testing.T) {
// Build a short internal chain: n0 -> n1 -> final (AAAA)
names := []string{"c0.example.org.", "c1.example.org.", "final.example.org."}
chain := map[string]string{names[0]: names[1], names[1]: names[2]}
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
if nxt, ok := chain[state.QName()]; ok {
return []msg.Service{{Host: nxt, TTL: 10}}, nil
}
if state.QName() == names[2] {
return []msg.Service{{Host: "::1", TTL: 20}}, nil
}
return nil, nil
},
}
req := new(dns.Msg)
req.SetQuestion(names[0], dns.TypeAAAA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, tc, err := AAAA(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc {
t.Fatal("unexpected truncation")
}
if len(recs) != 3 {
t.Fatalf("expected 2 CNAMEs + 1 AAAA, got %d", len(recs))
}
if _, ok := recs[0].(*dns.CNAME); !ok {
t.Fatalf("expected first record CNAME, got %T", recs[0])
}
if _, ok := recs[1].(*dns.CNAME); !ok {
t.Fatalf("expected second record CNAME, got %T", recs[1])
}
if aaaa, ok := recs[2].(*dns.AAAA); !ok || !aaaa.AAAA.Equal(net.ParseIP("::1")) {
t.Fatalf("expected final AAAA with ::1, got %T %v", recs[2], recs[2])
}
}
func TestAAAAWithExternalCNAMELookupTruncated(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{{Host: "alias.external."}}, nil
},
mockLookup: func(_ctx context.Context, _state request.Request, name string, typ uint16) (*dns.Msg, error) {
if name != "alias.external." || typ != dns.TypeAAAA {
t.Fatalf("unexpected mockLookup: %s %d", name, typ)
}
m := new(dns.Msg)
m.Truncated = true
aaaa := &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, AAAA: net.ParseIP("::1")}
m.Answer = []dns.RR{aaaa}
return m, nil
},
}
req := new(dns.Msg)
req.SetQuestion("cname6.example.org.", dns.TypeAAAA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, tc, err := AAAA(context.Background(), b, "example.org.", state, nil, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !tc {
t.Fatal("expected truncation true")
}
if len(recs) != 2 {
t.Fatalf("expected 2 records (CNAME+AAAA), got %d", len(recs))
}
if _, ok := recs[0].(*dns.CNAME); !ok {
t.Fatalf("expected first record CNAME, got %T", recs[0])
}
if _, ok := recs[1].(*dns.AAAA); !ok {
t.Fatalf("expected second record AAAA, got %T", recs[1])
}
}
func TestPTRDomainOnly(t *testing.T) {
b := &mockBackend{
mockReverse: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "name.example.org.", TTL: 20},
{Host: "1.2.3.4", TTL: 20},
}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("4.3.2.1.in-addr.arpa.", dns.TypePTR)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, err := PTR(context.Background(), b, "in-addr.arpa.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 PTR, got %d", len(recs))
}
if ptr, ok := recs[0].(*dns.PTR); !ok || ptr.Ptr != "name.example.org." {
t.Fatalf("unexpected PTR: %T %v", recs[0], recs[0])
}
}
func TestNSSuccess(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "1.2.3.4", TTL: 30, Key: "/skydns/org/example/ns1"},
{Host: "::1", TTL: 30, Key: "/skydns/org/example/ns2"},
}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeNS)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, extra, err := NS(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs) != 2 {
t.Fatalf("expected 2 NS records, got %d", len(recs))
}
if len(extra) != 2 {
t.Fatalf("expected 2 extra address records, got %d", len(extra))
}
}
func TestSRVAddressesAndExternalLookup(t *testing.T) {
// First service is IP host -> produces SRV + extra address; second is CNAME target -> triggers external lookup
lookedUp := false
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "1.2.3.4", Port: 80, Priority: 10, Weight: 5, TTL: 30, Key: "/skydns/org/example/s1"},
{Host: "alias.external.", Port: 80, Priority: 10, Weight: 5, TTL: 30, Key: "/skydns/org/example/s2"},
}, nil
},
mockLookup: func(_ctx context.Context, _state request.Request, name string, typ uint16) (*dns.Msg, error) {
if name == "alias.external." && (typ == dns.TypeA || typ == dns.TypeAAAA) {
lookedUp = true
m := new(dns.Msg)
if typ == dns.TypeA {
m.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, A: net.ParseIP("5.6.7.8")}}
} else {
m.Answer = []dns.RR{&dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, AAAA: net.ParseIP("::1")}}
}
return m, nil
}
return &dns.Msg{}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("_sip._tcp.example.org.", dns.TypeSRV)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, extra, err := SRV(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs) != 2 {
t.Fatalf("expected 2 SRV records, got %d", len(recs))
}
if len(extra) == 0 {
t.Fatalf("expected extra address records")
}
if !lookedUp {
t.Fatalf("expected external lookup for alias.external.")
}
}
func TestMXInternalAndExternalTargets(t *testing.T) {
lookedUp := false
b := &mockBackend{
mockServices: func(_ctx context.Context, _state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
return []msg.Service{
{Host: "1.2.3.4", Mail: true, Priority: 10, TTL: 60, Key: "/skydns/org/example/mx1"},
{Host: "alias.external.", Mail: true, Priority: 20, TTL: 60, Key: "/skydns/org/example/mx2"},
}, nil
},
mockLookup: func(_ctx context.Context, _state request.Request, name string, typ uint16) (*dns.Msg, error) {
if name == "alias.external." && (typ == dns.TypeA || typ == dns.TypeAAAA) {
lookedUp = true
m := new(dns.Msg)
if typ == dns.TypeA {
m.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, A: net.ParseIP("9.9.9.9")}}
} else {
m.Answer = []dns.RR{&dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, AAAA: net.ParseIP("::2")}}
}
return m, nil
}
return &dns.Msg{}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeMX)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, extra, err := MX(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs) != 2 {
t.Fatalf("expected 2 MX records, got %d", len(recs))
}
if len(extra) == 0 {
t.Fatalf("expected extra address records for MX")
}
if !lookedUp {
t.Fatalf("expected external lookup for alias.external.")
}
}
func TestSOA(t *testing.T) {
b := &mockBackend{minTTL: 30, serial: 1234}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeSOA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
recs, err := SOA(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 SOA, got %d", len(recs))
}
soa, ok := recs[0].(*dns.SOA)
if !ok {
t.Fatalf("expected SOA, got %T", recs[0])
}
if soa.Hdr.Ttl != 30 {
t.Fatalf("expected ttl 30, got %d", soa.Hdr.Ttl)
}
if soa.Ns != "ns.dns.example.org." {
t.Fatalf("unexpected NS in SOA: %s", soa.Ns)
}
if soa.Mbox != "hostmaster.example.org." {
t.Fatalf("unexpected Mbox in SOA: %s", soa.Mbox)
}
}
func TestBackendError(t *testing.T) {
b := &mockBackend{}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
w := &test.ResponseWriter{}
state := request.Request{Req: req, W: w}
code, err := BackendError(context.Background(), b, "example.org.", dns.RcodeServerFailure, state, fmt.Errorf("err"), Options{})
if code != dns.RcodeSuccess {
t.Fatalf("expected RcodeSuccess, got %d", code)
}
if err == nil {
t.Fatal("expected error to be returned")
}
}
func TestCheckForApex(t *testing.T) {
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
if state.QName() == "apex.dns.example.org." {
return []msg.Service{{Host: "1.2.3.4", TTL: 10}}, nil
}
return []msg.Service{{Host: "::1", TTL: 20}}, nil
},
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
services, err := checkForApex(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(services) != 1 || services[0].Host != "1.2.3.4" {
t.Fatalf("expected apex services, got %+v", services)
}
}
func TestCheckForApexFallback(t *testing.T) {
errName := errors.New("name not found")
calls := 0
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
calls++
if state.QName() == "apex.dns.example.org." {
return nil, errName
}
return []msg.Service{{Host: "::1", TTL: 20}}, nil
},
mockNameError: func(err error) bool { return errors.Is(err, errName) },
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
services, err := checkForApex(context.Background(), b, "example.org.", state, Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(services) != 1 || services[0].Host != "::1" {
t.Fatalf("expected fallback services, got %+v", services)
}
if calls != 2 {
t.Fatalf("expected apex and fallback lookups, got %d calls", calls)
}
if state.QName() != "example.org." {
t.Fatalf("query name was not restored: %s", state.QName())
}
}
func TestCheckForApexFallbackDisabled(t *testing.T) {
errName := errors.New("name not found")
calls := 0
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
calls++
if state.QName() != "apex.dns.example.org." {
t.Fatalf("unexpected fallback lookup for %s", state.QName())
}
return nil, errName
},
mockNameError: func(err error) bool { return errors.Is(err, errName) },
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
services, err := checkForApex(context.Background(), b, "example.org.", state, Options{NoApexFallback: true})
if !errors.Is(err, errName) {
t.Fatalf("expected name error, got %v", err)
}
if len(services) != 0 {
t.Fatalf("expected no services, got %+v", services)
}
if calls != 1 {
t.Fatalf("expected one apex lookup, got %d calls", calls)
}
if state.QName() != "example.org." {
t.Fatalf("query name was not restored: %s", state.QName())
}
}
func TestCheckForApexBackendError(t *testing.T) {
errBackend := context.DeadlineExceeded
calls := 0
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
calls++
if state.QName() != "apex.dns.example.org." {
t.Fatalf("unexpected fallback lookup for %s", state.QName())
}
return nil, errBackend
},
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
services, err := checkForApex(context.Background(), b, "example.org.", state, Options{})
if !errors.Is(err, errBackend) {
t.Fatalf("expected backend error, got %v", err)
}
if len(services) != 0 {
t.Fatalf("expected no services, got %+v", services)
}
if calls != 1 {
t.Fatalf("expected one backend lookup, got %d", calls)
}
if state.QName() != "example.org." {
t.Fatalf("query name was not restored: %s", state.QName())
}
}
func TestIsDuplicate(t *testing.T) {
m := map[item]struct{}{}
if isDuplicate(m, "name.", "", 53) {
t.Fatal("unexpected duplicate on first insert (port)")
}
if !isDuplicate(m, "name.", "", 53) {
t.Fatal("expected duplicate on second insert (port)")
}
if isDuplicate(m, "name.", "1.2.3.4", 0) {
t.Fatal("unexpected duplicate on first insert (addr)")
}
if !isDuplicate(m, "name.", "1.2.3.4", 0) {
t.Fatal("expected duplicate on second insert (addr)")
}
}
// TestCNAMETargetsChasedOnce checks that a backend returning the same CNAME
// target many times (one per endpoint address, say) causes a single chase of
// that target, both for targets outside the zone (one upstream lookup) and
// for targets inside the zone (one recursive backend lookup).
func TestCNAMETargetsChasedOnce(t *testing.T) {
const fanout = 50
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeTXT} {
lookups := 0
backendCalls := map[string]int{}
b := &mockBackend{
mockServices: func(_ context.Context, state request.Request, _ bool, _ Options) ([]msg.Service, error) {
backendCalls[state.Name()]++
switch state.Name() {
case "a.example.org.":
var svcs []msg.Service
for i := 0; i < fanout; i++ {
svcs = append(svcs, msg.Service{Host: "b.example.org", Key: "/skydns/org/example/a"})
svcs = append(svcs, msg.Service{Host: "ext.example.com", Key: "/skydns/org/example/a"})
}
return svcs, nil
case "b.example.org.":
if qtype == dns.TypeTXT {
return []msg.Service{{Text: "hello", Key: "/skydns/org/example/b"}}, nil
}
return []msg.Service{{Host: "10.0.0.1", Key: "/skydns/org/example/b"}, {Host: "2001:db8::1", Key: "/skydns/org/example/b"}}, nil
}
return nil, errors.New("not found")
},
mockLookup: func(_ context.Context, _ request.Request, name string, _ uint16) (*dns.Msg, error) {
lookups++
m := new(dns.Msg)
m.SetQuestion(name, qtype)
return m, nil
},
}
m := new(dns.Msg)
m.SetQuestion("a.example.org.", qtype)
state := request.Request{W: &test.ResponseWriter{}, Req: m, Zone: "example.org."}
var records []dns.RR
var err error
switch qtype {
case dns.TypeA:
records, _, err = A(context.TODO(), b, "example.org.", state, nil, Options{})
case dns.TypeAAAA:
records, _, err = AAAA(context.TODO(), b, "example.org.", state, nil, Options{})
case dns.TypeTXT:
records, _, err = TXT(context.TODO(), b, "example.org.", state, nil, Options{})
}
if err != nil {
t.Fatalf("%s: unexpected error: %v", dns.TypeToString[qtype], err)
}
if lookups != 1 {
t.Errorf("%s: expected 1 upstream lookup for the repeated external target, got %d", dns.TypeToString[qtype], lookups)
}
if backendCalls["b.example.org."] != 1 {
t.Errorf("%s: expected 1 backend lookup for the repeated internal target, got %d", dns.TypeToString[qtype], backendCalls["b.example.org."])
}
cnames := 0
for _, rr := range records {
if rr.Header().Rrtype == dns.TypeCNAME {
cnames++
}
}
// One CNAME to b (followed by its data) and one to ext (whose upstream answer is empty here).
if cnames != 2 {
t.Errorf("%s: expected 2 CNAME records in the answer, got %d: %v", dns.TypeToString[qtype], cnames, records)
}
}
}
func TestCNAMETargetsChasedOnceAcrossRecursion(t *testing.T) {
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeTXT} {
backendCalls := map[string]int{}
b := &mockBackend{
mockServices: func(_ context.Context, state request.Request, _ bool, _ Options) ([]msg.Service, error) {
backendCalls[state.Name()]++
switch state.Name() {
case "a.example.org.":
return []msg.Service{{Host: "b.example.org"}, {Host: "c.example.org"}}, nil
case "b.example.org.", "c.example.org.":
return []msg.Service{{Host: "d.example.org"}}, nil
case "d.example.org.":
switch qtype {
case dns.TypeA:
return []msg.Service{{Host: "10.0.0.1"}}, nil
case dns.TypeAAAA:
return []msg.Service{{Host: "2001:db8::1"}}, nil
default:
return []msg.Service{{Text: "hello"}}, nil
}
}
return nil, errors.New("not found")
},
mockLookup: func(_ context.Context, _ request.Request, _ string, _ uint16) (*dns.Msg, error) {
return nil, errors.New("unexpected external lookup")
},
}
m := new(dns.Msg)
m.SetQuestion("a.example.org.", qtype)
state := request.Request{W: &test.ResponseWriter{}, Req: m, Zone: "example.org."}
var err error
switch qtype {
case dns.TypeA:
_, _, err = A(context.Background(), b, "example.org.", state, nil, Options{})
case dns.TypeAAAA:
_, _, err = AAAA(context.Background(), b, "example.org.", state, nil, Options{})
case dns.TypeTXT:
_, _, err = TXT(context.Background(), b, "example.org.", state, nil, Options{})
}
if err != nil {
t.Fatalf("%s: unexpected error: %v", dns.TypeToString[qtype], err)
}
if backendCalls["d.example.org."] != 1 {
t.Errorf("%s: expected converged target to be chased once, got %d", dns.TypeToString[qtype], backendCalls["d.example.org."])
}
}
}