Add SRTT-based server selection system test

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.

(cherry picked from commit a8d11e14f5b4e4d53219ba751d1b741162b0b84b)
This commit is contained in:
Colin Vidal 2026-03-04 18:25:32 +01:00 committed by Michał Kępień
parent 4340b3537d
commit 2be8bdb3f4
No known key found for this signature in database
11 changed files with 417 additions and 0 deletions

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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";
};

View file

@ -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

View file

@ -0,0 +1 @@
-D srtt-ns6 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4

View file

@ -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";
};

View file

@ -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)

View file

@ -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")