]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add SRTT-based server selection system test
authorColin Vidal <colin@isc.org>
Wed, 4 Mar 2026 17:25:32 +0000 (18:25 +0100)
committerMichał Kępień <michal@isc.org>
Thu, 7 May 2026 11:32:15 +0000 (13:32 +0200)
Verify that the resolver selects authoritative servers in increasing
SRTT order.  Four servers are configured with increasing response
delays.  100 queries are sent, expecting most to go to the fastest
server (ns2).  Then ns2 stops responding, another 100 queries are
sent and should go to ns3 (the next fastest), and so on through
ns4 and ns5.  Each query uses a unique name to avoid cache hits.

bin/tests/system/srtt/README [new file with mode: 0644]
bin/tests/system/srtt/ans2/ans.py [new file with mode: 0644]
bin/tests/system/srtt/ans3/ans.py [new file with mode: 0644]
bin/tests/system/srtt/ans4/ans.py [new file with mode: 0644]
bin/tests/system/srtt/ans5/ans.py [new file with mode: 0644]
bin/tests/system/srtt/ns1/named.conf.j2 [new file with mode: 0644]
bin/tests/system/srtt/ns1/root.db [new file with mode: 0644]
bin/tests/system/srtt/ns6/named.args [new file with mode: 0644]
bin/tests/system/srtt/ns6/named.conf.j2 [new file with mode: 0644]
bin/tests/system/srtt/srtt_ans.py [new file with mode: 0644]
bin/tests/system/srtt/tests_srtt.py [new file with mode: 0644]

