mirror of
https://github.com/coredns/coredns.git
synced 2026-08-20 23:08:28 -04:00
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses An upstream may return NOERROR with a non-empty answer that still does not resolve the question and without an SOA record to bound a negative TTL: a CNAME chain that does not terminate in a record of the queried type at the chain's terminal name (an incomplete recursion result from a forwarder). Because the answer section is non-empty, response.Typify classifies it as NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and replays the non-answer to clients until the TTL expires. Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the effective owner name is the target at the end of the CNAME chain, and the response is NODATA unless it carries the queried type at that terminal name; this holds for every query type, not just A/AAAA. Skip caching such a response (mirroring the existing NameError && !hasSOA guard) and let the next query be resolved upstream again. An empty answer section is deliberately left cacheable: it is indistinguishable from a legitimate NOERROR positive response that carries its data outside the answer section (for example the whoami plugin). Refs coredns#6958, coredns#5077, coredns#4987. Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com> * plugin/cache: fail closed on malformed CNAME chains in isNODATA canonicalName now returns a validity flag and rejects chains that are not a single unambiguous path to a terminal name: an owner with more than one distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a non-answer, so a SOA-less response with such a chain is not cached. This makes the classification order-independent (previously the first CNAME per owner won, so a two-target owner was cacheable or not depending on wire order) and closes the loop-with-co-located-record case. Duplicate CNAME records naming the same target are still tolerated. Adds regression tests for the two-distinct-targets case in both orders, the CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581 Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com> * plugin/cache: split canonicalName into self-documenting helpers Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection into a small case-insensitive nameSet type, leaving canonicalName as a short driver. Signature and algorithm are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581 Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com> --------- Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
This commit is contained in:
241
plugin/cache/cache_test.go
vendored
241
plugin/cache/cache_test.go
vendored
@@ -282,6 +282,243 @@ func TestCacheInsertion(t *testing.T) {
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR dangling CNAME chain without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target1.example.net."),
|
||||
test.CNAME("target1.example.net. 3600 IN CNAME target2.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain ending in a different type without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.AAAA("target.example.net. 3600 IN AAAA ::1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR MX query CNAME chain without terminal MX without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR MX query CNAME chain terminating in MX caches",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.MX("target.example.net. 3600 IN MX 10 mail.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
out: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.MX("target.example.net. 3600 IN MX 10 mail.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR DNSSEC dangling CNAME chain without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Do: true,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.RRSIG("alias.example.org. 3600 IN RRSIG CNAME 8 2 3600 20170521031301 20170421031301 12051 example.org. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain with unrelated A off the chain without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.A("unrelated.example.net. 3600 IN A 192.0.2.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR lone CNAME answer to an ANY query caches",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeANY,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
out: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeANY,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain terminating in A record caches",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.A("target.example.net. 3600 IN A 127.0.0.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
out: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.A("target.example.net. 3600 IN A 127.0.0.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain terminating in A record out of order caches",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("target.example.net. 3600 IN A 127.0.0.1"),
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
out: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("target.example.net. 3600 IN A 127.0.0.1"),
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME answer to a CNAME query caches",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeCNAME,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
out: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeCNAME,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain with two distinct targets at one owner without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target1.example.net."),
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target2.example.net."),
|
||||
test.A("target1.example.net. 3600 IN A 192.0.2.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain with two distinct targets at one owner reversed order without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target2.example.net."),
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target1.example.net."),
|
||||
test.A("target1.example.net. 3600 IN A 192.0.2.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME loop with co-located A without SOA does not cache",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.CNAME("target.example.net. 3600 IN CNAME alias.example.org."),
|
||||
test.A("alias.example.org. 3600 IN A 192.0.2.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: false,
|
||||
},
|
||||
{
|
||||
name: "test NOERROR CNAME chain with duplicate identical CNAME terminating in A caches",
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.A("target.example.net. 3600 IN A 127.0.0.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
out: test.Case{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Qname: "alias.example.org.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.CNAME("alias.example.org. 3600 IN CNAME target.example.net."),
|
||||
test.A("target.example.net. 3600 IN A 127.0.0.1"),
|
||||
},
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
shouldCache: true,
|
||||
},
|
||||
}
|
||||
now, _ := time.Parse(time.UnixDate, "Fri Apr 21 10:51:21 BST 2017")
|
||||
utc := now.UTC()
|
||||
@@ -780,7 +1017,7 @@ func ttlBackend(ttl int) plugin.Handler {
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
|
||||
m.Answer = []dns.RR{test.A(fmt.Sprintf("example.org. %d IN A 127.0.0.53", ttl))}
|
||||
m.Answer = []dns.RR{test.A(fmt.Sprintf("%s %d IN A 127.0.0.53", r.Question[0].Name, ttl))}
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
@@ -813,7 +1050,7 @@ func slowTTLBackend(ttl int, delay time.Duration, done chan<- struct{}) plugin.H
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
m.Answer = []dns.RR{test.A(fmt.Sprintf("example.org. %d IN A 127.0.0.53", ttl))}
|
||||
m.Answer = []dns.RR{test.A(fmt.Sprintf("%s %d IN A 127.0.0.53", r.Question[0].Name, ttl))}
|
||||
w.WriteMsg(m)
|
||||
if done != nil {
|
||||
close(done)
|
||||
|
||||
Reference in New Issue
Block a user