]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Reproducer forwarder resend loop
authorMatthijs Mekking <matthijs@isc.org>
Wed, 8 Apr 2026 09:07:57 +0000 (11:07 +0200)
committerColin Vidal <colin@isc.org>
Thu, 18 Jun 2026 06:49:23 +0000 (08:49 +0200)
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.

bin/tests/system/resend_loop/ans3/ans.py
bin/tests/system/resend_loop/ns1/named.conf.j2 [new file with mode: 0644]
bin/tests/system/resend_loop/tests_resend_loop.py

index 17ff396f11365600aa2968e1739bf0401333a270..57c3ffafeb0333abbd666c37011e601732670dcf 100644 (file)
@@ -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 (file)
index 0000000..4ef0c35
--- /dev/null
@@ -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;
+};
index a9a236fa58fad792fda16cf7b5c550963d3a1a35..fbc791555b8ad43be890fcc8127808ee01740630 100644 (file)
@@ -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