[9.20] [CVE-2026-3592] sec: usr: Limit resolver server list size

When resolving a domain with many nameservers that share overlapping IP addresses (e.g., 10 NS records all pointing at the same set of addresses), BIND could previously waste time querying duplicate addresses and build up excessively large server lists. Deduplicate addresses in the resolver's server list so that each unique IP is only queried once per resolution attempt, regardless of how many NS records point to it and cap the number of addresses stored per nameserver name to 6 (combined A and AAAA), preventing memory and CPU overhead from domains with unusually large NS/glue sets.

Closes isc-projects/bind9#5641

Backport of !909

Merge branch 'backport-5641-selfpointedglue-9.20' into 'security-bind-9.20'

See merge request isc-private/bind9!951
This commit is contained in:
Colin Vidal 2026-04-30 20:49:27 +02:00 committed by Michał Kępień
commit 06a0738b2b
No known key found for this signature in database
25 changed files with 1075 additions and 131 deletions

View file

@ -123,6 +123,7 @@ extern unsigned int dns_zone_mkey_month;
extern unsigned int dns_adb_entrywindow;
extern unsigned int dns_adb_cachemin;
extern size_t dns_dispatch_tcppipelining;
extern size_t dns_adb_addrslimit;
static bool want_stats = false;
static char program_name[NAME_MAX] = "named";
@ -817,6 +818,13 @@ parse_T_opt(char *option) {
"least 1");
}
dns_dispatch_tcppipelining = pipelining;
} else if (!strncmp(option, "adbaddrslimit=", 14)) {
size_t adb_addrslimit = atoi(option + 14);
if (adb_addrslimit < 1) {
named_main_earlyfatal("adbaddrslimit must be at "
"least 1");
}
dns_adb_addrslimit = adb_addrslimit;
} else {
fprintf(stderr, "unknown -T flag '%s'\n", option);
}

View file

@ -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.
*/
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; };
recursion no;
dnssec-validation no;
};
zone "." {
type primary;
file "root.db";
};

View file

@ -0,0 +1,24 @@
; 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. (
2010 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
. NS a.root-servers.nil.
a.root-servers.nil. A 10.53.0.1
tld. NS ns.tld.
ns.tld. A 10.53.0.2

View file

@ -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.
*/
options {
query-source address 10.53.0.2;
notify-source 10.53.0.2;
transfer-source 10.53.0.2;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.2; };
recursion no;
dnssec-validation no;
};
zone "tld." {
type primary;
file "tld.db";
};

View file

