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) {