Commit 53a9669d06 for bind

commit 53a9669d062296a9afa6f1ee3ea1321a064100c8
Author: Štěpán Balážik <stepan@isc.org>
Date:   Tue Sep 8 19:34:10 2026 +0200

    Match on QTYPE with a matcher

    Qtype declares the QTYPE a handler answers, which was previously done
    with a match() method.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/dnssec/ans10/ans.py b/bin/tests/system/dnssec/ans10/ans.py
index 246701be3a..ff8993d7c1 100644
--- a/bin/tests/system/dnssec/ans10/ans.py
+++ b/bin/tests/system/dnssec/ans10/ans.py
@@ -16,11 +16,11 @@ import dns.rrset

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


 class AddRrsigToAHandler(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.A
+    matcher = Qtype(dns.rdatatype.A)

     async def get_responses(
         self, qctx: QueryContext
@@ -37,8 +37,7 @@ class AddRrsigToAHandler(ResponseHandler):


 class AddNsecToTxtHandler(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.TXT
+    matcher = Qtype(dns.rdatatype.TXT)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/isctest/asyncserver/handlers.py b/bin/tests/system/isctest/asyncserver/handlers.py
index 6acdd1964d..559790e7b1 100644
--- a/bin/tests/system/isctest/asyncserver/handlers.py
+++ b/bin/tests/system/isctest/asyncserver/handlers.py
@@ -32,6 +32,7 @@ from . import (
 )
 from .actions import BytesResponseSend, DnsResponseSend, ResponseDrop
 from .context import Peer, QueryContext
+from .matchers import Matcher, Qtype


 def block_reading(peer: Peer, writer_not_the_reader: asyncio.StreamWriter) -> None:
@@ -494,8 +495,7 @@ class AxfrHandler(ResponseHandler):
         """
         raise NotImplementedError

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.AXFR
+    matcher: Matcher = Qtype(dns.rdatatype.AXFR)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/isctest/asyncserver/matchers.py b/bin/tests/system/isctest/asyncserver/matchers.py
index 60a8151f41..c9a62f8983 100644
--- a/bin/tests/system/isctest/asyncserver/matchers.py
+++ b/bin/tests/system/isctest/asyncserver/matchers.py
@@ -12,6 +12,7 @@
 import abc

 import dns.name
+import dns.rdatatype

 from .context import QueryContext

@@ -126,3 +127,18 @@ class Qname(Matcher):

     def __str__(self) -> str:
         return f"QNAME in [{', '.join(str(name) for name in self.qnames)}]"
+
+
+class Qtype(Matcher):
+    """
+    Match queries whose QTYPE is one of the given types.
+    """
+
+    def __init__(self, *qtypes: dns.rdatatype.RdataType) -> None:
+        self.qtypes = qtypes
+
+    def match(self, qctx: QueryContext) -> bool:
+        return qctx.qtype in self.qtypes
+
+    def __str__(self) -> str:
+        return f"QTYPE in [{', '.join(map(dns.rdatatype.to_text, self.qtypes))}]"
diff --git a/bin/tests/system/ixfr/ans2/ans.py b/bin/tests/system/ixfr/ans2/ans.py
index a1234f91e9..64e3ad8182 100644
--- a/bin/tests/system/ixfr/ans2/ans.py
+++ b/bin/tests/system/ixfr/ans2/ans.py
@@ -26,6 +26,7 @@ from isctest.asyncserver import (
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.commands import SwitchControlCommand
 from isctest.asyncserver.handlers import AxfrHandler
+from isctest.asyncserver.matchers import Qtype


 def rrset(owner: str, rdtype: dns.rdatatype.RdataType, rdata: str) -> dns.rrset.RRset:
@@ -74,8 +75,7 @@ class SoaHandler(ResponseHandler):
     def __init__(self, serial: int):
         self._serial = serial

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.SOA
+    matcher = Qtype(dns.rdatatype.SOA)

     async def get_responses(
         self, qctx: QueryContext
@@ -94,8 +94,7 @@ class IxfrHandler(ResponseHandler):
         """
         raise NotImplementedError

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.IXFR
+    matcher = Qtype(dns.rdatatype.IXFR)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/nsec3/ans7/ans.py b/bin/tests/system/nsec3/ans7/ans.py
index 08ba3e4b22..f6227a3445 100644
--- a/bin/tests/system/nsec3/ans7/ans.py
+++ b/bin/tests/system/nsec3/ans7/ans.py
@@ -34,6 +34,7 @@ import dns.zone
 from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseAction
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import ForwarderHandler, ResponseHandlerWrapper
+from isctest.asyncserver.matchers import Always, Qtype
 from isctest.zone import FileZoneKey

 # The malicious proxy relays every query to the real signed authoritative
@@ -183,10 +184,7 @@ class RelayForwarder(ForwarderHandler):

     def __init__(self, *qtypes: dns.rdatatype.RdataType) -> None:
         super().__init__()
-        self._qtypes = qtypes
-
-    def match(self, qctx: QueryContext) -> bool:
-        return not self._qtypes or qctx.qtype in self._qtypes
+        self.matcher = Qtype(*qtypes) if qtypes else Always()


 class DsInjector(ResponseHandlerWrapper):
diff --git a/bin/tests/system/nsec3_impersonation/ans1/ans.py b/bin/tests/system/nsec3_impersonation/ans1/ans.py
index 6cd2986368..d0b42bd6bb 100644
--- a/bin/tests/system/nsec3_impersonation/ans1/ans.py
+++ b/bin/tests/system/nsec3_impersonation/ans1/ans.py
@@ -29,6 +29,7 @@ import dns.rrset

 from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
+from isctest.asyncserver.matchers import Qname, Qtype

 TTL = 300
 TLD = "tld.test."
@@ -176,8 +177,7 @@ class VictimForgedNxdomainHandler(SignedResponseHandler):
     This serves the forged response for the victim's domain.
     """

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A
+    matcher = Qname(VICTIM) & Qtype(dns.rdatatype.A)

     def respond(self, qctx: QueryContext) -> None:
         forged_nxdomain(qctx.response, self.keys)
@@ -189,8 +189,7 @@ class ChildDsHandler(SignedResponseHandler):
     It is actually a validly signed DS response.
     """

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS
+    matcher = Qname(ATTACKER) & Qtype(dns.rdatatype.DS)

     def respond(self, qctx: QueryContext) -> None:
         response = qctx.response
diff --git a/bin/tests/system/rpzrecurse/ans5/ans.py b/bin/tests/system/rpzrecurse/ans5/ans.py
index bc1b2a9f9c..4d4968d442 100644
--- a/bin/tests/system/rpzrecurse/ans5/ans.py
+++ b/bin/tests/system/rpzrecurse/ans5/ans.py
@@ -17,11 +17,11 @@ import dns.rrset

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


 class ReplyA(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.A
+    matcher = Qtype(dns.rdatatype.A)

     async def get_responses(
         self, qctx: QueryContext
@@ -34,8 +34,7 @@ class ReplyA(ResponseHandler):


 class DelayNs(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.NS
+    matcher = Qtype(dns.rdatatype.NS)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/rpzrecurse/ans6/ans.py b/bin/tests/system/rpzrecurse/ans6/ans.py
index 434ee2bc22..1ecdf74a5e 100644
--- a/bin/tests/system/rpzrecurse/ans6/ans.py
+++ b/bin/tests/system/rpzrecurse/ans6/ans.py
@@ -17,11 +17,11 @@ import dns.rrset

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


 class ReplyA(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.A
+    matcher = Qtype(dns.rdatatype.A)

     async def get_responses(
         self, qctx: QueryContext
@@ -34,8 +34,7 @@ class ReplyA(ResponseHandler):


 class DelayNs(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.NS
+    matcher = Qtype(dns.rdatatype.NS)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/serve_stale/serve_stale_ans.py b/bin/tests/system/serve_stale/serve_stale_ans.py
index 983b92df8a..9b96eeac48 100644
--- a/bin/tests/system/serve_stale/serve_stale_ans.py
+++ b/bin/tests/system/serve_stale/serve_stale_ans.py
@@ -17,8 +17,9 @@ import dns.rdataclass
 import dns.rdatatype
 import dns.rrset

-from isctest.asyncserver import QueryContext, ResponseHandler
+from isctest.asyncserver import ResponseHandler
 from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Always, Qname, Qtype


 def rrset(
@@ -82,20 +83,15 @@ class StaticHandler(StaticResponseHandler):
         self._qnames = (
             None if qnames is None else [dns.name.from_text(q) for q in qnames]
         )
-        self._qtypes = qtypes
+        self.matcher = (Qname(*self._qnames) if self._qnames else Always()) & (
+            Qtype(*qtypes) if qtypes else Always()
+        )
         self._answer = list(answer)
         self._authority = list(authority)
         self._additional = list(additional)
         self._rcode = rcode
         self._delay = delay

-    def match(self, qctx: QueryContext) -> bool:
-        if self._qnames is not None and qctx.qname not in self._qnames:
-            return False
-        if self._qtypes is not None and qctx.qtype not in self._qtypes:
-            return False
-        return True
-
     @property
     def answer(self) -> Sequence[dns.rrset.RRset]:
         return self._answer
diff --git a/bin/tests/system/xfer/ans5/ans.py b/bin/tests/system/xfer/ans5/ans.py
index be83dec455..72c383f90d 100644
--- a/bin/tests/system/xfer/ans5/ans.py
+++ b/bin/tests/system/xfer/ans5/ans.py
@@ -29,6 +29,7 @@ from isctest.asyncserver import (
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.commands import SwitchControlCommand
 from isctest.asyncserver.handlers import AxfrHandler, ResponseHandlerWrapper
+from isctest.asyncserver.matchers import Qtype
 from isctest.vars.algorithms import ALG_VARS

 GOOD_KEY_DATA = "LSAnCU+Z"
@@ -184,8 +185,7 @@ class SoaHandler(ResponseHandler):
     def __init__(self, serial: int = 1) -> None:
         self._serial = serial

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.SOA
+    matcher = Qtype(dns.rdatatype.SOA)

     async def get_responses(
         self, qctx: QueryContext
@@ -289,8 +289,7 @@ class WrongQnameInFinalSoa(ResponseHandlerWrapper):


 class IxfrNotimpHandler(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.IXFR
+    matcher = Qtype(dns.rdatatype.IXFR)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/xfer/ans9/ans.py b/bin/tests/system/xfer/ans9/ans.py
index 675d53de4f..dbe4b8b392 100644
--- a/bin/tests/system/xfer/ans9/ans.py
+++ b/bin/tests/system/xfer/ans9/ans.py
@@ -25,6 +25,7 @@ from isctest.asyncserver import (
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.commands import ToggleResponsesCommand
 from isctest.asyncserver.handlers import AxfrHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 TTL = 300

@@ -78,8 +79,7 @@ class SerialCounted(ResponseHandler):


 class SoaHandler(SerialCounted):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.SOA
+    matcher = Qtype(dns.rdatatype.SOA)

     async def get_responses(
         self, qctx: QueryContext
@@ -97,8 +97,9 @@ class ZoneAxfrHandler(AxfrHandler, SerialCounted):

     zone: str

-    def match(self, qctx: QueryContext) -> bool:
-        return super().match(qctx) and qctx.qname == dns.name.from_text(self.zone)
+    def __init__(self, serials: SerialCounter) -> None:
+        super().__init__(serials)
+        self.matcher = Qtype(dns.rdatatype.AXFR) & Qname(self.zone)

     async def get_responses(
         self, qctx: QueryContext