From 1bedd7f2441f2d81f2f45b9bfd5c2853d3f44e5c Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 5 Feb 2026 09:46:01 +0100 Subject: [PATCH 1/4] Limit the number of addresses returned per ADB find Add a hard limit on the number of addresses that ADB returns from a single NS lookup (dns_adbfind_t). This mitigates a flood attack where an attacker controls a zone with many addresses for a nameserver, each returning an invalid response. The global max-query count (default 50) also limits this, but significant harm can be done before that limit is reached. The default limit is now 6 (v4 and/or v6) addresses for an ADB find (so, ADB looking up for A/AAAA addresses of a name server name). It can be overridden for testing via 'named -T adbaddrslimit=N'. (cherry picked from commit 3ec37fc69356ee682bee7f67940613ac31d93d7b) --- bin/named/main.c | 8 ++++++++ lib/dns/adb.c | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/bin/named/main.c b/bin/named/main.c index f3b2ec80f5..b298d6e98d 100644 --- a/bin/named/main.c +++ b/bin/named/main.c @@ -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); } diff --git a/lib/dns/adb.c b/lib/dns/adb.c index 50d4d83bf3..1514fb46b9 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -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); } From b456007e2d9af38d128bbb1b19c2bbfe0de7a475 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 4 Feb 2026 10:18:42 +0100 Subject: [PATCH 2/4] Remove duplicate addresses from the resolver SLIST The SLIST (essentially `fctx->finds`, forwarders and dual-stack alternatives aside) can have duplicate server addresses when multiple in-domain nameservers share the same IP addresses: sub.example. NS ns1.sub.example. sub.example. NS ns2.sub.example. ns1.sub.example. A 1.2.3.4 ns1.sub.example. A 5.6.7.8 ns2.sub.example. A 1.2.3.4 ns2.sub.example. A 5.6.7.8 If both 1.2.3.4 and 5.6.7.8 fail to return a valid answer, the resolver would query each address twice. The problem is fixed by replacing the two-phase server selection (sort each find list by SRTT, sort finds by head SRTT) with a single linear scan in nextaddress() that finds the lowest-SRTT unmarked, non-duplicate address across all find lists. The old approach had a correctness bug: after sorting, the resolver picked the next address from the "current" find list rather than globally. For example, with find lists [1, 15, 26] and [3, 4, 5], the second pick would be SRTT 15 instead of the correct SRTT 3. The new approach is both simpler and correct: each call to nextaddress() walks all addresses, skips marked and duplicate entries, and returns the one with the lowest SRTT. While this walk is repeated for each server attempt, it operates on a small bounded list and is negligible compared to the network I/O of querying the server. (cherry picked from commit b1c5856a3764b4025e93f8baf06c45c8fa029752) --- lib/dns/resolver.c | 220 ++++++++++++++++++--------------------------- 1 file changed, 89 insertions(+), 131 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 6d6fb8eba3..8b76b7da23 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -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; } /* From 4340b3537de687d9606aebc4107fa06ebe8888ae Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 5 Feb 2026 11:20:11 +0100 Subject: [PATCH 3/4] Add system test for self-pointed glue deduplication Test the resolver's behavior with self-pointed glue where each NS has the same set of addresses. Verify that addresses are deduplicated and each unique IP is only queried once. Also test the ADB address limit knob (-T adbaddrslimit=). (cherry picked from commit c21fc6cb95d77312d6fb891f17ce9df41a25af6d) --- .../system/selfpointedglue/ns1/named.conf.j2 | 28 ++++ bin/tests/system/selfpointedglue/ns1/root.db | 24 +++ .../system/selfpointedglue/ns2/named.conf.j2 | 28 ++++ bin/tests/system/selfpointedglue/ns2/tld.db | 27 +++ .../system/selfpointedglue/ns3/example.tld.db | 155 ++++++++++++++++++ .../selfpointedglue/ns3/example2.tld.db | 33 ++++ .../system/selfpointedglue/ns3/named.conf.j2 | 44 +++++ .../system/selfpointedglue/ns4/named.args.j2 | 3 + .../system/selfpointedglue/ns4/named.conf.j2 | 59 +++++++ .../system/selfpointedglue/ns4/root.hint | 14 ++ .../selfpointedglue/tests_selfpointedglue.py | 122 ++++++++++++++ 11 files changed, 537 insertions(+) create mode 100644 bin/tests/system/selfpointedglue/ns1/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns1/root.db create mode 100644 bin/tests/system/selfpointedglue/ns2/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns2/tld.db create mode 100644 bin/tests/system/selfpointedglue/ns3/example.tld.db create mode 100644 bin/tests/system/selfpointedglue/ns3/example2.tld.db create mode 100644 bin/tests/system/selfpointedglue/ns3/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns4/named.args.j2 create mode 100644 bin/tests/system/selfpointedglue/ns4/named.conf.j2 create mode 100644 bin/tests/system/selfpointedglue/ns4/root.hint create mode 100644 bin/tests/system/selfpointedglue/tests_selfpointedglue.py diff --git a/bin/tests/system/selfpointedglue/ns1/named.conf.j2 b/bin/tests/system/selfpointedglue/ns1/named.conf.j2 new file mode 100644 index 0000000000..fd83fc3c19 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns1/named.conf.j2 @@ -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"; +}; diff --git a/bin/tests/system/selfpointedglue/ns1/root.db b/bin/tests/system/selfpointedglue/ns1/root.db new file mode 100644 index 0000000000..bfbf049b80 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns1/root.db @@ -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 diff --git a/bin/tests/system/selfpointedglue/ns2/named.conf.j2 b/bin/tests/system/selfpointedglue/ns2/named.conf.j2 new file mode 100644 index 0000000000..2993832da2 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/named.conf.j2 @@ -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"; +}; diff --git a/bin/tests/system/selfpointedglue/ns2/tld.db b/bin/tests/system/selfpointedglue/ns2/tld.db new file mode 100644 index 0000000000..5935fd841c --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/tld.db @@ -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 diff --git a/bin/tests/system/selfpointedglue/ns3/example.tld.db b/bin/tests/system/selfpointedglue/ns3/example.tld.db new file mode 100644 index 0000000000..83ea4d37ec --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example.tld.db @@ -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 diff --git a/bin/tests/system/selfpointedglue/ns3/example2.tld.db b/bin/tests/system/selfpointedglue/ns3/example2.tld.db new file mode 100644 index 0000000000..bcab6e38c1 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example2.tld.db @@ -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 diff --git a/bin/tests/system/selfpointedglue/ns3/named.conf.j2 b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 new file mode 100644 index 0000000000..b5c8bfcf33 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 @@ -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"; +}; diff --git a/bin/tests/system/selfpointedglue/ns4/named.args.j2 b/bin/tests/system/selfpointedglue/ns4/named.args.j2 new file mode 100644 index 0000000000..071508fd70 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.args.j2 @@ -0,0 +1,3 @@ +{% set adblimit = adblimit | default("") %} + +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 @adblimit@ diff --git a/bin/tests/system/selfpointedglue/ns4/named.conf.j2 b/bin/tests/system/selfpointedglue/ns4/named.conf.j2 new file mode 100644 index 0000000000..09fbdd4e70 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.conf.j2 @@ -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; }; +}; diff --git a/bin/tests/system/selfpointedglue/ns4/root.hint b/bin/tests/system/selfpointedglue/ns4/root.hint new file mode 100644 index 0000000000..d7d0e1faba --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/root.hint @@ -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 diff --git a/bin/tests/system/selfpointedglue/tests_selfpointedglue.py b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py new file mode 100644 index 0000000000..0fb6c3e374 --- /dev/null +++ b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py @@ -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") From 2be8bdb3f44f073954daa0c5f04a9cd6dde583b1 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 4 Mar 2026 18:25:32 +0100 Subject: [PATCH 4/4] 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) --- bin/tests/system/srtt/README | 18 +++++ bin/tests/system/srtt/ans2/ans.py | 36 ++++++++++ bin/tests/system/srtt/ans3/ans.py | 36 ++++++++++ bin/tests/system/srtt/ans4/ans.py | 36 ++++++++++ bin/tests/system/srtt/ans5/ans.py | 36 ++++++++++ bin/tests/system/srtt/ns1/named.conf.j2 | 29 ++++++++ bin/tests/system/srtt/ns1/root.db | 36 ++++++++++ bin/tests/system/srtt/ns6/named.args | 1 + bin/tests/system/srtt/ns6/named.conf.j2 | 41 ++++++++++++ bin/tests/system/srtt/srtt_ans.py | 59 ++++++++++++++++ bin/tests/system/srtt/tests_srtt.py | 89 +++++++++++++++++++++++++ 11 files changed, 417 insertions(+) create mode 100644 bin/tests/system/srtt/README create mode 100644 bin/tests/system/srtt/ans2/ans.py create mode 100644 bin/tests/system/srtt/ans3/ans.py create mode 100644 bin/tests/system/srtt/ans4/ans.py create mode 100644 bin/tests/system/srtt/ans5/ans.py create mode 100644 bin/tests/system/srtt/ns1/named.conf.j2 create mode 100644 bin/tests/system/srtt/ns1/root.db create mode 100644 bin/tests/system/srtt/ns6/named.args create mode 100644 bin/tests/system/srtt/ns6/named.conf.j2 create mode 100644 bin/tests/system/srtt/srtt_ans.py create mode 100644 bin/tests/system/srtt/tests_srtt.py diff --git a/bin/tests/system/srtt/README b/bin/tests/system/srtt/README new file mode 100644 index 0000000000..c86a697931 --- /dev/null +++ b/bin/tests/system/srtt/README @@ -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 index 0000000000..147a65f828 --- /dev/null +++ b/bin/tests/system/srtt/ans2/ans.py @@ -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 index 0000000000..ecd590afd0 --- /dev/null +++ b/bin/tests/system/srtt/ans3/ans.py @@ -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 index 0000000000..af337c27fb --- /dev/null +++ b/bin/tests/system/srtt/ans4/ans.py @@ -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 index 0000000000..8bac83a798 --- /dev/null +++ b/bin/tests/system/srtt/ans5/ans.py @@ -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 index 0000000000..eb079c95ab --- /dev/null +++ b/bin/tests/system/srtt/ns1/named.conf.j2 @@ -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 index 0000000000..29ecd1d89d --- /dev/null +++ b/bin/tests/system/srtt/ns1/root.db @@ -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 index 0000000000..b5de5874ec --- /dev/null +++ b/bin/tests/system/srtt/ns6/named.args @@ -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 index 0000000000..1d27505a8e --- /dev/null +++ b/bin/tests/system/srtt/ns6/named.conf.j2 @@ -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 index 0000000000..9387486993 --- /dev/null +++ b/bin/tests/system/srtt/srtt_ans.py @@ -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.example." + with QTYPE=A, where 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 index 0000000000..0ce18fcccf --- /dev/null +++ b/bin/tests/system/srtt/tests_srtt.py @@ -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")