diff --git a/bin/tests/system/srtt/README b/bin/tests/system/srtt/README
new file mode 100644 (file)
index 0000000..c86a697
--- /dev/null
@@ -0,0 +1,18 @@
+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.
+
+ns1 is root
+
+ans{2-5} simulates four NS servers making authority on the same domain
+`example.`. ans2 is the quickest to answer, followed by ans3, then ans4, with
+ans5 being the slowest.
+
+ns6 is a resolver
diff --git a/bin/tests/system/srtt/ans2/ans.py b/bin/tests/system/srtt/ans2/ans.py
new file mode 100644 (file)
index 0000000..147a65f
--- /dev/null
@@ -0,0 +1,36 @@
+"""
+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.rcode
+
+from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries
+
+from ..srtt_ans import DelayedQnameRangeHandler
+
+
+class Foo1ToFoo99Handler(DelayedQnameRangeHandler):
+    max_qname = 99
+    delay = 0.0
+
+
+def main() -> None:
+    server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+    server.install_response_handlers(
+        Foo1ToFoo99Handler(),
+        IgnoreAllQueries(),
+    )
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/srtt/ans3/ans.py b/bin/tests/system/srtt/ans3/ans.py
new file mode 100644 (file)
index 0000000..ecd590a
--- /dev/null
@@ -0,0 +1,36 @@
+"""
+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.rcode
+
+from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries
+
+from ..srtt_ans import DelayedQnameRangeHandler
+
+
+class Foo1ToFoo199Handler(DelayedQnameRangeHandler):
+    max_qname = 199
+    delay = 0.03
+
+
+def main() -> None:
+    server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+    server.install_response_handlers(
+        Foo1ToFoo199Handler(),
+        IgnoreAllQueries(),
+    )
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/srtt/ans4/ans.py b/bin/tests/system/srtt/ans4/ans.py
new file mode 100644 (file)
index 0000000..af337c2
--- /dev/null
@@ -0,0 +1,36 @@
+"""
+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.rcode
+
+from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries
+
+from ..srtt_ans import DelayedQnameRangeHandler
+
+
+class Foo1ToFoo299Handler(DelayedQnameRangeHandler):
+    max_qname = 299
+    delay = 0.08
+
+
+def main() -> None:
+    server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+    server.install_response_handlers(
+        Foo1ToFoo299Handler(),
+        IgnoreAllQueries(),
+    )
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/srtt/ans5/ans.py b/bin/tests/system/srtt/ans5/ans.py
new file mode 100644 (file)
index 0000000..8bac83a
--- /dev/null
@@ -0,0 +1,36 @@
+"""
+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.rcode
+
+from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries
+
+from ..srtt_ans import DelayedQnameRangeHandler
+
+
+class Foo1ToFoo399Handler(DelayedQnameRangeHandler):
+    max_qname = 399
+    delay = 0.15
+
+
+def main() -> None:
+    server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+    server.install_response_handlers(
+        Foo1ToFoo399Handler(),
+        IgnoreAllQueries(),
+    )
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/srtt/ns1/named.conf.j2 b/bin/tests/system/srtt/ns1/named.conf.j2
new file mode 100644 (file)
index 0000000..eb079c9
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+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 no;
+       notify yes;
+};
+
+zone "." {
+       type primary;
+       file "root.db";
+};
diff --git a/bin/tests/system/srtt/ns1/root.db b/bin/tests/system/srtt/ns1/root.db
new file mode 100644 (file)
index 0000000..29ecd1d
--- /dev/null
@@ -0,0 +1,36 @@
+; 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 300
+.                      IN SOA  owner.root-servers.nil. a.root-servers.nil. (
+                               2000042100      ; serial
+                               600             ; refresh
+                               600             ; retry
+                               1200            ; expire
+                               600             ; minimum
+                               )
+.                      NS      a.root-servers.nil.
+a.root-servers.nil.    A       10.53.0.1
+
+; The idea is that the resolver would do 2 ADB lookups, so there would be 2
+; find list, both with 2 IPs in it. ns1 (which is actually ans2 and ans5) would
+; have both the slowest and fastest addresses. ns2 (which is actually ans3 and
+; ans4) would have two addresses in the middle.
+
+example.               NS      ns1.example.
+example.               NS      ns1.example.
+example.               NS      ns2.example.
+example.               NS      ns2.example.
+
+ns1.example.           A       10.53.0.2 ; delay is 0
+ns1.example.           A       10.53.0.5 ; delay is 0.15
+ns2.example.           A       10.53.0.4 ; delay is 0.08
+ns2.example.           A       10.53.0.3 ; delay is 0.03
diff --git a/bin/tests/system/srtt/ns6/named.args b/bin/tests/system/srtt/ns6/named.args
new file mode 100644 (file)
index 0000000..b5de587
--- /dev/null
@@ -0,0 +1 @@
+-D srtt-ns6 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4
diff --git a/bin/tests/system/srtt/ns6/named.conf.j2 b/bin/tests/system/srtt/ns6/named.conf.j2
new file mode 100644 (file)
index 0000000..1d27505
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+
+options {
+       query-source address 10.53.0.6;
+       notify-source 10.53.0.6;
+       transfer-source 10.53.0.6;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.6; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+       dnstap { resolver query; };
+       dnstap-output file "dnstap.out";
+};
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.6 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "." {
+       type hint;
+       file "../../_common/root.hint";
+};
diff --git a/bin/tests/system/srtt/srtt_ans.py b/bin/tests/system/srtt/srtt_ans.py
new file mode 100644 (file)
index 0000000..9387486
--- /dev/null
@@ -0,0 +1,59 @@
+"""
+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 abc
+
+import dns.rdataclass
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import DnsResponseSend, QnameQtypeHandler, QueryContext
+
+
+class DelayedQnameRangeHandler(QnameQtypeHandler):
+    """
+    Respond to queries for QNAMEs "foo1.example." through "foo<N>.example."
+    with QTYPE=A, where <N> must be defined by the subclass.  Every response is
+    delayed by a fixed amount of time, which must also be defined (in seconds)
+    by the subclass.
+    """
+
+    @property
+    def qnames(self) -> list[str]:
+        return [f"foo{x}.example." for x in range(1, self.max_qname + 1)]
+
+    qtypes = [dns.rdatatype.A]
+
+    @property
+    @abc.abstractmethod
+    def max_qname(self) -> int:
+        raise NotImplementedError
+
+    @property
+    @abc.abstractmethod
+    def delay(self) -> float:
+        raise NotImplementedError
+
+    def __str__(self) -> str:
+        return f"{self.__class__.__name__}(foo[1-{self.max_qname}].example/A)"
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        a_rrset = dns.rrset.from_text(
+            qctx.qname, 300, dns.rdataclass.IN, dns.rdatatype.A, "10.53.9.9"
+        )
+        qctx.response.answer.append(a_rrset)
+        yield DnsResponseSend(qctx.response, delay=self.delay)
diff --git a/bin/tests/system/srtt/tests_srtt.py b/bin/tests/system/srtt/tests_srtt.py
new file mode 100644 (file)
index 0000000..0ce18fc
--- /dev/null
@@ -0,0 +1,89 @@
+# 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 os
+
+import isctest
+import isctest.mark
+
+pytestmark = [isctest.mark.with_dnstap]
+
+
+def line_to_dst_ips(line):
+    # dnstap-read output line example
+    # 05-Feb-2026 11:00:57.853 RQ 10.53.0.6:38507 -> 10.53.0.3:22047 TCP 56b fooXXX.example./IN/NS
+    _, _, _, _, _, dst, _, _, _ = line.split(" ", 9)
+    ip, _ = dst.split(":", 1)
+    return ip
+
+
+def extract_dnstap(ns):
+    ns.rndc("dnstap -roll 1")
+    path = os.path.join(ns.identifier, "dnstap.out.0")
+    dnstapread = isctest.run.cmd(
+        [isctest.vars.ALL["DNSTAPREAD"], path],
+    )
+
+    lines = dnstapread.out.splitlines()
+    return map(line_to_dst_ips, lines)
+
+
+def assert_used_auth(ns, authip):
+    ips = extract_dnstap(ns)
+    queries = 0
+    matches = 0
+    for ip in ips:
+        queries += 1
+        if ip == authip:
+            matches += 1
+    assert matches > 85
+    assert queries <= 115
+
+
+def test_srtt(ns6):
+    for i in range(1, 100):
+        msg = isctest.query.create(f"foo{i}.example.", "A")
+        res = isctest.query.udp(msg, ns6.ip)
+        isctest.check.noerror(res)
+        assert len(res.answer[0]) == 1
+        res.answer[0].ttl = 300
+        assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9"
+
+    assert_used_auth(ns6, "10.53.0.2")
+
+    for i in range(100, 200):
+        msg = isctest.query.create(f"foo{i}.example.", "A")
+        res = isctest.query.udp(msg, ns6.ip)
+        isctest.check.noerror(res)
+        assert len(res.answer[0]) == 1
+        res.answer[0].ttl = 300
+        assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9"
+
+    assert_used_auth(ns6, "10.53.0.3")
+
+    for i in range(200, 300):
+        msg = isctest.query.create(f"foo{i}.example.", "A")
+        res = isctest.query.udp(msg, ns6.ip)
+        isctest.check.noerror(res)
+        assert len(res.answer[0]) == 1
+        res.answer[0].ttl = 300
+        assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9"
+
+    assert_used_auth(ns6, "10.53.0.4")
+
+    for i in range(300, 400):
+        msg = isctest.query.create(f"foo{i}.example.", "A")
+        res = isctest.query.udp(msg, ns6.ip)
+        isctest.check.noerror(res)
+        assert len(res.answer[0]) == 1
+        res.answer[0].ttl = 300
+        assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9"
+    assert_used_auth(ns6, "10.53.0.5")