@ -0,0 +1,27 @@
; 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
tld. IN SOA owner.tld. ns.tld. (
2010 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
tld. NS ns.tld.
ns.tld. A 10.53.0.2
example.tld. NS ns.example.tld.
ns.example.tld. A 10.53.0.3
example2.tld. NS ns.example2.tld.
ns.example2.tld. A 10.53.0.3

View file

@ -0,0 +1,155 @@
; 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
example.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. (
2010 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
example.tld. NS ns.example.tld.
ns.example.tld. A 10.53.0.3
sub.example.tld. NS ns01.sub.example.tld.
sub.example.tld. NS ns02.sub.example.tld.
sub.example.tld. NS ns03.sub.example.tld.
sub.example.tld. NS ns04.sub.example.tld.
sub.example.tld. NS ns05.sub.example.tld.
sub.example.tld. NS ns06.sub.example.tld.
sub.example.tld. NS ns07.sub.example.tld.
sub.example.tld. NS ns08.sub.example.tld.
sub.example.tld. NS ns09.sub.example.tld.
sub.example.tld. NS ns10.sub.example.tld.
ns01.sub.example.tld. A 10.53.0.5
ns01.sub.example.tld. A 10.53.0.6
ns01.sub.example.tld. A 10.53.0.7
ns01.sub.example.tld. A 10.53.0.8
ns01.sub.example.tld. A 10.53.0.9
ns01.sub.example.tld. A 10.53.0.10
ns01.sub.example.tld. A 10.53.1.1
ns01.sub.example.tld. A 10.53.1.2
ns01.sub.example.tld. A 10.53.2.1
ns01.sub.example.tld. A 10.53.0.3
; Those RR (same below) pointing to 127.0.0.1 won't ever be used as they
; exceeded the ADB limit.
ns01.sub.example.tld. A 127.0.0.1
ns02.sub.example.tld. A 10.53.0.5
ns02.sub.example.tld. A 10.53.0.6
ns02.sub.example.tld. A 10.53.0.7
ns02.sub.example.tld. A 10.53.0.8
ns02.sub.example.tld. A 10.53.0.9
ns02.sub.example.tld. A 10.53.0.10
ns02.sub.example.tld. A 10.53.1.1
ns02.sub.example.tld. A 10.53.1.2
ns02.sub.example.tld. A 10.53.2.1
ns02.sub.example.tld. A 10.53.0.3
ns02.sub.example.tld. A 127.0.0.1
ns03.sub.example.tld. A 10.53.0.5
ns03.sub.example.tld. A 10.53.0.6
ns03.sub.example.tld. A 10.53.0.7
ns03.sub.example.tld. A 10.53.0.8
ns03.sub.example.tld. A 10.53.0.9
ns03.sub.example.tld. A 10.53.0.10
ns03.sub.example.tld. A 10.53.1.1
ns03.sub.example.tld. A 10.53.1.2
ns03.sub.example.tld. A 10.53.2.1
ns03.sub.example.tld. A 10.53.0.3
ns03.sub.example.tld. A 127.0.0.1
ns04.sub.example.tld. A 10.53.0.5
ns04.sub.example.tld. A 10.53.0.6
ns04.sub.example.tld. A 10.53.0.7
ns04.sub.example.tld. A 10.53.0.8
ns04.sub.example.tld. A 10.53.0.9
ns04.sub.example.tld. A 10.53.0.10
ns04.sub.example.tld. A 10.53.1.1
ns04.sub.example.tld. A 10.53.1.2
ns04.sub.example.tld. A 10.53.2.1
ns04.sub.example.tld. A 10.53.0.3
ns04.sub.example.tld. A 127.0.0.1
ns05.sub.example.tld. A 10.53.0.5
ns05.sub.example.tld. A 10.53.0.6
ns05.sub.example.tld. A 10.53.0.7
ns05.sub.example.tld. A 10.53.0.8
ns05.sub.example.tld. A 10.53.0.9
ns05.sub.example.tld. A 10.53.0.10
ns05.sub.example.tld. A 10.53.1.1
ns05.sub.example.tld. A 10.53.1.2
ns05.sub.example.tld. A 10.53.2.1
ns05.sub.example.tld. A 10.53.0.3
ns05.sub.example.tld. A 127.0.0.1
ns06.sub.example.tld. A 10.53.0.5
ns06.sub.example.tld. A 10.53.0.6
ns06.sub.example.tld. A 10.53.0.7
ns06.sub.example.tld. A 10.53.0.8
ns06.sub.example.tld. A 10.53.0.9
ns06.sub.example.tld. A 10.53.0.10
ns06.sub.example.tld. A 10.53.1.1
ns06.sub.example.tld. A 10.53.1.2
ns06.sub.example.tld. A 10.53.2.1
ns06.sub.example.tld. A 10.53.0.3
ns06.sub.example.tld. A 127.0.0.1
ns07.sub.example.tld. A 10.53.0.5
ns07.sub.example.tld. A 10.53.0.6
ns07.sub.example.tld. A 10.53.0.7
ns07.sub.example.tld. A 10.53.0.8
ns07.sub.example.tld. A 10.53.0.9
ns07.sub.example.tld. A 10.53.0.10
ns07.sub.example.tld. A 10.53.1.1
ns07.sub.example.tld. A 10.53.1.2
ns07.sub.example.tld. A 10.53.2.1
ns07.sub.example.tld. A 10.53.0.3
ns07.sub.example.tld. A 127.0.0.1
ns08.sub.example.tld. A 10.53.0.5
ns08.sub.example.tld. A 10.53.0.6
ns08.sub.example.tld. A 10.53.0.7
ns08.sub.example.tld. A 10.53.0.8
ns08.sub.example.tld. A 10.53.0.9
ns08.sub.example.tld. A 10.53.0.10
ns08.sub.example.tld. A 10.53.1.1
ns08.sub.example.tld. A 10.53.1.2
ns08.sub.example.tld. A 10.53.2.1
ns08.sub.example.tld. A 10.53.0.3
ns08.sub.example.tld. A 127.0.0.1
ns09.sub.example.tld. A 10.53.0.5
ns09.sub.example.tld. A 10.53.0.6
ns09.sub.example.tld. A 10.53.0.7
ns09.sub.example.tld. A 10.53.0.8
ns09.sub.example.tld. A 10.53.0.9
ns09.sub.example.tld. A 10.53.0.10
ns09.sub.example.tld. A 10.53.1.1
ns09.sub.example.tld. A 10.53.1.2
ns09.sub.example.tld. A 10.53.2.1
ns09.sub.example.tld. A 10.53.0.3
ns09.sub.example.tld. A 127.0.0.1
ns10.sub.example.tld. A 10.53.0.5
ns10.sub.example.tld. A 10.53.0.6
ns10.sub.example.tld. A 10.53.0.7
ns10.sub.example.tld. A 10.53.0.8
ns10.sub.example.tld. A 10.53.0.9
ns10.sub.example.tld. A 10.53.0.10
ns10.sub.example.tld. A 10.53.1.1
ns10.sub.example.tld. A 10.53.1.2
ns10.sub.example.tld. A 10.53.2.1
ns10.sub.example.tld. A 10.53.0.3
ns10.sub.example.tld. A 127.0.0.1

View file

@ -0,0 +1,33 @@
; 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
example2.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. (
2010 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
example2.tld. NS ns.example2.tld.
ns.example2.tld. A 10.53.0.3
sub.example2.tld. NS ns01.sub.example2.tld.
sub.example2.tld. NS ns02.sub.example2.tld.
sub.example2.tld. NS ns03.sub.example2.tld.
ns01.sub.example2.tld. A 10.53.1.1
ns01.sub.example2.tld. A 10.53.0.5
ns02.sub.example2.tld. A 10.53.1.2
ns02.sub.example2.tld. A 10.53.0.6
ns03.sub.example2.tld. A 10.53.2.1
ns03.sub.example2.tld. A 10.53.0.7

View file

@ -0,0 +1,44 @@
/*
* 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.3;
notify-source 10.53.0.3;
transfer-source 10.53.0.3;
port @PORT@;
pid-file "named.pid";
listen-on {
10.53.0.3;
10.53.0.5;
10.53.0.6;
10.53.0.7;
10.53.0.8;
10.53.0.9;
10.53.0.10;
10.53.1.1;
10.53.1.2;
10.53.2.1;
};
recursion no;
dnssec-validation no;
};
zone "example.tld." {
type primary;
file "example.tld.db";
};
zone "example2.tld." {
type primary;
file "example2.tld.db";
};

View file

@ -0,0 +1,3 @@
{% set adblimit = adblimit | default("") %}
-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 @adblimit@

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.
*/
{% set maxdelegationservers = maxdelegationservers | default(None) %}
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; };
recursion yes;
dnssec-validation no;
dnstap { resolver query; };
dnstap-output file "dnstap.out";
{% if maxdelegationservers %}
@maxdelegationservers@
{% endif %}
};
/*
* Forcing TCP ensures that ADDITIONAL won't be truncated (responses won't have
* the TC flag, hence the resolver won't retry using TCP by itself, see
* https://datatracker.ietf.org/doc/html/rfc2181#section-9)
*/
server 10.53.0.3 { tcp-only true; };
server 10.53.0.5 { tcp-only true; };
server 10.53.0.6 { tcp-only true; };
server 10.53.0.7 { tcp-only true; };
server 10.53.0.8 { tcp-only true; };
server 10.53.0.9 { tcp-only true; };
server 10.53.0.10 { tcp-only true; };
server 10.53.1.1 { tcp-only true; };
server 10.53.1.2 { tcp-only true; };
server 10.53.2.1 { tcp-only true; };
zone "." {
type hint;
file "root.hint";
};
key rndc_key {
secret "1234abcd8765";
algorithm @DEFAULT_HMAC@;
};
controls {
inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};

View file

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

View file

@ -0,0 +1,122 @@
# 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 subprocess
import isctest
import isctest.mark
pytestmark = [isctest.mark.with_dnstap]
def line_to_ips_and_queries(line):
# dnstap-read output line example
# 05-Feb-2026 11:00:57.853 RQ 10.53.0.4:38507 -> 10.53.0.3:22047 TCP 56b sub.example.tld/IN/NS
_, _, _, _, _, dst, _, _, query = line.split(" ", 9)
ip, _ = dst.split(":", 1)
return (ip, query)
def extract_dnstap(ns, expectedlen):
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()
assert expectedlen == len(lines)
return list(map(line_to_ips_and_queries, lines))
# Because DNSTAP doesn't have ordering guarantee, the order doesn't matter here.
def expect_ip_and_query(expected_ips_and_queries, ips_and_queries):
found_count = 0
for expected_ip, expected_query in expected_ips_and_queries:
found = False
for ip, query in ips_and_queries:
if ip == expected_ip and query == expected_query:
found = True
found_count += 1
break
assert found
assert found_count == len(expected_ips_and_queries)
def test_selfpointedglue(ns4):
msg = isctest.query.create("a.sub.example.tld.", "A")
res = isctest.query.tcp(msg, ns4.ip)
isctest.check.servfail(res)
ips_and_queries = extract_dnstap(ns4, 10)
# Thanks to the de-duplication, only the first 6 NS IPs are
# queried (once sub.example.tld. NS is found) instead of 60
# (60 per NS, with 10 NS).
expect_ip_and_query(
[
("10.53.0.1", "./IN/NS"),
("10.53.0.1", "tld/IN/NS"),
("10.53.0.2", "example.tld/IN/NS"),
("10.53.0.3", "sub.example.tld/IN/NS"),
("10.53.0.3", "a.sub.example.tld/IN/A"),
("10.53.0.5", "a.sub.example.tld/IN/A"),
("10.53.0.6", "a.sub.example.tld/IN/A"),
("10.53.0.7", "a.sub.example.tld/IN/A"),
("10.53.0.8", "a.sub.example.tld/IN/A"),
("10.53.0.9", "a.sub.example.tld/IN/A"),
],
ips_and_queries,
)
def test_selfpointedglue_adblimit(ns4, templates):
templates.render("ns4/named.args", {"adblimit": "-T adbaddrslimit=2"})
with ns4.watch_log_from_here() as watcher:
# Server needs a full stop/restart to read the new command line options.
ns4.stop()
ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
watcher.wait_for_line("running")
msg = isctest.query.create("a.sub.example.tld.", "A")
res = isctest.query.tcp(msg, ns4.ip)
isctest.check.servfail(res)
ips_and_queries = extract_dnstap(ns4, 6)
expect_ip_and_query(
[
("10.53.0.1", "./IN/NS"),
("10.53.0.1", "tld/IN/NS"),
("10.53.0.2", "example.tld/IN/NS"),
("10.53.0.3", "sub.example.tld/IN/NS"),
# Because of the ADB limit, only 2 IP are used instead
# of the 10 provided ones.
("10.53.0.3", "a.sub.example.tld/IN/A"),
("10.53.0.5", "a.sub.example.tld/IN/A"),
],
ips_and_queries,
)
def test_selfpointedglue_adblimitlower(ns4, templates):
templates.render("ns4/named.args", {"adblimit": "-T adbaddrslimit=0"})
with ns4.watch_log_from_here() as watcher:
# Server needs a full stop/restart to read the new command line options.
ns4.stop()
failed_to_start = False
try:
ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
except subprocess.CalledProcessError as _:
failed_to_start = True
assert failed_to_start is True
watcher.wait_for_line("adbaddrslimit must be at least 1")

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

View file

@ -78,6 +78,15 @@
#define DNS_ADB_MINADBSIZE (1024U * 1024U) /*%< 1 Megabyte */
/*
* Default and override for the per-find address limit, the sum of the number of
* A and AAAA RR from an ADB NS name resolution. When non-zero, this value is
* used instead of the default. Can be set via 'named -T adbaddrslimit=N' for
* testing.
*/
#define DEFAULT_ADDRSLIMIT 6
size_t dns_adb_addrslimit = 0;
typedef ISC_LIST(dns_adbname_t) dns_adbnamelist_t;
typedef struct dns_adbnamehook dns_adbnamehook_t;
typedef ISC_LIST(dns_adbnamehook_t) dns_adbnamehooklist_t;
@ -1473,6 +1482,9 @@ static void
copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) {
dns_adbnamehook_t *namehook = NULL;
dns_adbentry_t *entry = NULL;
size_t count = 0;
size_t limit = dns_adb_addrslimit != 0 ? dns_adb_addrslimit
: DEFAULT_ADDRSLIMIT;
if ((find->options & DNS_ADBFIND_INET) != 0) {
namehook = ISC_LIST_HEAD(name->v4);
@ -1493,6 +1505,12 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) {
* Found a valid entry. Add it to the find's list.
*/
ISC_LIST_APPEND(find->list, addrinfo, publink);
if (++count >= limit) {
DP(ISC_LOG_DEBUG(3), "skipping addresses");
return;
}
nextv4:
namehook = ISC_LIST_NEXT(namehook, name_link);
}
@ -1517,6 +1535,12 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) {
* Found a valid entry. Add it to the find's list.
*/
ISC_LIST_APPEND(find->list, addrinfo, publink);
if (++count >= limit) {
DP(ISC_LOG_DEBUG(3), "skipping addresses");
return;
}
nextv6:
namehook = ISC_LIST_NEXT(namehook, name_link);
}

