diff --git a/CHANGES b/CHANGES index 02e21e6513..b8fa5e403e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +5694. [bug] BIND looks up the deepest zone cut in cache in order + to iterate a query. When this node is stale, it may + bypass QNAME minimization. This has been fixed. + [GL #2665] + 5693. [func] Restore support for reading 'timeout' and 'attempts' options from /etc/resolv.conf, and use their values in dig, host and nslookup. (Previously this was diff --git a/bin/tests/system/qmin/ans2/ans.py b/bin/tests/system/qmin/ans2/ans.py index 730495eddb..1430401235 100755 --- a/bin/tests/system/qmin/ans2/ans.py +++ b/bin/tests/system/qmin/ans2/ans.py @@ -31,6 +31,9 @@ def logquery(type, qname): with open("qlog", "a") as f: f.write("%s %s\n", type, qname) +def endswith(domain, labels): + return domain.endswith("." + labels) or domain == labels + ############################################################################ # Respond to a DNS query. # For good. it serves: @@ -51,6 +54,12 @@ def logquery(type, qname): # 8.2.6.0.1.0.0.2.ip6.arpa IN NS ns3.good # 1.0.0.2.ip6.arpa. IN NS ns2.good # ip6.arpa. IN NS ns2.good +# +# For stale. it serves: +# a.b. NS ns.a.b.stale. +# ns.a.b.stale. IN A 10.53.0.3 +# b. NS ns.b.stale. +# ns.b.stale. IN A 10.53.0.4 ############################################################################ def create_response(msg): m = dns.message.from_wire(msg) @@ -75,9 +84,9 @@ def create_response(msg): r = dns.message.make_response(m) r.set_rcode(NOERROR) - if lqname.endswith("1.0.0.2.ip6.arpa."): + if endswith(lqname, "1.0.0.2.ip6.arpa."): # Direct query - give direct answer - if lqname.endswith("8.2.6.0.1.0.0.2.ip6.arpa."): + if endswith(lqname, "8.2.6.0.1.0.0.2.ip6.arpa."): # Delegate to ns3 r.authority.append(dns.rrset.from_text("8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, NS, "ns3.good.")) r.additional.append(dns.rrset.from_text("ns3.good.", 60, IN, A, "10.53.0.3")) @@ -89,7 +98,7 @@ def create_response(msg): # NS query at the apex r.answer.append(dns.rrset.from_text("1.0.0.2.ip6.arpa.", 30, IN, NS, "ns2.good.")) r.flags |= dns.flags.AA - elif "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa.".endswith(lqname): + elif endswith("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa.", lqname): # NODATA answer r.authority.append(dns.rrset.from_text("1.0.0.2.ip6.arpa.", 30, IN, SOA, "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1")) else: @@ -97,12 +106,12 @@ def create_response(msg): r.authority.append(dns.rrset.from_text("1.0.0.2.ip6.arpa.", 30, IN, SOA, "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1")) r.set_rcode(NXDOMAIN) return r - elif lqname.endswith("ip6.arpa."): + elif endswith(lqname, "ip6.arpa."): if lqname == "ip6.arpa." and rrtype == NS: # NS query at the apex r.answer.append(dns.rrset.from_text("ip6.arpa.", 30, IN, NS, "ns2.good.")) r.flags |= dns.flags.AA - elif "1.0.0.2.ip6.arpa.".endswith(lqname): + elif endswith("1.0.0.2.ip6.arpa.", lqname): # NODATA answer r.authority.append(dns.rrset.from_text("ip6.arpa.", 30, IN, SOA, "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1")) else: @@ -110,22 +119,47 @@ def create_response(msg): r.authority.append(dns.rrset.from_text("ip6.arpa.", 30, IN, SOA, "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1")) r.set_rcode(NXDOMAIN) return r - elif lqname.endswith("bad."): + elif endswith(lqname, "stale."): + if endswith(lqname, "a.b.stale."): + # Delegate to ns.a.b.stale. + r.authority.append(dns.rrset.from_text("a.b.stale.", 2, IN, NS, "ns.a.b.stale.")) + r.additional.append(dns.rrset.from_text("ns.a.b.stale.", 2, IN, A, "10.53.0.3")) + elif endswith(lqname, "b.stale."): + # Delegate to ns.b.stale. + r.authority.append(dns.rrset.from_text("b.stale.", 2, IN, NS, "ns.b.stale.")) + r.additional.append(dns.rrset.from_text("ns.b.stale.", 2, IN, A, "10.53.0.4")) + elif lqname == "stale." and rrtype == NS: + # NS query at the apex. + r.answer.append(dns.rrset.from_text("stale.", 2, IN, NS, "ns2.stale.")) + r.flags |= dns.flags.AA + elif lqname == "stale." and rrtype == SOA: + # SOA query at the apex. + r.answer.append(dns.rrset.from_text("stale.", 2, IN, SOA, "ns2.stale. hostmaster.stale. 1 2 3 4 5")) + r.flags |= dns.flags.AA + elif lqname == "stale.": + # NODATA answer + r.authority.append(dns.rrset.from_text("stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5")) + else: + # NXDOMAIN + r.authority.append(dns.rrset.from_text("stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5")) + r.set_rcode(NXDOMAIN) + return r + elif endswith(lqname, "bad."): bad = True suffix = "bad." lqname = lqname[:-4] - elif lqname.endswith("ugly."): + elif endswith(lqname, "ugly."): ugly = True suffix = "ugly." lqname = lqname[:-5] - elif lqname.endswith("good."): + elif endswith(lqname, "good."): suffix = "good." lqname = lqname[:-5] - elif lqname.endswith("slow."): + elif endswith(lqname, "slow."): slow = True suffix = "slow." lqname = lqname[:-5] - elif lqname.endswith("fwd."): + elif endswith(lqname, "fwd."): suffix = "fwd." lqname = lqname[:-4] else: @@ -133,7 +167,7 @@ def create_response(msg): return r # Good/bad/ugly differs only in how we treat non-empty terminals - if lqname.endswith("zoop.boing."): + if endswith(lqname, "zoop.boing."): r.authority.append(dns.rrset.from_text("zoop.boing." + suffix, 1, IN, NS, "ns3." + suffix)) elif lqname == "many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z." and rrtype == A: r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2")) @@ -168,9 +202,9 @@ def create_response(msg): else: r.authority.append(dns.rrset.from_text(suffix, 1, IN, SOA, "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1")) if bad or not \ - ("icky.icky.icky.ptang.zoop.boing.".endswith(lqname) or \ - "many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.".endswith(lqname) or \ - "a.bit.longer.ns.name.".endswith(lqname)): + (endswith("icky.icky.icky.ptang.zoop.boing.", lqname) or \ + endswith("many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.", lqname) or \ + endswith("a.bit.longer.ns.name.", lqname)): r.set_rcode(NXDOMAIN) if ugly: r.set_rcode(FORMERR) diff --git a/bin/tests/system/qmin/ans3/ans.py b/bin/tests/system/qmin/ans3/ans.py index d8c7766950..6d583904a9 100755 --- a/bin/tests/system/qmin/ans3/ans.py +++ b/bin/tests/system/qmin/ans3/ans.py @@ -31,6 +31,9 @@ def logquery(type, qname): with open("qlog", "a") as f: f.write("%s %s\n", type, qname) +def endswith(domain, labels): + return domain.endswith("." + labels) or domain == labels + ############################################################################ # Respond to a DNS query. # For good. it serves: @@ -43,6 +46,9 @@ def logquery(type, qname): # For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals # # For ugly. it works the same as for good., but returns garbage to non-empty terminals +# +# For stale. it serves: +# a.b.stale. IN TXT peekaboo (resolver did not do qname minimization) ############################################################################ def create_response(msg): m = dns.message.from_wire(msg) @@ -69,23 +75,46 @@ def create_response(msg): ip6req = False - if lqname.endswith("bad."): + if endswith(lqname, "bad."): bad = True suffix = "bad." lqname = lqname[:-4] - elif lqname.endswith("ugly."): + elif endswith(lqname, "ugly."): ugly = True suffix = "ugly." lqname = lqname[:-5] - elif lqname.endswith("good."): + elif endswith(lqname, "good."): suffix = "good." lqname = lqname[:-5] - elif lqname.endswith("slow."): + elif endswith(lqname, "slow."): slow = True suffix = "slow." lqname = lqname[:-5] - elif lqname.endswith("8.2.6.0.1.0.0.2.ip6.arpa."): - ip6req = True + elif endswith(lqname, "8.2.6.0.1.0.0.2.ip6.arpa."): + ip6req = True + elif endswith(lqname, "a.b.stale."): + if lqname == "a.b.stale.": + if rrtype == TXT: + # Direct query. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "peekaboo")) + r.flags |= dns.flags.AA + elif rrtype == NS: + # NS a.b. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale.")) + r.additional.append(dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")) + r.flags |= dns.flags.AA + elif rrtype == SOA: + # SOA a.b. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) + r.flags |= dns.flags.AA + else: + # NODATA. + r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) + else: + r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) + r.set_rcode(NXDOMAIN) + # NXDOMAIN. + return r else: r.set_rcode(REFUSED) return r @@ -94,15 +123,15 @@ def create_response(msg): if lqname == "zoop.boing." and rrtype == NS: r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, NS, "ns3."+suffix)) r.flags |= dns.flags.AA - elif lqname.endswith("icky.ptang.zoop.boing."): + elif endswith(lqname, "icky.ptang.zoop.boing."): r.authority.append(dns.rrset.from_text("icky.ptang.zoop.boing." + suffix, 1, IN, NS, "a.bit.longer.ns.name." + suffix)) - elif "icky.ptang.zoop.boing.".endswith(lqname): + elif endswith("icky.ptang.zoop.boing.", lqname): r.authority.append(dns.rrset.from_text("zoop.boing." + suffix, 1, IN, SOA, "ns3." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1")) if bad: r.set_rcode(NXDOMAIN) if ugly: r.set_rcode(FORMERR) - elif lqname.endswith("zoop.boing."): + elif endswith(lqname, "zoop.boing."): r.authority.append(dns.rrset.from_text("zoop.boing." + suffix, 1, IN, SOA, "ns3." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1")) r.set_rcode(NXDOMAIN) elif ip6req: diff --git a/bin/tests/system/qmin/ans4/ans.py b/bin/tests/system/qmin/ans4/ans.py index b5a993c320..110c09679d 100755 --- a/bin/tests/system/qmin/ans4/ans.py +++ b/bin/tests/system/qmin/ans4/ans.py @@ -31,6 +31,9 @@ def logquery(type, qname): with open("qlog", "a") as f: f.write("%s %s\n", type, qname) +def endswith(domain, labels): + return domain.endswith("." + labels) or domain == labels + ############################################################################ # Respond to a DNS query. # For good. it serves: @@ -44,6 +47,9 @@ def logquery(type, qname): # For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals # # For ugly. it works the same as for good., but returns garbage to non-empty terminals +# +# For stale. it serves: +# a.b.stale. IN TXT hooray (resolver did do qname minimization) ############################################################################ def create_response(msg): m = dns.message.from_wire(msg) @@ -70,23 +76,59 @@ def create_response(msg): ip6req = False - if lqname.endswith("bad."): + if endswith(lqname, "bad."): bad = True suffix = "bad." lqname = lqname[:-4] - elif lqname.endswith("ugly."): + elif endswith(lqname, "ugly."): ugly = True suffix = "ugly." lqname = lqname[:-5] - elif lqname.endswith("good."): + elif endswith(lqname, "good."): suffix = "good." lqname = lqname[:-5] - elif lqname.endswith("slow."): + elif endswith(lqname, "slow."): slow = True suffix = "slow." lqname = lqname[:-5] - elif lqname.endswith("1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."): + elif endswith(lqname, "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."): ip6req = True + elif endswith(lqname, "b.stale."): + if lqname == "a.b.stale.": + if rrtype == TXT: + # Direct query. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "hooray")) + r.flags |= dns.flags.AA + elif rrtype == NS: + # NS a.b. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale.")) + r.additional.append(dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")) + r.flags |= dns.flags.AA + elif rrtype == SOA: + # SOA a.b. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) + r.flags |= dns.flags.AA + else: + # NODATA. + r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) + elif lqname == "b.stale.": + if rrtype == NS: + # NS b. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.b.stale.")) + r.additional.append(dns.rrset.from_text("ns.b.stale.", 1, IN, A, "10.53.0.4")) + r.flags |= dns.flags.AA + elif rrtype == SOA: + # SOA b. + r.answer.append(dns.rrset.from_text(lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5")) + r.flags |= dns.flags.AA + else: + # NODATA. + r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5")) + else: + r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5")) + r.set_rcode(NXDOMAIN) + # NXDOMAIN. + return r else: r.set_rcode(REFUSED) return r @@ -101,9 +143,9 @@ def create_response(msg): elif lqname == "icky.ptang.zoop.boing." and rrtype == NS: r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, NS, "a.bit.longer.ns.name."+suffix)) r.flags |= dns.flags.AA - elif lqname.endswith("icky.ptang.zoop.boing."): + elif endswith(lqname, "icky.ptang.zoop.boing."): r.authority.append(dns.rrset.from_text("icky.ptang.zoop.boing." + suffix, 1, IN, SOA, "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1")) - if bad or not "more.icky.icky.icky.ptang.zoop.boing.".endswith(lqname): + if bad or not endswith("more.icky.icky.icky.ptang.zoop.boing.", lqname): r.set_rcode(NXDOMAIN) if ugly: r.set_rcode(FORMERR) @@ -111,7 +153,7 @@ def create_response(msg): r.flags |= dns.flags.AA if lqname == "test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa." and rrtype == TXT: r.answer.append(dns.rrset.from_text("test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 1, IN, TXT, "long_ip6_name")) - elif "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.".endswith(lqname): + elif endswith("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", lqname): #NODATA answer r.authority.append(dns.rrset.from_text("1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, SOA, "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16")) else: diff --git a/bin/tests/system/qmin/ns1/root.db b/bin/tests/system/qmin/ns1/root.db index 3b38a33bbd..351500adbb 100644 --- a/bin/tests/system/qmin/ns1/root.db +++ b/bin/tests/system/qmin/ns1/root.db @@ -33,3 +33,7 @@ ns2.ugly. A 10.53.0.2 fwd. NS ns2.fwd. ns2.fwd. A 10.53.0.2 + +$TTL 2 +stale. NS ns2.stale. +ns2.stale. A 10.53.0.2 diff --git a/bin/tests/system/qmin/tests.sh b/bin/tests/system/qmin/tests.sh index e6a2684643..5d85cf28b8 100755 --- a/bin/tests/system/qmin/tests.sh +++ b/bin/tests/system/qmin/tests.sh @@ -392,6 +392,146 @@ grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 # ;; ANSWER SECTION: # test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa. 1 IN TXT "long_ip6_name" grep 'ip6\.arpa.*TXT.*long_ip6_name' dig.out.test$n > /dev/null || ret=1 +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` + +# Below are test cases for GL #2665: The QNAME minimization (if enabled) should +# also occur on the second query, after the RRsets have expired from cache. +# BIND will still have the entries in cache, but marked stale. These stale +# entries should not prevent the resolver from minimizing the QNAME. +# We query for the test domain a.b.stale. in all cases (QNAME minimization off, +# strict mode, and relaxed mode) and expect it to behave the same the second +# time when we have a stale delegation structure in cache. +n=`expr $n + 1` +echo_i "query for .stale is not minimized when qname-minimization is off ($n)" +ret=0 +$CLEANQL +$RNDCCMD 10.53.0.5 flush +$DIG $DIGOPTS @10.53.0.5 txt a.b.stale. > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "a\.b\.stale\..*1.*IN.*TXT.*peekaboo" dig.out.test$n > /dev/null || ret=1 +sleep 1 +echo "TXT a.b.stale." | $DIFF ans2/query.log - > /dev/null || ret=1 +echo "TXT a.b.stale." | $DIFF ans3/query.log - > /dev/null || ret=1 +test -f ans4/query.log && ret=1 +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` + +n=`expr $n + 1` +echo_i "query for .stale is properly minimized when qname-minimization is in strict mode ($n)" +ret=0 +$CLEANQL +$RNDCCMD 10.53.0.6 flush +$DIG $DIGOPTS @10.53.0.6 txt a.b.stale. > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "a\.b\.stale\..*1.*IN.*TXT.*hooray" dig.out.test$n > /dev/null || ret=1 +sleep 1 +sort ans2/query.log > ans2/query.log.sorted +cat << __EOF | $DIFF ans2/query.log.sorted - > /dev/null || ret=1 +ADDR ns.b.stale. +ADDR ns2.stale. +NS b.stale. +NS stale. +__EOF +test -f ans3/query.log && ret=1 +sort ans4/query.log > ans4/query.log.sorted +cat << __EOF | $DIFF ans4/query.log.sorted - > /dev/null || ret=1 +ADDR ns.b.stale. +NS b.stale. +TXT a.b.stale. +__EOF +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` + +n=`expr $n + 1` +echo_i "query for .stale is properly minimized when qname-minimization is in relaxed mode ($n)" +ret=0 +$CLEANQL +$RNDCCMD 10.53.0.7 flush +$DIG $DIGOPTS @10.53.0.7 txt a.b.stale. > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "a\.b\.stale\..*1.*IN.*TXT.*hooray" dig.out.test$n > /dev/null || ret=1 +sleep 1 +sort ans2/query.log > ans2/query.log.sorted +cat << __EOF | $DIFF ans2/query.log.sorted - > /dev/null || ret=1 +ADDR _.b.stale. +ADDR ns.b.stale. +ADDR ns2.stale. +__EOF +test -f ans3/query.log && ret=1 +sort ans4/query.log > ans4/query.log.sorted +cat << __EOF | $DIFF ans4/query.log.sorted - > /dev/null || ret=1 +ADDR ns.b.stale. +TXT a.b.stale. +__EOF +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` + +echo_i "sleep 2, allow entries in cache to go stale" +sleep 2 + +n=`expr $n + 1` +echo_i "query for .stale is not minimized when qname-minimization is off (stale cache) ($n)" +ret=0 +$CLEANQL +$DIG $DIGOPTS @10.53.0.5 txt a.b.stale. > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "a\.b\.stale\..*1.*IN.*TXT.*peekaboo" dig.out.test$n > /dev/null || ret=1 +sleep 1 +echo "TXT a.b.stale." | $DIFF ans2/query.log - > /dev/null || ret=1 +echo "TXT a.b.stale." | $DIFF ans3/query.log - > /dev/null || ret=1 +test -f ans4/query.log && ret=1 +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` + +n=`expr $n + 1` +echo_i "query for .stale is properly minimized when qname-minimization is in strict mode (stale cache) ($n)" +ret=0 +$CLEANQL +$DIG $DIGOPTS @10.53.0.6 txt a.b.stale. > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "a\.b\.stale\..*1.*IN.*TXT.*hooray" dig.out.test$n > /dev/null || ret=1 +sleep 1 +sort ans2/query.log > ans2/query.log.sorted +cat << __EOF | $DIFF ans2/query.log.sorted - > /dev/null || ret=1 +NS b.stale. +NS stale. +__EOF +test -f ans3/query.log && ret=1 +sort ans4/query.log > ans4/query.log.sorted +cat << __EOF | $DIFF ans4/query.log.sorted - > /dev/null || ret=1 +NS b.stale. +TXT a.b.stale. +__EOF +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` + +n=`expr $n + 1` +echo_i "query for .stale is properly minimized when qname-minimization is in relaxed mode (stale cache) ($n)" +ret=0 +$CLEANQL +$DIG $DIGOPTS @10.53.0.7 txt a.b.stale. > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "a\.b\.stale\..*1.*IN.*TXT.*hooray" dig.out.test$n > /dev/null || ret=1 +sleep 1 +sort ans2/query.log > ans2/query.log.sorted +cat << __EOF | $DIFF ans2/query.log.sorted - > /dev/null || ret=1 +ADDR _.b.stale. +__EOF +test -f ans3/query.log && ret=1 +sort ans4/query.log > ans4/query.log.sorted +cat << __EOF | $DIFF ans4/query.log.sorted - > /dev/null || ret=1 +TXT a.b.stale. +__EOF +for ans in ans2 ans3 ans4; do mv -f $ans/query.log query-$ans-$n.log 2>/dev/null || true; done +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/doc/notes/notes-current.rst b/doc/notes/notes-current.rst index b0fa7eaab8..b6cee02efe 100644 --- a/doc/notes/notes-current.rst +++ b/doc/notes/notes-current.rst @@ -82,3 +82,7 @@ Bug Fixes When the affinity is not set, tests show a slight dip in the authoritative performance of around 5% (ranging from 3.8% to 7.8%), but the recursive performance is now consistently improved. :gl:`#2822` + +- When following QNAME minimization, BIND could use a stale zonecut from cache + to resolve the query, resulting in a non-minimized query. This has been + fixed :gl:`#2665` diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c index 98df4377c2..32355c1398 100644 --- a/lib/dns/rbtdb.c +++ b/lib/dns/rbtdb.c @@ -3523,7 +3523,9 @@ find_wildcard(rbtdb_search_t *search, dns_rbtnode_t **nodep, for (header = node->data; header != NULL; header = header->next) { if (header->serial <= search->serial && - !IGNORE(header) && EXISTS(header)) { + !IGNORE(header) && EXISTS(header) && + !ANCIENT(header)) + { break; } } @@ -3582,7 +3584,9 @@ find_wildcard(rbtdb_search_t *search, dns_rbtnode_t **nodep, for (header = wnode->data; header != NULL; header = header->next) { if (header->serial <= search->serial && - !IGNORE(header) && EXISTS(header)) { + !IGNORE(header) && EXISTS(header) && + !ANCIENT(header)) + { break; } } @@ -4677,11 +4681,13 @@ cache_zonecut_callback(dns_rbtnode_t *node, dns_name_t *name, void *arg) { &header_prev)) { /* Do nothing. */ } else if (header->type == dns_rdatatype_dname && - EXISTS(header)) { + EXISTS(header) && !ANCIENT(header)) + { dname_header = header; header_prev = header; } else if (header->type == RBTDB_RDATATYPE_SIGDNAME && - EXISTS(header)) { + EXISTS(header) && !ANCIENT(header)) + { sigdname_header = header; header_prev = header; } else { @@ -4751,7 +4757,7 @@ find_deepest_zonecut(rbtdb_search_t *search, dns_rbtnode_t *node, if (check_stale_header(node, header, &locktype, lock, search, &header_prev)) { /* Do nothing. */ - } else if (EXISTS(header)) { + } else if (EXISTS(header) && !ANCIENT(header)) { /* * We've found an extant rdataset. See if * we're interested in it. @@ -5357,6 +5363,7 @@ cache_findzonecut(dns_db_t *db, const dns_name_t *name, unsigned int options, } else if (!dcnull) { dns_name_copy(dcname, foundname); } + /* * We now go looking for an NS rdataset at the node. */ @@ -5372,8 +5379,23 @@ cache_findzonecut(dns_db_t *db, const dns_name_t *name, unsigned int options, header_next = header->next; if (check_stale_header(node, header, &locktype, lock, &search, &header_prev)) { - /* Do nothing. */ - } else if (EXISTS(header)) { + /* + * The function dns_rbt_findnode found us the a matching + * node for 'name' and stored the result in 'dcname'. + * This is the deepest known zonecut in our database. + * However, this node may be stale and if serve-stale + * is not enabled (in other words 'stale-answer-enable' + * is set to no), this node may not be used as a + * zonecut we know about. If so, find the deepest + * zonecut from this node up and return that instead. + */ + NODE_UNLOCK(lock, locktype); + result = find_deepest_zonecut(&search, node, nodep, + foundname, rdataset, + sigrdataset); + dns_name_copy(foundname, dcname); + goto tree_exit; + } else if (EXISTS(header) && !ANCIENT(header)) { /* * If we found a type we were looking for, remember * it.