diff --git a/plugin/file/alias_delegation_test.go b/plugin/file/alias_delegation_test.go new file mode 100644 index 000000000..8211b6c7a --- /dev/null +++ b/plugin/file/alias_delegation_test.go @@ -0,0 +1,272 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestAliasToDelegation(t *testing.T) { + zone, err := Parse(strings.NewReader(dbAliasDelegation), "example.org.", "stdin", 0) + if err != nil { + t.Fatalf("Expected no error when reading zone, got %q", err) + } + + fm := File{ + Next: test.ErrorHandler(), + Zones: Zones{ + Z: map[string]*Zone{"example.org.": zone}, + Names: []string{"example.org."}, + }, + } + + tests := []struct { + name string + qname string + qtype uint16 + authoritative bool + answer []dns.RR + authority []dns.RR + additional []dns.RR + }{ + { + name: "CNAME to delegated child", + qname: "alias.example.org.", + qtype: dns.TypeA, + authoritative: true, + answer: []dns.RR{ + test.CNAME("alias.example.org. 300 IN CNAME host.child.example.org."), + }, + authority: []dns.RR{ + test.NS("child.example.org. 300 IN NS ns.child.example.org."), + }, + additional: []dns.RR{ + test.A("ns.child.example.org. 300 IN A 192.0.2.2"), + test.AAAA("ns.child.example.org. 300 IN AAAA 2001:db8::2"), + }, + }, + { + name: "CNAME chain to delegated child", + qname: "chain.example.org.", + qtype: dns.TypeA, + authoritative: true, + answer: []dns.RR{ + test.CNAME("alias.example.org. 300 IN CNAME host.child.example.org."), + test.CNAME("chain.example.org. 300 IN CNAME alias.example.org."), + }, + authority: []dns.RR{ + test.NS("child.example.org. 300 IN NS ns.child.example.org."), + }, + additional: []dns.RR{ + test.A("ns.child.example.org. 300 IN A 192.0.2.2"), + test.AAAA("ns.child.example.org. 300 IN AAAA 2001:db8::2"), + }, + }, + { + name: "wildcard CNAME to delegated child", + qname: "name.wild.example.org.", + qtype: dns.TypeA, + authoritative: true, + answer: []dns.RR{ + test.CNAME("name.wild.example.org. 300 IN CNAME host.child.example.org."), + }, + authority: []dns.RR{ + test.NS("child.example.org. 300 IN NS ns.child.example.org."), + }, + additional: []dns.RR{ + test.A("ns.child.example.org. 300 IN A 192.0.2.2"), + test.AAAA("ns.child.example.org. 300 IN AAAA 2001:db8::2"), + }, + }, + { + name: "DNAME to delegated child", + qname: "host.mapped.example.org.", + qtype: dns.TypeA, + authoritative: true, + answer: []dns.RR{ + test.CNAME("host.mapped.example.org. 300 IN CNAME host.example.org."), + test.DNAME("mapped.example.org. 300 IN DNAME example.org."), + }, + authority: []dns.RR{ + test.NS("host.example.org. 300 IN NS ns.host.example.org."), + }, + additional: []dns.RR{ + test.A("ns.host.example.org. 300 IN A 192.0.2.3"), + test.AAAA("ns.host.example.org. 300 IN AAAA 2001:db8::3"), + }, + }, + { + name: "DNAME NS query to delegated child", + qname: "host.mapped.example.org.", + qtype: dns.TypeNS, + authoritative: true, + answer: []dns.RR{ + test.CNAME("host.mapped.example.org. 300 IN CNAME host.example.org."), + test.DNAME("mapped.example.org. 300 IN DNAME example.org."), + }, + authority: []dns.RR{ + test.NS("host.example.org. 300 IN NS ns.host.example.org."), + }, + additional: []dns.RR{ + test.A("ns.host.example.org. 300 IN A 192.0.2.3"), + test.AAAA("ns.host.example.org. 300 IN AAAA 2001:db8::3"), + }, + }, + { + name: "direct delegated child", + qname: "host.child.example.org.", + qtype: dns.TypeA, + authoritative: false, + authority: []dns.RR{ + test.NS("child.example.org. 300 IN NS ns.child.example.org."), + }, + additional: []dns.RR{ + test.A("ns.child.example.org. 300 IN A 192.0.2.2"), + test.AAAA("ns.child.example.org. 300 IN AAAA 2001:db8::2"), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req := new(dns.Msg) + req.SetQuestion(tc.qname, tc.qtype) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + if _, err := fm.ServeDNS(context.Background(), rec, req); err != nil { + t.Fatalf("ServeDNS() error = %v", err) + } + + want := test.Case{ + Qname: tc.qname, + Qtype: tc.qtype, + Answer: tc.answer, + Ns: tc.authority, + Extra: tc.additional, + } + if err := test.SortAndCheck(rec.Msg, want); err != nil { + t.Error(err) + } + if rec.Msg.Authoritative != tc.authoritative { + t.Errorf("Authoritative = %t, want %t", rec.Msg.Authoritative, tc.authoritative) + } + }) + } +} + +const dbAliasDelegation = ` +$TTL 300 +$ORIGIN example.org. +@ IN SOA ns.example.org. admin.example.org. 1 3600 600 86400 300 + IN NS ns.example.org. +ns IN A 192.0.2.1 + +alias IN CNAME host.child.example.org. +chain IN CNAME alias.example.org. +*.wild IN CNAME host.child.example.org. +child IN NS ns.child.example.org. +ns.child IN A 192.0.2.2 + IN AAAA 2001:db8::2 + +mapped IN DNAME example.org. +host IN NS ns.host.example.org. +ns.host IN A 192.0.2.3 + IN AAAA 2001:db8::3 +` + +func TestSignedAliasToDelegation(t *testing.T) { + zone, err := Parse(strings.NewReader(exampleOrgSigned), "example.org.", "stdin", 0) + if err != nil { + t.Fatalf("Expected no error when reading zone, got %q", err) + } + if err := zone.Insert(test.CNAME("alias.example.org. 1800 IN CNAME a.delegated.example.org.")); err != nil { + t.Fatalf("Insert() error = %v", err) + } + if err := zone.Insert(test.CNAME("ds-alias.example.org. 1800 IN CNAME delegated.example.org.")); err != nil { + t.Fatalf("Insert() error = %v", err) + } + + fm := File{ + Next: test.ErrorHandler(), + Zones: Zones{ + Z: map[string]*Zone{"example.org.": zone}, + Names: []string{"example.org."}, + }, + } + + referral := secureDelegationCase(t, "a.delegated.example.org.", dns.TypeTXT) + dsAtCut := secureDelegationCase(t, "delegated.example.org.", dns.TypeDS) + tests := []struct { + name string + qname string + qtype uint16 + answer []dns.RR + authority []dns.RR + additional []dns.RR + }{ + { + name: "signed partial referral", + qname: "alias.example.org.", + qtype: dns.TypeA, + answer: []dns.RR{ + test.CNAME("alias.example.org. 1800 IN CNAME a.delegated.example.org."), + }, + authority: referral.Ns, + additional: referral.Extra, + }, + { + name: "DS at zone cut remains authoritative", + qname: "ds-alias.example.org.", + qtype: dns.TypeDS, + answer: append( + append([]dns.RR{}, dsAtCut.Answer...), + test.CNAME("ds-alias.example.org. 1800 IN CNAME delegated.example.org."), + ), + authority: dsAtCut.Ns, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req := new(dns.Msg) + req.SetQuestion(tc.qname, tc.qtype) + req.SetEdns0(4096, true) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + if _, err := fm.ServeDNS(context.Background(), rec, req); err != nil { + t.Fatalf("ServeDNS() error = %v", err) + } + + want := test.Case{ + Qname: tc.qname, + Qtype: tc.qtype, + Do: true, + Answer: tc.answer, + Ns: tc.authority, + Extra: tc.additional, + } + if err := test.SortAndCheck(rec.Msg, want); err != nil { + t.Error(err) + } + if !rec.Msg.Authoritative { + t.Error("Authoritative = false, want true") + } + }) + } +} + +func secureDelegationCase(t *testing.T, qname string, qtype uint16) test.Case { + t.Helper() + for _, tc := range secureDelegationTestCases { + if tc.Qname == qname && tc.Qtype == qtype { + return tc + } + } + t.Fatalf("secure delegation fixture has no %s/%s case", qname, dns.TypeToString[qtype]) + return test.Case{} +} diff --git a/plugin/file/file.go b/plugin/file/file.go index 41e615131..aa3827968 100644 --- a/plugin/file/file.go +++ b/plugin/file/file.go @@ -117,7 +117,11 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i case NameError: m.Rcode = dns.RcodeNameError case Delegation: - m.Authoritative = false + // A referral-only response is not authoritative. A partial answer + // containing an authoritative alias keeps AA set for the original QNAME. + if len(m.Answer) == 0 { + m.Authoritative = false + } case ServerFailure: // If the result is SERVFAIL and the answer is non-empty, then the SERVFAIL came from an // external CNAME lookup and the answer contains the CNAME with no target record. We should diff --git a/plugin/file/lookup.go b/plugin/file/lookup.go index 6bbc310b3..6dc2f9b04 100644 --- a/plugin/file/lookup.go +++ b/plugin/file/lookup.go @@ -126,7 +126,7 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) rcode = Success } else { ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) - answer, ns, extra, rcode = z.externalLookup(ctx, state, elem, []dns.RR{cname}) + answer, ns, extra, rcode = z.externalLookup(ctx, state, tr, elem, []dns.RR{cname}) } if do { @@ -146,22 +146,8 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) // with (e.g. another DNAME). So there is nothing special left here. } - // If we see NS records, it means the name as been delegated, and we should return the delegation. - if nsrrs := elem.Type(dns.TypeNS); nsrrs != nil { - // If the query is specifically for DS and the qname matches the delegated name, we should - // return the DS in the answer section and leave the rest empty, i.e. just continue the loop - // and continue searching. - if qtype == dns.TypeDS && elem.Name() == qname { - i++ - continue - } - - glue := tr.Glue(nsrrs, do) - if do { - dss := typeFromElem(elem, dns.TypeDS, do) - nsrrs = append(nsrrs, dss...) - } - + // If we see NS records, it means the name has been delegated. + if nsrrs, glue, ok := delegationFromElem(tr, elem, qname, qtype, do); ok { return nil, nsrrs, glue, Delegation } @@ -172,7 +158,7 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) if found && shot { if rrs := elem.Type(dns.TypeCNAME); len(rrs) > 0 && qtype != dns.TypeCNAME { ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) - return z.externalLookup(ctx, state, elem, rrs) + return z.externalLookup(ctx, state, tr, elem, rrs) } rrs := elem.Type(qtype) @@ -211,7 +197,7 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) if rrs := wildElem.TypeForWildcard(dns.TypeCNAME, qname); len(rrs) > 0 && qtype != dns.TypeCNAME { ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) - return z.externalLookup(ctx, state, wildElem, rrs) + return z.externalLookup(ctx, state, tr, wildElem, rrs) } rrs := wildElem.TypeForWildcard(qtype, qname) @@ -353,7 +339,7 @@ func (z *Zone) authority(do bool, result Result) []dns.RR { } // externalLookup adds signatures and tries to resolve CNAMEs that point to external names. -func (z *Zone) externalLookup(ctx context.Context, state request.Request, elem *tree.Elem, rrs []dns.RR) ([]dns.RR, []dns.RR, []dns.RR, Result) { +func (z *Zone) externalLookup(ctx context.Context, state request.Request, tr *tree.Tree, elem *tree.Elem, rrs []dns.RR) ([]dns.RR, []dns.RR, []dns.RR, Result) { qtype := state.QType() do := state.Do() @@ -364,7 +350,10 @@ func (z *Zone) externalLookup(ctx context.Context, state request.Request, elem * } targetName := rrs[0].(*dns.CNAME).Target - elem, _ = z.Search(targetName) + elem, _ = tr.Search(targetName) + if ns, extra, ok := z.findDelegation(tr, targetName, qtype, do, elem); ok { + return rrs, ns, extra, Delegation + } if elem == nil || (qtype == dns.TypeNS || qtype == dns.TypeSOA && targetName == z.origin) { lookupRRs, result := z.doLookup(ctx, state, targetName, qtype) rrs = append(rrs, lookupRRs...) @@ -384,7 +373,10 @@ Redo: rrs = append(rrs, sigs...) } targetName := cname[0].(*dns.CNAME).Target - elem, _ = z.Search(targetName) + elem, _ = tr.Search(targetName) + if ns, extra, ok := z.findDelegation(tr, targetName, qtype, do, elem); ok { + return rrs, ns, extra, Delegation + } if elem == nil || (qtype == dns.TypeNS || qtype == dns.TypeSOA && targetName == z.origin) { lookupRRs, result := z.doLookup(ctx, state, targetName, qtype) rrs = append(rrs, lookupRRs...) @@ -413,6 +405,44 @@ Redo: return rrs, z.ns(do), nil, Success } +// findDelegation returns the first zone cut between the zone apex and qname. +func (z *Zone) findDelegation(tr *tree.Tree, qname string, qtype uint16, do bool, exact *tree.Elem) (ns, extra []dns.RR, ok bool) { + for i := 1; ; i++ { + name, shot := z.nameFromRight(qname, i) + if shot { + return nil, nil, false + } + if name == qname { + if exact == nil { + return nil, nil, false + } + return delegationFromElem(tr, exact, qname, qtype, do) + } + elem, found := tr.Search(name) + if !found { + continue + } + if ns, extra, ok := delegationFromElem(tr, elem, qname, qtype, do); ok { + return ns, extra, true + } + } +} + +// delegationFromElem builds a referral from a zone-cut element. A DS query at +// the cut itself is answered by the parent zone instead of returning a referral. +func delegationFromElem(tr *tree.Tree, elem *tree.Elem, qname string, qtype uint16, do bool) (ns, extra []dns.RR, ok bool) { + ns = elem.Type(dns.TypeNS) + if ns == nil || (qtype == dns.TypeDS && elem.Name() == qname) { + return nil, nil, false + } + + extra = tr.Glue(ns, do) + if do { + ns = append(ns, typeFromElem(elem, dns.TypeDS, do)...) + } + return ns, extra, true +} + func (z *Zone) doLookup(ctx context.Context, state request.Request, target string, qtype uint16) ([]dns.RR, Result) { m, e := z.Upstream.Lookup(ctx, state, target, qtype) if e != nil {