View file

@ -374,7 +374,16 @@ struct fetchctx {
dns_message_t *qmessage;
ISC_LIST(resquery_t) queries;
dns_adbfindlist_t finds;
dns_adbfind_t *find;
/*
* This is a state to keep track of the latest upstream server which is
* being queried. See `nextaddress()`.
*
* `addrinfo` is basically a copy of `foundaddrinfo` but came from the
* response of the query, so fields like the SRTT/timing might have been
* altered. So it might be possible (?) to wrap those two in an union
* for clarity (and memory saving).
*/
dns_adbaddrinfo_t *foundaddrinfo;
/*
* altfinds are names and/or addresses of dual stack servers that
* should be used when iterative resolution to a server is not
@ -1314,7 +1323,7 @@ fctx_cleanup(fetchctx_t *fctx) {
dns_adb_destroyfind(&find);
fetchctx_unref(fctx);
}
fctx->find = NULL;
fctx->foundaddrinfo = NULL;
for (find = ISC_LIST_HEAD(fctx->altfinds); find != NULL;
find = next_find)
@ -3164,89 +3173,6 @@ add_bad(fetchctx_t *fctx, dns_message_t *rmessage, dns_adbaddrinfo_t *addrinfo,
isc_result_totext(reason), namebuf, typebuf, classbuf, addrbuf);
}
/*
* Sort addrinfo list by RTT.
*/
static void
sort_adbfind(dns_adbfind_t *find, unsigned int bias) {
dns_adbaddrinfo_t *best, *curr;
dns_adbaddrinfolist_t sorted;
/* Lame N^2 bubble sort. */
ISC_LIST_INIT(sorted);
while (!ISC_LIST_EMPTY(find->list)) {
unsigned int best_srtt;
best = ISC_LIST_HEAD(find->list);
best_srtt = best->srtt;
if (isc_sockaddr_pf(&best->sockaddr) != AF_INET6) {
best_srtt += bias;
}
curr = ISC_LIST_NEXT(best, publink);
while (curr != NULL) {
unsigned int curr_srtt = curr->srtt;
if (isc_sockaddr_pf(&curr->sockaddr) != AF_INET6) {
curr_srtt += bias;
}
if (curr_srtt < best_srtt) {
best = curr;
best_srtt = curr_srtt;
}
curr = ISC_LIST_NEXT(curr, publink);
}
ISC_LIST_UNLINK(find->list, best, publink);
ISC_LIST_APPEND(sorted, best, publink);
}
find->list = sorted;
}
/*
* Sort a list of finds by server RTT.
*/
static void
sort_finds(dns_adbfindlist_t *findlist, unsigned int bias) {
dns_adbfind_t *best, *curr;
dns_adbfindlist_t sorted;
dns_adbaddrinfo_t *addrinfo, *bestaddrinfo;
/* Sort each find's addrinfo list by SRTT. */
for (curr = ISC_LIST_HEAD(*findlist); curr != NULL;
curr = ISC_LIST_NEXT(curr, publink))
{
sort_adbfind(curr, bias);
}
/* Lame N^2 bubble sort. */
ISC_LIST_INIT(sorted);
while (!ISC_LIST_EMPTY(*findlist)) {
unsigned int best_srtt;
best = ISC_LIST_HEAD(*findlist);
bestaddrinfo = ISC_LIST_HEAD(best->list);
INSIST(bestaddrinfo != NULL);
best_srtt = bestaddrinfo->srtt;
if (isc_sockaddr_pf(&bestaddrinfo->sockaddr) != AF_INET6) {
best_srtt += bias;
}
curr = ISC_LIST_NEXT(best, publink);
while (curr != NULL) {
unsigned int curr_srtt;
addrinfo = ISC_LIST_HEAD(curr->list);
INSIST(addrinfo != NULL);
curr_srtt = addrinfo->srtt;
if (isc_sockaddr_pf(&addrinfo->sockaddr) != AF_INET6) {
curr_srtt += bias;
}
if (curr_srtt < best_srtt) {
best = curr;
best_srtt = curr_srtt;
}
curr = ISC_LIST_NEXT(curr, publink);
}
ISC_LIST_UNLINK(*findlist, best, publink);
ISC_LIST_APPEND(sorted, best, publink);
}
*findlist = sorted;
}
/*
* Return true iff the ADB find has an already pending fetch for 'type'. This
* is used to find out whether we're in a loop, where a fetch is waiting for a
@ -3367,6 +3293,7 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port,
}
}
}
if ((flags & FCTX_ADDRINFO_DUALSTACK) != 0) {
ISC_LIST_APPEND(fctx->altfinds, find, publink);
} else {
@ -3841,8 +3768,6 @@ out:
* We've found some addresses. We might still be
* looking for more addresses.
*/
sort_finds(&fctx->finds, res->view->v6bias);
sort_finds(&fctx->altfinds, 0);
result = ISC_R_SUCCESS;
}
@ -3914,6 +3839,80 @@ possibly_mark(fetchctx_t *fctx, dns_adbaddrinfo_t *addr) {
}
}
static dns_adbaddrinfo_t *
nextaddress(fetchctx_t *fctx) {
dns_adbaddrinfo_t *prevai = fctx->foundaddrinfo, *lowestsrttai = NULL;
unsigned int v6bias = fctx->res->view->v6bias, lowestsrtt = 0;
/*
* Let's walk through the list of dns_adbaddrinfo_t to find the best
* next server address to query. This is linear on the number of
* dns_adbaddrinfo_t which are grouped in find list (for each ADB find).
*/
for (dns_adbfind_t *find = ISC_LIST_HEAD(fctx->finds); find != NULL;
find = ISC_LIST_NEXT(find, publink))
{
for (dns_adbaddrinfo_t *ai = ISC_LIST_HEAD(find->list);
ai != NULL; ai = ISC_LIST_NEXT(ai, publink))
{
/*
* This address has been marked already, skip it.
*/
if (!UNMARKED(ai)) {
continue;
}
/*
* This address is the same as the previously used
* address, it's a duplicate, mark it and skip it!
*/
if (prevai != NULL) {
if (prevai->entry == ai->entry) {
ai->flags |= FCTX_ADDRINFO_MARK;
continue;
}
}
/*
* Mark and skip this address if incompatible (i.e. IPv6
* address on a v4 only server, or for ACL reason, etc.)
*/
possibly_mark(fctx, ai);
if (!UNMARKED(ai)) {
continue;
}
/*
* This address hasn't been tried yet and is a
* good candidate. Let's keep track of it if it
* has the lowest SRTT so far (or if there is no
* address with lowest SRTT found yet).
*/
unsigned int aisrtt = ai->srtt;
if (isc_sockaddr_pf(&ai->sockaddr) != AF_INET6) {
aisrtt += v6bias;
}
if (lowestsrttai == NULL || aisrtt < lowestsrtt) {
lowestsrttai = ai;
lowestsrtt = aisrtt;
continue;
}
}
}
/*
* This is the next address to query. If this is NULL, we're done.
*/
if (lowestsrttai != NULL) {
lowestsrttai->flags |= FCTX_ADDRINFO_MARK;
}
fctx->foundaddrinfo = lowestsrttai;
return lowestsrttai;
}
static dns_adbaddrinfo_t *
fctx_nextaddress(fetchctx_t *fctx) {
dns_adbfind_t *find, *start;
@ -3936,7 +3935,6 @@ fctx_nextaddress(fetchctx_t *fctx) {
possibly_mark(fctx, addrinfo);
if (UNMARKED(addrinfo)) {
addrinfo->flags |= FCTX_ADDRINFO_MARK;
fctx->find = NULL;
fctx->forwarding = true;
/*
@ -3957,49 +3955,9 @@ fctx_nextaddress(fetchctx_t *fctx) {
fctx->forwarding = false;
FCTX_ATTR_SET(fctx, FCTX_ATTR_TRIEDFIND);
find = fctx->find;
if (find == NULL) {
find = ISC_LIST_HEAD(fctx->finds);
} else {
find = ISC_LIST_NEXT(find, publink);
if (find == NULL) {
find = ISC_LIST_HEAD(fctx->finds);
}
}
/*
* Find the first unmarked addrinfo.
*/
addrinfo = NULL;
if (find != NULL) {
start = find;
do {
for (addrinfo = ISC_LIST_HEAD(find->list);
addrinfo != NULL;
addrinfo = ISC_LIST_NEXT(addrinfo, publink))
{
if (!UNMARKED(addrinfo)) {
continue;
}
possibly_mark(fctx, addrinfo);
if (UNMARKED(addrinfo)) {
addrinfo->flags |= FCTX_ADDRINFO_MARK;
break;
}
}
if (addrinfo != NULL) {
break;
}
find = ISC_LIST_NEXT(find, publink);
if (find == NULL) {
find = ISC_LIST_HEAD(fctx->finds);
}
} while (find != start);
}
fctx->find = find;
if (addrinfo != NULL) {
return addrinfo;
faddrinfo = nextaddress(fctx);
if (faddrinfo != NULL) {
return faddrinfo;
}
/*