From c0f01b60fdf7c01de84ed28d8942bab28650e27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 01/10] Make static response handlers more specific The RootNSHandler and ExampleNSHandler classes are only equipped to respond to specific QNAME/QTYPE tuples, not all queries for a specific QNAME. Turn them into subclasses of QnameQtypeHandler and make them only respond to QTYPE=NS queries to prevent sending NS responses for non-NS queries. --- bin/tests/system/resend_loop/ans3/ans.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index d0cb6d2935..b5a1e75463 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -21,7 +21,7 @@ import dns.rrset from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, - QnameHandler, + QnameQtypeHandler, QueryContext, ResponseHandler, StaticResponseHandler, @@ -53,24 +53,18 @@ def rrset( return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata) -class RootNSHandler(QnameHandler, StaticResponseHandler): +class RootNSHandler(QnameQtypeHandler, StaticResponseHandler): qnames = ["."] - answer = [ - rrset(".", dns.rdatatype.NS, "a.root-servers.nil."), - ] - additional = [ - rrset("a.root-servers.nil.", dns.rdatatype.A, "10.53.0.3"), - ] + qtypes = [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(QnameHandler, StaticResponseHandler): +class ExampleNSHandler(QnameQtypeHandler, StaticResponseHandler): qnames = ["example."] - answer = [ - rrset("example.", dns.rdatatype.NS, "ns.example."), - ] - additional = [ - rrset("ns.example.", dns.rdatatype.A, "10.53.0.3"), - ] + qtypes = [dns.rdatatype.NS] + answer = [rrset("example.", dns.rdatatype.NS, "ns.example.")] + additional = [rrset("ns.example.", dns.rdatatype.A, "10.53.0.3")] class CookieHandler(ResponseHandler): From ba6eee2b80064c459d21bb7a8723bea0a3d208fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 02/10] Simplify match criteria for CookieHandler The CookieHandler class handles all traffic for the "example." domain. Make it a subclass of DomainHandler to simplify its definition. --- bin/tests/system/resend_loop/ans3/ans.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index b5a1e75463..18074bcfb8 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -21,6 +21,7 @@ import dns.rrset from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, + DomainHandler, QnameQtypeHandler, QueryContext, ResponseHandler, @@ -67,10 +68,8 @@ class ExampleNSHandler(QnameQtypeHandler, StaticResponseHandler): additional = [rrset("ns.example.", dns.rdatatype.A, "10.53.0.3")] -class CookieHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - example = dns.name.from_text("example") - return qctx.qname.is_subdomain(example) +class CookieHandler(DomainHandler): + domains = ["example."] async def get_responses( self, qctx: QueryContext From a296bcf587eb78b40e15cff0e8f4f4a2bd6e99e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 03/10] Remove NoErrorHandler The NoErrorHandler class does not get matched to any query sent by ns4 in the "resend_loop" test. Remove it as it is redundant. --- bin/tests/system/resend_loop/ans3/ans.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 18074bcfb8..f8a745bfaa 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -24,7 +24,6 @@ from isctest.asyncserver import ( DomainHandler, QnameQtypeHandler, QueryContext, - ResponseHandler, StaticResponseHandler, ) @@ -92,27 +91,12 @@ class CookieHandler(DomainHandler): yield DnsResponseSend(qctx.response, authoritative=True) -class NoErrorHandler(ResponseHandler): - """ - If the query is NOT a subdomain of example, respond with standard NOERROR empty answer - """ - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - - qctx.prepare_new_response() - qctx.response.set_rcode(dns.rcode.NOERROR) - yield DnsResponseSend(qctx.response, authoritative=True) - - def resend_server() -> AsyncDnsServer: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) server.install_response_handlers( RootNSHandler(), ExampleNSHandler(), CookieHandler(), - NoErrorHandler(), ) return server From 802c03313f99f622b979aa3bb548e3a4eb4340d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 04/10] Drop unnecessary qctx.prepare_new_response() call The ans3 custom server does not have any zones defined, so the responses passed to its handlers by core isctest.asyncserver code are guaranteed to be empty. Remove a call to qctx.prepare_new_response() from CookieHandler.get_responses() as it is redundant. --- bin/tests/system/resend_loop/ans3/ans.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index f8a745bfaa..860a2403ee 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -74,8 +74,6 @@ class CookieHandler(DomainHandler): self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.prepare_new_response() - # Check for client cookie cookie = _get_cookie(qctx) From c61539279d4ecc04f9816b2ae62d63ed8a143c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 05/10] Drop redundant uses of authoritative=True The ans3 custom server instance is created with default_aa=True. Do not pass the authoritative=True keyword argument to the DnsResponseSend constructor in CookieHandler.get_responses() as it is redundant. --- bin/tests/system/resend_loop/ans3/ans.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 860a2403ee..69ea0be6c7 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -80,13 +80,13 @@ class CookieHandler(DomainHandler): # If missing cookie entirely, just return SERVFAIL if cookie is None: qctx.response.set_rcode(dns.rcode.SERVFAIL) - yield DnsResponseSend(qctx.response, authoritative=True) + yield DnsResponseSend(qctx.response) # If there is a client cookie, mock BADCOOKIE to trigger # the resend loop logic. qctx.response.use_edns(options=[cookie]) qctx.response.set_rcode(dns.rcode.BADCOOKIE) - yield DnsResponseSend(qctx.response, authoritative=True) + yield DnsResponseSend(qctx.response) def resend_server() -> AsyncDnsServer: From de42425bbd6f51edf2abc0e57d4d3e3dd2e92159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 06/10] Fix flawed response logic for COOKIE-less queries The "yield" keyword does not cause a function to return. By design, get_responses() may yield multiple DNS responses in a single call. As currently implemented, CookieHandler.get_responses() sends two responses to each client query that does not contain a COOKIE option. Make the logic in that method consistent with code comments by only sending one response to every query - either SERVFAIL or BADCOOKIE, never both. --- bin/tests/system/resend_loop/ans3/ans.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 69ea0be6c7..47275bf377 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -73,21 +73,17 @@ class CookieHandler(DomainHandler): async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: - - # Check for client cookie - cookie = _get_cookie(qctx) - - # If missing cookie entirely, just return SERVFAIL - if cookie is None: + if cookie := _get_cookie(qctx): + # If there is a client cookie, mock BADCOOKIE to trigger + # the resend loop logic. + qctx.response.use_edns(options=[cookie]) + qctx.response.set_rcode(dns.rcode.BADCOOKIE) + yield DnsResponseSend(qctx.response) + else: + # If missing cookie entirely, just return SERVFAIL qctx.response.set_rcode(dns.rcode.SERVFAIL) yield DnsResponseSend(qctx.response) - # If there is a client cookie, mock BADCOOKIE to trigger - # the resend loop logic. - qctx.response.use_edns(options=[cookie]) - qctx.response.set_rcode(dns.rcode.BADCOOKIE) - yield DnsResponseSend(qctx.response) - def resend_server() -> AsyncDnsServer: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) From c9ceb191e8e45c461b8f03e853bef0d9f0eb403f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 07/10] Remove workarounds for dnspython < 2.7.0 dnspython 2.7.0 is now required to run the BIND 9 system test suite. Drop the workarounds for older dnspython versions as they are now redundant. --- bin/tests/system/cookie/cookie_ans.py | 8 ++------ bin/tests/system/resend_loop/ans3/ans.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/bin/tests/system/cookie/cookie_ans.py b/bin/tests/system/cookie/cookie_ans.py index 3b6f0406f2..59f3d1fdf5 100644 --- a/bin/tests/system/cookie/cookie_ans.py +++ b/bin/tests/system/cookie/cookie_ans.py @@ -44,12 +44,8 @@ def _add_cookie(qctx: QueryContext) -> None: for o in qctx.query.options: if o.otype == dns.edns.OptionType.COOKIE: cookie = o - try: - if len(cookie.server) == 0: - cookie.server = cookie.client - except AttributeError: # dnspython<2.7.0 compat - if len(o.data) == 8: - cookie.data *= 2 + if len(cookie.server) == 0: + cookie.server = cookie.client qctx.response.use_edns(options=[cookie]) return diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 47275bf377..2b8cf4d018 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -32,12 +32,8 @@ def _get_cookie(qctx: QueryContext): for o in qctx.query.options: if o.otype == dns.edns.OptionType.COOKIE: cookie = o - try: - if len(cookie.server) == 0: - cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" - except AttributeError: # dnspython<2.7.0 compat - if len(o.data) == 8: - cookie.data *= 2 + if len(cookie.server) == 0: + cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" return cookie From 5fa2bd7e53e1d6ee6ebcc04b0bf5f303d3e85570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 08/10] Tweak the _get_cookie() method The "len(cookie.server) == 0" condition is superfluous for the "resend_loop" system test, so remove it. Add a return type annotation to the _get_cookie() function. --- bin/tests/system/resend_loop/ans3/ans.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 2b8cf4d018..9dcdec83b5 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -28,13 +28,11 @@ from isctest.asyncserver import ( ) -def _get_cookie(qctx: QueryContext): +def _get_cookie(qctx: QueryContext) -> dns.edns.CookieOption | None: for o in qctx.query.options: if o.otype == dns.edns.OptionType.COOKIE: cookie = o - if len(cookie.server) == 0: - cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" - + cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" return cookie return None From c3839e830cfa5a8cd3ef4bdd3e5db7c0c0ee01dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 09/10] Turn _get_cookie() into a method Since the _get_cookie() function is only used by the CookieHandler class, make the former a method of the latter to keep related logic close in the source code. --- bin/tests/system/resend_loop/ans3/ans.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 9dcdec83b5..37ed3482f0 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -28,16 +28,6 @@ from isctest.asyncserver import ( ) -def _get_cookie(qctx: QueryContext) -> dns.edns.CookieOption | None: - for o in qctx.query.options: - if o.otype == dns.edns.OptionType.COOKIE: - cookie = o - cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" - return cookie - - return None - - def rrset( qname: dns.name.Name | str, rtype: dns.rdatatype.RdataType, @@ -64,10 +54,19 @@ class ExampleNSHandler(QnameQtypeHandler, StaticResponseHandler): class CookieHandler(DomainHandler): domains = ["example."] + def _get_cookie(self, qctx: QueryContext) -> dns.edns.CookieOption | None: + for o in qctx.query.options: + if o.otype == dns.edns.OptionType.COOKIE: + cookie = o + cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" + return cookie + + return None + async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: - if cookie := _get_cookie(qctx): + if cookie := self._get_cookie(qctx): # If there is a client cookie, mock BADCOOKIE to trigger # the resend loop logic. qctx.response.use_edns(options=[cookie]) From c5a30a722098f23c1fd3a7cd53de4d5164941dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 11:52:56 +0200 Subject: [PATCH 10/10] Follow common naming and coding conventions Make the handlers defined in bin/tests/system/resend_loop/ans3/ans.py follow canonical naming conventions used in other system tests. Keep all server initialization code in the main() function. --- bin/tests/system/resend_loop/ans3/ans.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 37ed3482f0..17ff396f11 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -37,21 +37,21 @@ def rrset( return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata) -class RootNSHandler(QnameQtypeHandler, StaticResponseHandler): +class RootNsHandler(QnameQtypeHandler, StaticResponseHandler): qnames = ["."] qtypes = [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): +class ExampleNsHandler(QnameQtypeHandler, StaticResponseHandler): qnames = ["example."] qtypes = [dns.rdatatype.NS] answer = [rrset("example.", dns.rdatatype.NS, "ns.example.")] additional = [rrset("ns.example.", dns.rdatatype.A, "10.53.0.3")] -class CookieHandler(DomainHandler): +class ExampleCookieHandler(DomainHandler): domains = ["example."] def _get_cookie(self, qctx: QueryContext) -> dns.edns.CookieOption | None: @@ -78,18 +78,14 @@ class CookieHandler(DomainHandler): yield DnsResponseSend(qctx.response) -def resend_server() -> AsyncDnsServer: +def main() -> None: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) server.install_response_handlers( - RootNSHandler(), - ExampleNSHandler(), - CookieHandler(), + RootNsHandler(), + ExampleNsHandler(), + ExampleCookieHandler(), ) - return server - - -def main() -> None: - resend_server().run() + server.run() if __name__ == "__main__":