From: Matthijs Mekking Date: Wed, 8 Apr 2026 09:07:57 +0000 (+0200) Subject: Reproducer forwarder resend loop X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4c22fa0d5649be3c8d6791a26599d51845baae73;p=thirdparty%2Fbind9.git Reproducer forwarder resend loop Run malicious server: resend_loop/ans2/ans.py Start BIND: ns1 Send single query to test.com OBSERVED BEHAVIOR The malicious server receives tens of thousands of resend packets within seconds. CPU usage of the named worker thread remains elevated (50–100% of one core) until the default fetch timeout (~10 seconds) terminates the request. Instrumentation during testing confirmed that isc_counter_used(fctx->qc) remains constant (value 1) throughout the entire resend loop. --- diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 17ff396f113..57c3ffafeb0 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -22,6 +22,7 @@ from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, DomainHandler, + QnameHandler, QnameQtypeHandler, QueryContext, StaticResponseHandler, @@ -78,12 +79,19 @@ class ExampleCookieHandler(DomainHandler): yield DnsResponseSend(qctx.response) +class TestDotComServFailHandler(QnameHandler, StaticResponseHandler): + qnames = ["test.com."] + authoritative = False + rcode = dns.rcode.SERVFAIL + + def main() -> None: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) server.install_response_handlers( RootNsHandler(), ExampleNsHandler(), ExampleCookieHandler(), + TestDotComServFailHandler(), ) server.run() diff --git a/bin/tests/system/resend_loop/ns1/named.conf.j2 b/bin/tests/system/resend_loop/ns1/named.conf.j2 new file mode 100644 index 00000000000..4ef0c35531b --- /dev/null +++ b/bin/tests/system/resend_loop/ns1/named.conf.j2 @@ -0,0 +1,13 @@ +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + recursion yes; + allow-query { any; }; + forwarders { 10.53.0.3 port @PORT@; }; + forward only; +}; diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py index a9a236fa58f..fbc791555b8 100644 --- a/bin/tests/system/resend_loop/tests_resend_loop.py +++ b/bin/tests/system/resend_loop/tests_resend_loop.py @@ -83,3 +83,39 @@ def test_resend_loop_badcookie(ns4): prohibited_log = "query failed (timed out) for test.example/IN/A" assert prohibited_log not in ns4.log + + +def test_resend_loop_forward(ns1): + sending_packet = Re("sending packet from 10.53.0.1#[0-9]+ to 10.53.0.3#[0-9]+") + received_packet = Re("received packet from 10.53.0.3#[0-9]+ to 10.53.0.1#[0-9]+") + + # Make sure the server is done with priming and ./DNSKEY refresh (RFC5011) + msg = dns.message.make_query(".", "NS") + isctest.query.udp(msg, ns1.ip) + query_count_start = len(ns1.log.grep(sending_packet)) + + log_sequence = [ + sending_packet, + "flags: rd; QUESTION: 1", + received_packet, + "opcode: QUERY, status: SERVFAIL", + "flags: qr rd; QUESTION: 1", + sending_packet, + "flags: rd cd; QUESTION: 1", + received_packet, + "opcode: QUERY, status: SERVFAIL", + "flags: qr rd; QUESTION: 1", + ] + + msg = dns.message.make_query("test.com.", "A") + with ns1.watch_log_from_here() as watcher: + res = isctest.query.udp(msg, ns1.ip) + watcher.wait_for_sequence(log_sequence) + + isctest.check.servfail(res) + + query_count_end = len(ns1.log.grep(sending_packet)) + assert query_count_end - query_count_start == 2 + + prohibited_log = "query failed (timed out) for test.com/IN/A" + assert prohibited_log not in ns1.log