]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add reproducer for BADCOOKIE resend loop
authorMatthijs Mekking <matthijs@isc.org>
Thu, 9 Apr 2026 09:32:07 +0000 (11:32 +0200)
committerMichał Kępień <michal@isc.org>
Thu, 7 May 2026 11:32:15 +0000 (13:32 +0200)
Run malicious server: resend_loop/ans3/ans.py

Start BIND: ns4

Send single query to test.example

The resolver will repeatedly resend queries until the fetch timeout
expires, resulting in resulting in thousands of qrysent while the quota
counter remains 0.

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

diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py
new file mode 100644 (file)
index 0000000..217bae0
--- /dev/null
@@ -0,0 +1,126 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from collections.abc import AsyncGenerator
+
+import dns.edns
+import dns.name
+import dns.rcode
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    DnsResponseSend,
+    QueryContext,
+    ResponseHandler,
+)
+
+
+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
+
+            return cookie
+
+    return None
+
+
+class PrimeHandler(ResponseHandler):
+    """
+    Specifically handle priming query for "." NS (type 2)
+    """
+
+    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 CookieHandler(ResponseHandler):
+    def match(self, qctx: QueryContext) -> bool:
+        example = dns.name.from_text("example")
+        return qctx.qname.is_subdomain(example)
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+
+        qctx.prepare_new_response()
+
+        # Check for client cookie
+        cookie = _get_cookie(qctx)
+
+        # If missing cookie entirely, just return SERVFAIL
+        if cookie is None:
+            qctx.response.set_rcode(dns.rcode.SERVFAIL)
+            yield DnsResponseSend(qctx.response, authoritative=True)
+
+        # 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)
+
+
+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(
+        PrimeHandler(),
+        CookieHandler(),
+        NoErrorHandler(),
+    )
+    return server
+
+
+def main() -> None:
+    resend_server().run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/resend_loop/ns4/named.conf.j2 b/bin/tests/system/resend_loop/ns4/named.conf.j2
new file mode 100644 (file)
index 0000000..360bc12
--- /dev/null
@@ -0,0 +1,16 @@
+options {
+       query-source address 10.53.0.4;
+       notify-source 10.53.0.4;
+       transfer-source 10.53.0.4;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.4; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+};
+
+zone "." IN {
+       type hint;
+       file "root.hint";
+};
diff --git a/bin/tests/system/resend_loop/ns4/root.hint b/bin/tests/system/resend_loop/ns4/root.hint
new file mode 100644 (file)
index 0000000..3889a8b
--- /dev/null
@@ -0,0 +1,14 @@
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0.  If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$TTL 999999
+.                       IN NS  a.root-servers.nil.
+a.root-servers.nil.     IN A   10.53.0.3
diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py
new file mode 100644 (file)
index 0000000..f7ed4d3
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+import dns.message
+
+import isctest
+
+
+def test_resend_loop_badcookie(ns4):
+    expected_log = "exceeded max queries resolving 'test.example/A'"
+
+    msg = dns.message.make_query("test.example", "A")
+    with ns4.watch_log_from_here() as watcher:
+        res = isctest.query.udp(msg, ns4.ip)
+        watcher.wait_for_line(expected_log)
+
+    isctest.check.servfail(res)
+
+    prohibited_log = "query failed (timed out) for test.example/IN/A"
+    assert prohibited_log not in ns4.log