Commit 448b052b2c for bind

commit 448b052b2cdcb0fe6ea350d334d55c1b1390909b
Author: Štěpán Balážik <stepan@isc.org>
Date:   Tue Sep 8 19:35:51 2026 +0200

    Match queries a zone has no data for with a matcher

    The stuffed-NXDOMAIN handler keeps the zone lookup in a matcher of its
    own, which was previously done with a match() method.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/dnssec_cname_response/ans2/ans.py b/bin/tests/system/dnssec_cname_response/ans2/ans.py
index bbfe05b133..5722c4de1d 100644
--- a/bin/tests/system/dnssec_cname_response/ans2/ans.py
+++ b/bin/tests/system/dnssec_cname_response/ans2/ans.py
@@ -14,14 +14,13 @@ from collections.abc import AsyncGenerator
 import dns.name
 import dns.rcode
 import dns.rdataclass
-import dns.rdataset
 import dns.rdatatype
 import dns.rrset
 import dns.zone

 from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.matchers import Domain, Qtype
+from isctest.asyncserver.matchers import Domain, Matcher, Qtype

 # 'example.' answers DNSKEY/NSEC/NSEC3/RRSIG queries with a CNAME (the
 # meta-types whose CNAME answer the resolver and validator must cope with).
@@ -144,6 +143,24 @@ class SignedZoneHandler(ResponseHandler):
         yield DnsResponseSend(qctx.response)


+class NoDataIn(Matcher):
+    """
+    Match queries the given zone holds no rdataset for.
+    """
+
+    def __init__(self, zone: dns.zone.Zone) -> None:
+        self._zone = zone
+
+    def match(self, qctx: QueryContext) -> bool:
+        node = self._zone.get_node(qctx.qname)
+        if node is None:
+            return True
+        return node.get_rdataset(dns.rdataclass.IN, qctx.qtype) is None
+
+    def __str__(self) -> str:
+        return f"no data in {self._zone.origin}"
+
+
 class StuffedNxdomainHandler(ResponseHandler):
     """
     Answer NXDOMAIN with every NSEC3 RRset from a signed zone.
@@ -152,7 +169,7 @@ class StuffedNxdomainHandler(ResponseHandler):
     def __init__(self, zone: dns.zone.Zone) -> None:
         self._zone = zone
         assert self._zone.origin
-        self.matcher = Domain(self._zone.origin)
+        self.matcher = Domain(self._zone.origin) & NoDataIn(zone)
         self._nsec3_authority = self._build_authority()

     def _build_authority(self) -> list[dns.rrset.RRset]:
@@ -161,15 +178,6 @@ class StuffedNxdomainHandler(ResponseHandler):
             authority.extend(rrset_with_rrsig(self._zone, name, dns.rdatatype.NSEC3))
         return authority

-    def match(self, qctx: QueryContext) -> bool:
-        return super().match(qctx) and self._answer_rds(qctx) is None
-
-    def _answer_rds(self, qctx: QueryContext) -> dns.rdataset.Rdataset | None:
-        node = self._zone.get_node(qctx.qname)
-        if node is None:
-            return None
-        return node.get_rdataset(dns.rdataclass.IN, qctx.qtype)
-
     async def get_responses(
         self, qctx: QueryContext
     ) -> AsyncGenerator[DnsResponseSend, None]: