Commit c469d23734 for bind

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

    Replace QnameQtypeHandler with a matcher

    QnameQtypeHandler existed only to match on a QNAME and a QTYPE; its
    users now declare `Qname(...) & Qtype(...)` themselves.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/cyclic_glue/ans5/ans.py b/bin/tests/system/cyclic_glue/ans5/ans.py
index 0d5867ef92..86d7d0e90e 100644
--- a/bin/tests/system/cyclic_glue/ans5/ans.py
+++ b/bin/tests/system/cyclic_glue/ans5/ans.py
@@ -16,7 +16,8 @@ import dns.rdatatype
 import dns.rrset

 from isctest.asyncserver import AsyncDnsServer
-from isctest.asyncserver.handlers import QnameQtypeHandler, StaticResponseHandler
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype


 def rrset(
@@ -36,9 +37,8 @@ def a(owner: str, address: str) -> dns.rrset.RRset:
     return rrset(owner, dns.rdatatype.A, address)


-class BrokenFooHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["a.foo.test."]
-    qtypes = [dns.rdatatype.A]
+class BrokenFooHandler(StaticResponseHandler):
+    matcher = Qname("a.foo.test.") & Qtype(dns.rdatatype.A)
     authority = [
         ns("foo.test.", "ns.foo.test."),
         ns("foo.test.", "ns.bar.test."),
@@ -55,9 +55,8 @@ class BrokenFooHandler(QnameQtypeHandler, StaticResponseHandler):
     ]


-class BrokenBarHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["a.bar.test."]
-    qtypes = [dns.rdatatype.A]
+class BrokenBarHandler(StaticResponseHandler):
+    matcher = Qname("a.bar.test.") & Qtype(dns.rdatatype.A)
     authority = [
         ns("bar.test.", "ns.bar.test."),
         # This NS is valid but outside the bar.test domain.
diff --git a/bin/tests/system/dnssec_wildcard/ans1/ans.py b/bin/tests/system/dnssec_wildcard/ans1/ans.py
index 843d1935dd..e9f362880c 100644
--- a/bin/tests/system/dnssec_wildcard/ans1/ans.py
+++ b/bin/tests/system/dnssec_wildcard/ans1/ans.py
@@ -15,9 +15,9 @@ import dns.name
 import dns.rdatatype
 import dns.rrset

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


 def append_forged_a_rrset_to_additional(qctx: QueryContext, owner_name: str) -> None:
@@ -53,9 +53,8 @@ def append_forged_a_rrset_to_additional(qctx: QueryContext, owner_name: str) ->
     # from a wildcard record.


-class WildcardAdditionalHandler(QnameQtypeHandler):
-    qnames = ["svc.f043.test."]
-    qtypes = [dns.rdatatype.MX]
+class WildcardAdditionalHandler(ResponseHandler):
+    matcher = Qname("svc.f043.test.") & Qtype(dns.rdatatype.MX)

     async def get_responses(
         self, qctx: QueryContext
@@ -64,9 +63,8 @@ class WildcardAdditionalHandler(QnameQtypeHandler):
         yield DnsResponseSend(qctx.response)


-class ParentWildcardHandler(QnameQtypeHandler):
-    qnames = ["q.f045.test."]
-    qtypes = [dns.rdatatype.MX]
+class ParentWildcardHandler(ResponseHandler):
+    matcher = Qname("q.f045.test.") & Qtype(dns.rdatatype.MX)

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/filters/ans6/ans.py b/bin/tests/system/filters/ans6/ans.py
index f2cacb3171..ef77810bfe 100644
--- a/bin/tests/system/filters/ans6/ans.py
+++ b/bin/tests/system/filters/ans6/ans.py
@@ -15,7 +15,8 @@ import dns.rdatatype
 import dns.rrset

 from isctest.asyncserver import AsyncDnsServer, QueryContext
-from isctest.asyncserver.handlers import QnameQtypeHandler, StaticResponseHandler
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 DNS64_TRIGGER = "nodata.test."

@@ -36,15 +37,14 @@ def aaaa() -> dns.rrset.RRset:
     return rrset(DNS64_TRIGGER, dns.rdatatype.AAAA, "2001:db8::1")


-class NodataOnceHandler(QnameQtypeHandler, StaticResponseHandler):
+class NodataOnceHandler(StaticResponseHandler):
     """
     Answer only the first AAAA query, with NODATA, so the DNS64 resolver looks
     up an A record; the delayed A lookup lets filter-a re-enter and re-query
     AAAA (answered by AaaaHandler), triggering the bug.
     """

-    qnames = [DNS64_TRIGGER]
-    qtypes = [dns.rdatatype.AAAA]
+    matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA)
     authority = [soa()]

     def __init__(self) -> None:
@@ -58,15 +58,13 @@ class NodataOnceHandler(QnameQtypeHandler, StaticResponseHandler):
         return super().match(qctx)


-class AaaaHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [DNS64_TRIGGER]
-    qtypes = [dns.rdatatype.AAAA]
+class AaaaHandler(StaticResponseHandler):
+    matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA)
     answer = [aaaa()]


-class DelayedAHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [DNS64_TRIGGER]
-    qtypes = [dns.rdatatype.A]
+class DelayedAHandler(StaticResponseHandler):
+    matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.A)
     answer = [a()]
     delay = 2.0

diff --git a/bin/tests/system/forward/ans6/ans.py b/bin/tests/system/forward/ans6/ans.py
index cdff896a69..89c08d0018 100644
--- a/bin/tests/system/forward/ans6/ans.py
+++ b/bin/tests/system/forward/ans6/ans.py
@@ -24,11 +24,8 @@ from isctest.asyncserver import (
 )
 from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.commands import ToggleResponsesCommand
-from isctest.asyncserver.handlers import (
-    DomainHandler,
-    QnameQtypeHandler,
-    StaticResponseHandler,
-)
+from isctest.asyncserver.handlers import DomainHandler, StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 SLD = "sld.tld."
 NS1 = f"ns1.{SLD}"
@@ -56,16 +53,14 @@ def soa(owner: dns.name.Name | str) -> dns.rrset.RRset:
     return rrset(owner, dns.rdatatype.SOA, ". . 0 0 0 0 0")


-class Ns1AHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [NS1]
-    qtypes = [dns.rdatatype.A]
+class Ns1AHandler(StaticResponseHandler):
+    matcher = Qname(NS1) & Qtype(dns.rdatatype.A)
     answer = [a(NS1)]
     edns = None


-class Ns1AaaaHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [NS1]
-    qtypes = [dns.rdatatype.AAAA]
+class Ns1AaaaHandler(StaticResponseHandler):
+    matcher = Qname(NS1) & Qtype(dns.rdatatype.AAAA)
     answer = [aaaa(NS1)]
     edns = None

diff --git a/bin/tests/system/fwdfirst/ans3/ans.py b/bin/tests/system/fwdfirst/ans3/ans.py
index da2624995f..ad4ee7f321 100644
--- a/bin/tests/system/fwdfirst/ans3/ans.py
+++ b/bin/tests/system/fwdfirst/ans3/ans.py
@@ -15,7 +15,8 @@ import dns.rdatatype
 import dns.rrset

 from isctest.asyncserver import AsyncDnsServer
-from isctest.asyncserver.handlers import QnameQtypeHandler, StaticResponseHandler
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 VICTIM = "victim.sibling.hack."
 POISON_ADDRESS = "6.6.6.6"
@@ -27,9 +28,8 @@ def a(name: str) -> dns.rrset.RRset:
     )


-class PoisonedAHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [VICTIM]
-    qtypes = [dns.rdatatype.A]
+class PoisonedAHandler(StaticResponseHandler):
+    matcher = Qname(VICTIM) & Qtype(dns.rdatatype.A)
     answer = [a(VICTIM)]


diff --git a/bin/tests/system/isctest/asyncserver/handlers.py b/bin/tests/system/isctest/asyncserver/handlers.py
index 559790e7b1..9f6f18a0d3 100644
--- a/bin/tests/system/isctest/asyncserver/handlers.py
+++ b/bin/tests/system/isctest/asyncserver/handlers.py
@@ -188,45 +188,6 @@ class IgnoreAllQueries(ResponseHandler):
         yield ResponseDrop()


-class QnameQtypeHandler(ResponseHandler):
-    """
-    Handle queries for which both of the following conditions are true:
-
-    - the query's QNAME is present in `self.qnames`,
-    - the query's QTYPE is present in `self.qtypes`.
-    """
-
-    @property
-    @abc.abstractmethod
-    def qnames(self) -> list[str]:
-        """
-        A list of QNAMEs handled by this class.
-        """
-        raise NotImplementedError
-
-    @property
-    @abc.abstractmethod
-    def qtypes(self) -> list[dns.rdatatype.RdataType]:
-        """
-        A list of QTYPEs handled by this class.
-        """
-        raise NotImplementedError
-
-    def __init__(self) -> None:
-        self._qnames: list[dns.name.Name] = [dns.name.from_text(d) for d in self.qnames]
-        self._qtypes: list[dns.rdatatype.RdataType] = self.qtypes
-
-    def __str__(self) -> str:
-        return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)}; QTYPEs: {', '.join(map(str, self.qtypes))})"
-
-    def match(self, qctx: QueryContext) -> bool:
-        """
-        Handle queries whose QNAME and QTYPE match any of the QNAMEs and
-        QTYPEs handled by this class.
-        """
-        return qctx.qtype in self._qtypes and qctx.qname in self._qnames
-
-
 class _UnsetEdnsType:
     pass

diff --git a/bin/tests/system/mismatchtcp/ans2/ans.py b/bin/tests/system/mismatchtcp/ans2/ans.py
index 650b209f7d..d80a0dfc51 100644
--- a/bin/tests/system/mismatchtcp/ans2/ans.py
+++ b/bin/tests/system/mismatchtcp/ans2/ans.py
@@ -11,14 +11,19 @@

 from collections.abc import AsyncGenerator

+import dns.name
 import dns.rdatatype

-from isctest.asyncserver import AsyncDnsServer, DnsProtocol, QueryContext
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    DnsProtocol,
+    QueryContext,
+    ResponseHandler,
+)
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import QnameQtypeHandler


-class MismatchedIdOnUdpHandler(QnameQtypeHandler):
+class MismatchedIdOnUdpHandler(ResponseHandler):
     """
     Simulate Kaminsky-style off-path spoofing: answer every UDP query for
     trigger.example./A with the correct response prepared from zone data,
@@ -28,11 +33,12 @@ class MismatchedIdOnUdpHandler(QnameQtypeHandler):
     the correct answer.
     """

-    qnames = ["trigger.example."]
-    qtypes = [dns.rdatatype.A]
-
     def match(self, qctx: QueryContext) -> bool:
-        return qctx.protocol == DnsProtocol.UDP and super().match(qctx)
+        return (
+            qctx.qname == dns.name.from_text("trigger.example.")
+            and qctx.qtype == dns.rdatatype.A
+            and qctx.protocol == DnsProtocol.UDP
+        )

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/nsupdate/ans11/ans.py b/bin/tests/system/nsupdate/ans11/ans.py
index 114b518cda..13a27573c9 100644
--- a/bin/tests/system/nsupdate/ans11/ans.py
+++ b/bin/tests/system/nsupdate/ans11/ans.py
@@ -16,11 +16,8 @@ import dns.rdatatype
 import dns.rrset

 from isctest.asyncserver import AsyncDnsServer
-from isctest.asyncserver.handlers import (
-    AxfrHandler,
-    QnameQtypeHandler,
-    StaticResponseHandler,
-)
+from isctest.asyncserver.handlers import AxfrHandler, StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 ZONE = "sigaxfr.nil."
 NS_NAME = "ns.sigaxfr.nil."
@@ -60,13 +57,12 @@ def sig() -> dns.rrset.RRset:
     )


-class SoaHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [ZONE]
-    qtypes = [dns.rdatatype.SOA]
+class SoaHandler(StaticResponseHandler):
+    matcher = Qname(ZONE) & Qtype(dns.rdatatype.SOA)
     answer = [soa()]


