From 1c15aaf95584ec1e841c544f7c072e632d3c0fff Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Wed, 8 Mar 2023 13:11:08 -0500 Subject: [PATCH] Support for CNAMEs, including out-of-zone Add support for CNAMEs both inside and outside of the zones set by this plugin. CNAME support is robust against excessive stack depth and loops. Includes tests for all changes. Signed-off-by: Dan Fuhry --- records.go | 64 +++++++++++++++++++++++++++++++++++++++++++++---- records_test.go | 44 +++++++++++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/records.go b/records.go index 4a414f7..61394b6 100644 --- a/records.go +++ b/records.go @@ -4,15 +4,19 @@ import ( "context" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/request" "github.com/miekg/dns" ) +const maxCnameStackDepth = 10 + // Records is the plugin handler. type Records struct { - origins []string // for easy matching, these strings are the index in the map m. - m map[string][]dns.RR + origins []string // for easy matching, these strings are the index in the map m. + m map[string][]dns.RR + upstream *upstream.Upstream Next plugin.Handler } @@ -33,16 +37,55 @@ func (re *Records) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms m.Authoritative = true nxdomain := true + // cnameMaybeUpstream tracks whether we are currently trying to resolve a CNAME. We always look for a match among + // the records handled by this plugin first, then we go upstream. This is required to enforce stack depth and loop + // detection. + cnameMaybeUpstream := false var soa dns.RR + cnameStack := make(map[string]struct{}, 0) + +resolveLoop: for _, r := range re.m[zone] { + if _, ok := cnameStack[qname]; ok { + log.Errorf("detected loop in CNAME chain, name [%s] already processed", qname) + goto servfail + } + if len(cnameStack) > maxCnameStackDepth { + log.Errorf("maximum CNAME stack depth of %d exceeded", maxCnameStackDepth) + goto servfail + } + if r.Header().Rrtype == dns.TypeSOA && soa == nil { soa = r } if r.Header().Name == qname { nxdomain = false - if r.Header().Rrtype == state.QType() { + if r.Header().Rrtype == state.QType() || r.Header().Rrtype == dns.TypeCNAME { m.Answer = append(m.Answer, r) } + if r.Header().Rrtype == dns.TypeCNAME { + cnameStack[qname] = struct{}{} + qname = r.(*dns.CNAME).Target + cnameMaybeUpstream = true + // restart resolution with new query name + goto resolveLoop + } else { + // If we found a match but the record type in the zone we control isn't + // another CNAME, that means we have reached the end of our chain and we + // don't need to go upstream. + cnameMaybeUpstream = false + } + } + } + + if cnameMaybeUpstream { + // we've found a CNAME but it doesn't point to a record managed by this + // plugin. In these cases we always restart with upstream. + msgs, err := re.upstream.Lookup(ctx, state, qname, state.QType()) + if err == nil && len(msgs.Answer) > 0 { + for _, ans := range msgs.Answer { + m.Answer = append(m.Answer, ans) + } } } @@ -64,6 +107,15 @@ func (re *Records) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms w.WriteMsg(m) return dns.RcodeSuccess, nil + +servfail: + m.Rcode = dns.RcodeServerFailure + m.Answer = nil + if soa != nil { + m.Ns = []dns.RR{soa} + } + w.WriteMsg(m) + return dns.RcodeServerFailure, nil } // Name implements the plugin.Handle interface. @@ -71,7 +123,9 @@ func (re *Records) Name() string { return "records" } // New returns a pointer to a new and intialized Records. func New() *Records { - re := new(Records) - re.m = make(map[string][]dns.RR) + re := &Records{ + m: make(map[string][]dns.RR), + upstream: upstream.New(), + } return re } diff --git a/records_test.go b/records_test.go index ef9d11d..7b02c66 100644 --- a/records_test.go +++ b/records_test.go @@ -7,7 +7,7 @@ import ( "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/test" - "github.com/caddyserver/caddy" + "github.com/coredns/caddy" "github.com/miekg/dns" ) @@ -74,8 +74,25 @@ var testCases = []test.Case{ func TestLookupNoSOA(t *testing.T) { const input = ` records { - example.org. 60 IN MX 10 mx.example.org. - mx.example.org. 60 IN A 127.0.0.1 + example.org. 60 IN MX 10 mx.example.org. + mx.example.org. 60 IN A 127.0.0.1 + cname.example.org. 60 IN CNAME mx.example.org. + cnameloop1.example.org. 60 IN CNAME cnameloop2.example.org. + cnameloop2.example.org. 60 IN CNAME cnameloop1.example.org. + cnameext.example.org. 60 IN CNAME mx.example.net. + + cnamedepth.example.org. 60 IN CNAME cnamedepth1.example.org. + cnamedepth1.example.org. 60 IN CNAME cnamedepth2.example.org. + cnamedepth2.example.org. 60 IN CNAME cnamedepth3.example.org. + cnamedepth3.example.org. 60 IN CNAME cnamedepth4.example.org. + cnamedepth4.example.org. 60 IN CNAME cnamedepth5.example.org. + cnamedepth5.example.org. 60 IN CNAME cnamedepth6.example.org. + cnamedepth6.example.org. 60 IN CNAME cnamedepth7.example.org. + cnamedepth7.example.org. 60 IN CNAME cnamedepth8.example.org. + cnamedepth8.example.org. 60 IN CNAME cnamedepth9.example.org. + cnamedepth9.example.org. 60 IN CNAME cnamedepth10.example.org. + cnamedepth10.example.org. 60 IN CNAME cnamedepth11.example.org. + cnamedepth11.example.org. 60 IN A 127.0.0.1 } ` @@ -122,6 +139,27 @@ var testCasesNoSOA = []test.Case{ { Qname: "mx.example.org.", Qtype: dns.TypeAAAA, }, + { + Qname: "cname.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("cname.example.org. 60 IN CNAME mx.example.org."), + test.A("mx.example.org. 60 IN A 127.0.0.1"), + }, + }, + { + Qname: "cnameext.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("cnameext.example.org. 60 IN CNAME mx.example.net."), + }, + }, + { + Rcode: dns.RcodeServerFailure, + Qname: "cnameloop1.example.org.", Qtype: dns.TypeA, + }, + { + Rcode: dns.RcodeServerFailure, + Qname: "cnamedepth.example.org.", Qtype: dns.TypeA, + }, } func TestLookupMultipleOrigins(t *testing.T) {