Commit 737831182b for bind
commit 737831182bb09e189079a585f8d16911f96fdbbf
Author: OndÅ™ej Surý <ondrej@isc.org>
Date: Tue Nov 10 12:12:27 2026 +0100
Clean up tests that relied on standalone RRSIG queries
With RRSIG queries rejected up front, several checks kept passing
without reaching the code they were written for. The malformed RRSIG
regression test now makes the resolver fetch the record with an ANY
query, and the resolver test looks for the leftover additional RRSIG
in a cache dump, which also confirms the signature was cached in the
first place.
The remaining checks covered only the removed handling of RRSIG as a
subset of ANY, or repeated the FORMERR assertion, and are removed.
Assisted-by: Claude:claude-fable-5-2
diff --git a/bin/tests/system/dnssec/ns3/secure.example.db.in b/bin/tests/system/dnssec/ns3/secure.example.db.in
index 4d66e5150a..e0c11168b7 100644
--- a/bin/tests/system/dnssec/ns3/secure.example.db.in
+++ b/bin/tests/system/dnssec/ns3/secure.example.db.in
@@ -37,9 +37,6 @@ ns2.insecure A 10.53.0.2
nosoa NS ns6.nosoa
ns6.nosoa A 10.53.0.6
-normalthenrrsig A 10.0.0.28
-rrsigonly A 10.0.0.29
-
cnameandkey CNAME @
dnameandkey DNAME @
diff --git a/bin/tests/system/dnssec/tests_validation.py b/bin/tests/system/dnssec/tests_validation.py
index 6d9d795c81..e9699c51b8 100644
--- a/bin/tests/system/dnssec/tests_validation.py
+++ b/bin/tests/system/dnssec/tests_validation.py
@@ -711,25 +711,6 @@ def test_cache(ns4):
for rrset in res2.answer:
assert rrset.ttl <= 300
- # RRSIG queries return FORMERR, no matter what the cache
- # holds for the queried name: prime the cache with a normal query,
- # then follow it with a query for the corresponding RRSIG
- msg = isctest.query.create("normalthenrrsig.secure.example", "A")
- isctest.query.tcp(msg, "10.53.0.4")
-
- msg = isctest.query.create("normalthenrrsig.secure.example", "RRSIG")
- res1 = isctest.query.tcp(msg, "10.53.0.3")
- res2 = isctest.query.tcp(msg, "10.53.0.4")
- for res in (res1, res2):
- isctest.check.formerr(res)
- isctest.check.empty_answer(res)
- isctest.check.ede(res, 30)
-
- msg = isctest.query.create("rrsigonly.secure.example", "RRSIG")
- res1 = isctest.query.tcp(msg, "10.53.0.4")
- isctest.check.formerr(res1)
- isctest.check.empty_answer(res1)
-
# check that a DNSKEY query with no data still gets cached
msg = isctest.query.create("insecure.example", "DNSKEY")
res1 = isctest.query.tcp(msg, "10.53.0.4")
diff --git a/bin/tests/system/dnssec_cname_response/ans2/ans.py b/bin/tests/system/dnssec_cname_response/ans2/ans.py
index 99ce5ea834..08fe230b98 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/ans.py
+++ b/bin/tests/system/dnssec_cname_response/ans2/ans.py
@@ -22,7 +22,7 @@ from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
from isctest.asyncserver.actions import DnsResponseSend
from isctest.asyncserver.matchers import Domain, Matcher, Qname, Qtype
-# 'example.' answers DNSKEY/NSEC/NSEC3/RRSIG queries with a CNAME (the
+# 'example.' answers DNSKEY/NSEC/NSEC3 queries with a CNAME (the
# meta-types whose CNAME answer the resolver and validator must cope with).
EXAMPLE = dns.zone.from_file("example.signed.db", origin="example.", relativize=False)
@@ -37,10 +37,6 @@ SECURE = dns.zone.from_file("secure.signed.db", origin="secure.", relativize=Fal
STUFFED = dns.zone.from_file("stuffed.signed.zone", origin="stuffed.", relativize=False)
-def a(owner: dns.name.Name, address: str) -> dns.rrset.RRset:
- return dns.rrset.from_text(owner, 300, dns.rdataclass.IN, dns.rdatatype.A, address)
-
-
def cname(owner: dns.name.Name, target: str) -> dns.rrset.RRset:
return dns.rrset.from_text(
owner, 300, dns.rdataclass.IN, dns.rdatatype.CNAME, target
@@ -61,24 +57,6 @@ def rrset_with_rrsig(
return rrsets
-class LoneAHandler(ResponseHandler):
- """
- Answer any query with a single unrelated A record (no RRSIG and no
- alias). An RRSIG query is handled by the resolver as a subset of ANY,
- and such an answer used to be dropped entirely, leaving the fetch waiting
- for a validator that was never started.
- """
-
- matcher = Domain("lone-a.example.")
-
- async def get_responses(
- self, qctx: QueryContext
- ) -> AsyncGenerator[DnsResponseSend, None]:
- qctx.prepare_new_response(with_zone_data=False)
- qctx.response.answer.append(a(qctx.qname, "192.0.2.1"))
- yield DnsResponseSend(qctx.response)
-
-
class CnameHandler(ResponseHandler):
"""
Answer with a CNAME instead of the real records.
@@ -98,7 +76,6 @@ class ExampleMetatypeCnameHandler(CnameHandler):
dns.rdatatype.DNSKEY,
dns.rdatatype.NSEC,
dns.rdatatype.NSEC3,
- dns.rdatatype.RRSIG,
)
@@ -199,7 +176,6 @@ def main() -> None:
server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR, default_aa=True)
server.install_response_handlers(
InsecureApexCnameHandler(),
- LoneAHandler(),
ExampleMetatypeCnameHandler(),
SignedZoneHandler(EXAMPLE),
SecureDsCnameHandler(),
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 7ea3e08add..66a36fb67a 100644
--- a/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
+++ b/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py
@@ -138,7 +138,7 @@ def _check_insecure_cname_chain(res):
assert answers[dns.rdatatype.A] == EXPECTED_A, res
-@pytest.mark.parametrize("qtype", ["DNSKEY", "NSEC", "NSEC3", "RRSIG"])
+@pytest.mark.parametrize("qtype", ["DNSKEY", "NSEC", "NSEC3"])
def test_direct_metatype_query_does_not_crash_resolver(qtype):
"""
A direct recursive client query for a DNSSEC meta-type, answered by a
@@ -158,10 +158,7 @@ def test_direct_metatype_query_does_not_crash_resolver(qtype):
res = isctest.query.tcp(msg, "10.53.0.3", timeout=8)
elapsed_time = time.time() - start_time
- # The resolver must answer promptly. An RRSIG query is handled as a
- # subset of ANY, and a CNAME answer to it used to be dropped without
- # caching or validation, leaving the fetch waiting ~12s for a
- # validator that was never started.
+ # The resolver must answer promptly.
assert elapsed_time < 5.0, f"{qtype} query took too long: {elapsed_time}s"
# We do not assert a particular rcode here -- SERVFAIL or a chased
@@ -170,26 +167,6 @@ def test_direct_metatype_query_does_not_crash_resolver(qtype):
_assert_alive("10.53.0.3")
-def test_rrsig_lone_record_does_not_stall_resolver():
- """
- A direct recursive RRSIG query answered with an unrelated record
- (here a lone A, with no RRSIG and no alias) must not stall the
- resolver. An RRSIG query is handled as a subset of ANY; every record
- of the wrong type is filtered out, and when nothing is left the
- answer used to be accepted as success with no answer bound, leaving
- the fetch waiting ~12s for a validator that was never started.
- """
- msg = isctest.query.create("lone-a.example.", "RRSIG")
-
- start_time = time.time()
- res = isctest.query.tcp(msg, "10.53.0.3", timeout=8)
- elapsed_time = time.time() - start_time
-
- assert elapsed_time < 5.0, f"RRSIG query took too long: {elapsed_time}s"
- assert res is not None, "no response to lone-record RRSIG query"
- _assert_alive("10.53.0.3")
-
-
def test_cname_for_validator_dnskey_fetch(ns3):
"""
A malicious authoritative server returning a CNAME for the
diff --git a/bin/tests/system/masterformat/tests.sh b/bin/tests/system/masterformat/tests.sh
index bd4615974d..fbf8977c38 100755
--- a/bin/tests/system/masterformat/tests.sh
+++ b/bin/tests/system/masterformat/tests.sh
@@ -63,7 +63,7 @@ ret=0
set -- 1 2 3
for zone in example example-explicit example-compat; do
for server in "$@"; do
- for qname in ns mx a aaaa cname dname txt rrsig nsec \
+ for qname in ns mx a aaaa cname dname txt nsec \
dnskey ds cdnskey cds; do
qtype="$qname"
dig_with_opts @10.53.0.${server} -q ${qname}.${zone}. -t ${qtype}
diff --git a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
index ab10c4cc81..5444f823df 100644
--- a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
+++ b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
@@ -17,6 +17,8 @@ by the resolver. ns3 picks the Type-Covered field from the leftmost
label of QNAME.
"""
+from dns import rdatatype
+
import pytest
import isctest
@@ -36,17 +38,30 @@ SIG_TYPES = ["RRSIG"]
REJECTED_TYPES = META_TYPES + SIG_TYPES
+def any_query(qname):
+ """
+ Standalone RRSIG queries are rejected before any fetch is made, so
+ use an ANY query to make the resolver fetch the lone RRSIG from ans3;
+ with DO set, a cached signature would be returned in the answer.
+ """
+ msg = isctest.query.create(qname, "ANY", dnssec=True, ad=False)
+ return isctest.query.tcp(msg, "10.53.0.2")
+
+
@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
-def test_rrsig_query_returns_formerr(covered_type):
+def test_rrsig_covers_rejected_type_is_servfail(covered_type):
+ res = any_query(f"{covered_type.lower()}.attacker.test.")
+ isctest.check.servfail(res)
+
+
+def test_rrsig_covers_ordinary_type_is_answered():
"""
- The resolver rejects RRSIG queries with FORMERR, so the malformed
- RRSIG can never be pulled into the cache with a direct RRSIG
- query.
+ The same query is answered when the RRSIG covers an ordinary type,
+ so the SERVFAIL above is caused by the malformed RRSIG.
"""
- qname = f"{covered_type.lower()}.attacker.test."
- msg = isctest.query.create(qname, "RRSIG", dnssec=False, ad=False)
- res = isctest.query.tcp(msg, "10.53.0.2")
- isctest.check.formerr(res)
+ res = any_query("a.attacker.test.")
+ isctest.check.noerror(res)
+ assert [rrset.rdtype for rrset in res.answer] == [rdatatype.RRSIG], res
@pytest.mark.parametrize("covered_type", REJECTED_TYPES)
diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh
index bbe9747990..61a284114f 100755
--- a/bin/tests/system/resolver/tests.sh
+++ b/bin/tests/system/resolver/tests.sh
@@ -306,11 +306,23 @@ stop_server ns4
rm ns4/named.noaa
start_server --noclean --restart --port ${PORT} ns4 || ret=1
+# Check whether the cache dump in file $2 has an RRSIG covering A at the
+# name $1; the dump prints the owner name only with the first RRset.
+cached_rrsig_a() {
+ awk -v owner="$1" '
+ /^;/ { next }
+ /^[^ \t]/ { current = $1 }
+ current == owner && /[ \t]RRSIG[ \t]+A[ \t]/ { found = 1 }
+ END { exit !found }' "$2"
+}
+
n=$((n + 1))
echo_i "check that replacement of additional data by a negative cache no data entry clears the additional RRSIGs ($n)"
ret=0
dig_with_opts +tcp mx example.net @10.53.0.7 >dig.ns7.out.${n} || ret=1
grep "status: NOERROR" dig.ns7.out.${n} >/dev/null || ret=1
+rndc_dumpdb ns7 -cache || ret=1
+cached_rrsig_a mail.example.net. ns7/named_dump.db.test${n} || ret=1
if [ $ret = 1 ]; then echo_i "mx priming failed"; fi
$NSUPDATE <<EOF
server 10.53.0.6 ${PORT}
@@ -325,9 +337,8 @@ grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=2
if [ $ret = 2 ]; then echo_i "ncache priming failed"; fi
dig_with_opts +tcp mx example.net @10.53.0.7 >dig.ns7.out.${n} || ret=3
grep "status: NOERROR" dig.ns7.out.${n} >/dev/null || ret=3
-dig_with_opts +tcp rrsig mail.example.net +norec @10.53.0.7 >dig.ns7.out.${n} || ret=4
-grep "status: FORMERR" dig.ns7.out.${n} >/dev/null || ret=4
-grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=4
+rndc_dumpdb ns7 -cache || ret=4
+cached_rrsig_a mail.example.net. ns7/named_dump.db.test${n} && ret=4
dig_with_opts +tcp aaaa mail.example.net +norec @10.53.0.7 >dig.ns7.out.${n} || ret=5
grep "status: SERVFAIL" dig.ns7.out.${n} >/dev/null || ret=5
grep "ANSWER: 0" dig.ns7.out.${n} >/dev/null || ret=5
diff --git a/bin/tests/system/resolver/tests_sh_resolver.py b/bin/tests/system/resolver/tests_sh_resolver.py
index 77d3536e75..603a2e23d4 100644
--- a/bin/tests/system/resolver/tests_sh_resolver.py
+++ b/bin/tests/system/resolver/tests_sh_resolver.py
@@ -19,6 +19,7 @@ pytestmark = pytest.mark.extra_artifacts(
"dig.*.bar.*",
"dig.*.prime.*",
"nextpart.out.*",
+ "rndc.out.test*",
"ans*/query.log",
"ns1/named.stats*",
"ns4/tld.db",
@@ -33,6 +34,7 @@ pytestmark = pytest.mark.extra_artifacts(
"ns6/example.net.db.signed.jnl",
"ns6/to-be-removed.tld.db",
"ns6/to-be-removed.tld.db.jnl",
+ "ns7/named_dump.db*",
"ns7/server.db",
"ns7/server.db.jnl",
]