-class SigAxfrHandler(QnameQtypeHandler, AxfrHandler):
+class SigAxfrHandler(AxfrHandler):
     """
     Serve an AXFR carrying two legacy SIG (24) rdatas at one owner whose
     body "covered type" fields differ (A, MX); per RFC 3755 SIG has no
@@ -74,8 +70,7 @@ class SigAxfrHandler(QnameQtypeHandler, AxfrHandler):
     in a single opaque rdataset.
     """

-    qnames = [ZONE]
-    qtypes = [dns.rdatatype.AXFR]
+    matcher = Qname(ZONE) & Qtype(dns.rdatatype.AXFR)
     initial_soa = soa()
     zone_contents = [
         rrset(ZONE, dns.rdatatype.NS, NS_NAME),
diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py
index 8ace998757..6dc750c644 100644
--- a/bin/tests/system/resend_loop/ans3/ans.py
+++ b/bin/tests/system/resend_loop/ans3/ans.py
@@ -20,12 +20,8 @@ import dns.rrset

 from isctest.asyncserver import AsyncDnsServer, QueryContext
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import (
-    DomainHandler,
-    QnameQtypeHandler,
-    StaticResponseHandler,
-)
-from isctest.asyncserver.matchers import Qname
+from isctest.asyncserver.handlers import DomainHandler, StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype


 def rrset(
@@ -37,16 +33,14 @@ def rrset(
     return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata)


-class RootNsHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["."]
-    qtypes = [dns.rdatatype.NS]
+class RootNsHandler(StaticResponseHandler):
+    matcher = Qname(".") & Qtype(dns.rdatatype.NS)
     answer = [rrset(".", dns.rdatatype.NS, "a.root-servers.nil.")]
     additional = [rrset("a.root-servers.nil.", dns.rdatatype.A, "10.53.0.3")]


-class ExampleNsHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["example."]
-    qtypes = [dns.rdatatype.NS]
+class ExampleNsHandler(StaticResponseHandler):
+    matcher = Qname("example.") & Qtype(dns.rdatatype.NS)
     answer = [rrset("example.", dns.rdatatype.NS, "ns.example.")]
     additional = [rrset("ns.example.", dns.rdatatype.A, "10.53.0.3")]

diff --git a/bin/tests/system/resolver/ans2/ans.py b/bin/tests/system/resolver/ans2/ans.py
index 1421340f61..7f8e446b3f 100644
--- a/bin/tests/system/resolver/ans2/ans.py
+++ b/bin/tests/system/resolver/ans2/ans.py
@@ -22,10 +22,9 @@ from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import (
     DomainHandler,
     IgnoreAllQueries,
-    QnameQtypeHandler,
     StaticResponseHandler,
 )
-from isctest.asyncserver.matchers import Qname
+from isctest.asyncserver.matchers import Qname, Qtype

 from ..resolver_ans import (
     DelegationHandler,
@@ -39,12 +38,10 @@ from ..resolver_ans import (
 )


-class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = [
-        "baddname.example.org.",
-        "gooddname.example.org.",
-    ]
-    qtypes = [dns.rdatatype.NS]
+class BadGoodDnameNsHandler(StaticResponseHandler):
+    matcher = Qname("baddname.example.org.", "gooddname.example.org.") & Qtype(
+        dns.rdatatype.NS
+    )
     answer = [rrset("example.org.", dns.rdatatype.NS, "a.root-servers.nil.")]
     authoritative = True

@@ -98,16 +95,10 @@ class NoResponseExampleUdpHandler(IgnoreAllQueries):
     matcher = Qname("noresponse.exampleudp.net.")


-class RootNsHandler(QnameQtypeHandler):
-    qnames = [
-        "example.com.",
-        "com.",
-        "example.org.",
-        "org.",
-        "net.",
-    ]
-
-    qtypes = [dns.rdatatype.NS]
+class RootNsHandler(ResponseHandler):
+    matcher = Qname("example.com.", "com.", "example.org.", "org.", "net.") & Qtype(
+        dns.rdatatype.NS
+    )

     async def get_responses(
         self, qctx: QueryContext
diff --git a/bin/tests/system/resolver/ans3/ans.py b/bin/tests/system/resolver/ans3/ans.py
index a819b6c1f6..e53ac71796 100644
--- a/bin/tests/system/resolver/ans3/ans.py
+++ b/bin/tests/system/resolver/ans3/ans.py
@@ -20,10 +20,9 @@ from isctest.asyncserver.actions import DnsResponseSend
 from isctest.asyncserver.handlers import (
     DomainHandler,
     IgnoreAllQueries,
-    QnameQtypeHandler,
     StaticResponseHandler,
 )
-from isctest.asyncserver.matchers import Qname
+from isctest.asyncserver.matchers import Qname, Qtype

 from ..resolver_ans import (
     DelegationHandler,
@@ -37,11 +36,18 @@ from ..resolver_ans import (
 )


-class ApexNSHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["example.net."]
-    qtypes = [dns.rdatatype.NS]
-    answer = [rrset(qnames[0], dns.rdatatype.NS, f"ns.{qnames[0]}")]
-    additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.3")]
+class ApexNSHandler(StaticResponseHandler):
+    matcher = Qname("example.net.") & Qtype(dns.rdatatype.NS)
+    answer = [
+        rrset(
+            matcher.of(Qname).qnames[0],
+            dns.rdatatype.NS,
+            f"ns.{matcher.of(Qname).qnames[0]}",
+        )
+    ]
+    additional = [
+        rrset(f"ns.{matcher.of(Qname).qnames[0]}", dns.rdatatype.A, "10.53.0.3")
+    ]


 class AttackDnameHandler(StaticResponseHandler):
@@ -54,9 +60,10 @@ class BadCnameHandler(StaticResponseHandler):
     answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")]


-class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["baddname.example.net.", "gooddname.example.net."]
-    qtypes = [dns.rdatatype.NS]
+class BadGoodDnameNsHandler(StaticResponseHandler):
+    matcher = Qname("baddname.example.net.", "gooddname.example.net.") & Qtype(
+        dns.rdatatype.NS
+    )
     authority = [soa_rrset("example.net.")]


@@ -65,10 +72,11 @@ class CnameSubHandler(StaticResponseHandler):
     answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")]


-class ExampleOrgHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["example.org."]
-    qtypes = [dns.rdatatype.A]
-    answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")]
+class ExampleOrgHandler(StaticResponseHandler):
+    matcher = Qname("example.org.") & Qtype(dns.rdatatype.A)
+    answer = [
+        rrset(matcher.of(Qname).qnames[0], matcher.of(Qtype).qtypes[0], "1.2.3.4")
+    ]


 class FooBadDnameHandler(StaticResponseHandler):
@@ -100,10 +108,11 @@ class GoodCnameHandler(StaticResponseHandler):
     answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")]


-class IscHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["isc.org."]
-    qtypes = [dns.rdatatype.A]
-    answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")]
+class IscHandler(StaticResponseHandler):
+    matcher = Qname("isc.org.") & Qtype(dns.rdatatype.A)
+    answer = [
+        rrset(matcher.of(Qname).qnames[0], matcher.of(Qtype).qtypes[0], "1.2.3.4")
+    ]


 class LameExampleOrgDelegation(DelegationHandler):
@@ -111,12 +120,11 @@ class LameExampleOrgDelegation(DelegationHandler):
     server_number = 3


-class LargeReferralHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["large-referral.example.net."]
-    qtypes = [dns.rdatatype.NS]
+class LargeReferralHandler(StaticResponseHandler):
+    matcher = Qname("large-referral.example.net.") & Qtype(dns.rdatatype.NS)
     authority = [
         rrset_from_list(
-            qnames[0],
+            matcher.of(Qname).qnames[0],
             dns.rdatatype.NS,
             [f"ns{i}.fake.redirect.com." for i in range(1, 1000)],
         )
diff --git a/bin/tests/system/resolver/ans8/ans.py b/bin/tests/system/resolver/ans8/ans.py
index d11cfc7af6..b5506aedca 100644
--- a/bin/tests/system/resolver/ans8/ans.py
+++ b/bin/tests/system/resolver/ans8/ans.py
@@ -26,12 +26,8 @@ from isctest.asyncserver import (
     ResponseHandler,
 )
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import (
-    DomainHandler,
-    QnameQtypeHandler,
-    StaticResponseHandler,
-)
-from isctest.asyncserver.matchers import Qname
+from isctest.asyncserver.handlers import DomainHandler, StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 from ..resolver_ans import rrset

@@ -86,11 +82,18 @@ class FormerrToAllHandler(DomainHandler, StaticResponseHandler):
     rcode = dns.rcode.FORMERR


-class NoQuestionsNSHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["no-questions."]
-    qtypes = [dns.rdatatype.NS]
-    answer = [rrset(qnames[0], dns.rdatatype.NS, f"ns.{qnames[0]}")]
-    additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.8")]
+class NoQuestionsNSHandler(StaticResponseHandler):
+    matcher = Qname("no-questions.") & Qtype(dns.rdatatype.NS)
+    answer = [
+        rrset(
+            matcher.of(Qname).qnames[0],
+            dns.rdatatype.NS,
+            f"ns.{matcher.of(Qname).qnames[0]}",
+        )
+    ]
+    additional = [
+        rrset(f"ns.{matcher.of(Qname).qnames[0]}", dns.rdatatype.A, "10.53.0.8")
+    ]


 class NsNoQuestionsAHandler(ResponseHandler):
diff --git a/bin/tests/system/serve_stale/ans2/ans.py b/bin/tests/system/serve_stale/ans2/ans.py
index 417fb9944d..e01d93841f 100644
--- a/bin/tests/system/serve_stale/ans2/ans.py
+++ b/bin/tests/system/serve_stale/ans2/ans.py
@@ -14,8 +14,8 @@ import dns.rdatatype

 from isctest.asyncserver import ControllableAsyncDnsServer, ResponseHandler
 from isctest.asyncserver.commands import ToggleResponsesCommand
-from isctest.asyncserver.handlers import QnameQtypeHandler, StaticResponseHandler
-from isctest.asyncserver.matchers import Qname
+from isctest.asyncserver.handlers import StaticResponseHandler
+from isctest.asyncserver.matchers import Qname, Qtype

 from ..serve_stale_ans import (
     a_handler,
@@ -51,9 +51,8 @@ class LongttlNxdomainExampleHandler(StaticResponseHandler):
     authority = [soa("example.", ttl=600, minimum=600)]


-class OthertypeExampleCaaHandler(QnameQtypeHandler, StaticResponseHandler):
-    qnames = ["othertype.example."]
-    qtypes = [dns.rdatatype.CAA]
+class OthertypeExampleCaaHandler(StaticResponseHandler):
+    matcher = Qname("othertype.example.") & Qtype(dns.rdatatype.CAA)
     answer = [
         rrset(
             "othertype.example.",
diff --git a/bin/tests/system/srtt/srtt_ans.py b/bin/tests/system/srtt/srtt_ans.py
index 0433604e64..55f9d88bd1 100644
--- a/bin/tests/system/srtt/srtt_ans.py
+++ b/bin/tests/system/srtt/srtt_ans.py
@@ -17,12 +17,12 @@ import dns.rdataclass
 import dns.rdatatype
 import dns.rrset

-from isctest.asyncserver import QueryContext
+from isctest.asyncserver import QueryContext, ResponseHandler
 from isctest.asyncserver.actions import DnsResponseSend
-from isctest.asyncserver.handlers import QnameQtypeHandler
+from isctest.asyncserver.matchers import Qname, Qtype


-class DelayedQnameRangeHandler(QnameQtypeHandler):
+class DelayedQnameRangeHandler(ResponseHandler):
     """
     Respond to queries for QNAMEs "foo1.example." through "foo<N>.example."
     with QTYPE=A, where <N> must be defined by the subclass.  Every response is
@@ -30,11 +30,10 @@ class DelayedQnameRangeHandler(QnameQtypeHandler):
     by the subclass.
     """

-    @property
-    def qnames(self) -> list[str]:
-        return [f"foo{x}.example." for x in range(1, self.max_qname + 1)]
-
-    qtypes = [dns.rdatatype.A]
+    def __init__(self) -> None:
+        self.matcher = Qname(
+            *(f"foo{x}.example." for x in range(1, self.max_qname + 1))
+        ) & Qtype(dns.rdatatype.A)

     @property
     @abc.abstractmethod