From 7ef692746ae47f01c980cca951d1fd0b1949b157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Thu, 21 May 2026 12:02:20 +0200 Subject: [PATCH 01/10] Simplify ./NS query handling Replace PrimeHandler with a StaticResponseHandler subclass achieving the same goal. (cherry picked from commit c0f01b60fdf7c01de84ed28d8942bab28650e27c) --- bin/tests/system/resend_loop/ans3/ans.py | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 217bae0301..89da4e204a 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -14,14 +14,17 @@ from collections.abc import AsyncGenerator import dns.edns import dns.name import dns.rcode +import dns.rdataclass import dns.rdatatype import dns.rrset from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, + QnameQtypeHandler, QueryContext, ResponseHandler, + StaticResponseHandler, ) @@ -41,31 +44,20 @@ def _get_cookie(qctx: QueryContext): return None -class PrimeHandler(ResponseHandler): - """ - Specifically handle priming query for "." NS (type 2) - """ +def rrset( + qname: dns.name.Name | str, + rtype: dns.rdatatype.RdataType, + rdata: str, + ttl: int = 300, +) -> dns.rrset.RRset: + return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata) - def match(self, qctx: QueryContext) -> bool: - return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - - ns_rrset = dns.rrset.from_text( - ".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil." - ) - a_rrset = dns.rrset.from_text( - "a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3" - ) - - response = qctx.prepare_new_response(with_zone_data=False) - response.set_rcode(dns.rcode.NOERROR) - response.answer.append(ns_rrset) - response.additional.append(a_rrset) - - yield DnsResponseSend(response, authoritative=True) +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 CookieHandler(ResponseHandler): @@ -111,7 +103,7 @@ class NoErrorHandler(ResponseHandler): def resend_server() -> AsyncDnsServer: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) server.install_response_handlers( - PrimeHandler(), + RootNSHandler(), CookieHandler(), NoErrorHandler(), ) From c0e52f4ec29903ba5675cc6b71b5872fd9570978 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. (cherry picked from commit ba6eee2b80064c459d21bb7a8723bea0a3d208fd) --- 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 89da4e204a..a1ee2a85c4 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, @@ -60,10 +61,8 @@ class RootNSHandler(QnameQtypeHandler, StaticResponseHandler): additional = [rrset("a.root-servers.nil.", 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 9c24d5a16d5d252cda8af362fcbe21d7ef33f90f 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. (cherry picked from commit a296bcf587eb78b40e15cff0e8f4f4a2bd6e99e1) --- 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 a1ee2a85c4..9a1ccf97cf 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, ) @@ -85,26 +84,11 @@ 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(), CookieHandler(), - NoErrorHandler(), ) return server From 5b73bbac144b182a7fbeec8a54838e63672e420f 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. (cherry picked from commit 802c03313f99f622b979aa3bb548e3a4eb4340d3) --- 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 9a1ccf97cf..f6543e0eac 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -67,8 +67,6 @@ class CookieHandler(DomainHandler): self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.prepare_new_response() - # Check for client cookie cookie = _get_cookie(qctx) From cfa16e3f247ce35e7d78f3c1c62a9721b7f924eb 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. (cherry picked from commit c61539279d4ecc04f9816b2ae62d63ed8a143c19) --- 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 f6543e0eac..f3c6c9fec1 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -73,13 +73,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 ef0502bcc6173a5c8b1d5d35ab6703566b836868 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. (cherry picked from commit de42425bbd6f51edf2abc0e57d4d3e3dd2e92159) --- 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 f3c6c9fec1..94f986c8e7 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -66,21 +66,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 5a828431f8e9290b539294529a59d640829022f4 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. (cherry picked from commit c9ceb191e8e45c461b8f03e853bef0d9f0eb403f) --- 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 94f986c8e7..1378dd7ea5 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 c42aea399d41e3a236b56591532c76510e23102b 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. (cherry picked from commit 5fa2bd7e53e1d6ee6ebcc04b0bf5f303d3e85570) --- 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 1378dd7ea5..8cd94fbfee 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 0cbf295bcacc946fbb987ac1d70dcdc9ce8ccbb0 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. (cherry picked from commit c3839e830cfa5a8cd3ef4bdd3e5db7c0c0ee01dc) --- 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 8cd94fbfee..ad23c82939 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, @@ -57,10 +47,19 @@ class RootNSHandler(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 553c97834af2489be9136a7f43cefead7fa23945 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. (cherry picked from commit c5a30a722098f23c1fd3a7cd53de4d5164941dcd) --- bin/tests/system/resend_loop/ans3/ans.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index ad23c82939..2423fe1314 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -37,14 +37,14 @@ 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 CookieHandler(DomainHandler): +class ExampleCookieHandler(DomainHandler): domains = ["example."] def _get_cookie(self, qctx: QueryContext) -> dns.edns.CookieOption | None: @@ -71,17 +71,13 @@ 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(), - CookieHandler(), + RootNsHandler(), + ExampleCookieHandler(), ) - return server - - -def main() -> None: - resend_server().run() + server.run() if __name__ == "__main__":