Commit 73a55d08a4 for bind
commit 73a55d08a430ba4a848f54adfad24507c98eb5dc
Author: OndÅ™ej Surý <ondrej@isc.org>
Date: Tue Oct 13 16:32:21 2026 +0200
Test DS lookups at CNAME names from both sides of the cache
Cover the two behaviours the previous commit relies on: a CNAME answers
a DS query from the cache whether it was learned from a DS query or from
an earlier A query, and at a child's apex CNAME the parent's cached
denial of DS still takes precedence over the CNAME, without an upstream
fetch. The unit test gains the fresh/stale combinations of a CNAME and
a DS at one node.
Assisted-by: Claude:claude-fable-5-1
diff --git a/bin/tests/system/dnssec_cname_response/ans2/parent.db.in b/bin/tests/system/dnssec_cname_response/ans2/parent.db.in
index cc5ab1d475..b6289e72a9 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/parent.db.in
+++ b/bin/tests/system/dnssec_cname_response/ans2/parent.db.in
@@ -11,3 +11,7 @@ ns IN A 10.53.0.2
insecure IN NS ns.insecure.parent.
ns.insecure IN A 10.53.0.2
+
+alias IN CNAME target.parent.
+primed IN CNAME target.parent.
+target IN A 192.0.2.2
diff --git a/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db b/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db
index ea8daf590d..6d7472bbb2 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db
+++ b/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db
@@ -11,3 +11,5 @@ ns IN A 10.53.0.2
cname-target IN A 192.0.2.1
@ IN MX 10 mail.insecure.parent.
mail IN A 10.53.0.2
+alias IN CNAME cname-target.insecure.parent.
+primed IN CNAME cname-target.insecure.parent.
diff --git a/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py b/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
index 28a1fec05a..7ea3e08add 100644
--- a/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
+++ b/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
@@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import ec
from dns.rdtypes.dnskeybase import Flag
import dns.dnssec
+import dns.name
import dns.rdataclass
import dns.rdatatype
import dns.rrset
@@ -124,8 +125,8 @@ EXPECTED_A = dns.rrset.from_text(
)
-def _query_insecure_parent(ns, qtype):
- return isctest.query.tcp(isctest.query.create("insecure.parent.", qtype), ns.ip)
+def _query(ns, qname, qtype):
+ return isctest.query.tcp(isctest.query.create(qname, qtype), ns.ip)
def _check_insecure_cname_chain(res):
@@ -243,26 +244,92 @@ def test_cname_at_insecure_delegation_is_accepted(servers, resolver):
"""
ns = servers[resolver]
- res = _query_insecure_parent(ns, "A")
+ res = _query(ns, "insecure.parent.", "A")
_check_insecure_cname_chain(res)
- res = _query_insecure_parent(ns, "DS")
+ res = _query(ns, "insecure.parent.", "DS")
isctest.check.noerror(res)
isctest.check.empty_answer(res)
- res = _query_insecure_parent(ns, "NS")
+ res = _query(ns, "insecure.parent.", "NS")
isctest.check.noerror(res)
answers = {rrset.rdtype: rrset for rrset in res.answer}
assert answers.get(dns.rdatatype.CNAME) == EXPECTED_CNAME, res
# Repeat with a warm cache.
- res = _query_insecure_parent(ns, "A")
+ res = _query(ns, "insecure.parent.", "A")
_check_insecure_cname_chain(res)
assert not ns.log.grep(Re(r"deadlock found resolving 'insecure\.parent"))
assert not ns.log.grep(Re(r"fetch loop detected resolving 'insecure\.parent"))
+# Ordinary (non-apex) CNAMEs in the signed 'parent.' and the unsigned
+# 'insecure.parent.' zones; the 'primed' ones are cached by an A query first.
+CNAME_DS_CASES = [
+ ("alias.parent.", False),
+ ("primed.parent.", True),
+ ("alias.insecure.parent.", False),
+ ("primed.insecure.parent.", True),
+]
+
+
+@pytest.mark.parametrize("resolver", RESOLVERS)
+@pytest.mark.parametrize(
+ "qname,primed", CNAME_DS_CASES, ids=[qname for qname, _ in CNAME_DS_CASES]
+)
+def test_ds_query_at_cname_is_answered_from_cache(servers, resolver, qname, primed):
+ """
+ A cached CNAME answers DS queries like any other type: a DS query at an
+ uncached name is fetched once and then served from the cache, and a CNAME
+ already cached by an A query is used right away (GL#6455).
+ """
+ ns = servers[resolver]
+ name = qname.rstrip(".").replace(".", r"\.")
+ fetch = Re(f"fetch: {name}/DS")
+
+ if primed:
+ res = _query(ns, qname, "A")
+ isctest.check.noerror(res)
+ assert res.answer[0].rdtype == dns.rdatatype.CNAME, res
+
+ for _ in range(2):
+ res = _query(ns, qname, "DS")
+ isctest.check.noerror(res)
+ assert res.answer, res
+ assert res.answer[0].rdtype == dns.rdatatype.CNAME, res
+ assert res.answer[0].name == dns.name.from_text(qname), res
+
+ fetches = len(ns.log.grep(fetch))
+ assert fetches == (0 if primed else 1), f"{qname}/DS fetched {fetches} times"
+
+
+@pytest.mark.parametrize("resolver", RESOLVERS)
+def test_ds_denial_beside_apex_cname_is_served_from_cache(servers, resolver):
+ """
+ ans2 answers "insecure.parent/A" with a CNAME to
+ "cname-target.insecure.parent" and every other query normally.
+ Validating that chain caches the nonexistence of "insecure.parent/DS".
+ The cached denial, not the CNAME, must answer a later DS query, without
+ another upstream fetch (GL#6455).
+ """
+ ns = servers[resolver]
+ fetch = Re(r"fetch: insecure\.parent/DS")
+
+ ns.rndc("flush")
+
+ # Validating the unsigned CNAME chain caches the parent's denial of DS.
+ res = _query(ns, "insecure.parent.", "A")
+ _check_insecure_cname_chain(res)
+ fetched = len(ns.log.grep(fetch))
+
+ res = _query(ns, "insecure.parent.", "DS")
+ isctest.check.noerror(res)
+ isctest.check.adflag(res)
+ isctest.check.empty_answer(res)
+ assert len(ns.log.grep(fetch)) == fetched, "the DS query was fetched upstream"
+
+
def test_apex_cname_coexists_with_other_types(ns3):
"""
Caching an apex CNAME must preserve an existing MX RRset.
@@ -283,13 +350,13 @@ def test_apex_cname_coexists_with_other_types(ns3):
# Cache MX first; a cached CNAME would answer the MX query via the alias.
ns3.rndc("flushtree insecure.parent")
- res = _query_insecure_parent(ns3, "MX")
+ res = _query(ns3, "insecure.parent.", "MX")
check_mx(res)
- res = _query_insecure_parent(ns3, "A")
+ res = _query(ns3, "insecure.parent.", "A")
_check_insecure_cname_chain(res)
- res = _query_insecure_parent(ns3, "MX")
+ res = _query(ns3, "insecure.parent.", "MX")
check_mx(res)
with ns3.watch_log_from_here() as watcher:
diff --git a/tests/dns/qpdb_test.c b/tests/dns/qpdb_test.c
index 58bccbafaf..063284bc95 100644
--- a/tests/dns/qpdb_test.c
+++ b/tests/dns/qpdb_test.c
@@ -451,8 +451,8 @@ ISC_LOOP_TEST_IMPL(cname_precedence) {
.expected_type = cname,
.expected_stale = true,
},
- /* A lone CNAME answers ordinary types, including NS, but not
- DS. */
+ /* A lone CNAME answers every ordinary type, NS and DS
+ included. */
{
.type1 = cname,
.rank1 = fresh,
@@ -483,8 +483,7 @@ ISC_LOOP_TEST_IMPL(cname_precedence) {
.expected_type = cname,
.expected_stale = false,
},
- /* A lone stale CNAME answers ordinary types, including NS, but
- not DS. */
+ /* A lone stale CNAME does the same. */
{
.type1 = cname,
.rank1 = stale,
@@ -536,6 +535,38 @@ ISC_LOOP_TEST_IMPL(cname_precedence) {
.expected_type = ns,
.expected_stale = false,
},
+ /* DS beside a CNAME follows the same rules as any other type:
+ fresher wins, the requested type wins a tie. */
+ {
+ .type1 = cname,
+ .rank1 = fresh,
+ .type2 = ds,
+ .rank2 = stale,
+ .qtype = ds,
+ .expected_result = DNS_R_CNAME,
+ .expected_type = cname,
+ .expected_stale = false,
+ },
+ {
+ .type1 = cname,
+ .rank1 = stale,
+ .type2 = ds,
+ .rank2 = fresh,
+ .qtype = ds,
+ .expected_result = ISC_R_SUCCESS,
+ .expected_type = ds,
+ .expected_stale = false,
+ },
+ {
+ .type1 = cname,
+ .rank1 = stale,
+ .type2 = ds,
+ .rank2 = stale,
+ .qtype = ds,
+ .expected_result = ISC_R_SUCCESS,
+ .expected_type = ds,
+ .expected_stale = true,
+ },
/* DS does not hide a CNAME from ordinary queries. */
{
.type1 = cname,