Commit 4f8bf0f0af for bind

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

    Match on the transport protocol with a matcher

    Protocol declares the transport a handler answers on, which was
    previously done with a match() method.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/bailiwick/bailiwick_ans.py b/bin/tests/system/bailiwick/bailiwick_ans.py
index ce08119fef..6c1a38e0ae 100644
--- a/bin/tests/system/bailiwick/bailiwick_ans.py
+++ b/bin/tests/system/bailiwick/bailiwick_ans.py
@@ -11,7 +11,6 @@

 import abc

-import dns.name
 import dns.rcode
 import dns.rdatatype

@@ -22,6 +21,7 @@ from isctest.asyncserver import (
     QueryContext,
     ResponseHandler,
 )
+from isctest.asyncserver.matchers import Protocol, Qname, Qtype


 class ResponseSpoofer(ResponseHandler, abc.ABC):
@@ -44,11 +44,9 @@ class ResponseSpoofer(ResponseHandler, abc.ABC):
     def qname(self) -> str:
         raise NotImplementedError

-    def match(self, qctx: QueryContext) -> bool:
-        return (
-            qctx.qname == dns.name.from_text(self.qname)
-            and qctx.qtype == dns.rdatatype.TXT
-            and qctx.protocol == DnsProtocol.UDP
+    def __init__(self) -> None:
+        self.matcher = (
+            Qname(self.qname) & Qtype(dns.rdatatype.TXT) & Protocol(DnsProtocol.UDP)
         )


diff --git a/bin/tests/system/cookie/cookie_ans.py b/bin/tests/system/cookie/cookie_ans.py
index e1326d1466..063ee175f7 100644
--- a/bin/tests/system/cookie/cookie_ans.py
+++ b/bin/tests/system/cookie/cookie_ans.py
@@ -25,6 +25,7 @@ from isctest.asyncserver import (
     ResponseHandler,
 )
 from isctest.asyncserver.actions import DnsResponseSend
+from isctest.asyncserver.matchers import Protocol, Qtype
 from isctest.name import prepend_label
 from isctest.vars.algorithms import ALG_VARS

@@ -122,8 +123,7 @@ class GlueHandler(_SpoofableHandler):


 class TcpAHandler(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.A and qctx.protocol == DnsProtocol.TCP
+    matcher = Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.TCP)

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


 class UdpAHandler(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.A and qctx.protocol == DnsProtocol.UDP
+    matcher = Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.UDP)

     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 bc56dc6255..41a6d49305 100644
--- a/bin/tests/system/isctest/asyncserver/matchers.py
+++ b/bin/tests/system/isctest/asyncserver/matchers.py
@@ -17,7 +17,7 @@ import abc
 import dns.name
 import dns.rdatatype

-from .context import QueryContext
+from .context import DnsProtocol, QueryContext

 M = TypeVar("M", bound="Matcher")

@@ -204,3 +204,18 @@ class Domain(Matcher):

     def __str__(self) -> str:
         return f"QNAME under [{', '.join(str(name) for name in self._domains)}]"
+
+
+class Protocol(Matcher):
+    """
+    Match queries received over the given transport protocol.
+    """
+
+    def __init__(self, protocol: DnsProtocol) -> None:
+        self._protocol = protocol
+
+    def match(self, qctx: QueryContext) -> bool:
+        return qctx.protocol == self._protocol
+
+    def __str__(self) -> str:
+        return f"over {self._protocol.name}"
diff --git a/bin/tests/system/mismatchtcp/ans2/ans.py b/bin/tests/system/mismatchtcp/ans2/ans.py
index d80a0dfc51..ebb30a350d 100644
--- a/bin/tests/system/mismatchtcp/ans2/ans.py
+++ b/bin/tests/system/mismatchtcp/ans2/ans.py
@@ -11,7 +11,6 @@

 from collections.abc import AsyncGenerator

-import dns.name
 import dns.rdatatype

 from isctest.asyncserver import (
@@ -21,6 +20,7 @@ from isctest.asyncserver import (
     ResponseHandler,
 )
 from isctest.asyncserver.actions import DnsResponseSend
+from isctest.asyncserver.matchers import Protocol, Qname, Qtype


 class MismatchedIdOnUdpHandler(ResponseHandler):
@@ -33,12 +33,9 @@ class MismatchedIdOnUdpHandler(ResponseHandler):
     the correct answer.
     """

-    def match(self, qctx: QueryContext) -> bool:
-        return (
-            qctx.qname == dns.name.from_text("trigger.example.")
-            and qctx.qtype == dns.rdatatype.A
-            and qctx.protocol == DnsProtocol.UDP
-        )
+    matcher = (
+        Qname("trigger.example.") & Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.UDP)
+    )

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/resolver/ans8/ans.py b/bin/tests/system/resolver/ans8/ans.py
index 00ddfcdf91..bea20ff04c 100644
--- a/bin/tests/system/resolver/ans8/ans.py
+++ b/bin/tests/system/resolver/ans8/ans.py
@@ -15,7 +15,6 @@ import abc

 import dns.flags
 import dns.message
-import dns.name
 import dns.rcode
 import dns.rdatatype

@@ -27,7 +26,7 @@ from isctest.asyncserver import (
 )
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import StaticResponseHandler
-from isctest.asyncserver.matchers import Domain, Qname, Qtype
+from isctest.asyncserver.matchers import Domain, Protocol, Qname, Qtype

 from ..resolver_ans import rrset

@@ -57,18 +56,13 @@ class HeaderOnlyHandler(ResponseHandler):


 class RefusedOnTcpHandler(HeaderOnlyHandler):
+    matcher = Qname("tcpalso.no-questions.") & Protocol(DnsProtocol.TCP)
     flags = dns.flags.QR
     rcode = dns.rcode.REFUSED

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.protocol == DnsProtocol.TCP and qctx.qname == dns.name.from_text(
-            "tcpalso.no-questions."
-        )
-

 class TcpFallbackHandler(ResponseHandler):
-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.protocol == DnsProtocol.TCP
+    matcher = Protocol(DnsProtocol.TCP)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/xfer/ans11/ans.py b/bin/tests/system/xfer/ans11/ans.py
index 41d9009bac..c2282af8ed 100644
--- a/bin/tests/system/xfer/ans11/ans.py
+++ b/bin/tests/system/xfer/ans11/ans.py
@@ -25,6 +25,7 @@ from isctest.asyncserver import (
 )
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import AxfrHandler, StaticResponseHandler
+from isctest.asyncserver.matchers import Protocol, Qtype

 ZONE = "ixfr-race."
 NS_NAME = f"ns.{ZONE}"
@@ -144,8 +145,7 @@ class TruncatedIxfrHandler(ResponseHandler):
     Set TC on an IXFR received over UDP to force the secondary to retry over TCP.
     """

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.IXFR and qctx.protocol == DnsProtocol.UDP
+    matcher = Qtype(dns.rdatatype.IXFR) & Protocol(DnsProtocol.UDP)

     async def get_responses(
         self, qctx: QueryContext
@@ -165,8 +165,7 @@ class RaceIxfrHandler(ResponseHandler):
     then has to detach the queued second chunk before freeing it.
     """

-    def match(self, qctx: QueryContext) -> bool:
-        return qctx.qtype == dns.rdatatype.IXFR and qctx.protocol == DnsProtocol.TCP
+    matcher = Qtype(dns.rdatatype.IXFR) & Protocol(DnsProtocol.TCP)

     async def get_responses(
         self, qctx: QueryContext