From e986b19d0d61aa87348fe509933cadd5a05f24c9 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 5 Feb 2026 09:46:01 +0100 Subject: [PATCH 01/36] Limit the number of addresses returned per ADB find The number of `dns_adbaddrfind_t` (NS address with metadata like SRTT) returned from an ADB NS name lookup is now limited by the caller. The default value (outside the resolver) uses `max-delegation-servers`, and the resolver, for a given fetch, start with `max-delegation-servers` and decrement it at each ADB fetch. This ensures that, for a given delegation, no more than 13 nameservers will be contacted. This is the same mechanism used when looking up `dns_adbaddrfind_t` from a list of glues (addresses). --- lib/dns/adb.c | 29 +++++++++++++++++++++++------ lib/dns/include/dns/adb.h | 4 +++- lib/dns/notify.c | 7 ++++--- lib/dns/resolver.c | 26 +++++++++++++++++--------- lib/dns/zone.c | 6 ++++-- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/lib/dns/adb.c b/lib/dns/adb.c index f189664f80..f51b2a8821 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -1370,8 +1370,10 @@ log_quota(dns_adbentry_t *entry, const char *fmt, ...) { } static void -copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { +copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name, + size_t maxfindlen, size_t *findlen) { dns_adbentry_t *entry = NULL; + size_t count = 0; if ((find->options & DNS_ADBFIND_INET) != 0) { ISC_LIST_FOREACH(name->v4, namehook, name_link) { @@ -1391,6 +1393,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); + + count++; + if (maxfindlen - count == 0) { + SET_IF_NOT_NULL(findlen, count); + return; + } } } @@ -1412,8 +1420,16 @@ 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); + + count++; + if (maxfindlen - count == 0) { + SET_IF_NOT_NULL(findlen, count); + return; + } } } + + SET_IF_NOT_NULL(findlen, count); } static bool @@ -1735,7 +1751,7 @@ out: void dns_adb_createaddrinfosfind(dns_adb_t *adb, isc_netaddrlist_t *addrs, in_port_t port, unsigned int options, - isc_stdtime_t now, size_t maxaddrs, + isc_stdtime_t now, size_t maxfindlen, dns_adbfind_t **findp, size_t *findlen) { dns_adbfind_t *find = NULL; isc_sockaddr_t sockaddr = {}; @@ -1743,7 +1759,7 @@ dns_adb_createaddrinfosfind(dns_adb_t *adb, isc_netaddrlist_t *addrs, REQUIRE(DNS_ADB_VALID(adb)); REQUIRE(addrs != NULL); REQUIRE(findp != NULL && *findp == NULL); - REQUIRE(maxaddrs > 0); + REQUIRE(maxfindlen > 0); rcu_read_lock(); @@ -1794,7 +1810,7 @@ dns_adb_createaddrinfosfind(dns_adb_t *adb, isc_netaddrlist_t *addrs, ISC_LIST_APPEND(find->list, addrinfo, publink); (*findlen)++; - if (maxaddrs - *findlen == 0) { + if (maxfindlen - *findlen == 0) { break; } } @@ -1825,7 +1841,7 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, const dns_name_t *name, unsigned int options, isc_stdtime_t now, in_port_t port, unsigned int depth, isc_counter_t *qc, isc_counter_t *gqc, fetchctx_t *parent, - dns_adbfind_t **findp) { + size_t maxfindlen, dns_adbfind_t **findp, size_t *findlen) { isc_result_t result = ISC_R_UNEXPECTED; dns_adbfind_t *find = NULL; dns_adbname_t *adbname = NULL; @@ -1844,6 +1860,7 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, } REQUIRE(name != NULL); REQUIRE(findp != NULL && *findp == NULL); + REQUIRE(maxfindlen > 0); REQUIRE((options & DNS_ADBFIND_ADDRESSMASK) != 0); @@ -2056,7 +2073,7 @@ fetch: * Run through the name and copy out the bits we are * interested in. */ - copy_namehook_lists(adb, find, adbname); + copy_namehook_lists(adb, find, adbname, maxfindlen, findlen); post_copy: if (NAME_FETCH_A(adbname)) { diff --git a/lib/dns/include/dns/adb.h b/lib/dns/include/dns/adb.h index 7c1aef4cc5..82abbf98f5 100644 --- a/lib/dns/include/dns/adb.h +++ b/lib/dns/include/dns/adb.h @@ -285,7 +285,7 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, const dns_name_t *name, unsigned int options, isc_stdtime_t now, in_port_t port, unsigned int depth, isc_counter_t *qc, isc_counter_t *gqc, fetchctx_t *parent, - dns_adbfind_t **find); + size_t maxfindlen, dns_adbfind_t **find, size_t *findlen); /*%< * Main interface for clients. The adb will look up the name given in * "name" and will build up a list of found addresses, and perhaps start @@ -333,6 +333,8 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, * *\li find != NULL && *find == NULL. * + *\li findlen is optional, if not NULL, it will be set to the length of find. + * * Returns: * *\li #ISC_R_SUCCESS Addresses might have been returned, and events will be diff --git a/lib/dns/notify.c b/lib/dns/notify.c index 46766ea711..3b2ec98f5d 100644 --- a/lib/dns/notify.c +++ b/lib/dns/notify.c @@ -761,9 +761,10 @@ dns_notify_find_address(dns_notify_t *notify) { goto destroy; } - result = dns_adb_createfind(adb, loop, process_notify_adb_event, notify, - ¬ify->ns, options, 0, notify->port, 0, - NULL, NULL, NULL, ¬ify->find); + result = dns_adb_createfind( + adb, loop, process_notify_adb_event, notify, ¬ify->ns, + options, 0, notify->port, 0, NULL, NULL, NULL, + view->max_delegation_servers, ¬ify->find, NULL); dns_adb_detach(&adb); /* Something failed? */ diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 742748d2f3..ba48796189 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -3375,7 +3375,8 @@ already_waiting_for(dns_adbfind_t *find, dns_rdatatype_t type) { static void findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port, unsigned int options, unsigned int flags, isc_stdtime_t now, - bool *overquota, bool *need_alternate, bool *have_address) { + bool *overquota, bool *need_alternate, bool *have_address, + size_t maxfindlen, size_t *findlen) { dns_adbfind_t *find = NULL; dns_resolver_t *res = fctx->res; bool unshared = ((fctx->options & DNS_FETCHOPT_UNSHARED) != 0); @@ -3416,7 +3417,7 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port, result = dns_adb_createfind(fctx->adb, fctx->loop, fctx_finddone, fctx, name, options, now, res->view->dstport, fctx->depth + 1, fctx->qc, fctx->gqc, fctx, - &find); + maxfindlen, &find, findlen); isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), "fctx %p(%s): createfind for %s - %s", @@ -3670,7 +3671,7 @@ fctx_getaddresses_addresses(fetchctx_t *fctx, isc_stdtime_t now, ISC_LIST_FOREACH(fctx->delegset->delegs, deleg, link) { dns_adbfind_t *find = NULL; - size_t maxaddrs = max_delegation_servers - *ns_processed; + size_t maxfindlen = max_delegation_servers - *ns_processed; size_t findlen = 0; if (*ns_processed >= max_delegation_servers) { @@ -3690,7 +3691,7 @@ fctx_getaddresses_addresses(fetchctx_t *fctx, isc_stdtime_t now, fetchctx_ref(fctx); dns_adb_createaddrinfosfind(fctx->adb, &deleg->addresses, fctx->res->view->dstport, options, - now, maxaddrs, &find, &findlen); + now, maxfindlen, &find, &findlen); if (find == NULL) { fetchctx_unref(fctx); @@ -3778,6 +3779,12 @@ shufflens: unsigned int static_stub = 0; unsigned int no_fetch = 0; dns_name_t *ns = nameservers[i]; + size_t maxfindlen = max_delegation_servers - *ns_processed; + size_t findlen = 0; + + if (*ns_processed >= max_delegation_servers) { + break; + } if (fctx->delegset->staticstub && dns_name_equal(ns, fctx->domain)) @@ -3794,15 +3801,15 @@ shufflens: } findname(fctx, ns, 0, stdoptions | static_stub | no_fetch, 0, - now, &overquota, need_alternatep, &have_address); + now, &overquota, need_alternatep, &have_address, + maxfindlen, &findlen); if (!overquota) { *all_spilledp = false; } + *ns_processed += findlen; - if (++(*ns_processed) >= max_delegation_servers) { - break; - } + INSIST(*ns_processed <= max_delegation_servers); } if (fctx->pending_running == 0 && !have_address) { @@ -3828,7 +3835,8 @@ fctx_getaddresses_alternate(fetchctx_t *fctx, isc_stdtime_t now, if (!a->isaddress) { findname(fctx, &a->_u._n.name, a->_u._n.port, stdoptions, FCTX_ADDRINFO_DUALSTACK, now, NULL, - NULL, NULL); + NULL, NULL, + fctx->res->view->max_delegation_servers, NULL); continue; } if (isc_sockaddr_pf(&a->_u.addr) != family) { diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 3414ed8d35..4ab816c1f2 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -17278,9 +17278,11 @@ checkds_find_address(dns_checkds_t *checkds) { isc_result_t result; unsigned int options; dns_adb_t *adb = NULL; + dns_view_t *view = NULL; REQUIRE(DNS_CHECKDS_VALID(checkds)); + view = checkds->zone->view; options = DNS_ADBFIND_WANTEVENT; if (isc_net_probeipv4() != ISC_R_DISABLED) { options |= DNS_ADBFIND_INET; @@ -17289,7 +17291,7 @@ checkds_find_address(dns_checkds_t *checkds) { options |= DNS_ADBFIND_INET6; } - dns_view_getadb(checkds->zone->view, &adb); + dns_view_getadb(view, &adb); if (adb == NULL) { goto destroy; } @@ -17297,7 +17299,7 @@ checkds_find_address(dns_checkds_t *checkds) { result = dns_adb_createfind( adb, checkds->zone->loop, process_checkds_adb_event, checkds, &checkds->ns, options, 0, checkds->zone->view->dstport, 0, NULL, - NULL, NULL, &checkds->find); + NULL, NULL, view->max_delegation_servers, &checkds->find, NULL); dns_adb_detach(&adb); /* Something failed? */ From 0fcaa37c3af3ccc5b603d6d0b46a6c8163fb2c7e Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 4 Feb 2026 10:18:42 +0100 Subject: [PATCH 02/36] 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. --- lib/dns/resolver.c | 209 ++++++++++++++++++--------------------------- 1 file changed, 83 insertions(+), 126 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index ba48796189..e66e602888 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -368,7 +368,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 @@ -1409,7 +1418,7 @@ fctx_cleanup(fetchctx_t *fctx) { dns_adb_destroyfind(&find); fetchctx_unref(fctx); } - fctx->find = NULL; + fctx->foundaddrinfo = NULL; ISC_LIST_FOREACH(fctx->altfinds, find, publink) { ISC_LIST_UNLINK(fctx->altfinds, find, publink); @@ -3251,89 +3260,6 @@ add_bad(fetchctx_t *fctx, dns_message_t *rmessage, dns_adbaddrinfo_t *addrinfo, 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 = NULL; - dns_adbfindlist_t sorted; - dns_adbaddrinfo_t *addrinfo, *bestaddrinfo; - - /* Sort each find's addrinfo list by SRTT. */ - ISC_LIST_FOREACH(*findlist, curr, publink) { - sort_adbfind(curr, bias); - } - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(*findlist)) { - dns_adbfind_t *curr = NULL; - 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 @@ -3459,6 +3385,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 { @@ -4027,8 +3954,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); return ISC_R_SUCCESS; } @@ -4140,6 +4065,76 @@ 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). + */ + ISC_LIST_FOREACH(fctx->finds, find, publink) { + ISC_LIST_FOREACH(find->list, 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 = NULL, *start = NULL; @@ -4159,7 +4154,6 @@ fctx_nextaddress(fetchctx_t *fctx) { possibly_mark(fctx, ai); if (UNMARKED(ai)) { ai->flags |= FCTX_ADDRINFO_MARK; - fctx->find = NULL; fctx->forwarding = true; /* @@ -4180,44 +4174,7 @@ 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. - */ - if (find != NULL) { - start = find; - do { - ISC_LIST_FOREACH(find->list, ai, publink) { - if (!UNMARKED(ai)) { - continue; - } - possibly_mark(fctx, ai); - if (UNMARKED(ai)) { - ai->flags |= FCTX_ADDRINFO_MARK; - faddrinfo = ai; - break; - } - } - if (faddrinfo != NULL) { - break; - } - find = ISC_LIST_NEXT(find, publink); - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } - } while (find != start); - } - - fctx->find = find; + faddrinfo = nextaddress(fctx); if (faddrinfo != NULL) { return faddrinfo; } From 9ae83a0e4ee0ac9362ad633a6e521c21b53dc492 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Thu, 9 Apr 2026 11:32:07 +0200 Subject: [PATCH 03/36] Add reproducer for BADCOOKIE resend loop Run malicious server: resend_loop/ans3/ans.py Start BIND: ns4 Send single query to test.example The resolver will repeatedly resend queries until the fetch timeout expires, resulting in resulting in thousands of qrysent while the quota counter remains 0. --- bin/tests/system/resend_loop/ans3/ans.py | 126 ++++++++++++++++++ .../system/resend_loop/ns4/named.conf.j2 | 16 +++ bin/tests/system/resend_loop/ns4/root.hint | 14 ++ .../system/resend_loop/tests_resend_loop.py | 28 ++++ 4 files changed, 184 insertions(+) create mode 100644 bin/tests/system/resend_loop/ans3/ans.py create mode 100644 bin/tests/system/resend_loop/ns4/named.conf.j2 create mode 100644 bin/tests/system/resend_loop/ns4/root.hint create mode 100644 bin/tests/system/resend_loop/tests_resend_loop.py diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py new file mode 100644 index 0000000000..217bae0301 --- /dev/null +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -0,0 +1,126 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +from collections.abc import AsyncGenerator + +import dns.edns +import dns.name +import dns.rcode +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseHandler, +) + + +def _get_cookie(qctx: QueryContext): + for o in qctx.query.options: + if o.otype == dns.edns.OptionType.COOKIE: + cookie = o + try: + if len(cookie.server) == 0: + cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" + except AttributeError: # dnspython<2.7.0 compat + if len(o.data) == 8: + cookie.data *= 2 + + return cookie + + return None + + +class PrimeHandler(ResponseHandler): + """ + Specifically handle priming query for "." NS (type 2) + """ + + def match(self, qctx: QueryContext) -> bool: + return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + ns_rrset = dns.rrset.from_text( + ".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil." + ) + a_rrset = dns.rrset.from_text( + "a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3" + ) + + response = qctx.prepare_new_response(with_zone_data=False) + response.set_rcode(dns.rcode.NOERROR) + response.answer.append(ns_rrset) + response.additional.append(a_rrset) + + yield DnsResponseSend(response, authoritative=True) + + +class CookieHandler(ResponseHandler): + def match(self, qctx: QueryContext) -> bool: + example = dns.name.from_text("example") + return qctx.qname.is_subdomain(example) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + qctx.prepare_new_response() + + # Check for client cookie + cookie = _get_cookie(qctx) + + # If missing cookie entirely, just return SERVFAIL + if cookie is None: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + yield DnsResponseSend(qctx.response, authoritative=True) + + # If there is a client cookie, mock BADCOOKIE to trigger + # the resend loop logic. + qctx.response.use_edns(options=[cookie]) + qctx.response.set_rcode(dns.rcode.BADCOOKIE) + yield DnsResponseSend(qctx.response, authoritative=True) + + +class NoErrorHandler(ResponseHandler): + """ + If the query is NOT a subdomain of example, respond with standard NOERROR empty answer + """ + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + qctx.prepare_new_response() + qctx.response.set_rcode(dns.rcode.NOERROR) + yield DnsResponseSend(qctx.response, authoritative=True) + + +def resend_server() -> AsyncDnsServer: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + PrimeHandler(), + CookieHandler(), + NoErrorHandler(), + ) + return server + + +def main() -> None: + resend_server().run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/resend_loop/ns4/named.conf.j2 b/bin/tests/system/resend_loop/ns4/named.conf.j2 new file mode 100644 index 0000000000..360bc12e17 --- /dev/null +++ b/bin/tests/system/resend_loop/ns4/named.conf.j2 @@ -0,0 +1,16 @@ +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation no; +}; + +zone "." IN { + type hint; + file "root.hint"; +}; diff --git a/bin/tests/system/resend_loop/ns4/root.hint b/bin/tests/system/resend_loop/ns4/root.hint new file mode 100644 index 0000000000..3889a8b353 --- /dev/null +++ b/bin/tests/system/resend_loop/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.3 diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py new file mode 100644 index 0000000000..f7ed4d3da6 --- /dev/null +++ b/bin/tests/system/resend_loop/tests_resend_loop.py @@ -0,0 +1,28 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +import dns.message + +import isctest + + +def test_resend_loop_badcookie(ns4): + expected_log = "exceeded max queries resolving 'test.example/A'" + + msg = dns.message.make_query("test.example", "A") + with ns4.watch_log_from_here() as watcher: + res = isctest.query.udp(msg, ns4.ip) + watcher.wait_for_line(expected_log) + + isctest.check.servfail(res) + + prohibited_log = "query failed (timed out) for test.example/IN/A" + assert prohibited_log not in ns4.log From 4094938fa3ea7d92371cf3057824f22566ff1299 Mon Sep 17 00:00:00 2001 From: Alessio Podda Date: Mon, 13 Apr 2026 15:55:38 +0200 Subject: [PATCH 04/36] Add xfr quota starvation system test Add a starvation test that tries to starve the XFR quota with unautorized requests. --- bin/tests/system/xferquota/ns1/named.conf.j2 | 2 + bin/tests/system/xferquota/ns3/named.conf.j2 | 46 +++++++++++++++++++ bin/tests/system/xferquota/ns3/quota.db | 22 +++++++++ bin/tests/system/xferquota/ns3/root.db | 21 +++++++++ bin/tests/system/xferquota/tests_xferquota.py | 42 +++++++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 bin/tests/system/xferquota/ns3/named.conf.j2 create mode 100644 bin/tests/system/xferquota/ns3/quota.db create mode 100644 bin/tests/system/xferquota/ns3/root.db diff --git a/bin/tests/system/xferquota/ns1/named.conf.j2 b/bin/tests/system/xferquota/ns1/named.conf.j2 index 9bba994dd4..90e9417c42 100644 --- a/bin/tests/system/xferquota/ns1/named.conf.j2 +++ b/bin/tests/system/xferquota/ns1/named.conf.j2 @@ -10,6 +10,8 @@ options { recursion no; dnssec-validation no; notify yes; + + transfers-out 3; }; key rndc_key { diff --git a/bin/tests/system/xferquota/ns3/named.conf.j2 b/bin/tests/system/xferquota/ns3/named.conf.j2 new file mode 100644 index 0000000000..4a0d70ca55 --- /dev/null +++ b/bin/tests/system/xferquota/ns3/named.conf.j2 @@ -0,0 +1,46 @@ +/* + * 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; }; + listen-on-v6 { none; }; + recursion no; + dnssec-validation no; + + transfers-out 1; + allow-transfer { 10.53.0.2; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "." { + type primary; + file "root.db"; +}; + +zone "quota." { + type primary; + file "quota.db"; +}; diff --git a/bin/tests/system/xferquota/ns3/quota.db b/bin/tests/system/xferquota/ns3/quota.db new file mode 100644 index 0000000000..12a67d3d2a --- /dev/null +++ b/bin/tests/system/xferquota/ns3/quota.db @@ -0,0 +1,22 @@ +; 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 ns1.quota. hostmaster.quota. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 604800 ; expire + 600 ; minimum + ) + IN NS ns1.quota. +ns1 IN A 10.53.0.3 +www IN A 10.0.0.1 diff --git a/bin/tests/system/xferquota/ns3/root.db b/bin/tests/system/xferquota/ns3/root.db new file mode 100644 index 0000000000..a5ff0fc697 --- /dev/null +++ b/bin/tests/system/xferquota/ns3/root.db @@ -0,0 +1,21 @@ +; 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 ns.root. hostmaster.root. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 604800 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.3 diff --git a/bin/tests/system/xferquota/tests_xferquota.py b/bin/tests/system/xferquota/tests_xferquota.py index 6d255085cb..5d394d070e 100644 --- a/bin/tests/system/xferquota/tests_xferquota.py +++ b/bin/tests/system/xferquota/tests_xferquota.py @@ -12,12 +12,15 @@ from re import compile as Re import glob +import multiprocessing import os import re import shutil import signal import time +import dns.message +import dns.query import dns.zone import pytest @@ -60,6 +63,9 @@ def test_xferquota(named_port, ns1, ns2): matching_line_count += 1 return matching_line_count == 300 + # The primary has 'transfers-out 3;', while the secondary has + # 'transfers-in 5; transfer-per-ns 5;'. This will allow all the zones + # to be eventually transferred, hitting the quotas now and then. isctest.run.retry_with_timeout(check_line_count, timeout=360) axfr_msg = isctest.query.create("zone000099.example.", "AXFR") @@ -80,3 +86,39 @@ def test_xferquota(named_port, ns1, ns2): with ns2.watch_log_from_start(timeout=30) as watcher: watcher.wait_for_line(pattern) query_and_compare(a_msg) + + +def _flood_unauthorized_axfrs(port, duration): + """Child process: send unauthorized AXFR requests for `duration` seconds.""" + deadline = time.monotonic() + duration + while time.monotonic() < deadline: + try: + msg = dns.message.make_query("quota.", "AXFR") + dns.query.tcp(msg, "10.53.0.3", port=port, timeout=2, source="10.53.0.1") + except Exception: # pylint: disable=broad-exception-caught + pass + + +def test_xfrquota_unauthorized_no_starve(named_port): + """Unauthorized AXFR clients must not consume XFR-out quota (GL #3859). + + ns3 is configured with transfers-out 1 and allow-transfer { 10.53.0.2; }. + We flood AXFR requests from unauthorized source processes (10.53.0.1) and + verify that an authorized client (10.53.0.2) can still transfer. + """ + with multiprocessing.Pool(10) as pool: + pool.starmap_async(_flood_unauthorized_axfrs, [(named_port, 5)] * 10) + + # Give the flood a moment to saturate + time.sleep(1) + + # Try an authorized AXFR from 10.53.0.2 multiple times to increase + # the chance of hitting the race window where quota is consumed. + zone = dns.zone.Zone("quota.") + dns.query.inbound_xfr( + "10.53.0.3", + zone, + port=named_port, + timeout=10, + source="10.53.0.2", + ) From bfb027fecdcb0229eb515982f8293825f3ae2627 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 3 Mar 2026 14:00:38 -0800 Subject: [PATCH 05/36] Disable recursion for non-IN classes Force recursion off, and set allow-recursion/allow-recursion-on ACLs to none, for views with a class other than IN. Log a configuration warning if recursion is explicitly enabled for a non-IN view. This addresses YWH-PGM40640-74 and YWH-PGM40640-75 by preventing any attempt at recursive processing in a class-CHAOS view, ensuring that server addresses used for recursive queries and received in recursive responses are of the expected format. Fixes: isc-projects/bind9#5780 Fixes: isc-projects/bind9#5781 --- bin/named/server.c | 37 +++++-------------- bin/tests/system/checkconf/tests.sh | 1 + .../nohintswarn_bindchaos/ns1/named.conf.j2 | 23 ------------ .../tests_nohintswarn_bindchaos.py | 25 ------------- bin/tests/system/resolver/tests.sh | 8 ++-- lib/isccfg/check.c | 21 +++++++++-- 6 files changed, 33 insertions(+), 82 deletions(-) delete mode 100644 bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 delete mode 100644 bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py diff --git a/bin/named/server.c b/bin/named/server.c index e9f2329b7c..1a98450e81 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -3926,7 +3926,8 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, obj = NULL; result = named_config_get(maps, "recursion", &obj); INSIST(result == ISC_R_SUCCESS); - view->recursion = cfg_obj_asboolean(obj); + view->recursion = (view->rdclass == dns_rdataclass_in && + cfg_obj_asboolean(obj)); max_cache_size = configure_max_cache_size(view, maps); @@ -4543,35 +4544,13 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, /* * We have default root hints for class IN if we need them. * Each view gets its own rootdb so a priming response only - * writes into that view's copy. + * writes into that view's copy. Other classes don't support + * recursion and don't need hints. */ if (view->rdclass == dns_rdataclass_in && view->rootdb == NULL) { CHECK(configure_rootdb(view, NULL)); } - /* - * If we still have no root hints, this is a non-IN view with no - * "hints zone" configured. Issue a warning, except if this - * is a root server. Root servers never need to consult - * their hints, so it's no point requiring users to configure - * them. - */ - if (view->rootdb == NULL) { - dns_zone_t *rootzone = NULL; - (void)dns_view_findzone(view, dns_rootname, DNS_ZTFIND_EXACT, - &rootzone); - if (rootzone != NULL) { - dns_zone_detach(&rootzone); - } else if (strcmp(view->name, "_bind") != 0 || - view->rdclass != dns_rdataclass_chaos) - { - isc_log_write(NAMED_LOGCATEGORY_GENERAL, - NAMED_LOGMODULE_SERVER, ISC_LOG_WARNING, - "no root hints for view '%s'", - view->name); - } - } - /* * Configure the view's transports (DoT/DoH) */ @@ -4794,9 +4773,11 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, CHECK(configure_view_acl(vconfig, config, "allow-proxy-on", NULL, aclctx, isc_g_mctx, &view->proxyonacl)); - if (strcmp(view->name, "_bind") != 0 && - view->rdclass != dns_rdataclass_chaos) - { + if (view->rdclass != dns_rdataclass_in) { + view->recursion = false; + dns_acl_none(isc_g_mctx, &view->recursionacl); + dns_acl_none(isc_g_mctx, &view->recursiononacl); + } else { CHECK(configure_view_acl(vconfig, config, "allow-recursion", NULL, aclctx, isc_g_mctx, &view->recursionacl)); diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 69d96b7ea8..f927d78919 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -515,6 +515,7 @@ $CHECKCONF -l good.conf \ | grep -v "is not implemented" \ | grep -v "is not recommended" \ | grep -v "no longer exists" \ + | grep -v "recursion will be disabled" \ | grep -v "is obsolete" >checkconf.out$n || ret=1 diff good.zonelist checkconf.out$n >diff.out$n || ret=1 if [ $ret -ne 0 ]; then diff --git a/bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 b/bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 deleted file mode 100644 index 57389c0521..0000000000 --- a/bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 +++ /dev/null @@ -1,23 +0,0 @@ -options { - port @PORT@; - pid-file "named.pid"; - listen-on { 10.53.0.1; }; -}; - -key rndc_key { - secret "1234abcd8765"; - algorithm @DEFAULT_HMAC@; -}; - -controls { - inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; -}; - -view _bind { -}; - -view foo { -}; - -view bar ch { -}; diff --git a/bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py b/bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py deleted file mode 100644 index bb788d1708..0000000000 --- a/bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 isctest - - -def test_nohintswarn_bindchaos(ns1): - found = True - try: - with ns1.watch_log_from_start(timeout=1) as watcher: - watcher.wait_for_line("no root hints for view '_bind'") - except isctest.log.watchlog.WatchLogTimeout: - found = False - assert found is False - - with ns1.watch_log_from_start() as watcher: - watcher.wait_for_line("no root hints for view 'bar'") diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh index 18893edd69..06ef98f697 100755 --- a/bin/tests/system/resolver/tests.sh +++ b/bin/tests/system/resolver/tests.sh @@ -783,10 +783,12 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) n=$((n + 1)) -echo_i "checking NXDOMAIN is returned when querying non existing domain in CH class ($n)" +echo_i "checking REFUSED is returned when querying non existing domain in CH class ($n)" ret=0 -dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.${n} || ret=1 -grep "status: NXDOMAIN" dig.ns1.out.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 hostname.chaostest txt ch >dig.ns1.out.1.${n} || ret=1 +grep "status: NOERROR" dig.ns1.out.1.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.2.${n} || ret=1 +grep "status: REFUSED" dig.ns1.out.2.${n} >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c index 232d4d1890..2995319627 100644 --- a/lib/isccfg/check.c +++ b/lib/isccfg/check.c @@ -2869,13 +2869,17 @@ check_mirror_zone_notify(const cfg_obj_t *zoptions, const char *znamestr) { */ static bool check_recursion(const cfg_obj_t *config, const cfg_obj_t *voptions, - const cfg_obj_t *goptions, cfg_aclconfctx_t *aclctx, - isc_mem_t *mctx) { + dns_rdataclass_t vclass, const cfg_obj_t *goptions, + cfg_aclconfctx_t *aclctx, isc_mem_t *mctx) { dns_acl_t *acl = NULL; const cfg_obj_t *obj; isc_result_t result = ISC_R_SUCCESS; bool retval = true; + if (vclass != dns_rdataclass_in) { + return false; + } + /* * Check the "recursion" option first. */ @@ -3827,7 +3831,7 @@ isccfg_check_zoneconf(const cfg_obj_t *zconfig, const cfg_obj_t *voptions, * contradicts the purpose of the former. */ if (ztype == CFG_ZONE_MIRROR && - !check_recursion(config, voptions, goptions, aclctx, mctx)) + !check_recursion(config, voptions, zclass, goptions, aclctx, mctx)) { cfg_obj_log(zoptions, ISC_LOG_ERROR, "zone '%s': mirror zones cannot be used if " @@ -5646,6 +5650,17 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, cfg_aclconfctx_create(mctx, &aclctx); + if (vclass != dns_rdataclass_in) { + if (check_recursion(config, voptions, dns_rdataclass_in, + options, aclctx, mctx)) + { + cfg_obj_log(opts, ISC_LOG_WARNING, + "recursion will be disabled for " + "non-IN view '%s'", + viewname); + } + } + if (voptions != NULL) { (void)cfg_map_get(voptions, "zone", &zones); } else { From c9997e0dd9222178187a63d5b78447a430906574 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 5 Feb 2026 11:20:11 +0100 Subject: [PATCH 06/36] 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 NS processing limit (max-delegation-servers) and the ADB address limit (adbaddrslimit), both individually and combined. --- .../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 | 30 +++ .../system/selfpointedglue/ns3/example.tld.db | 184 ++++++++++++++++++ .../selfpointedglue/ns3/example2.tld.db | 33 ++++ .../selfpointedglue/ns3/example3.tld.db | 63 ++++++ .../system/selfpointedglue/ns3/named.conf.j2 | 49 +++++ .../system/selfpointedglue/ns4/named.args.j2 | 1 + .../system/selfpointedglue/ns4/named.conf.j2 | 59 ++++++ .../system/selfpointedglue/ns4/root.hint | 14 ++ .../selfpointedglue/tests_selfpointedglue.py | 157 +++++++++++++++ 12 files changed, 670 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/example3.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..66f7925d99 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/tld.db @@ -0,0 +1,30 @@ +; 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 + +example3.tld. NS ns.example3.tld. +ns.example3.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..2a599ee876 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example.tld.db @@ -0,0 +1,184 @@ +; 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 +ns01.sub.example.tld. A 127.0.0.1 +ns01.sub.example.tld. A 127.0.0.2 +; Those addresses won't be used (exceed the max-delegation-servers). +ns01.sub.example.tld. A 127.0.0.3 +ns01.sub.example.tld. A 127.0.0.4 + +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 +ns02.sub.example.tld. A 127.0.0.2 +ns02.sub.example.tld. A 127.0.0.3 +ns02.sub.example.tld. A 127.0.0.4 + +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 +ns03.sub.example.tld. A 127.0.0.2 +ns03.sub.example.tld. A 127.0.0.3 +ns03.sub.example.tld. A 127.0.0.4 + +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 +ns04.sub.example.tld. A 127.0.0.2 +ns04.sub.example.tld. A 127.0.0.3 +ns04.sub.example.tld. A 127.0.0.4 + +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 +ns05.sub.example.tld. A 127.0.0.2 +ns05.sub.example.tld. A 127.0.0.3 +ns05.sub.example.tld. A 127.0.0.4 + +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 +ns06.sub.example.tld. A 127.0.0.2 +ns06.sub.example.tld. A 127.0.0.3 +ns06.sub.example.tld. A 127.0.0.4 + +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 +ns07.sub.example.tld. A 127.0.0.2 +ns07.sub.example.tld. A 127.0.0.3 +ns07.sub.example.tld. A 127.0.0.4 + +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 +ns08.sub.example.tld. A 127.0.0.2 +ns08.sub.example.tld. A 127.0.0.3 +ns08.sub.example.tld. A 127.0.0.4 + +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 +ns09.sub.example.tld. A 127.0.0.2 +ns09.sub.example.tld. A 127.0.0.3 +ns09.sub.example.tld. A 127.0.0.4 + +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 +ns10.sub.example.tld. A 127.0.0.2 +ns10.sub.example.tld. A 127.0.0.3 +ns10.sub.example.tld. A 127.0.0.4 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/example3.tld.db b/bin/tests/system/selfpointedglue/ns3/example3.tld.db new file mode 100644 index 0000000000..e2c522cc3e --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example3.tld.db @@ -0,0 +1,63 @@ +; 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 +example3.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example3.tld. NS ns.example3.tld. +ns.example3.tld. A 10.53.0.3 + +sub.example3.tld. NS ns01.sub.example3.tld. +sub.example3.tld. NS ns02.sub.example3.tld. +sub.example3.tld. NS ns03.sub.example3.tld. +sub.example3.tld. NS ns04.sub.example3.tld. +sub.example3.tld. NS ns05.sub.example3.tld. +sub.example3.tld. NS ns06.sub.example3.tld. +sub.example3.tld. NS ns07.sub.example3.tld. +sub.example3.tld. NS ns08.sub.example3.tld. +sub.example3.tld. NS ns09.sub.example3.tld. +sub.example3.tld. NS ns10.sub.example3.tld. + +ns01.sub.example3.tld. A 10.53.0.5 +ns01.sub.example3.tld. A 10.53.0.6 + +ns02.sub.example3.tld. A 10.53.0.5 +ns02.sub.example3.tld. A 10.53.0.6 + +ns03.sub.example3.tld. A 10.53.0.5 +ns03.sub.example3.tld. A 10.53.0.6 + +ns04.sub.example3.tld. A 10.53.0.5 +ns04.sub.example3.tld. A 10.53.0.6 + +ns05.sub.example3.tld. A 10.53.0.5 +ns05.sub.example3.tld. A 10.53.0.6 + +ns06.sub.example3.tld. A 10.53.0.5 +ns06.sub.example3.tld. A 10.53.0.6 + +ns07.sub.example3.tld. A 10.53.0.5 +ns07.sub.example3.tld. A 10.53.0.6 + +ns08.sub.example3.tld. A 10.53.0.5 +ns08.sub.example3.tld. A 10.53.0.6 + +ns09.sub.example3.tld. A 10.53.0.5 +ns09.sub.example3.tld. A 10.53.0.6 + +ns10.sub.example3.tld. A 10.53.0.5 +ns10.sub.example3.tld. A 10.53.0.6 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..0c50ca8658 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 @@ -0,0 +1,49 @@ +/* + * 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"; +}; + +zone "example3.tld." { + type primary; + file "example3.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..68f1511c7c --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.args.j2 @@ -0,0 +1 @@ +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 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..d559b76ea8 --- /dev/null +++ b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py @@ -0,0 +1,157 @@ +# 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_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 expect_query(expected_query, expected_query_count, ips_and_queries): + count = 0 + for _, query in ips_and_queries: + if query == expected_query: + count += 1 + assert count == expected_query_count + + +def test_selfpointedglue1(ns4): + msg = isctest.query.create("a.sub.example.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + # 4 queries to get to the delegation. + # 13 queries to delegation NS servers. + ips_and_queries = extract_dnstap(ns4, 17) + + # Thanks to the de-duplication, only the first 13 NS IPs are + # queried (once sub.example.tld. NS is found) instead of 13*10 + # (13 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"), + ("10.53.0.10", "a.sub.example.tld/IN/A"), + ("10.53.1.1", "a.sub.example.tld/IN/A"), + ("10.53.1.2", "a.sub.example.tld/IN/A"), + ("10.53.2.1", "a.sub.example.tld/IN/A"), + ("10.53.0.3", "a.sub.example.tld/IN/A"), + ("127.0.0.1", "a.sub.example.tld/IN/A"), + ("127.0.0.2", "a.sub.example.tld/IN/A"), + ], + ips_and_queries, + ) + + +# This test is useful because the one above hits the max-delegation-servers +# from the first NS name lookup. This one doesn't, because there is only 2 +# addresses per NS, but the deduplication avoid the explosion of duplicate +# addresses. +def test_selfpointedglue2(ns4): + with ns4.watch_log_from_here() as watcher: + ns4.rndc("flush") + ns4.rndc("reload") + watcher.wait_for_line("running") + msg = isctest.query.create("a.sub.example3.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + # 4 queries to get to the delegation. + # 2 queries to delegation NS servers. + ips_and_queries = extract_dnstap(ns4, 6) + + # Thanks to the de-duplication, only the first 2 NS IPs are + # queried (once sub.example.tld. NS is found) instead of 2*10 + # (2 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", "example3.tld/IN/NS"), + ("10.53.0.3", "sub.example3.tld/IN/NS"), + ("10.53.0.5", "a.sub.example3.tld/IN/A"), + ("10.53.0.6", "a.sub.example3.tld/IN/A"), + ], + ips_and_queries, + ) + + +def test_selfpointedglue_nslimit(ns4, templates): + templates.render( + "ns4/named.conf", {"maxdelegationservers": "max-delegation-servers 2;"} + ) + with ns4.watch_log_from_here() as watcher: + ns4.rndc("flush") + ns4.rndc("reload") + watcher.wait_for_line("running") + + msg = isctest.query.create("a.sub.example2.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, 6) + + # Checking the beginning of the resolution + expect_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example2.tld/IN/NS"), + ("10.53.0.3", "sub.example2.tld/IN/NS"), + ], + ips_and_queries, + ) + + expect_query("a.sub.example2.tld/IN/A", 2, ips_and_queries) From 11aae777a7da87974951ca75e58ddd49d72ed076 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Tue, 7 Apr 2026 22:18:10 +0200 Subject: [PATCH 07/36] Refactor incrementing query counters Move the logic incrementing the query counter and the global query counter into a dedicated helper function. --- lib/dns/resolver.c | 60 ++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 19c4c29f0d..5fed2760de 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -4285,6 +4285,39 @@ fctx_nextaddress(fetchctx_t *fctx) { return addrinfo; } +static isc_result_t +incr_query_counters(fetchctx_t *fctx) { + isc_result_t result; + + result = isc_counter_increment(fctx->qc); +#if WANT_QUERYTRACE + FCTXTRACE5("query", "max-recursion-queries, querycount=", + isc_counter_used(fctx->qc)); +#endif + if (result != ISC_R_SUCCESS) { + isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, + ISC_LOG_DEBUG(3), + "exceeded max queries resolving '%s' " + "(max-recursion-queries, querycount=%u)", + fctx->info, isc_counter_used(fctx->qc)); + } else if (fctx->gqc != NULL) { + result = isc_counter_increment(fctx->gqc); +#if WANT_QUERYTRACE + FCTXTRACE5("query", "max-query-count, querycount=", + isc_counter_used(fctx->gqc)); +#endif + if (result != ISC_R_SUCCESS) { + isc_log_write(DNS_LOGCATEGORY_RESOLVER, + DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), + "exceeded global max queries resolving " + "'%s' (max-query-count, querycount=%u)", + fctx->info, isc_counter_used(fctx->gqc)); + } + } + + return result; +} + static void fctx_try(fetchctx_t *fctx, bool retrying) { isc_result_t result; @@ -4425,36 +4458,11 @@ fctx_try(fetchctx_t *fctx, bool retrying) { return; } - result = isc_counter_increment(fctx->qc); -#if WANT_QUERYTRACE - FCTXTRACE5("query", "max-recursion-queries, querycount=", - isc_counter_used(fctx->qc)); -#endif + result = incr_query_counters(fctx); if (result != ISC_R_SUCCESS) { - isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, - ISC_LOG_DEBUG(3), - "exceeded max queries resolving '%s' " - "(max-recursion-queries, querycount=%u)", - fctx->info, isc_counter_used(fctx->qc)); goto done; } - if (fctx->gqc != NULL) { - result = isc_counter_increment(fctx->gqc); -#if WANT_QUERYTRACE - FCTXTRACE5("query", "max-query-count, querycount=", - isc_counter_used(fctx->gqc)); -#endif - if (result != ISC_R_SUCCESS) { - isc_log_write(DNS_LOGCATEGORY_RESOLVER, - DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), - "exceeded global max queries resolving " - "'%s' (max-query-count, querycount=%u)", - fctx->info, isc_counter_used(fctx->gqc)); - goto done; - } - } - result = fctx_query(fctx, addrinfo, fctx->options); if (result != ISC_R_SUCCESS) { goto done; From e66ec9b67fbba045f081041ed6d4c7fb53076502 Mon Sep 17 00:00:00 2001 From: Aram Sargsyan Date: Tue, 31 Mar 2026 13:00:00 +0000 Subject: [PATCH 08/36] Apply XFR-out quota after ACL is checked Unauthorized clients can consume XFR-out quota and block authorized XFR clients. Apply the quota after ACL is checked. --- lib/ns/xfrout.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/ns/xfrout.c b/lib/ns/xfrout.c index 98c7374b4f..1865f6b1f4 100644 --- a/lib/ns/xfrout.c +++ b/lib/ns/xfrout.c @@ -738,6 +738,7 @@ ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { bool is_poll = false; bool is_dlz = false; bool is_ixfr = false; + bool is_quota_applied = false; bool useviewacl = false; uint32_t begin_serial = 0, current_serial; @@ -754,16 +755,6 @@ ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(6), "%s request", mnemonic); - /* - * Apply quota. - */ - result = isc_quota_acquire(&client->manager->sctx->xfroutquota); - if (result != ISC_R_SUCCESS) { - isc_log_write(DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, - ISC_LOG_WARNING, "%s request denied: %s", - mnemonic, isc_result_totext(result)); - goto max_quota; - } /* * Interpret the question section. @@ -922,6 +913,19 @@ got_soa: FAILC(DNS_R_FORMERR, "attempted AXFR over UDP"); } + /* + * Apply quota after ACL is checked, so that unauthorized clients + * can not starve the authorized clients. + */ + result = isc_quota_acquire(&client->manager->sctx->xfroutquota); + if (result != ISC_R_SUCCESS) { + isc_log_write(DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, + ISC_LOG_WARNING, "%s request denied: %s", + mnemonic, isc_result_totext(result)); + goto cleanup; + } + is_quota_applied = true; + /* * Look up the requesting server in the peer table. */ @@ -1060,7 +1064,7 @@ have_stream: CHECK(dns_message_getquerytsig(request, mctx, &tsigbuf)); /* * Create the xfrout context object. This transfers the ownership - * of "stream", "db", "ver", and "quota" to the xfrout context object. + * of "stream", "db" and "ver" to the xfrout context object. */ if (is_dlz) { @@ -1179,10 +1183,13 @@ cleanup: } if (xfr != NULL) { + /* The quota will be released in xfrout_ctx_destroy(). */ + INSIST(is_quota_applied); xfrout_fail(xfr, result, "setting up zone transfer"); } else if (result != ISC_R_SUCCESS) { - isc_quota_release(&client->manager->sctx->xfroutquota); - max_quota: + if (is_quota_applied) { + isc_quota_release(&client->manager->sctx->xfroutquota); + } ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(3), "zone transfer setup failed"); From 6ba5e87a08a0ec38efbbfc2cedef71660221778f Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Wed, 4 Mar 2026 13:24:52 -0800 Subject: [PATCH 09/36] Disable UPDATE and NOTIFY for non-IN classes Return NOTIMP for UPDATE and NOTIFY requests received for views with a class other than IN. Only QUERY is now supported for non-IN views such as CHAOS. When running dns dns_rdata_tostruct() with types that are only defined for class IN, ensure that the class is correct before proceeding. Add an assertion that any zone being updated is of class IN. (Note that previously, a DLZ zone could have its class value set incorrectly to NONE; this has been fixed.) This addresses YWH-PGM40640-70 and YWH-PGM40640-73 (as well as any similar problems that might have occurred in the future) by minimizing the code paths that can be reached by rdata classes other than IN, so it is safe for the implementation to assume that rdatatypes that are only defined for class IN, such as SVCB or WKS, have been parsed and validated, and not accepted as unknown/opaque data. Fixes: isc-projects/bind9#5777 Fixes: isc-projects/bind9#5779 --- bin/named/server.c | 1 + lib/dns/adb.c | 5 +++-- lib/ns/client.c | 8 ++++++++ lib/ns/update.c | 42 ++++++++++++++++++++++-------------------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/bin/named/server.c b/bin/named/server.c index 1a98450e81..d584c1249e 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -1715,6 +1715,7 @@ dlzconfigure_callback(dns_view_t *view, dns_dlzdb_t *dlzdb, dns_zone_t *zone) { dns_name_t *origin = dns_zone_getorigin(zone); dns_rdataclass_t zclass = view->rdclass; + dns_zone_setclass(zone, zclass); RETERR(dns_zonemgr_managezone(named_g_server->zonemgr, zone)); dns_zone_setstats(zone, named_g_server->zonestats); diff --git a/lib/dns/adb.c b/lib/dns/adb.c index f51b2a8821..758f0d8187 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -566,6 +566,9 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdtype = rdataset->type; + REQUIRE(rdataset->rdclass == dns_rdataclass_in); + REQUIRE(dns_rdatatype_isaddr(rdtype)); + switch (rdataset->trust) { case dns_trust_glue: case dns_trust_additional: @@ -584,8 +587,6 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdataset->ttl = ttlclamp(rdataset->ttl); } - REQUIRE(dns_rdatatype_isaddr(rdtype)); - DNS_RDATASET_FOREACH(rdataset) { /* FIXME: Move to a separate function */ dns_adbnamehooklist_t *hookhead = NULL; diff --git a/lib/ns/client.c b/lib/ns/client.c index 7a4fd19771..2faf971041 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -2459,6 +2459,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_update: CTRACE("update"); + if (client->inner.view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } #ifdef HAVE_DNSTAP dns_dt_send(client->inner.view, DNS_DTTYPE_UQ, &client->inner.peeraddr, @@ -2472,6 +2476,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_notify: CTRACE("notify"); + if (client->inner.view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } ns_client_settimeout(client, 60); ns_notify_start(client, client->inner.handle); break; diff --git a/lib/ns/update.c b/lib/ns/update.c index 93240ca250..b5ea582879 100644 --- a/lib/ns/update.c +++ b/lib/ns/update.c @@ -966,7 +966,9 @@ ssu_checkrr(void *data, rr_t *rr) { RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &ptr.ptr; } - if (rr->rdata.type == dns_rdatatype_srv) { + if (rr->rdata.rdclass == dns_rdataclass_in && + rr->rdata.type == dns_rdatatype_srv) + { result = dns_rdata_tostruct(&rr->rdata, &srv, NULL); RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &srv.target; @@ -1311,7 +1313,10 @@ replaces_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { return true; } } - if (db_rr->type == dns_rdatatype_wks) { + + if (db_rr->rdclass == dns_rdataclass_in && + db_rr->type == dns_rdatatype_wks) + { /* * Compare the address and protocol fields only. These * form the first five bytes of the RR data. Do a @@ -1451,9 +1456,8 @@ add_rr_prepare_action(void *data, rr_t *rr) { * 'rdata', and 'ttl', respectively. */ static void -get_current_rr(dns_rdataclass_t zoneclass, dns_name_t *name, dns_rdata_t *rdata, - dns_rdatatype_t *covers, dns_ttl_t *ttl, - dns_rdataclass_t *update_class) { +get_current_rr(dns_name_t *name, dns_rdata_t *rdata, dns_rdatatype_t *covers, + dns_ttl_t *ttl, dns_rdataclass_t *update_class) { dns_rdataset_t *rdataset; isc_result_t result; rdataset = ISC_LIST_HEAD(name->list); @@ -1466,7 +1470,7 @@ get_current_rr(dns_rdataclass_t zoneclass, dns_name_t *name, dns_rdata_t *rdata, dns_rdataset_current(rdataset, rdata); INSIST(dns_rdataset_next(rdataset) == ISC_R_NOMORE); *update_class = rdata->rdclass; - rdata->rdclass = zoneclass; + rdata->rdclass = dns_rdataclass_in; } /*% @@ -1562,7 +1566,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { dns_message_t *request = client->message; isc_mem_t *mctx = client->manager->mctx; dns_aclenv_t *env = client->manager->aclenv; - dns_rdataclass_t zoneclass; dns_rdatatype_t covers; dns_name_t *zonename = NULL; unsigned int *maxbytype = NULL; @@ -1574,11 +1577,13 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); dns_zone_getssutable(zone, &ssutable); options = dns_zone_getoptions(zone); dns_db_currentversion(db, &ver); + /* Updates are only supported for class IN. */ + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + /* * Update message processing can leak record existence information * so check that we are allowed to query this zone. Additionally, @@ -1623,13 +1628,12 @@ send_update(ns_client_t *client, dns_zone_t *zone) { dns_rdataclass_t update_class; INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(zoneclass, name, &rdata, &covers, &ttl, - &update_class); + get_current_rr(name, &rdata, &covers, &ttl, &update_class); if (!dns_name_issubdomain(name, zonename)) { FAILC(DNS_R_NOTZONE, "update RR is outside zone"); } - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * Check for meta-RRs. The RFC2136 pseudocode says * check for ANY|AXFR|MAILA|MAILB, but the text adds @@ -1643,6 +1647,7 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CLEANUP(DNS_R_REFUSED); } if ((options & DNS_ZONEOPT_CHECKSVCB) != 0 && + rdata.rdclass == dns_rdataclass_in && rdata.type == dns_rdatatype_svcb) { result = dns_rdata_checksvcb(name, &rdata); @@ -1731,7 +1736,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { } if (update_class == dns_rdataclass_any && - zoneclass == dns_rdataclass_in && (rdata.type == dns_rdatatype_ptr || rdata.type == dns_rdatatype_srv)) { @@ -2629,7 +2633,6 @@ update_action(void *arg) { isc_mem_t *mctx = client->manager->mctx; dns_rdatatype_t covers; dns_message_t *request = client->message; - dns_rdataclass_t zoneclass; dns_name_t *zonename = NULL; dns_fixedname_t tmpnamefixed; dns_name_t *tmpname = NULL; @@ -2646,9 +2649,10 @@ update_action(void *arg) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); options = dns_zone_getoptions(zone); + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + is_inline = (!dns_zone_israw(zone) && dns_zone_issecure(zone)); is_maintain = (dns_zone_getkasp(zone) != NULL) && !dns_zone_israw(zone); is_signing = is_inline || is_maintain; @@ -2669,8 +2673,7 @@ update_action(void *arg) { dns_rdataclass_t update_class; bool flag; - get_current_rr(zoneclass, name, &rdata, &covers, &ttl, - &update_class); + get_current_rr(name, &rdata, &covers, &ttl, &update_class); if (ttl != 0) { PREREQFAILC(DNS_R_FORMERR, @@ -2733,7 +2736,7 @@ update_action(void *arg) { "prerequisite not satisfied"); } } - } else if (update_class == zoneclass) { + } else if (update_class == dns_rdataclass_in) { /* "temp += rr;" */ temp_append(&temp, name, &rdata); } else { @@ -2784,10 +2787,9 @@ update_action(void *arg) { INSIST(ssutable == NULL || maxidx < maxbytypelen); - get_current_rr(zoneclass, name, &rdata, &covers, &ttl, - &update_class); + get_current_rr(name, &rdata, &covers, &ttl, &update_class); - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * RFC1123 doesn't allow MF and MD in master files. */ From 9bf3df7073b600aa519f5488c6c408aa45002336 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 4 Mar 2026 18:25:32 +0100 Subject: [PATCH 10/36] 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. --- 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") From 4aedf7e9dda89bed7b3c6f22ad7078cd3bcbcb8b Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Fri, 10 Apr 2026 14:54:49 +0200 Subject: [PATCH 11/36] Do not resend after BADCOOKIE answer on TCP When an upstream server answers BADCOOKIE, no matter the transport used, the resolver eventually resends the query using TCP. However, if the upstream server responds with BADCOOKIE again over TCP, the resolver would keep resending until the maximum query count is reached. This is now fixed by stopping resending once the query has already been sent over TCP. --- lib/dns/resolver.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index d7418d494a..742748d2f3 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -9804,7 +9804,9 @@ rctx_badserver(respctx_t *rctx, isc_result_t result) { rctx->broken_server = DNS_R_BADVERS; rctx->next_server = true; #endif /* if DNS_EDNS_VERSION > 0 */ - } else if (rcode == dns_rcode_badcookie && rctx->query->rmessage->cc_ok) + } else if (rcode == dns_rcode_badcookie && + rctx->query->rmessage->cc_ok && + (rctx->retryopts & DNS_FETCHOPT_TCP) == 0) { /* * We have recorded the new cookie. From d9ee3b1de087ce07c768e339f52c645c1b981740 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Tue, 7 Apr 2026 22:18:58 +0200 Subject: [PATCH 12/36] rctx_resend() increment query counters Calls to `rctx_resend()` are done internally within the resolver, in flow which are not supposed to happens more than once. For instance, if some query fails, and a specific flag "F" wasn't set, then set the flag and try again. This wouldn't occur more than once because if the query fails the next attempt, the flag "F" would be set already, so the resolver would move to the next server (or give up). However, a subtle bug missing checking a flag, for instance, could lead to an unbounded loop re-trying to query the same server. This is now impossible as `rctx_resend()` also increment the query counters (so if such case occurs, it would stop once the maximum limit is reached). The dns_resstatscounter_retry are also only incremented if the `fctx_query()` succeeds, similar to as is done in `fctx_try()`. --- lib/dns/resolver.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 5fed2760de..d7418d494a 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -9467,9 +9467,9 @@ rctx_nextserver(respctx_t *rctx, dns_message_t *message, * rctx_resend(): * * Resend the query, probably with the options changed. Calls - * fctx_query(), passing rctx->retryopts (which is based on - * query->options, but may have been updated since the last time - * fctx_query() was called). + * fctx_query(), unless query counter limits are hit, passing + * rctx->retryopts (which is based on query->options, but may have + * been updated since the last time fctx_query() was called). */ static void rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) { @@ -9477,8 +9477,15 @@ rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) { isc_result_t result; FCTXTRACE("resend"); - inc_stats(fctx->res, dns_resstatscounter_retry); + + CHECK(incr_query_counters(fctx)); + result = fctx_query(fctx, addrinfo, rctx->retryopts); + if (result == ISC_R_SUCCESS) { + inc_stats(fctx->res, dns_resstatscounter_retry); + } + +cleanup: if (result != ISC_R_SUCCESS) { fctx_failure_detach(&rctx->fctx, result); } From 967776d94dc2cd13a7243352e70c318422052e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 4 Mar 2026 10:46:58 +0100 Subject: [PATCH 13/36] Validate DNS message CLASS early in request processing Reject requests with unsupported or misused CLASS values before further processing. Only IN, CH, HS, RESERVED0 (for DNS Cookies), ANY (for TKEY negotiation), and NONE (for DNS UPDATE) are accepted; all other classes return NOTIMP. Misuse of NONE or ANY outside their allowed contexts returns FORMERR. This adds further protection against bugs of the same general class as YWH-PGM40640-70 and YWH-PGM40640-73. --- bin/tests/system/unknown/tests.sh | 17 ++++++---- lib/ns/client.c | 55 ++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/bin/tests/system/unknown/tests.sh b/bin/tests/system/unknown/tests.sh index eb61f21f28..cbc2943f17 100644 --- a/bin/tests/system/unknown/tests.sh +++ b/bin/tests/system/unknown/tests.sh @@ -25,6 +25,11 @@ dig_cmd() { "$DIG" $DIGOPTS "$@" | grep -v '^;' } +dig_full() { + # shellcheck disable=SC2086 + "$DIG" $DIGOPTS "$@" +} + n=$((n + 1)) echo_i "querying for various representations of an IN A record ($n)" for i in 1 2 3 4 5 6 7 8 9 10 11 12; do @@ -81,8 +86,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE1 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n - echo '\# 4 0A000001' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -93,8 +98,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TXT record ($n)" for i in 1 2 3 4; do ret=0 - dig_cmd +short @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n - echo '"hello"' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -105,8 +110,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE123 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n - echo '\# 1 00' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi diff --git a/lib/ns/client.c b/lib/ns/client.c index 2faf971041..7a48d613af 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -2082,7 +2083,9 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, } } - if (client->message->rdclass == 0) { + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + switch (client->message->rdclass) { + case dns_rdataclass_reserved0: if ((client->inner.attributes & NS_CLIENTATTR_WANTCOOKIE) != 0 && client->message->opcode == dns_opcode_query && @@ -2102,12 +2105,46 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, return; } + ns_client_dumpmessage(client, + "message class could not be determined"); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + return; + case dns_rdataclass_in: + break; + case dns_rdataclass_chaos: + break; + case dns_rdataclass_hs: + break; + case dns_rdataclass_none: + if (client->message->opcode != dns_opcode_update) { + ns_client_dumpmessage(client, + "message class NONE can be only " + "used in DNS updates"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + case dns_rdataclass_any: + /* + * Required for TKEY negotiation. + */ + if (client->message->tkey == 0) { + ns_client_dumpmessage(client, + "message class ANY can be only " + "used for TKEY negotiation"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + default: + dns_rdataclass_format(client->message->rdclass, classbuf, + sizeof(classbuf)); + ns_client_dumpmessage(client, NULL); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), - "message class could not be determined"); - ns_client_dumpmessage(client, "message class could not be " - "determined"); - ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + "invalid message class: %s", classbuf); + + ns_client_error(client, DNS_R_NOTIMP); return; } @@ -2192,9 +2229,6 @@ ns_client_request_continue(void *arg) { "SIG(0) checks quota reached"); if (can_log_sigchecks_quota()) { - ns_client_log(client, NS_LOGCATEGORY_CLIENT, - NS_LOGMODULE_CLIENT, ISC_LOG_INFO, - "SIG(0) checks quota reached"); ns_client_dumpmessage( client, "SIG(0) checks quota reached"); } @@ -2204,12 +2238,11 @@ ns_client_request_continue(void *arg) { dns_rdataclass_format(client->message->rdclass, classname, sizeof(classname)); + ns_client_dumpmessage(client, NULL); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "no matching view in class '%s'", classname); - ns_client_dumpmessage(client, - "no matching view in class"); } dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); @@ -2843,7 +2876,7 @@ ns_client_dumpmessage(ns_client_t *client, const char *reason) { int len = 1024; isc_result_t result; - if (!isc_log_wouldlog(ISC_LOG_DEBUG(1))) { + if (!isc_log_wouldlog(ISC_LOG_DEBUG(1)) || reason == NULL) { return; } From c50a743794e6047d67e42a15ba639b01340b34de Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 1 Apr 2026 22:31:50 +0200 Subject: [PATCH 14/36] add max-delegation-servers tests for out domain NS Add a new system test which ensures that the `max-delegation-servers` limit is correctly respected also in the case a domain has only NS names (and no glues). In particular, this test when there are multiple NS names and multiples IPs per names. If the number of IP (even from the first picked NS name) reaches `max-delegation-servers`, and the resolution is not a success, the resolver won't attempt another NS name, as it already used all its "credit". --- .../nslimit_outdomain/ns1/named.conf.j2 | 28 +++ .../system/nslimit_outdomain/ns1/root.db | 24 +++ .../nslimit_outdomain/ns2/dnshoster.tld.db | 33 ++++ .../nslimit_outdomain/ns2/named.conf.j2 | 41 ++++ bin/tests/system/nslimit_outdomain/ns2/tld.db | 36 ++++ .../nslimit_outdomain/ns3/example.tld.db | 184 ++++++++++++++++++ .../nslimit_outdomain/ns3/example4.tld.db | 24 +++ .../nslimit_outdomain/ns3/named.conf.j2 | 30 +++ .../nslimit_outdomain/ns4/named.args.j2 | 1 + .../nslimit_outdomain/ns4/named.conf.j2 | 59 ++++++ .../system/nslimit_outdomain/ns4/root.hint | 14 ++ .../tests_nslimit_outdomain.py | 120 ++++++++++++ 12 files changed, 594 insertions(+) create mode 100644 bin/tests/system/nslimit_outdomain/ns1/named.conf.j2 create mode 100644 bin/tests/system/nslimit_outdomain/ns1/root.db create mode 100644 bin/tests/system/nslimit_outdomain/ns2/dnshoster.tld.db create mode 100644 bin/tests/system/nslimit_outdomain/ns2/named.conf.j2 create mode 100644 bin/tests/system/nslimit_outdomain/ns2/tld.db create mode 100644 bin/tests/system/nslimit_outdomain/ns3/example.tld.db create mode 100644 bin/tests/system/nslimit_outdomain/ns3/example4.tld.db create mode 100644 bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 create mode 100644 bin/tests/system/nslimit_outdomain/ns4/named.args.j2 create mode 100644 bin/tests/system/nslimit_outdomain/ns4/named.conf.j2 create mode 100644 bin/tests/system/nslimit_outdomain/ns4/root.hint create mode 100644 bin/tests/system/nslimit_outdomain/tests_nslimit_outdomain.py diff --git a/bin/tests/system/nslimit_outdomain/ns1/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns1/named.conf.j2 new file mode 100644 index 0000000000..fd83fc3c19 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/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/nslimit_outdomain/ns1/root.db b/bin/tests/system/nslimit_outdomain/ns1/root.db new file mode 100644 index 0000000000..bfbf049b80 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/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/nslimit_outdomain/ns2/dnshoster.tld.db b/bin/tests/system/nslimit_outdomain/ns2/dnshoster.tld.db new file mode 100644 index 0000000000..9540da4743 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns2/dnshoster.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 +dnshoster.tld. IN SOA owner.tld. ns.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +dnshoster.tld. NS ns1.dnshoster.tld. +ns1.dnshoster.tld. A 10.53.0.5 +ns1.dnshoster.tld. A 10.53.0.6 + +dnshoster.tld. NS ns2.dnshoster.tld. +ns2.dnshoster.tld. A 10.53.1.1 +ns2.dnshoster.tld. A 10.53.1.2 + +dnshoster.tld. NS ns3.dnshoster.tld. +ns3.dnshoster.tld. A 10.53.2.1 +ns3.dnshoster.tld. A 10.53.2.2 + + diff --git a/bin/tests/system/nslimit_outdomain/ns2/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns2/named.conf.j2 new file mode 100644 index 0000000000..037ac60fe0 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns2/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.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; + 10.53.0.5; + 10.53.0.6; + 10.53.0.7; + 10.53.1.1; + 10.53.1.2; + 10.53.2.2; + }; + recursion no; + dnssec-validation no; +}; + +zone "tld." { + type primary; + file "tld.db"; +}; + +zone "dnshoster.tld." { + type primary; + file "dnshoster.tld.db"; +}; diff --git a/bin/tests/system/nslimit_outdomain/ns2/tld.db b/bin/tests/system/nslimit_outdomain/ns2/tld.db new file mode 100644 index 0000000000..e29bf91f7d --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns2/tld.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 +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 + +example4.tld. NS ns.example4.tld. +ns.example4.tld. A 10.53.0.3 + +dnshoster.tld. NS ns1.dnshoster.tld. +ns1.dnshoster.tld. A 10.53.0.5 +ns1.dnshoster.tld. A 10.53.0.6 + +dnshoster.tld. NS ns2.dnshoster.tld. +ns2.dnshoster.tld. A 10.53.1.1 +ns2.dnshoster.tld. A 10.53.1.2 + +dnshoster.tld. NS ns3.dnshoster.tld. +ns3.dnshoster.tld. A 10.53.2.1 +ns3.dnshoster.tld. A 10.53.2.2 diff --git a/bin/tests/system/nslimit_outdomain/ns3/example.tld.db b/bin/tests/system/nslimit_outdomain/ns3/example.tld.db new file mode 100644 index 0000000000..2a599ee876 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns3/example.tld.db @@ -0,0 +1,184 @@ +; 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 +ns01.sub.example.tld. A 127.0.0.1 +ns01.sub.example.tld. A 127.0.0.2 +; Those addresses won't be used (exceed the max-delegation-servers). +ns01.sub.example.tld. A 127.0.0.3 +ns01.sub.example.tld. A 127.0.0.4 + +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 +ns02.sub.example.tld. A 127.0.0.2 +ns02.sub.example.tld. A 127.0.0.3 +ns02.sub.example.tld. A 127.0.0.4 + +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 +ns03.sub.example.tld. A 127.0.0.2 +ns03.sub.example.tld. A 127.0.0.3 +ns03.sub.example.tld. A 127.0.0.4 + +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 +ns04.sub.example.tld. A 127.0.0.2 +ns04.sub.example.tld. A 127.0.0.3 +ns04.sub.example.tld. A 127.0.0.4 + +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 +ns05.sub.example.tld. A 127.0.0.2 +ns05.sub.example.tld. A 127.0.0.3 +ns05.sub.example.tld. A 127.0.0.4 + +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 +ns06.sub.example.tld. A 127.0.0.2 +ns06.sub.example.tld. A 127.0.0.3 +ns06.sub.example.tld. A 127.0.0.4 + +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 +ns07.sub.example.tld. A 127.0.0.2 +ns07.sub.example.tld. A 127.0.0.3 +ns07.sub.example.tld. A 127.0.0.4 + +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 +ns08.sub.example.tld. A 127.0.0.2 +ns08.sub.example.tld. A 127.0.0.3 +ns08.sub.example.tld. A 127.0.0.4 + +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 +ns09.sub.example.tld. A 127.0.0.2 +ns09.sub.example.tld. A 127.0.0.3 +ns09.sub.example.tld. A 127.0.0.4 + +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 +ns10.sub.example.tld. A 127.0.0.2 +ns10.sub.example.tld. A 127.0.0.3 +ns10.sub.example.tld. A 127.0.0.4 diff --git a/bin/tests/system/nslimit_outdomain/ns3/example4.tld.db b/bin/tests/system/nslimit_outdomain/ns3/example4.tld.db new file mode 100644 index 0000000000..f1c64be066 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns3/example4.tld.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 +example4.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example4.tld. NS ns.example4.tld. +ns.example4.tld. A 10.53.0.3 +sub.example4.tld. NS ns1.dnshoster.tld. +sub.example4.tld. NS ns2.dnshoster.tld. diff --git a/bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 new file mode 100644 index 0000000000..f7e03a82e7 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 @@ -0,0 +1,30 @@ +/* + * 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; + }; + recursion no; + dnssec-validation no; +}; + +zone "example4.tld." { + type primary; + file "example4.tld.db"; +}; diff --git a/bin/tests/system/nslimit_outdomain/ns4/named.args.j2 b/bin/tests/system/nslimit_outdomain/ns4/named.args.j2 new file mode 100644 index 0000000000..68f1511c7c --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns4/named.args.j2 @@ -0,0 +1 @@ +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 diff --git a/bin/tests/system/nslimit_outdomain/ns4/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns4/named.conf.j2 new file mode 100644 index 0000000000..09fbdd4e70 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/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/nslimit_outdomain/ns4/root.hint b/bin/tests/system/nslimit_outdomain/ns4/root.hint new file mode 100644 index 0000000000..d7d0e1faba --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/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/nslimit_outdomain/tests_nslimit_outdomain.py b/bin/tests/system/nslimit_outdomain/tests_nslimit_outdomain.py new file mode 100644 index 0000000000..228fb07918 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/tests_nslimit_outdomain.py @@ -0,0 +1,120 @@ +# 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_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 has_ip_and_query(expected_ips_and_queries, ips_and_queries): + found_count = 0 + for expected_ip, expected_query in expected_ips_and_queries: + for ip, query in ips_and_queries: + if ip == expected_ip and query == expected_query: + found_count += 1 + break + return found_count == len(expected_ips_and_queries) + + +# Test the max-delegation-servers limit on flow where ADB attempt +# a lookup from an NS name rather than directly with the NS addresses. +def test_nslimit_outdomain(ns4, templates): + templates.render( + "ns4/named.conf", {"maxdelegationservers": "max-delegation-servers 2;"} + ) + with ns4.watch_log_from_here() as watcher: + ns4.rndc("flush") + ns4.rndc("reload") + watcher.wait_for_line("running") + + msg = isctest.query.create("sub.example4.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, 9) + + # The resolver first resolve example4.tld. and gets the NS for sub.example.tld. + # which is out-domain. So it resolves it. + assert has_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example4.tld/IN/NS"), + ("10.53.0.3", "sub.example4.tld/IN/A"), + ("10.53.0.2", "dnshoster.tld/IN/NS"), + ], + ips_and_queries, + ) + + # Then, because max-delegation-servers is 2, the resolver will try to use either + # the NS ns1.dnshoster.tld or the NS ns2.dnshoster.tld. or the NS ns3.dnshoster.tld. + # + # What is important here, is that the NS of sub.example4.tld are _names_, so + # this is going through the dns_adb_createfind() flow, and it does stop after 2 + # queries (on the two IPs of one of the NS server above) and _won't_ try another + # NS name (becuse max-delegation-servers will be reached). + # + # Note that the sum of all the queries checked here is 8 and not 9. This is because + # when dnshoster.tld has been resolved, the resolver resolved 2 names. But the IPs + # of only one of the two names has been used. (This is checked below). + + used_ns1 = has_ip_and_query( + [ + ("10.53.0.2", "ns1.dnshoster.tld/IN/A"), + ("10.53.0.5", "sub.example4.tld/IN/A"), + ("10.53.0.6", "sub.example4.tld/IN/A"), + ], + ips_and_queries, + ) + + used_ns2 = has_ip_and_query( + [ + ("10.53.0.2", "ns2.dnshoster.tld/IN/A"), + ("10.53.1.1", "sub.example4.tld/IN/A"), + ("10.53.1.2", "sub.example4.tld/IN/A"), + ], + ips_and_queries, + ) + + used_ns3 = has_ip_and_query( + [ + ("10.53.0.2", "ns3.dnshoster.tld/IN/A"), + ("10.53.2.1", "sub.example4.tld/IN/A"), + ("10.53.2.2", "sub.example4.tld/IN/A"), + ], + ips_and_queries, + ) + + assert used_ns1 or used_ns2 or used_ns3 From 47a80bbd8769a013e5caffea3737d30372ab4755 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Fri, 10 Apr 2026 14:55:09 +0200 Subject: [PATCH 15/36] Update resend_loop_badcookie system test Update the resend_loop_badcookie system test to ensure there is no attempt to resend the query using TCP when getting BADCOOKIE from an upstream server using this transport already. --- bin/tests/system/resend_loop/ans3/ans.py | 50 ++++++++------- .../system/resend_loop/ns4/named.conf.j2 | 1 + .../system/resend_loop/tests_resend_loop.py | 61 ++++++++++++++++++- 3 files changed, 88 insertions(+), 24 deletions(-) diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py index 217bae0301..d0cb6d2935 100644 --- a/bin/tests/system/resend_loop/ans3/ans.py +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -14,14 +14,17 @@ from collections.abc import AsyncGenerator import dns.edns import dns.name import dns.rcode +import dns.rdataclass import dns.rdatatype import dns.rrset from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, + QnameHandler, QueryContext, ResponseHandler, + StaticResponseHandler, ) @@ -41,31 +44,33 @@ def _get_cookie(qctx: QueryContext): return None -class PrimeHandler(ResponseHandler): - """ - Specifically handle priming query for "." NS (type 2) - """ +def rrset( + qname: dns.name.Name | str, + rtype: dns.rdatatype.RdataType, + rdata: str, + ttl: int = 300, +) -> dns.rrset.RRset: + return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata) - def match(self, qctx: QueryContext) -> bool: - return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: +class RootNSHandler(QnameHandler, StaticResponseHandler): + qnames = ["."] + answer = [ + rrset(".", dns.rdatatype.NS, "a.root-servers.nil."), + ] + additional = [ + rrset("a.root-servers.nil.", dns.rdatatype.A, "10.53.0.3"), + ] - ns_rrset = dns.rrset.from_text( - ".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil." - ) - a_rrset = dns.rrset.from_text( - "a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3" - ) - response = qctx.prepare_new_response(with_zone_data=False) - response.set_rcode(dns.rcode.NOERROR) - response.answer.append(ns_rrset) - response.additional.append(a_rrset) - - yield DnsResponseSend(response, authoritative=True) +class ExampleNSHandler(QnameHandler, StaticResponseHandler): + qnames = ["example."] + answer = [ + rrset("example.", dns.rdatatype.NS, "ns.example."), + ] + additional = [ + rrset("ns.example.", dns.rdatatype.A, "10.53.0.3"), + ] class CookieHandler(ResponseHandler): @@ -111,7 +116,8 @@ class NoErrorHandler(ResponseHandler): def resend_server() -> AsyncDnsServer: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) server.install_response_handlers( - PrimeHandler(), + RootNSHandler(), + ExampleNSHandler(), CookieHandler(), NoErrorHandler(), ) diff --git a/bin/tests/system/resend_loop/ns4/named.conf.j2 b/bin/tests/system/resend_loop/ns4/named.conf.j2 index 360bc12e17..e686eaaa42 100644 --- a/bin/tests/system/resend_loop/ns4/named.conf.j2 +++ b/bin/tests/system/resend_loop/ns4/named.conf.j2 @@ -6,6 +6,7 @@ options { pid-file "named.pid"; listen-on { 10.53.0.4; }; listen-on-v6 { none; }; + query-source-v6 none; recursion yes; dnssec-validation no; }; diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py index f7ed4d3da6..a9a236fa58 100644 --- a/bin/tests/system/resend_loop/tests_resend_loop.py +++ b/bin/tests/system/resend_loop/tests_resend_loop.py @@ -9,18 +9,75 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +from re import compile as Re + import dns.message import isctest +# This test verifies the query pattern when the upstream behaves badly. +# In this scenario, the upstream server (ans3) always responds with a +# BADCOOKIE error for queries within the "example" zone, even on TCP. +# The resolver (ns4), should not resend the same queries over and over +# again, up to the max-query-count threshold. Instead, the expected +# pattern is: +# 1. Priming query, getting the NS for . +# 2. Getting the NS for example. +# 3. Trying to resolve test.example. +# 4. Trying again, but now with the server cookie. +# 5. Trying again, now over TCP. +# +# This means we expect 5 recursion queries trying to resolve test.example. def test_resend_loop_badcookie(ns4): - expected_log = "exceeded max queries resolving 'test.example/A'" + sending_packet = Re("sending packet from 10.53.0.4#[0-9]+ to 10.53.0.3#[0-9]+") + received_packet = Re("received packet from 10.53.0.3#[0-9]+ to 10.53.0.4#[0-9]+") + + log_sequence = [ + # 1. Priming query, getting the NS for . + sending_packet, + Re("COOKIE: [0-9a-z]{16}$"), + Re(".\\s+IN\\s+NS"), + # 2. Getting the NS for example. + sending_packet, + Re("COOKIE: [0-9a-z]{16}$"), + Re("example.\\s+IN\\s+NS"), + # 3. Trying to resolve test.example. + sending_packet, + Re("COOKIE: [0-9a-z]{16}$"), + Re("test.example.\\s+IN\\s+A"), + # Get the first BADCOOKIE error. + "UDP response", + received_packet, + "BADCOOKIE", + Re("COOKIE: [0-9a-z]{16}1122334455667788"), + Re("test.example.\\s+IN\\s+A"), + # 4. Trying again, but now with the server cookie. + sending_packet, + Re("test.example.\\s+IN\\s+A"), + # Get BADCOOKIE error again. + "UDP response", + received_packet, + "BADCOOKIE", + Re("COOKIE: [0-9a-z]{16}1122334455667788"), + Re("test.example.\\s+IN\\s+A"), + # 5. Trying again, now over TCP. + sending_packet, + Re("test.example.\\s+IN\\s+A"), + # Fails and give up. + "TCP response", + received_packet, + "BADCOOKIE", + Re("COOKIE: [0-9a-z]{16}1122334455667788"), + Re("test.example.\\s+IN\\s+A"), + ] msg = dns.message.make_query("test.example", "A") with ns4.watch_log_from_here() as watcher: res = isctest.query.udp(msg, ns4.ip) - watcher.wait_for_line(expected_log) + watcher.wait_for_sequence(log_sequence) + + assert len(ns4.log.grep(sending_packet)) == 5 isctest.check.servfail(res) From 71221a1402ceaef3a1554801515dbbb6dfcee134 Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Wed, 4 Mar 2026 10:00:56 +1100 Subject: [PATCH 16/36] Reject meta-classes in UPDATE and NOTIFY messages NOTIFY and UPDATE messages must specify a data class in the QUESTION/ZONE section. NONE and ANY are meta-classes and not appropriate here. Return FORMERR if either is used. Rejecting messages with a query class of NONE addresses YWH-PGM40640-72, YWH-PGM40640-82, and YWH-PGM40640-83. Rejecting messages with a query class of ANY addresses YWH-PGM40640-87, YWH-PGM40640-88, and YWH-PGM40640-117. Fixes: isc-projects/bind9#5778 Fixes: isc-projects/bind9#5782 Fixes: isc-projects/bind9#5783 Fixes: isc-projects/bind9#5797 Fixes: isc-projects/bind9#5798 Fixes: isc-projects/bind9#5853 --- lib/dns/message.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/dns/message.c b/lib/dns/message.c index 1a7181b105..2dc904c3f1 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -972,6 +972,17 @@ getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, rdtype = isc_buffer_getuint16(source); rdclass = isc_buffer_getuint16(source); + /* + * Notify and update messages need to specify the data class. + */ + if ((msg->opcode == dns_opcode_update || + msg->opcode == dns_opcode_notify) && + (rdclass == dns_rdataclass_none || + rdclass == dns_rdataclass_any)) + { + DO_ERROR(DNS_R_FORMERR); + } + /* * If this class is different than the one we already read, * this is an error. From 51dde6ef431c4290f7fa0a0a45670e1d67c6f195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 18 Mar 2026 00:10:35 +0100 Subject: [PATCH 17/36] Fix GSS-API context leak in TKEY negotiation Reject multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) in dst_gssapi_acceptctx(). Each call to gss_accept_sec_context() allocates a context inside the GSS library; without this fix, the context handle was passed back to process_gsstkey() which did not store it persistently, leaking it on every incomplete negotiation. An unauthenticated attacker could exhaust server memory by sending repeated TKEY queries with GSSAPI tokens, each leaking one GSS context. The leaked memory is allocated by the GSS library via malloc(), bypassing BIND's memory accounting. In practice, Kerberos/SPNEGO (the only mechanism used with BIND) completes in a single round, so rejecting continuation does not affect real-world deployments. See RFC 3645 Section 4.1.3. --- lib/dns/gssapictx.c | 94 +++++++++++++++++++----------------- lib/dns/include/dst/gssapi.h | 15 +++--- lib/dns/tkey.c | 14 +++--- 3 files changed, 62 insertions(+), 61 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index b500e94cda..bbfebb9acc 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -336,7 +336,14 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, GSS_SPNEGO_MECHANISM, flags, 0, NULL, gintokenp, NULL, &gouttoken, &ret_flags, NULL); - if (gret != GSS_S_COMPLETE && gret != GSS_S_CONTINUE_NEEDED) { + switch (gret) { + case GSS_S_COMPLETE: + result = ISC_R_SUCCESS; + break; + case GSS_S_CONTINUE_NEEDED: + result = DNS_R_CONTINUE; + break; + default: gss_err_message(mctx, gret, minor, err_message); if (err_message != NULL && *err_message != NULL) { gss_log(3, "Failure initiating security context: %s", @@ -361,12 +368,6 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, CHECK(isc_buffer_copyregion(outtoken, &r)); } - if (gret == GSS_S_COMPLETE) { - result = ISC_R_SUCCESS; - } else { - result = DNS_R_CONTINUE; - } - cleanup: if (gouttoken.length != 0U) { (void)gss_release_buffer(&minor, &gouttoken); @@ -390,15 +391,10 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, char buf[1024]; REQUIRE(outtoken != NULL && *outtoken == NULL); + REQUIRE(*ctxout == NULL); REGION_TO_GBUFFER(*intoken, gintoken); - if (*ctxout == NULL) { - context = GSS_C_NO_CONTEXT; - } else { - context = *ctxout; - } - if (gssapi_keytab != NULL) { #if HAVE_GSSAPI_GSSAPI_KRB5_H || HAVE_GSSAPI_KRB5_H gret = gsskrb5_register_acceptor_identity(gssapi_keytab); @@ -442,8 +438,15 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, switch (gret) { case GSS_S_COMPLETE: - case GSS_S_CONTINUE_NEEDED: break; + /* + * RFC 3645 4.1.3: we don't handle GSS_S_CONTINUE_NEEDED + * Multi-round GSS-API negotiation is not supported. + */ + case GSS_S_CONTINUE_NEEDED: + gss_log(3, "multi-round GSS-API negotiation not supported"); + (void)gss_delete_sec_context(&minor, &context, NULL); + FALLTHROUGH; case GSS_S_DEFECTIVE_TOKEN: case GSS_S_DEFECTIVE_CREDENTIAL: case GSS_S_BAD_SIG: @@ -456,7 +459,7 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, case GSS_S_BAD_MECH: case GSS_S_FAILURE: result = DNS_R_INVALIDTKEY; - /* fall through */ + FALLTHROUGH; default: gss_log(3, "failed gss_accept_sec_context: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); @@ -474,42 +477,43 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, (void)gss_release_buffer(&minor, &gouttoken); } - if (gret == GSS_S_COMPLETE) { - gret = gss_display_name(&minor, gname, &gnamebuf, NULL); - if (gret != GSS_S_COMPLETE) { - gss_log(3, "failed gss_display_name: %s", - gss_error_tostring(gret, minor, buf, - sizeof(buf))); - CLEANUP(ISC_R_FAILURE); - } + INSIST(gret == GSS_S_COMPLETE); - /* - * Compensate for a bug in Solaris8's implementation - * of gss_display_name(). Should be harmless in any - * case, since principal names really should not - * contain null characters. - */ - if (gnamebuf.length > 0U && - ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') - { - gnamebuf.length--; - } - - gss_log(3, "gss-api source name (accept) is %.*s", - (int)gnamebuf.length, (char *)gnamebuf.value); - - GBUFFER_TO_REGION(gnamebuf, r); - isc_buffer_init(&namebuf, r.base, r.length); - isc_buffer_add(&namebuf, r.length); - - CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0)); - } else { - result = DNS_R_CONTINUE; + gret = gss_display_name(&minor, gname, &gnamebuf, NULL); + if (gret != GSS_S_COMPLETE) { + gss_log(3, "failed gss_display_name: %s", + gss_error_tostring(gret, minor, buf, sizeof(buf))); + CLEANUP(ISC_R_FAILURE); } + /* + * Compensate for a bug in Solaris8's implementation + * of gss_display_name(). Should be harmless in any + * case, since principal names really should not + * contain null characters. + */ + if (gnamebuf.length > 0U && + ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') + { + gnamebuf.length--; + } + + gss_log(3, "gss-api source name (accept) is %.*s", (int)gnamebuf.length, + (char *)gnamebuf.value); + + GBUFFER_TO_REGION(gnamebuf, r); + isc_buffer_init(&namebuf, r.base, r.length); + isc_buffer_add(&namebuf, r.length); + + CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0)); + *ctxout = context; cleanup: + if (result != ISC_R_SUCCESS && context != GSS_C_NO_CONTEXT) { + (void)gss_delete_sec_context(&minor, &context, NULL); + } + if (gnamebuf.length != 0U) { gret = gss_release_buffer(&minor, &gnamebuf); if (gret != GSS_S_COMPLETE) { diff --git a/lib/dns/include/dst/gssapi.h b/lib/dns/include/dst/gssapi.h index a519487da0..6551e014ce 100644 --- a/lib/dns/include/dst/gssapi.h +++ b/lib/dns/include/dst/gssapi.h @@ -71,18 +71,17 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, * generated by gss_accept_sec_context() to be sent to the * initiator * 'context' is a valid pointer to receive the generated context handle. - * On the initial call, it should be a pointer to NULL, which - * will be allocated as a dns_gss_ctx_id_t. Subsequent calls - * should pass in the handle generated on the first call. * * Requires: - * 'outtoken' to != NULL && *outtoken == NULL. + * 'outtoken' != NULL && *outtoken == NULL. + * 'context' != NULL && *context == NULL. * * Returns: - * ISC_R_SUCCESS msg was successfully updated to include the - * query to be sent - * DNS_R_CONTINUE transaction still in progress - * other an error occurred while building the message + * ISC_R_SUCCESS msg was successfully updated to include + * the query to be sent + * DNS_R_INVALIDTKEY an error occurred while accepting the + * context + * ISC_R_FAILURE other error occurred */ isc_result_t diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c index a65661670f..38bdbd3ad7 100644 --- a/lib/dns/tkey.c +++ b/lib/dns/tkey.c @@ -181,7 +181,7 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, intoken = (isc_region_t){ tkeyin->key, tkeyin->keylen }; result = dst_gssapi_acceptctx(tctx->gssapi_keytab, &intoken, &outtoken, &gss_ctx, principal, tctx->mctx); - if (result == DNS_R_INVALIDTKEY) { + if (result != ISC_R_SUCCESS) { if (tsigkey != NULL) { dns_tsigkey_detach(&tsigkey); } @@ -189,12 +189,11 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, tkey_log("process_gsstkey(): dns_tsigerror_badkey"); return ISC_R_SUCCESS; } - if (result != DNS_R_CONTINUE && result != ISC_R_SUCCESS) { - CHECK(result); - } /* - * XXXDCL Section 4.1.3: Limit GSS_S_CONTINUE_NEEDED to 10 times. + * Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is + * rejected in dst_gssapi_acceptctx(), so if we reach here the + * negotiation is complete and the principal must be set. */ if (dns_name_countlabels(principal) == 0U) { if (tsigkey != NULL) { @@ -678,9 +677,8 @@ dns_tkey_gssnegotiate(dns_message_t *qmsg, dns_message_t *rmsg, NULL)); /* - * XXXSRA This seems confused. If we got CONTINUE from initctx, - * the GSS negotiation hasn't completed yet, so we can't sign - * anything yet. + * GSS negotiation is complete (CONTINUE returned earlier). + * Create the TSIG key from the established context. */ CHECK(dns_tsigkey_createfromkey(tkeyname, DST_ALG_GSSAPI, dstkey, true, false, NULL, rtkey.inception, From 156039fef5fd6381dd0b79092c6032d702fc285c Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 2 Apr 2026 10:43:00 +0200 Subject: [PATCH 18/36] update `max-delegation-servers` documentation Clarify how `max-delegation-servers` is used in the resolver, in particular, the fact that it, in practice, caps the maximum outgoing queries to resolve a name at a given delegation point. --- doc/arm/reference.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index a42a08d326..10ba133ef1 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -4187,14 +4187,11 @@ Tuning .. namedconf:statement:: max-delegation-servers :tags: server - :short: Configure the maximum number of nameserver names considered for a delegation + :short: Configure the maximum number of nameservers considered for a delegation When looking up remote nameservers for a delegation, the list of nameserver names is sorted according to Canonical RR Ordering within an RRset (see - :rfc:`4034` Section 6.3), and the number of names for which :iscman:`named` - looks up IP addresses is capped at :any:`max-delegation-servers`. - - This capped list of nameserver names is then randomly shuffled every time + :rfc:`4034` Section 6.3). This list is then randomly shuffled every time :iscman:`named` needs additional remote addresses for those nameservers. This randomized selection works around situations where the first few nameserver names in the zone are unresponsive. @@ -4207,6 +4204,12 @@ Tuning outgoing DNS query is initiated only if the DNS resolver does not already have existing IP addresses for any of the nameserver names in the cache. + The known NS addresses for an NS name (cached from a previous resolution, or + the NS name has glues, or it is defined from a local zone or hints) are + counted as delegation servers. Thus, the maximum queries the resolver does + to resolve a name at a delegation point is capped at + :any:`max-delegation-servers`. + The default and recommended value is ``13``. This limit prevents excessive resource use while processing large or misconfigured delegations. The default value should only be increased in controlled environments where a remote From 787b9bc45097b59812b0128cf6172d70105d9d18 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:24:43 -0700 Subject: [PATCH 19/36] Skip "deny-answer-address" for non-IN addresses Ensure that we don't attempt an ACL match for answer addresses when handling a class-CHAOS zone. This is an additional line of defense for YWH-PGM40640-74. --- lib/dns/resolver.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 50f88209dd..7410431284 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -6825,6 +6825,13 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name, return true; } + /* + * deny-answer-address doesn't apply to non-IN classes. + */ + if (rdataset->rdclass != dns_rdataclass_in) { + return true; + } + /* * Otherwise, search the filter list for a match for each * address record. If a match is found, the address should be From 4e455365bf2f8d5eeb185f3b7141ba2519fbbc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 18 Mar 2026 00:28:19 +0100 Subject: [PATCH 20/36] Implement RFC 3645 Section 3.1.1 ret_flags check in GSS-API client After gss_init_sec_context() completes, verify that both MUTUAL and INTEG flags are set in ret_flags. RFC 3645 Section 3.1.1 requires the client to abandon the algorithm if either flag is missing, as the security context would not provide mutual authentication or message integrity. Also fix uninitialized gss_name_t variable in dst_gssapi_initctx() that could cause undefined behavior if gss_import_name() fails and the cleanup path calls gss_release_name() on the uninitialized value. --- lib/dns/gssapictx.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index a2d55de3e0..ea86c4b43b 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -296,7 +296,7 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, isc_mem_t *mctx, char **err_message) { isc_region_t r; isc_buffer_t namebuf; - gss_name_t gname; + gss_name_t gname = NULL; OM_uint32 gret, minor, ret_flags, flags; gss_buffer_desc gintoken, *gintokenp, gouttoken = GSS_C_EMPTY_BUFFER; isc_result_t result; @@ -356,9 +356,20 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, } /* - * XXXSRA Not handled yet: RFC 3645 3.1.1: check ret_flags - * MUTUAL and INTEG flags, fail if either not set. + * RFC 3645 Section 3.1.1: verify that mutual authentication + * and integrity are supported. If either is missing, the + * security context does not meet the protocol requirements. */ + if (gret == GSS_S_COMPLETE && + (ret_flags & (GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) != + (GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) + { + gss_log(3, + "GSS-API context lacks required MUTUAL or " + "INTEG flags (ret_flags=0x%x)", + (unsigned int)ret_flags); + CLEANUP(ISC_R_FAILURE); + } /* * RFC 2744 states the a valid output token has a non-zero length. @@ -372,7 +383,9 @@ cleanup: if (gouttoken.length != 0U) { (void)gss_release_buffer(&minor, &gouttoken); } - (void)gss_release_name(&minor, &gname); + if (gname != NULL) { + (void)gss_release_name(&minor, &gname); + } return result; } From f14fac5a331d0c2176701dbf6ff3b8dcdb33473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Fri, 20 Mar 2026 08:43:28 +0100 Subject: [PATCH 21/36] Add regression test for GSS-API context leak via TKEY CONTINUE Send crafted SPNEGO NegTokenInit tokens that propose the krb5 mechanism without a mechToken. This causes gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED, which on unfixed code leaks the GSS context handle (~520 bytes per query). The test verifies that the server rejects the negotiation (TKEY error != 0, no continuation token) rather than returning a CONTINUE response (error=0 with output token). --- bin/tests/system/tkeyleak/ns1/dns.keytab | Bin 0 -> 460 bytes bin/tests/system/tkeyleak/ns1/example.db.in | 21 +++ bin/tests/system/tkeyleak/ns1/named.conf.j2 | 39 ++++++ bin/tests/system/tkeyleak/prereq.sh | 21 +++ bin/tests/system/tkeyleak/setup.sh | 17 +++ bin/tests/system/tkeyleak/tests_tkeyleak.py | 145 ++++++++++++++++++++ 6 files changed, 243 insertions(+) create mode 100644 bin/tests/system/tkeyleak/ns1/dns.keytab create mode 100644 bin/tests/system/tkeyleak/ns1/example.db.in create mode 100644 bin/tests/system/tkeyleak/ns1/named.conf.j2 create mode 100644 bin/tests/system/tkeyleak/prereq.sh create mode 100644 bin/tests/system/tkeyleak/setup.sh create mode 100644 bin/tests/system/tkeyleak/tests_tkeyleak.py diff --git a/bin/tests/system/tkeyleak/ns1/dns.keytab b/bin/tests/system/tkeyleak/ns1/dns.keytab new file mode 100644 index 0000000000000000000000000000000000000000..d5a09b060a2077c465dfcbbf36bc37cb241eeb6a GIT binary patch literal 460 zcmZQ&VqjnhVqjw6c8zfK4e)W*^Yip!V0Q5fX5db(NX#wBN!82C%mFH5O!{Ltm5D)! zLBWW7Wq*p(GKZ&v?D(;ENo!OT)C zNV&EkN#%}B;vXTXDPdSm;ZMpb)x+h!Sjm(%N0KtMg#IdAagny#v{(CG*3>RnR}%$^ z=n1NfdQg+yNHiJf#>HEN)mOiq9=t=C@8{&ie`ZjVJQ)~K!;d{BuUHr8M4&t(*it-a z3ssr9E&s6I>~HYOyUGuIF0&|f8@_sXj5jZ%_QN!&VU~mq1G;39!KSA+ENvPE++H3( KuWqpfX$Alz=7X02 literal 0 HcmV?d00001 diff --git a/bin/tests/system/tkeyleak/ns1/example.db.in b/bin/tests/system/tkeyleak/ns1/example.db.in new file mode 100644 index 0000000000..dd200dc9bc --- /dev/null +++ b/bin/tests/system/tkeyleak/ns1/example.db.in @@ -0,0 +1,21 @@ +; 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 ns.example. admin.example. ( + 1 ; serial + 3600 ; refresh + 900 ; retry + 604800 ; expire + 300 ; minimum + ) +@ IN NS ns.example. +ns IN A 10.53.0.1 diff --git a/bin/tests/system/tkeyleak/ns1/named.conf.j2 b/bin/tests/system/tkeyleak/ns1/named.conf.j2 new file mode 100644 index 0000000000..f16b53414c --- /dev/null +++ b/bin/tests/system/tkeyleak/ns1/named.conf.j2 @@ -0,0 +1,39 @@ +/* + * 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; + dnssec-validation no; + tkey-gssapi-keytab "dns.keytab"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "example" { + type primary; + file "example.db"; +}; diff --git a/bin/tests/system/tkeyleak/prereq.sh b/bin/tests/system/tkeyleak/prereq.sh new file mode 100644 index 0000000000..8a68ae7df1 --- /dev/null +++ b/bin/tests/system/tkeyleak/prereq.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# 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. + +. ../conf.sh + +$FEATURETEST --gssapi || { + echo_i "gssapi not supported - skipping tkeyleak test" + exit 255 +} + +exit 0 diff --git a/bin/tests/system/tkeyleak/setup.sh b/bin/tests/system/tkeyleak/setup.sh new file mode 100644 index 0000000000..24a0026665 --- /dev/null +++ b/bin/tests/system/tkeyleak/setup.sh @@ -0,0 +1,17 @@ +#!/bin/sh -e + +# 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. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/example.db.in ns1/example.db diff --git a/bin/tests/system/tkeyleak/tests_tkeyleak.py b/bin/tests/system/tkeyleak/tests_tkeyleak.py new file mode 100644 index 0000000000..fd97c8540c --- /dev/null +++ b/bin/tests/system/tkeyleak/tests_tkeyleak.py @@ -0,0 +1,145 @@ +# 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. + +""" +Regression test for GSS-API context leak via repeated TKEY queries. + +An unauthenticated attacker could exhaust server memory by sending +repeated TKEY queries with crafted SPNEGO NegTokenInit tokens. +Each query triggers gss_accept_sec_context() which returns +GSS_S_CONTINUE_NEEDED and allocates a GSS context. On the unfixed +code path, the context handle in process_gsstkey() is never stored +or freed, leaking ~520 bytes per query. + +The fix rejects GSS_S_CONTINUE_NEEDED in dst_gssapi_acceptctx() and +deletes the context immediately. + +The key distinguishing signal in the TKEY response: + - CONTINUE (vulnerable): error=0, output token present, no TSIG + - BADKEY (fixed): error=17, no output token +""" + +import struct +import time + +import dns.name +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.TKEY +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + +TKEY_NAME = dns.name.from_text("test.key.") +GSSAPI_ALGORITHM = dns.name.from_text("gss-tsig.") +TKEY_MODE_GSSAPI = 3 + +# OID 1.2.840.113554.1.2.2 (Kerberos 5) +KRB5_OID = b"\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" + +# OID 1.3.6.1.5.5.2 (SPNEGO) +SPNEGO_OID = b"\x06\x06\x2b\x06\x01\x05\x05\x02" + + +def der_encode(tag, data): + """Encode data in ASN.1 DER TLV format.""" + length = len(data) + if length < 128: + return tag + bytes([length]) + data + if length < 256: + return tag + b"\x81" + bytes([length]) + data + return tag + b"\x82" + struct.pack(">H", length) + data + + +def spnego_negtokeninit(): + """Build a SPNEGO NegTokenInit proposing krb5 without a mechToken. + + This forces gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED + because the acceptor recognizes the krb5 mechanism but has not + received an actual AP-REQ token yet. + """ + # MechTypeList ::= SEQUENCE OF MechType + mechtype_list = der_encode(b"\x30", KRB5_OID) + # [0] mechTypes + mechtypes = der_encode(b"\xa0", mechtype_list) + # NegTokenInit ::= SEQUENCE { mechTypes, ... } + negtokeninit = der_encode(b"\x30", mechtypes) + # [0] CONSTRUCTED (wrapping NegTokenInit) + wrapped = der_encode(b"\xa0", negtokeninit) + # APPLICATION 0 CONSTRUCTED (SPNEGO OID + body) + return der_encode(b"\x60", SPNEGO_OID + wrapped) + + +def make_tkey_query(token): + """Build a TKEY query with a GSS-API token in the additional section.""" + now = int(time.time()) + tkey_rdata = dns.rdtypes.ANY.TKEY.TKEY( + rdclass=dns.rdataclass.ANY, + rdtype=dns.rdatatype.TKEY, + algorithm=GSSAPI_ALGORITHM, + inception=now, + expiration=now + 86400, + mode=TKEY_MODE_GSSAPI, + error=0, + key=token, + other=b"", + ) + + msg = isctest.query.create(TKEY_NAME, dns.rdatatype.TKEY, dns.rdataclass.ANY) + rrset = msg.find_rrset( + msg.additional, + TKEY_NAME, + dns.rdataclass.ANY, + dns.rdatatype.TKEY, + create=True, + ) + rrset.add(tkey_rdata) + return msg + + +def test_tkey_gssapi_no_continuation(ns1): + """TKEY with a SPNEGO NegTokenInit must be rejected, not continued. + + On unfixed code, gss_accept_sec_context() returns CONTINUE_NEEDED + and the response has error=0 with an output token (the leaked path). + On fixed code, CONTINUE_NEEDED is rejected and the response has + error=BADKEY(17) with no output token. + """ + port = ns1.ports.dns + ip = ns1.ip + + msg = make_tkey_query(spnego_negtokeninit()) + res = dns.query.tcp(msg, ip, port=port, timeout=5) + + assert res is not None + + tkey = get_tkey_answer(res) + assert tkey is not None, "server did not return a TKEY answer" + assert ( + tkey.error != 0 + ), "server returned error=0 (GSS_S_CONTINUE_NEEDED not rejected)" + assert len(tkey.key) == 0, "server returned a continuation token" + + +def get_tkey_answer(response): + """Extract TKEY rdata from a DNS response, or None.""" + for rrset in response.answer: + if rrset.rdtype == dns.rdatatype.TKEY: + for rdata in rrset: + return rdata + return None From a6f53d47b2253a04a9875e889c2bcd95bf70f10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Tue, 17 Mar 2026 04:45:16 +0100 Subject: [PATCH 22/36] Fix use-after-free in resolver SIG(0) async verification path When a SIG(0)-signed response triggers async ECDSA verification via dns_message_checksig_async(), the respctx_t holds a raw pointer to the resquery_t. If the fetch context is shut down while verification is in flight (e.g. due to recursive-clients quota exhaustion), the query is destroyed and the callback dereferences a dangling pointer. Take a reference on the resquery_t when initializing the respctx_t, and release it in both cleanup paths. The query's own reference to the fetch context keeps the fctx alive transitively. --- lib/dns/resolver.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index e66e602888..50f88209dd 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -7543,6 +7543,7 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { return; cleanup: + resquery_detach(&rctx->query); isc_mem_putanddetach(&rctx->mctx, rctx, sizeof(*rctx)); } @@ -7903,6 +7904,7 @@ resquery_response_continue(void *arg, isc_result_t result) { rctx_done(rctx, result); cleanup: + resquery_detach(&rctx->query); isc_mem_putanddetach(&rctx->mctx, rctx, sizeof(*rctx)); } @@ -7916,7 +7918,7 @@ static void rctx_respinit(resquery_t *query, fetchctx_t *fctx, isc_result_t result, isc_region_t *region, respctx_t *rctx) { *rctx = (respctx_t){ .result = result, - .query = query, + .query = resquery_ref(query), .fctx = fctx, .broken_type = badns_response, .retryopts = query->options }; From dd4af20dc8235b861b745abd651daa0d5e55e09d Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:45:11 -0700 Subject: [PATCH 23/36] Test CHAOS view recursion behavior Check that recursive and forward queries to views of type CHAOS are REFUSED, but that authoritative queries are answered correctly. --- bin/tests/system/checkconf/tests.sh | 11 ++++ .../checkconf/warn-chaos-recursion.conf | 12 +++++ bin/tests/system/class/ns1/chaos.db.in | 4 ++ bin/tests/system/class/ns1/named.conf.j2 | 31 +++++++++++ bin/tests/system/class/ns2/example.db.in | 6 +++ bin/tests/system/class/ns2/localhost.db.in | 6 +++ bin/tests/system/class/ns2/named.conf.j2 | 42 +++++++++++++++ bin/tests/system/class/ns3/named.conf.j2 | 28 ++++++++++ bin/tests/system/class/setup.sh | 19 +++++++ bin/tests/system/class/tests_class_chaos.py | 54 +++++++++++++++++++ bin/tests/system/isctest/check.py | 4 ++ 11 files changed, 217 insertions(+) create mode 100644 bin/tests/system/checkconf/warn-chaos-recursion.conf create mode 100644 bin/tests/system/class/ns1/chaos.db.in create mode 100644 bin/tests/system/class/ns1/named.conf.j2 create mode 100644 bin/tests/system/class/ns2/example.db.in create mode 100644 bin/tests/system/class/ns2/localhost.db.in create mode 100644 bin/tests/system/class/ns2/named.conf.j2 create mode 100644 bin/tests/system/class/ns3/named.conf.j2 create mode 100644 bin/tests/system/class/setup.sh create mode 100644 bin/tests/system/class/tests_class_chaos.py diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index f927d78919..330da510e3 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -739,5 +739,16 @@ if [ $ret != 0 ]; then fi status=$((status + ret)) +n=$((n + 1)) +echo_i "check 'recursion yes;' is warned and disabled in a non-IN view ($n)" +ret=0 +$CHECKCONF warn-chaos-recursion.conf >checkconf.out$n 2>&1 || ret=1 +grep -F "recursion will be disabled" checkconf.out$n >/dev/null || ret=1 +if [ $ret != 0 ]; then + echo_i "failed" + ret=1 +fi +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/checkconf/warn-chaos-recursion.conf b/bin/tests/system/checkconf/warn-chaos-recursion.conf new file mode 100644 index 0000000000..01965102a4 --- /dev/null +++ b/bin/tests/system/checkconf/warn-chaos-recursion.conf @@ -0,0 +1,12 @@ +options { + directory "."; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.hints"; + }; +}; diff --git a/bin/tests/system/class/ns1/chaos.db.in b/bin/tests/system/class/ns1/chaos.db.in new file mode 100644 index 0000000000..43ca58ffa8 --- /dev/null +++ b/bin/tests/system/class/ns1/chaos.db.in @@ -0,0 +1,4 @@ +. CH NS ns.root. +ns.root. CH A ns.root. 1 +ns.root. CH AAAA \# 1 00 + diff --git a/bin/tests/system/class/ns1/named.conf.j2 b/bin/tests/system/class/ns1/named.conf.j2 new file mode 100644 index 0000000000..76f85fc6c9 --- /dev/null +++ b/bin/tests/system/class/ns1/named.conf.j2 @@ -0,0 +1,31 @@ +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; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.db"; + }; + zone "version.bind" { + type primary; + database "_builtin version"; + }; +}; diff --git a/bin/tests/system/class/ns2/example.db.in b/bin/tests/system/class/ns2/example.db.in new file mode 100644 index 0000000000..a658ddbd89 --- /dev/null +++ b/bin/tests/system/class/ns2/example.db.in @@ -0,0 +1,6 @@ +$TTL 300 +@ CH SOA ns.example. hostmaster.example. 1 3600 1200 604800 300 +@ CH NS ns.example. +ns CH TXT "ns" +a CH A target.example. 1 +target CH TXT "target" diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in new file mode 100644 index 0000000000..baa5f74862 --- /dev/null +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -0,0 +1,6 @@ +$ORIGIN 1.0.0.127.in-addr.arpa. +$TTL 300 +@ IN SOA ns hostmaster 1 3600 900 604800 300 +@ IN NS ns +ns IN A 127.0.0.1 +@ IN KX 10 target.example. diff --git a/bin/tests/system/class/ns2/named.conf.j2 b/bin/tests/system/class/ns2/named.conf.j2 new file mode 100644 index 0000000000..5618c15216 --- /dev/null +++ b/bin/tests/system/class/ns2/named.conf.j2 @@ -0,0 +1,42 @@ +options { + directory "."; + 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; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view default { + match-clients { any; }; + recursion no; + dnssec-validation no; + zone "1.0.0.127.in-addr.arpa." { + type primary; + file "localhost.db"; + update-policy { + grant * tcp-self . ANY; + }; + }; +}; + +view chaos ch { + match-clients { any; }; + recursion no; + zone example { + type primary; + file "example.db"; + allow-update { any; }; + }; +}; diff --git a/bin/tests/system/class/ns3/named.conf.j2 b/bin/tests/system/class/ns3/named.conf.j2 new file mode 100644 index 0000000000..3016333aad --- /dev/null +++ b/bin/tests/system/class/ns3/named.conf.j2 @@ -0,0 +1,28 @@ +options { + directory "."; + 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; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + dnssec-validation no; + forward only; + forwarders port @PORT@ { 10.53.0.2; }; + deny-answer-addresses { 0.0.0.0/0; ::/0; }; +}; diff --git a/bin/tests/system/class/setup.sh b/bin/tests/system/class/setup.sh new file mode 100644 index 0000000000..c70a2f8290 --- /dev/null +++ b/bin/tests/system/class/setup.sh @@ -0,0 +1,19 @@ +#!/bin/sh -e + +# 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. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/chaos.db.in ns1/chaos.db +cp ns2/example.db.in ns2/example.db +cp ns2/localhost.db.in ns2/localhost.db diff --git a/bin/tests/system/class/tests_class_chaos.py b/bin/tests/system/class/tests_class_chaos.py new file mode 100644 index 0000000000..5b4fef9ae4 --- /dev/null +++ b/bin/tests/system/class/tests_class_chaos.py @@ -0,0 +1,54 @@ +# 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.opcode +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def test_chaos_recursion(): + msg = isctest.query.create("foo.example.", "TXT", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.refused(res) + + +def test_chaos_auth(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + + +def test_chaos_forward(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.3") + isctest.check.refused(res) + + +def test_chaos_notify(): + msg = isctest.query.create("example.", "SOA", qclass="CH", rd=False, dnssec=False) + msg.set_opcode(dns.opcode.NOTIFY) + msg.flags = dns.opcode.to_flags(dns.opcode.NOTIFY) + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.notimp(res) + + +def test_query_class_none(): + msg = isctest.query.create("example.", "A", qclass="NONE") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.formerr(res) diff --git a/bin/tests/system/isctest/check.py b/bin/tests/system/isctest/check.py index 7c5e30c4e1..f723dd95c0 100644 --- a/bin/tests/system/isctest/check.py +++ b/bin/tests/system/isctest/check.py @@ -47,6 +47,10 @@ def servfail(message: dns.message.Message) -> None: rcode(message, dns.rcode.SERVFAIL) +def formerr(message: dns.message.Message) -> None: + rcode(message, dns.rcode.FORMERR) + + def adflag(message: dns.message.Message) -> None: assert (message.flags & dns.flags.AD) != 0, str(message) From 45c93af5c0f9bcc2ff864f7f122fdfe5a2e9382c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 18 Mar 2026 01:02:24 +0100 Subject: [PATCH 24/36] Verify integrity flag on server-side GSS-API context After gss_accept_sec_context() completes, verify that the INTEG flag is set in ret_flags. Without integrity protection, GSS-TSIG message authentication cannot function correctly. The server side was previously passing NULL for ret_flags, meaning it never verified the negotiated security properties. The client side was fixed in the previous commit; this fixes the server side. --- lib/dns/gssapictx.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index ea86c4b43b..8f4c1c56c9 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -442,15 +442,30 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, #endif } + OM_uint32 ret_flags = 0; + gret = gss_accept_sec_context(&minor, &context, GSS_C_NO_CREDENTIAL, &gintoken, GSS_C_NO_CHANNEL_BINDINGS, - &gname, NULL, &gouttoken, NULL, NULL, - NULL); + &gname, NULL, &gouttoken, &ret_flags, + NULL, NULL); result = ISC_R_FAILURE; switch (gret) { case GSS_S_COMPLETE: + /* + * RFC 2743 Section 1.2.2: verify that the negotiated + * context provides integrity protection. + */ + if ((ret_flags & GSS_C_INTEG_FLAG) == 0) { + gss_log(3, + "GSS-API context lacks required INTEG " + "flag (ret_flags=0x%x)", + (unsigned int)ret_flags); + (void)gss_delete_sec_context(&minor, &context, NULL); + result = DNS_R_INVALIDTKEY; + goto cleanup; + } break; /* * RFC 3645 4.1.3: we don't handle GSS_S_CONTINUE_NEEDED From 1083ef30279a38d26eae5df98a0d66bc96917526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Fri, 10 Apr 2026 12:51:31 +0200 Subject: [PATCH 25/36] Fix output token and GSS context leaks in TKEY/GSS-API error paths In dst_gssapi_acceptctx(), rename outtoken to outtokenp (matching BIND convention for output pointer parameters) and free the allocated output token buffer on error in the cleanup path. In process_gsstkey(), route the empty-principal error path through cleanup via CLEANUP() instead of returning early, so that the output token, GSS context, and TSIG key are all freed consistently by the existing cleanup block. --- lib/dns/gssapictx.c | 12 ++++++++---- lib/dns/tkey.c | 15 +++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index bbfebb9acc..a2d55de3e0 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -378,7 +378,7 @@ cleanup: isc_result_t dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, - isc_buffer_t **outtoken, dns_gss_ctx_id_t *ctxout, + isc_buffer_t **outtokenp, dns_gss_ctx_id_t *ctxout, dns_name_t *principal, isc_mem_t *mctx) { isc_region_t r; isc_buffer_t namebuf; @@ -390,7 +390,7 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, isc_result_t result; char buf[1024]; - REQUIRE(outtoken != NULL && *outtoken == NULL); + REQUIRE(outtokenp != NULL && *outtokenp == NULL); REQUIRE(*ctxout == NULL); REGION_TO_GBUFFER(*intoken, gintoken); @@ -470,10 +470,10 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, } if (gouttoken.length > 0U) { - isc_buffer_allocate(mctx, outtoken, + isc_buffer_allocate(mctx, outtokenp, (unsigned int)gouttoken.length); GBUFFER_TO_REGION(gouttoken, r); - CHECK(isc_buffer_copyregion(*outtoken, &r)); + CHECK(isc_buffer_copyregion(*outtokenp, &r)); (void)gss_release_buffer(&minor, &gouttoken); } @@ -510,6 +510,10 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, *ctxout = context; cleanup: + if (result != ISC_R_SUCCESS && *outtokenp != NULL) { + isc_buffer_free(outtokenp); + } + if (result != ISC_R_SUCCESS && context != GSS_C_NO_CONTEXT) { (void)gss_delete_sec_context(&minor, &context, NULL); } diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c index 38bdbd3ad7..b22fed78a4 100644 --- a/lib/dns/tkey.c +++ b/lib/dns/tkey.c @@ -182,12 +182,9 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, result = dst_gssapi_acceptctx(tctx->gssapi_keytab, &intoken, &outtoken, &gss_ctx, principal, tctx->mctx); if (result != ISC_R_SUCCESS) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } tkeyout->error = dns_tsigerror_badkey; tkey_log("process_gsstkey(): dns_tsigerror_badkey"); - return ISC_R_SUCCESS; + CLEANUP(ISC_R_SUCCESS); } /* @@ -196,14 +193,10 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, * negotiation is complete and the principal must be set. */ if (dns_name_countlabels(principal) == 0U) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } - dst_gssapi_deletectx(tctx->mctx, &gss_ctx); tkeyout->error = dns_tsigerror_badkey; tkey_log("process_gsstkey(): " "completed context with empty principal"); - return ISC_R_SUCCESS; + CLEANUP(ISC_R_SUCCESS); } else if (tsigkey == NULL) { #if HAVE_GSSAPI OM_uint32 gret, minor, lifetime; @@ -282,7 +275,9 @@ cleanup: isc_buffer_free(&outtoken); } - tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + if (result != ISC_R_SUCCESS) { + tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + } return result; } From 3b596adbd21a8cf04478464b2a012d15b0349453 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:45:11 -0700 Subject: [PATCH 26/36] Test UPDATE behavior in CHAOS and other non-IN classes Send various UPDATE requests that are known to have caused crashes previously with deliberately misconfigured non-IN zones; confirm that UPDATE is not processed. --- bin/named/server.c | 1 - bin/tests/system/class/ns2/localhost.db.in | 5 + bin/tests/system/class/tests_class_update.py | 96 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 bin/tests/system/class/tests_class_update.py diff --git a/bin/named/server.c b/bin/named/server.c index d584c1249e..7f3f0162a5 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -4775,7 +4775,6 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, aclctx, isc_g_mctx, &view->proxyonacl)); if (view->rdclass != dns_rdataclass_in) { - view->recursion = false; dns_acl_none(isc_g_mctx, &view->recursionacl); dns_acl_none(isc_g_mctx, &view->recursiononacl); } else { diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in index baa5f74862..a50e5167a9 100644 --- a/bin/tests/system/class/ns2/localhost.db.in +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -3,4 +3,9 @@ $TTL 300 @ IN SOA ns hostmaster 1 3600 900 604800 300 @ IN NS ns ns IN A 127.0.0.1 + @ IN KX 10 target.example. +@ IN PX 10 map822.example. mapx400.example. +@ IN NSAP 0x47000580ffff0000000001e133ffffff00016200 +@ IN NSAP-PTR target.example. +@ in EID \# 01 aa diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py new file mode 100644 index 0000000000..e53bbc77ea --- /dev/null +++ b/bin/tests/system/class/tests_class_update.py @@ -0,0 +1,96 @@ +# 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 socket +import struct + +from dns import rdataclass, rdatatype, update + +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def encode_name(name: str) -> bytes: + out = b"" + for label in name.rstrip(".").split("."): + out += bytes([len(label)]) + label.encode("ascii") + return out + b"\x00" + + +@pytest.mark.parametrize( + "rdtype,rdclass,ttl,rdata", + [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), + (rdatatype.KX, rdataclass.NONE, 0, b""), + (rdatatype.PX, rdataclass.NONE, 0, b""), + (rdatatype.NSAP, rdataclass.NONE, 0, b""), + (rdatatype.NSAP_PTR, rdataclass.NONE, 0, b""), + (31, rdataclass.NONE, 0, b""), # dnspython doesn't define type EID + ], +) +def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): + # these update messages are badly formatted, so we construct + # them manually instead of using dnspython. + + # opcode=UPDATE, 1 RRset in ZONE, 1 RRset in UPDATE + header = struct.pack("!HHHHHH", 0, 0x2800, 1, 0, 1, 0) + + # ZONE section: QNAME=, QTYPE=SOA, QCLASS=ANY + zone_q = encode_name("1.0.0.127.in-addr.arpa") + struct.pack("!HH", 6, 255) + + # UPDATE section RR: + update_rr = ( + encode_name("1.0.0.127.in-addr.arpa") + + struct.pack("!HHIH", rdtype, rdclass, ttl, len(rdata)) + + rdata + ) + + m = header + zone_q + update_rr + packet = struct.pack("!H", len(m)) + m + + with socket.create_connection( + ("10.53.0.2", named_port), source_address=("127.0.0.1", 0), timeout=2.0 + ) as s: + s.sendall(packet) + try: + rwire = s.recv(4096) + res = dns.message.from_wire(rwire) + isctest.check.formerr(res) + except Exception: # pylint: disable=broad-except + pass + + # check the server is answering + msg = isctest.query.create("1.0.0.127.in-addr.arpa", "SRV") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 0) + + +@pytest.mark.parametrize( + "rdtype,rdata", + [ + (rdatatype.SVCB, "\\# 02 0000"), + (rdatatype.WKS, "\\# 02 4142"), + (rdatatype.WKS, "\\# 02 4344"), + ], +) +def test_class_chaosupdate(rdtype, rdata): + up = update.UpdateMessage("example.", rdclass=rdataclass.CHAOS) + up.add("foo.example.", 300, rdtype, rdata) + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) From 2b0f5aeb8148fd70ea881798b82f2774c57e8901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Tue, 7 Apr 2026 15:58:31 +0200 Subject: [PATCH 27/36] Check GSS_C_REPLAY_FLAG in client-side ret_flags validation RFC 3645 Section 3.1.1 mandates that the client MUST abandon the algorithm if replay_det_state is FALSE after GSS_Init_sec_context completes. The previous commit checked MUTUAL and INTEG but missed REPLAY, even though it was already requested in the input flags. Add GSS_C_REPLAY_FLAG to the ret_flags bitmask check so all three required properties (replay detection, mutual authentication, and integrity) are verified. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/dns/gssapictx.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index 8f4c1c56c9..9e025b1a3b 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -356,17 +356,19 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, } /* - * RFC 3645 Section 3.1.1: verify that mutual authentication - * and integrity are supported. If either is missing, the - * security context does not meet the protocol requirements. + * RFC 3645 Section 3.1.1: verify that replay detection, mutual + * authentication and integrity are supported. The RFC mandates + * checking replay_det_state and mutual_state; integ_avail is + * also verified because GSS-TSIG cannot function without it. */ if (gret == GSS_S_COMPLETE && - (ret_flags & (GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) != - (GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) + (ret_flags & + (GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) != + (GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) { gss_log(3, - "GSS-API context lacks required MUTUAL or " - "INTEG flags (ret_flags=0x%x)", + "GSS-API context lacks required REPLAY, MUTUAL, " + "or INTEG flags (ret_flags=0x%x)", (unsigned int)ret_flags); CLEANUP(ISC_R_FAILURE); } From bb24573580b96fc659ab186000b0da4370fd64e3 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Mon, 9 Mar 2026 15:50:04 +1100 Subject: [PATCH 28/36] Test server behavior when sending various UPDATE requests Send update messages for zones with CLASS0, ANY and NONE. The class ANY UPDATE also attempts to delete a KX record in an existing IN class zone to trigger a REQUIRE. Test that the server is still running. --- bin/tests/system/class/tests_class_update.py | 45 +++++++++++++++++++- bin/tests/system/nsupdate/setup.sh | 1 + bin/tests/system/nsupdate/tests.sh | 20 +++------ bin/tests/system/packet.pl | 25 +++++++++-- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py index e53bbc77ea..30e3ba6d2a 100644 --- a/bin/tests/system/class/tests_class_update.py +++ b/bin/tests/system/class/tests_class_update.py @@ -12,7 +12,7 @@ import socket import struct -from dns import rdataclass, rdatatype, update +from dns import message, rdataclass, rdatatype, update import pytest @@ -35,6 +35,7 @@ def encode_name(name: str) -> bytes: @pytest.mark.parametrize( "rdtype,rdclass,ttl,rdata", [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00\x00\x00\x00\x00\x00\x01"), (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), (rdatatype.KX, rdataclass.NONE, 0, b""), (rdatatype.PX, rdataclass.NONE, 0, b""), @@ -69,7 +70,7 @@ def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): s.sendall(packet) try: rwire = s.recv(4096) - res = dns.message.from_wire(rwire) + res = message.from_wire(rwire) isctest.check.formerr(res) except Exception: # pylint: disable=broad-except pass @@ -94,3 +95,43 @@ def test_class_chaosupdate(rdtype, rdata): up.add("foo.example.", 300, rdtype, rdata) res = isctest.query.tcp(up, "10.53.0.2") isctest.check.notimp(res) + + +def test_class_undefined(ns2): + up = update.UpdateMessage(".", rdclass=257) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) + watcher.wait_for_line("invalid message class: CLASS257") + + +def test_class_zero(ns2): + up = update.UpdateMessage(".", rdclass=0) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message class could not be determined") + + +def test_class_any(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.ANY) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") + + +def test_class_none(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.NONE) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") diff --git a/bin/tests/system/nsupdate/setup.sh b/bin/tests/system/nsupdate/setup.sh index 299330773a..9d27b20a76 100644 --- a/bin/tests/system/nsupdate/setup.sh +++ b/bin/tests/system/nsupdate/setup.sh @@ -35,6 +35,7 @@ update.nil IN SOA ns1.example.nil. hostmaster.example.nil. ( 3600 ; minimum (1 hour) ) update.nil. NS ns1.update.nil. +update.nil. KX 0 . ns1.update.nil. A 10.53.0.2 ns2.update.nil. AAAA ::1 EOF diff --git a/bin/tests/system/nsupdate/tests.sh b/bin/tests/system/nsupdate/tests.sh index 7a77faea6b..d71136c563 100755 --- a/bin/tests/system/nsupdate/tests.sh +++ b/bin/tests/system/nsupdate/tests.sh @@ -485,8 +485,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 update is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000100000000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { @@ -497,20 +499,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 additional data is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000000010000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 -$DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 -grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 -[ $ret = 0 ] || { - echo_i "failed" - status=1 -} - -n=$((n + 1)) -ret=0 -echo_i "check that update to undefined class is handled ($n)" -echo "a0e4280000010001000000000000060101c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { diff --git a/bin/tests/system/packet.pl b/bin/tests/system/packet.pl index 900a0c071e..afb9f4784d 100644 --- a/bin/tests/system/packet.pl +++ b/bin/tests/system/packet.pl @@ -40,6 +40,7 @@ # -p : specify port # -t : specify UDP or TCP # -r : send packet times +# -b: blocking io # -d: dump response packets # # If not specified, address defaults to 127.0.0.1, port to 53, protocol @@ -51,6 +52,8 @@ use strict; use Getopt::Std; use IO::File; use IO::Socket; +use Net::DNS; +use Net::DNS::Packet; sub usage { print ("Usage: packet.pl [-a address] [-d] [-p port] [-t (tcp|udp)] [-r ] [file]\n"); @@ -61,8 +64,6 @@ my $sock; my $proto; sub dumppacket { - use Net::DNS; - use Net::DNS::Packet; my $rin; my $rout; @@ -96,7 +97,7 @@ sub dumppacket { } my %options={}; -getopts("a:dp:t:r:", \%options); +getopts("a:bdp:t:r:", \%options); my $addr = "127.0.0.1"; $addr = $options{a} if defined $options{a}; @@ -111,6 +112,8 @@ usage if ($proto !~ /^(udp|tcp)$/); my $repeats = 1; $repeats = $options{r} if defined $options{r}; +my $blocking = defined $options{b} ? 1 : 0; + my $file = "STDIN"; if (@ARGV >= 1) { my $filename = shift @ARGV; @@ -132,8 +135,22 @@ my $len = length $data; my $output = unpack("H*", $data); print ("sending $repeats time(s): $output\n"); + +if (defined $options{d}) { + my $request; + if ($Net::DNS::VERSION > 0.68) { + $request = new Net::DNS::Packet(\$data, 0); + $@ and die $@; + } else { + my $err; + ($request, $err) = new Net::DNS::Packet(\$data, 0); + $err and die $err; + } + $request->print; +} + $sock = IO::Socket::INET->new(PeerAddr => $addr, PeerPort => $port, - Blocking => 0, + Blocking => $blocking, Proto => $proto,) or die "$!"; STDOUT->autoflush(1); From b794b4eeed404ac5ed2ed613295e6d20d1817359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= Date: Mon, 9 Mar 2026 15:48:34 +0300 Subject: [PATCH 29/36] Add system test for HTTP/2 SETTINGS frame flood Send a valid DoH query followed by a flood of SETTINGS frames to trigger a use-after-free in the write buffer. Under ASan, named will abort if the bug is present. --- bin/tests/system/doth/tests_malicious.py | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 bin/tests/system/doth/tests_malicious.py diff --git a/bin/tests/system/doth/tests_malicious.py b/bin/tests/system/doth/tests_malicious.py new file mode 100644 index 0000000000..7529f2b7e1 --- /dev/null +++ b/bin/tests/system/doth/tests_malicious.py @@ -0,0 +1,73 @@ +# 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 socket +import ssl + +from h2.config import H2Configuration +from h2.connection import H2Connection +from h2.settings import SettingCodes + +import dns.message + + +def test_settings_frame_flood(ns1, named_httpsport): + msg = dns.message.make_query(".", "SOA") + wire = msg.to_wire() + + with socket.create_connection((ns1.ip, named_httpsport), timeout=10) as sock: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.set_alpn_protocols(["h2"]) + + with ctx.wrap_socket(sock, server_hostname=ns1.ip) as tls: + config = H2Configuration(client_side=True, header_encoding="utf-8") + conn = H2Connection(config=config) + conn.initiate_connection() + tls.sendall(conn.data_to_send()) + + stream_id = conn.get_next_available_stream_id() + conn.send_headers( + stream_id, + [ + (":method", "POST"), + (":path", "/dns-query"), + (":scheme", "https"), + (":authority", f"{ns1.ip}:{named_httpsport}"), + ("content-type", "application/dns-message"), + ("accept", "application/dns-message"), + ("content-length", str(len(wire))), + ], + ) + conn.send_data(stream_id, wire, end_stream=True) + tls.sendall(conn.data_to_send()) + + for i in range(4096): + try: + conn.update_settings( + { + SettingCodes.MAX_CONCURRENT_STREAMS: (i % 100) + 1, + SettingCodes.INITIAL_WINDOW_SIZE: i + 1, + } + ) + tls.sendall(conn.data_to_send()) + except Exception: # pylint: disable=broad-except + break + + if i % 500 == 0: + tls.settimeout(0.05) + try: + while (data := tls.recv(65535)) != b"": + conn.receive_data(data) + tls.sendall(conn.data_to_send()) + except Exception: # pylint: disable=broad-except + pass From 24ac3392d99bf2d22976290f76ddad4317597349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Sun, 19 Apr 2026 21:36:43 +0200 Subject: [PATCH 30/36] Make isc_mem_isovermem() probabilistic Replace the hysteretic hi_water/lo_water switch with a stochastic check: always false below lo_water, always true at or above hi_water, linearly ramped probability in between. This spreads cache cleaning across many inserts instead of triggering a thundering herd once the hi_water mark is crossed (which causes every addrdataset to enter the LRU purge path simultaneously and serializes lookups behind the node write locks). The is_overmem atomic and its stores are no longer needed and are removed. The existing tests that asserted specific hysteretic state transitions are simplified to check only the deterministic boundaries. --- lib/isc/mem.c | 67 +++++++++++++++---------------------------- tests/dns/qpdb_test.c | 44 ++++++++++++++-------------- tests/isc/mem_test.c | 27 +++++++++++------ 3 files changed, 64 insertions(+), 74 deletions(-) diff --git a/lib/isc/mem.c b/lib/isc/mem.c index 9385d4a83a..2ef4e28583 100644 --- a/lib/isc/mem.c +++ b/lib/isc/mem.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -126,7 +127,6 @@ static isc_mutex_t contextslock; typedef union { struct { atomic_int_fast64_t inuse; - atomic_bool is_overmem; }; char padding[ISC_OS_CACHELINE_SIZE]; } isc__mem_stat_t; @@ -620,7 +620,6 @@ mem_create(const char *name, isc_mem_t **ctxp, unsigned int debugging, for (size_t i = 0; i < ARRAY_SIZE(ctx->stat_s); i++) { atomic_init(&ctx->stat_s[i].inuse, 0); - atomic_init(&ctx->stat_s[i].is_overmem, false); } /* Reserve the [-1] index for ISC_TID_UNKNOWN */ @@ -1020,50 +1019,30 @@ bool isc_mem_isovermem(isc_mem_t *ctx) { REQUIRE(VALID_CONTEXT(ctx)); - int32_t tid = isc_tid(); - - bool is_overmem = atomic_load_relaxed(&ctx->stat[tid].is_overmem); - - if (!is_overmem) { - /* We are not overmem, check whether we should be? */ - size_t hiwater = atomic_load_relaxed(&ctx->hi_water); - if (hiwater == 0) { - return false; - } - - size_t inuse = isc_mem_inuse(ctx); - if (inuse <= hiwater) { - return false; - } - - if ((ctx->debugging & ISC_MEM_DEBUGUSAGE) != 0) { - fprintf(stderr, - "overmem %s mctx %p inuse %zu hi_water %zu\n", - ctx->name, ctx, inuse, hiwater); - } - - atomic_store_relaxed(&ctx->stat[tid].is_overmem, true); - return true; - } else { - /* We are overmem, check whether we should not be? */ - size_t lowater = atomic_load_relaxed(&ctx->lo_water); - if (lowater == 0) { - return false; - } - - size_t inuse = isc_mem_inuse(ctx); - if (inuse >= lowater) { - return true; - } - - if ((ctx->debugging & ISC_MEM_DEBUGUSAGE) != 0) { - fprintf(stderr, - "overmem %s mctx %p inuse %zu lo_water %zu\n", - ctx->name, ctx, inuse, lowater); - } - atomic_store_relaxed(&ctx->stat[tid].is_overmem, false); + size_t hiwater = atomic_load_relaxed(&ctx->hi_water); + if (hiwater == 0) { return false; } + + size_t inuse = isc_mem_inuse(ctx); + if (inuse >= hiwater) { + return true; + } + + size_t lowater = atomic_load_relaxed(&ctx->lo_water); + if (inuse <= lowater) { + return false; + } + + /* + * Between lo_water and hi_water, return true with a probability + * that ramps linearly from 0 at lo_water to 1 at hi_water. This + * spreads cache cleaning across many inserts instead of triggering + * a thundering herd once the hi_water mark is crossed. + */ + uint32_t prob = (uint32_t)(((uint64_t)(inuse - lowater) * 256) / + (hiwater - lowater)); + return isc_random8() < prob; } const char * diff --git a/tests/dns/qpdb_test.c b/tests/dns/qpdb_test.c index de3ea1245b..8293c971aa 100644 --- a/tests/dns/qpdb_test.c +++ b/tests/dns/qpdb_test.c @@ -128,7 +128,7 @@ ISC_LOOP_TEST_IMPL(overmempurge_bigrdata) { dns_db_t *db = NULL; isc_mem_t *mctx = NULL; isc_stdtime_t now = isc_stdtime_now(); - size_t i; + size_t i = 0; isc_mem_create("test", &mctx); @@ -140,21 +140,21 @@ ISC_LOOP_TEST_IMPL(overmempurge_bigrdata) { isc_mem_setwater(mctx, hiwater, lowater); /* - * Add cache entries with minimum size of data until 'overmem' - * condition is triggered. - * This should eventually happen, but we also limit the number of - * iteration to avoid an infinite loop in case something gets wrong. + * Add a lot of data entries sufficient to push the context + * above the hi_water mark. */ - for (i = 0; !isc_mem_isovermem(mctx) && i < (maxcache / 10); i++) { - overmempurge_addrdataset(db, now, i, 50053, 0, false); + while (isc_mem_inuse(mctx) < hiwater) { + overmempurge_addrdataset(db, now, i, 50053, 0, true); + i++; } - assert_true(isc_mem_isovermem(mctx)); + assert_true(isc_mem_inuse(mctx) >= hiwater); + assert_true(isc_mem_inuse(mctx) < maxcache); /* * Then try to add the same number of entries, each has very large data. - * 'overmem purge' should keep the total cache size from exceeding - * the 'hiwater' mark too much. So we should be able to assume the - * cache size doesn't reach the "max". + * Probabilistic LRU cleaning should keep the total cache size from + * exceeding the 'hiwater' mark too much. So we should be able to + * assume the cache size doesn't reach the "max". */ while (i-- > 0) { overmempurge_addrdataset(db, now, i, 50054, @@ -180,7 +180,7 @@ ISC_LOOP_TEST_IMPL(overmempurge_longname) { dns_db_t *db = NULL; isc_mem_t *mctx = NULL; isc_stdtime_t now = isc_stdtime_now(); - size_t i; + size_t i = 0; isc_mem_create("test", &mctx); @@ -192,21 +192,21 @@ ISC_LOOP_TEST_IMPL(overmempurge_longname) { isc_mem_setwater(mctx, hiwater, lowater); /* - * Add cache entries with minimum size of data until 'overmem' - * condition is triggered. - * This should eventually happen, but we also limit the number of - * iteration to avoid an infinite loop in case something gets wrong. + * Add a lot of data entries sufficient to push the context + * above the hi_water mark. */ - for (i = 0; !isc_mem_isovermem(mctx) && i < (maxcache / 10); i++) { - overmempurge_addrdataset(db, now, i, 50053, 0, false); + while (isc_mem_inuse(mctx) < hiwater) { + overmempurge_addrdataset(db, now, i, 50053, 0, true); + i++; } - assert_true(isc_mem_isovermem(mctx)); + assert_true(isc_mem_inuse(mctx) >= hiwater); + assert_true(isc_mem_inuse(mctx) < maxcache); /* * Then try to add the same number of entries, each has very long name. - * 'overmem purge' should keep the total cache size from not exceeding - * the 'hiwater' mark too much. So we should be able to assume the cache - * size doesn't reach the "max". + * Probabilistic LRU cleaning should keep the total cache size from + * exceeding the 'hiwater' mark too much. So we should be able to + * assume the cache size doesn't reach the "max". */ while (i-- > 0) { overmempurge_addrdataset(db, now, i, 50054, 0, true); diff --git a/tests/isc/mem_test.c b/tests/isc/mem_test.c index 7724488935..5462b628d4 100644 --- a/tests/isc/mem_test.c +++ b/tests/isc/mem_test.c @@ -291,6 +291,17 @@ ISC_RUN_TEST_IMPL(isc_mem_reallocate) { isc_mem_free(isc_g_mctx, data); } +static bool +at_least_one_overmem(isc_mem_t *mctx) { + for (size_t i = 0; i < UINT16_MAX; i++) { + /* The overmem is probability based in this range */ + if (isc_mem_isovermem(mctx)) { + return true; + } + } + return false; +} + ISC_RUN_TEST_IMPL(isc_mem_overmem) { isc_mem_t *mctx = NULL; isc_mem_create("test", &mctx); @@ -298,27 +309,27 @@ ISC_RUN_TEST_IMPL(isc_mem_overmem) { isc_mem_setwater(mctx, 1024, 512); - /* inuse < lo_water */ + /* inuse <= lo_water is always false */ void *data1 = isc_mem_allocate(mctx, 256); assert_false(isc_mem_isovermem(mctx)); - /* lo_water < inuse < hi_water */ + /* lo_water < inuse < hi_water might be true or false */ void *data2 = isc_mem_allocate(mctx, 512); - assert_false(isc_mem_isovermem(mctx)); + assert_true(at_least_one_overmem(mctx)); - /* hi_water < inuse */ + /* hi_water <= inuse is always true */ void *data3 = isc_mem_allocate(mctx, 512); assert_true(isc_mem_isovermem(mctx)); - /* lo_water < inuse < hi_water */ + /* lo_water < inuse < hi_water might be true or false */ isc_mem_free(mctx, data2); - assert_true(isc_mem_isovermem(mctx)); + assert_true(at_least_one_overmem(mctx)); - /* inuse < lo_water */ + /* inuse <= lo_water is always false */ isc_mem_free(mctx, data3); assert_false(isc_mem_isovermem(mctx)); - /* inuse == 0 */ + /* inuse == 0 is always false */ isc_mem_free(mctx, data1); assert_false(isc_mem_isovermem(mctx)); From 4d16a8c9f2d9a5e88cd4588246d51eb5731d3b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= Date: Tue, 10 Mar 2026 14:48:02 +0300 Subject: [PATCH 31/36] Fix use-after-free in DoH write buffer after HTTP/2 send After the send callback completes, the UV request is freed but the HTTP/2 socket's write buffer still points to the freed memory. If nghttp2 subsequently needs to send frames (e.g. SETTINGS ACK), the server_read_callback reads from the dangling buffer. Clear the write buffer before freeing the UV request. --- lib/isc/netmgr/http.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/isc/netmgr/http.c b/lib/isc/netmgr/http.c index 4d8fe48174..0055311cb2 100644 --- a/lib/isc/netmgr/http.c +++ b/lib/isc/netmgr/http.c @@ -2743,6 +2743,8 @@ server_httpsend(isc_nmhandle_t *handle, isc_nmsocket_t *sock, } else { cb(handle, result, cbarg); } + + isc_buffer_initnull(&sock->h2->wbuf); isc__nm_uvreq_put(&req); } From f060971f84ebdaa3814f3dbfb0345c48f749b47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 6 May 2026 10:12:35 +0200 Subject: [PATCH 32/36] Pass empty string instead of NULL to ns_client_dumpmessage() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two new call sites added by the CLASS-validation work passed NULL as the reason, but ns_client_dumpmessage() bails out early on a NULL reason — so the message dump never happened. The intent was to dump the message and let the follow-up ns_client_log() carry the reason text, so pass "" to suppress the prefix without short-circuiting the dump. --- lib/ns/client.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ns/client.c b/lib/ns/client.c index 7a48d613af..7d2f77e646 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -2139,7 +2139,7 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, default: dns_rdataclass_format(client->message->rdclass, classbuf, sizeof(classbuf)); - ns_client_dumpmessage(client, NULL); + ns_client_dumpmessage(client, ""); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "invalid message class: %s", classbuf); @@ -2238,7 +2238,7 @@ ns_client_request_continue(void *arg) { dns_rdataclass_format(client->message->rdclass, classname, sizeof(classname)); - ns_client_dumpmessage(client, NULL); + ns_client_dumpmessage(client, ""); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "no matching view in class '%s'", From 2879b22960bac007c9c4693a134b0c1161363763 Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Thu, 7 May 2026 14:12:40 +0200 Subject: [PATCH 33/36] Generate changelog for BIND 9.21.22 --- doc/arm/changelog.rst | 1 + doc/changelog/changelog-9.21.22.rst | 768 ++++++++++++++++++++++++++++ 2 files changed, 769 insertions(+) create mode 100644 doc/changelog/changelog-9.21.22.rst diff --git a/doc/arm/changelog.rst b/doc/arm/changelog.rst index c02a5680d4..742ae801cb 100644 --- a/doc/arm/changelog.rst +++ b/doc/arm/changelog.rst @@ -18,6 +18,7 @@ Changelog development. Regular users should refer to :ref:`Release Notes ` for changes relevant to them. +.. include:: ../changelog/changelog-9.21.22.rst .. include:: ../changelog/changelog-9.21.21.rst .. include:: ../changelog/changelog-9.21.20.rst .. include:: ../changelog/changelog-9.21.19.rst diff --git a/doc/changelog/changelog-9.21.22.rst b/doc/changelog/changelog-9.21.22.rst new file mode 100644 index 0000000000..8d876fe5e9 --- /dev/null +++ b/doc/changelog/changelog-9.21.22.rst @@ -0,0 +1,768 @@ +.. 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. + +BIND 9.21.22 +------------ + +Security Fixes +~~~~~~~~~~~~~~ + +- Fix outgoing zone transfers' quota issue. ``3ddd7b8695`` + + Unauthorized clients could consume outgoing zone transfers quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` + +- [CVE-2026-3592] Limit resolver server list size. ``e249148d75`` + + 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. :gl:`#5641` + +- [CVE-2026-3039] Fix GSS-API resource leak. ``01bdb7abeb`` + + Fixed a memory leak where each GSS-API TKEY negotiation leaked a + security context inside the GSS library. An unauthenticated attacker + could exhaust server memory by sending repeated TKEY queries to a + server with tkey-gssapi-keytab configured. The leaked memory was + allocated by the GSS library, bypassing BIND's memory accounting. + + Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now + rejected, as BIND never supported it correctly and Kerberos/SPNEGO + completes in a single round. :gl:`#5752` + +- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN + views. ``21c8ba4f0b`` + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with recursion yes in a non-IN view will log a warning at + startup, and named-checkconf flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. :gl:`#5784` + +- [CVE-2026-5950] Avoid unbounded recursion loop. ``5319c21761`` + + A bug during bad server handling could cause the resolver to enter an + infinite loop, continuously sending queries to an upstream server with + no exit condition, until the resolver query timeout was hit. This has + been fixed. + + ISC would like to thank Billy Baraja (BielraX) for bringing this issue + to our attention. :gl:`#5804` + +- [CVE-2026-5947] Fix crash in resolver when SIG(0)-signed responses are + received under load. ``9831f41894`` + + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. :gl:`#5819` + +- Fix race condition in getsigningtime() ``d35a527ffb`` + + Compute qpzone_get_lock(elem->node) into a local variable while the + heap lock is still held, rather than dereferencing the stale elem + pointer after releasing the lock. A concurrent thread running + setsigningtime() (e.g. via IXFR apply on a worker thread) could free + the top-of-heap element between the heap lock release and the + dereference, causing a use-after-free. :gl:`#5883` :gl:`!11875` + +- [CVE-2026-3593] Fix use-after-free in DNS-over-HTTPS when processing + HTTP/2 SETTINGS frames. ``e33ff6bb0a`` + + A use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause named to crash when a client sends a flood of HTTP/2 + SETTINGS frames while a DoH response is being written. This affects + servers with DoH (DNS-over-HTTPS) enabled. + + ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting + this. + + For: #5755 + +New Features +~~~~~~~~~~~~ + +- Add DTRACE probes to the delegation cache. ``780ffe375f`` + + The new delegation cache, which stores NS-based and DELEG-based + delegations per view, is now instrumented with static user-space + tracing probes so that cache hit rate, insertion and lookup latency, + eviction pressure under memory limits, and removals triggered by rndc + flush-delegation can be observed on a running named. :gl:`!11855` + +Removed Features +~~~~~~~~~~~~~~~~ + +- Remove obsolete KEY record flags deprecated by RFC 3445. + ``1535b32dab`` + + KEY resource records originally defined NOAUTH, NOCONF, EXTENDED, and + ENTITY flags that were removed by RFC 3445 back in 2002. BIND still + carried code to parse and emit them, including the additional + two-octet flags field that followed when the EXTENDED bit was set. + That handling has been removed and the affected bit positions are now + reserved. + + Dropping the extended-flags handling also eliminates a possible crash + that could be reached when signing a zone containing an invalid key. + :gl:`#5900` :gl:`!11961` + +Feature Changes +~~~~~~~~~~~~~~~ + +- Embed default sanitizer flags in executables. ``7c60b7da8a`` + + Replicating CI failures requires the developer to piece together the + sanitizer flags by hand, reducing ergonomics. + + Fix this problem by embedding the relevant settings to the + executables. Symbol resolution still needs manual intervention by + setting the env variable `*SAN_SYMBOLIZER_PATH`. However, this doesn't + affect any behavior. + + The flags are passed though a meson-configured `sanitize.c.in` + template file to toggle which flags are included for the executable. + Using the built-in `__SANITIZE_XXX__` or `__has_feature` for this task + is more trouble than it's worth because only one of the two is + available in most GCC/clang versions, alongside the lack of + `__SANITIZE_UNDEFINED__` from GCC. + + Meson's own unit test execution sets its own `ASAN_OPTIONS` etc. To + prevent it from overriding the default options, we also pass the same + options to unit tests environment variables. + + A new script `ci/sanitizer-default-check.py` is used in CI to detect + if a build directory with sanitizers enabled has a meson `executable` + definition that doesn't include the sanitizer flag source file. + :gl:`#5469` :gl:`!10919` + +- Catch rare named crash in recursive resolution earlier for diagnosis. + ``3c9a848be7`` + + A rare crash has been observed in named while it is resolving upstream + nameserver addresses for a recursive query, surfacing as a + segmentation fault with no immediate clue as to the cause. This change + adds internal consistency checks so that a future occurrence of the + same condition aborts named with a diagnostic message at the point the + inconsistency arises, rather than corrupting state and crashing later + in an unrelated location. :gl:`#5602` :gl:`!11943` + +- Revert isdelegation() to return boolean value again. ``69d439cd1b`` + + :gl:`#5838` :gl:`!11792` + +- Fix CPU spikes and slow queries when cache approaches memory limit. + ``2e7e9f51db`` + + Spread cache cleanup probabilistically to avoid CPU usage spikes and a + drop in query throughput. :gl:`#5891` + +- Add a refcount to the vecheaders. ``a743ff1e44`` + + This MR changes the way the ownership of the vecheaders is tracked. + Before this MR, the ownership of the vecheader was implicitely tracked + through a mix of the refcount on the node owning the header, the + external refcount of the same node and the version. This has some + adverse consequences in terms of contention, such as that querying A + and AAAA glue hits the same refcount. + + This MR adds a refcount to the vecheader itself, allowing it to exist + independently of the node it is contained in. On its own, this would + create a cycle, where the node has a reference to the header, which + has a reference to the heap, which in turn has a reference to the + node. + + To break this cycle, this MR also moves from an "intrusive" heap, to a + more traditional one where pointers to the node and vecheader in the + heap are stored in a hashmap. :gl:`!11397` + +- Change NSEC3 and NSEC3PARAM rdata struct fields to use isc_region_t. + ``245c71dfac`` + + Replace the separate pointer+length field pairs in the NSEC3 and + NSEC3PARAM rdata structures (salt/salt_length, next/next_length, + typebits/len) with isc_region_t, making the fields self-describing and + eliminating a class of length-mismatch bugs. :gl:`!11592` + +- Document that named-checkzone must not run on untrusted input. + ``5b164b551f`` + + The zone-file parser implements $INCLUDE by opening whatever local + path the zone text names, and fragments of the included file leak + through parser error messages. There is no safe way to validate + untrusted zone text with named-checkzone or named-compilezone, so the + manual pages for both tools now warn against doing so. :gl:`!11901` + +- Don't set named curves explicitly in pre-3.0 libcrypto. ``2e92389905`` + + The function EC_KEY_set_asn1_flag is deprecated in AWS-LC. Fortunately + calling it to make sure we use named curve keys is entirely + unnecessary. + + More information for pre-3.0 libcrypto and significant forks are as + following: + + OpenSSL: Named curves were the default between 1.1.0 and 3.6.1 [1],[2] + + AWS-LC: Library only supports named curves in the first place [3] + + BoringSSL: Likewise with AWS-LC [4] + + LibreSSL: EC_GROUPs are named by default [5] + + [1]: https://github.com/openssl/openssl/commit/86f300d38540ead85543aee + 0cb30c32145931744 [2]: https://github.com/openssl/openssl/commit/9db6a + f922c48c5cab5398ef9f37e425e382f9440 [3]: https://github.com/aws/aws-lc + /blob/a605df416bc6ddd0a3b79d728770664ce2302e71/include/openssl/ec_key. + h#L442-L445 [4]: https://github.com/google/boringssl/blob/514abb73bb80 + 130000b46cf589190c967c6647cd/include/openssl/ec_key.h#L279-L280 [5]: h + ttps://github.com/libressl/openbsd/blob/c9338745181f31ae01336081edfdb7 + 38c0b76d5f/src/lib/libcrypto/ec/ec_lib.c#L94 :gl:`!11530` + +- Fix off by one error in dnssec-ksr sign. ``ae739daec2`` + + If the inception time of the signature is exactly equal to the + inactive time of the key, add the signature. :gl:`!11791` + +- Harden GSS-API context establishment in TKEY negotiation. + ``9212e1ac50`` + + Implement RFC 3645 Section 3.1.1 client-side check for REPLAY, MUTUAL, + and INTEG flags after gss_init_sec_context() completes. Add + server-side INTEG flag check after gss_accept_sec_context(). Also + fixes an uninitialized gss_name_t on the error path in + dst_gssapi_initctx(). + +- Implement RFC 3645 Section 4.1.1 key expiry check in TKEY. + ``6b6913c83b`` + + Check for existing TSIG keys before accepting a new GSS-API + negotiation and delete the key if it has expired. Previously, an + expired GSS key would permanently block re-negotiation for that name + until the server was restarted. :gl:`!11713` + +- Reduce memory footprint by actively returning unused memory to the OS. + ``460bf794a5`` + + Previously, :iscman:`named` relied on the default allocator settings + for releasing unused memory back to the operating system, which could + result in unnecessarily high resident memory usage. :iscman:`named` + now actively manages memory page purging. On systems using jemalloc, + background cleanup threads are enabled and the dirty page decay time + is reduced from 10 seconds to 5 seconds. Additionally, a volume-based + decay pass is triggered after every 16 MiB of freed memory. On + glibc-based systems, a similar volume-based mechanism using + malloc_trim() is used instead. :gl:`!11761` + +- Split up zone.c (zone manager) ``e99b5f80be`` + + In order to make `zone.c` more readable, split it up in separate + source files. This moves zone manager related code to `zonemgr.c`. + :gl:`!11726` + +- Split up zone.c (zone properties) ``36acc92131`` + + In order to make `zone.c` more readable, split it up in separate + source files. This moves most of the set and get functions to + `zoneproperties.c`. :gl:`!11501` + +Bug Fixes +~~~~~~~~~ + +- Check validator name when adding EDE text. ``a6b44a6007`` + + When a validator is being shut down, the associated name `val->name` + is set to NULL. This could cause a crash if a worker thread + subsequently added an EDE code with `val->name` in the extra text. + + `validator_addede()` now checks whether the name is NULL before trying + to add it to the extra text. :gl:`#5613` :gl:`!11945` + +- Use the zone file's basename as origin in DNSSEC tools. ``08fa344014`` + + In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not + specified using the `-o` parameter, the default behavior is to try to + sign using the zone's file name as the origin. So, for example, + `dnssec-signzone -S example.com` will work, so long as the file name + matches the zone name. + + This now also works if the zone is in a different directory. For + example, `dnssec-signzone -S zones/example.com` will set the origin + value to `example.com`. :gl:`#5678` :gl:`!11360` + +- Fix a possible race condition during zone transfers. ``175b121bc3`` + + The :iscman:`named` process could terminate unexpectedly when + processing an IXFR message during a zone transfer. This has been + fixed. :gl:`#5767` :gl:`!11781` + +- Do not resend query after BADCOOKIE answer on TCP. ``53593e8e13`` + + When an upstream server answers BADCOOKIE, no matter which transport + is used, the resolver resends the query using TCP. However, if the + upstream server responded with BADCOOKIE again over TCP, the resolver + would keep resending until the maximum query count was reached. + + This is now fixed by no longer resending once the query has already + been sent over TCP. :gl:`#5804` + +- Make BIND9 compatible with OpenSSL 4. ``80794c5e87`` + + OPENSSL_cleanup() in OpenSSL 4 doesn't free the memory, and that is + not compatible with BIND 9's memory leak detection code. Don't use + custom allocation/deallocation functions for OpenSSL's internal memory + management. + + See https://github.com/openssl/openssl/pull/29721 :gl:`#5808` + :gl:`!11865` + +- Fix named crash when processing SIG records in dynamic updates. + ``5f0b2f255f`` + + Previously, :iscman:`named` could abort if a client sent a dynamic + update containing a SIG record (the legacy signature type) to a zone + configured with an update-policy. The function `dns_db_findrdataset` + had an incorrect requirements prerequisite that prevented SIG records + being looked up, which was triggered as part of processing an UPDATE + request and could be triggered remotely by any client permitted to + send updates. This has been fixed by ensuring that SIG records are + handled consistently with RRSIG records during update processing. + :gl:`#5818` :gl:`!11864` + +- Fix wrong NSEC proof for empty non-terminals after IXFR. + ``15f058b5a2`` + + When a secondary received an IXFR that transitioned a zone from + unsigned to NSEC-signed, queries for empty non-terminal names returned + the zone apex NSEC record instead of the NSEC that actually covers the + queried name. The issue only occurred with incremental transfers; a + full AXFR or a server restart resolved it. :gl:`#5824` :gl:`!11786` + +- Fix rndc modzone behavior for a zone in named.conf. ``c56d4c13f9`` + + If a zone was present in the configuration file and not originally + added by `rndc addzone`, `rndc modzone` for that zone would succeed + once but subsequent `modzone` attempts would fail. This has been + fixed. :gl:`#5826` :gl:`!11744` + +- Fix zone verification of NSEC3 signed zones. ``0633effb5b`` + + Previously, when computing the compressed bitmap during verification + of an NSEC3-signed zone, an undersized buffer was used that resulted + in an out-of-bounds write if there were too many active windows in the + bitmap. This impacted mirror zones which are NSEC3-signed, + `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + :gl:`#5834` :gl:`!11804` + +- Fix 'rndc modzone' issue with non-existing zones. ``09a4b80301`` + + The :iscman:`named` process could terminate unexpectedly or become + subject to undefined behavior when issued an :option:`rndc modzone` + operation for a non-existing zone. This has been fixed. :gl:`#5848` + :gl:`!11844` + +- Fix zone filename token-parsing bug. ``ef29555ba4`` + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog member zone containing special characters like + '%' or '$' which could be interpreted as zone filename tokens and + trigger a case-sensitivity bug in the token-parsing code. This has + been fixed. :gl:`#5849` :gl:`!11839` + +- Prevent a crash when using both dns64 and filter-aaaa. ``3c60322fa3`` + + An assertion failure could be triggered if both `dns64` and the + `filter-aaaa` plugin were in use simultaneously. This happened if the + plugin triggered a second recursion process, which then attempted to + store DNS64 state information in a pointer that had already been set + by the original recursion process. This has been fixed. :gl:`#5854` + :gl:`!11949` + +- Remove unnecessary dns_name_free call. ``bbdca691c0`` + + When processing a catalog zone member's primaries definition and there + is a TXT record containing an invalid name TSIG key name, + dns_name_free was incorrectly called triggering an assertion. This has + been fixed. :gl:`#5858` :gl:`!11832` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + ``120eaf546f`` + + A DNSSEC-signed zone could publish a DNSKEY with an unusually large + RSA public exponent and force any validator resolving names in that + zone to spend disproportionate CPU verifying signatures. The + validator now rejects such DNSKEYs, matching the limit already applied + to keys read from files or HSMs. :gl:`#5881` :gl:`!11917` + +- Add missing parenthesis to fxhash. ``a8d412ab21`` + + The fxhash implementation had a missing parenthesis that caused it to + diverge from Rust's reference implementation. This commit fixes this. + :gl:`#5882` :gl:`!11857` + +- Fix strict weak ordering violation in resign_sooner() ``13a6867757`` + + resign_sooner_values() only checked whether rhs was SOA-typed when + resign times were equal, but did not check lhs. When both entries were + SOA-typed with equal resign times, the comparison returned true in + both directions, violating irreflexivity and corrupting heap + invariants. + + Add lhs_typepair parameter and require lhs to be non-SOA for the + tie-breaking logic to apply. :gl:`#5884` :gl:`!11874` + +- Fix inverted gethostname() check in rndc status. ``4a8c6a0933`` + + The replacement of named_os_gethostname() with raw gethostname() + inverted the success check: the "localhost" fallback runs on success, + and on failure the uninitialized hostname buffer is read by + snprintf(), leaking stack memory via the rndc status reply. + :gl:`#5889` :gl:`!11879` + +- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. + ``c137dcd1a4`` + + `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + range of 1..1024, but any value above 512 aborted on hardened builds + instead of producing a key. The full advertised range now works. + :gl:`#5903` :gl:`!11903` + +- Validate key names in rndc-confgen, tsig-keygen, ddns-confgen. + ``b8e09a5b5f`` + + The three tools embedded the key-name argument verbatim into the + generated `named.conf` block, so a name containing characters like + `"`, `{`, or `;` produced output that did not match the intended `key` + clause. Key names are now restricted to letters, digits, dots, + hyphens, and underscores. :gl:`#5904` :gl:`!11904` + +- Do not follow symlinks when chowning the NZD database. ``ce77138b5c`` + + When `named` runs as root, the per-view NZD database file is chowned + to the user `named` drops to. The chown call followed symlinks, so a + symlink at the database path could redirect the ownership change to an + unrelated file. The chown now refuses non-regular files and never + follows symlinks. :gl:`#5905` :gl:`!11907` + +- Prevent crafted queries from degrading RRL performance. ``cf18479882`` + + With response rate limiting enabled, an attacker sending queries from + many spoofed source addresses could steer entries into the same slot + of the internal rate-limit table and slow down query processing on the + affected server. The table now uses a per-process keyed hash so the + placement of entries cannot be predicted or influenced from the + network. :gl:`#5906` :gl:`!11950` + +- Fix swapped arguments in redirect2() single-label branch. + ``f5853e765f`` + + On a recursive resolver with nxdomain-redirect configured, an NXDOMAIN + result for a query whose qname is the root could corrupt the view's + nxdomain-redirect target, after which the redirect feature stopped + working for every subsequent query in that view until named was + restarted. :gl:`#5908` :gl:`!11908` + +- Avoid named assertion failure during parent-NS lookups when none + exist. ``90c7385000`` + + Configuring the root zone as a signed primary with parental agents (or + with notify-on-cds-changes) caused named to exit on an internal + assertion as soon as the DS-publication machinery tried to look up the + parent NS RRset — the root has no parent. The lookup is now + short-circuited cleanly. + + Similar, a zone with no NS records in the parent caused named to exit + in the same way. :gl:`#5910` :gl:`!11909` + +- Remove the rndc testgen command. ``28025ceff8`` + + testgen existed only to let the rndc system test generate large + response payloads. It accepted an unbounded count and was reachable + from read-only control channels, so any read-only rndc client could + drive named into memory exhaustion. The command and its supporting + test helper are gone; remaining rndc commands already produce + non-trivial responses, so transport coverage is preserved. :gl:`#5911` + :gl:`!11912` + +- Free per-command rndc state when response serialisation fails. + ``bf7ee390ba`` + + When isccc_cc_towire failed while building an rndc reply, + control_respond returned without releasing the per-command request, + response, HMAC secret copy, and text buffer. They were eventually + freed when the connection closed, but until then the HMAC key copy + stayed in named's memory. The failure path now goes through the same + cleanup label as every other error. :gl:`#5913` :gl:`!11915` + +- Handle KSR files with DNSKEY records before any header. ``55213079c6`` + + A DNSKEY record appearing before the first ';; KeySigningRequest' + header in a KSR file made dnssec-ksr abort on an internal assertion + instead of producing a structured error, killing pipelines that fed it + crafted or corrupted input. The tool now exits with a fatal error + naming the file and line. :gl:`#5914` :gl:`!11916` + +- Prevent rare named crash when notifies are cancelled. ``d079ca1b92`` + + Under heavy load, named could occasionally crash when a queued + outbound notify or zone refresh was cancelled at the moment it was + being sent — for example, while a zone was being reloaded or removed. + The race that caused the crash is now prevented. :gl:`#5915` + :gl:`!11918` + +- Stop delv from aborting on a malformed query name. ``ddf6239534`` + + delv aborts with SIGABRT instead of exiting cleanly when given a query + name that fails wire-format conversion (e.g. a label longer than 63 + octets). After this change delv prints the parse error and exits with + a normal failure code. :gl:`#5916` :gl:`!11921` + +- Fix dig -x crash on excessively long arguments. ``62ced63e22`` + + dig -x crashed with a segmentation fault rather than printing an error + when given an argument with thousands of dot-separated components. dig + -x now rejects such inputs cleanly with "Invalid IP address". + :gl:`#5917` :gl:`!11928` + +- Reject RSA DNSKEYs with degenerate modulus. ``d455794d0d`` + + A crafted DNSKEY rdata whose declared exponent length consumed the + whole buffer produced an RSA key with no modulus, which + dnssec-importkey accepted as valid and wrote to a .private file with + no key material. The wire-format parser now rejects RSA public keys + with a modulus smaller than 512 bits, the lowest legitimate size + across the RSA DNSSEC algorithms. :gl:`#5920` :gl:`!11929` + +- Reject negative and out-of-range TTLs in dnssec-* tools. + ``31818f6417`` + + The dnssec-* tools accepted negative and out-of-range values for TTL + flags such as dnssec-keygen -L, dnssec-signzone -t and dnssec-settime + -L, silently turning them into TTLs of around 136 years in the + resulting key or zone files. The flag values are now validated and + rejected with a clear "TTL must be non-negative" or "TTL out of range" + error. :gl:`#5923` :gl:`!11933` + +- Fix a crash when reconfiguring while an NTA is being rechecked. + ``386177ec67`` + + When named was reconfigured or shut down while a negative trust anchor + was being rechecked against authoritative servers, the in-flight + recheck could outlive the view that owned it and cause `named` to + crash. This has been fixed. :gl:`#5938` :gl:`!11948` + +- Fix a bug in allow-query/allow-transfer catalog zone custom + properties. ``774e08dee3`` + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog zone with an invalid ``allow-query`` or + ``allow-transfer`` custom property (i.e. having a non-APL type) + coexisting with the valid property. This has been fixed. :gl:`#5941` + :gl:`!11954` + +- Fix a stack use-after-free in qpzone. ``82f67fc633`` + + In previous_closest_nsec(), a new qpreader was opened to search the + NSEC tree. It was possible for that to be used to update a QP iterator + object owned by the caller, and then be destroyed when the function + returned. + + This qpreader object isn't necessary anymore; since namespaces were + added to the QP trie in commit 15653c54a0, we can now just reuse the + existing reader for the main tree. :gl:`#5942` :gl:`!11955` + +- Fix a memory leak issue in the catalog zones. ``deb3694a63`` + + The :iscman:`named` process could leak small amounts of memory when + processing a catalog zone entry which had defined custom primary + servers with TSIG keys using both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) at the + same time. This has been fixed. :gl:`#5943` :gl:`!11951` + +- Avoid extra round trips for DS lookups when the parent delegation is + already cached. ``07c8cddb4c`` + + DS queries could take two unnecessary extra round trips when the + resolver sent them to the child zone instead of the parent. The child + responds with NODATA, forcing a recovery path to rediscover the parent + delegation even though it was already cached. The resolver now + consults its delegation cache before starting DS fetches, sending + queries directly to the correct parent nameservers and eliminating the + extra latency. :gl:`!11835` + +- Enforce dns_adb_createaddrinfofind() invariant. ``bb330e533b`` + + ADB `dns_adb_createaddrinfofind()` expects `maxaddrs` paramaters is + always strictly positive. Add an assertion to enforce it. :gl:`!11819` + +- Fix a bug with template filename reuse. ``cf11b88e0e`` + + When a zone filename is defined in `named.conf` which will be written + to by the server - i.e., for secondary or dynamically updated zones - + there is a test at configuration time to ensure that the filename is + non-unique. + + This test is run before the zone is actually created, so a zone + configured using a template may not have had its filename expanded + yet. This can cause a configuration to fail because, for example, + multiple zones appear to using the filename `$name.db`. This has + been fixed by adding a new function `dns_zone_expandzonefile()` and + calling it during the uniqueness check. :gl:`!11769` + +- Fix suppressed missing-glue check in named-checkzone. ``e75f146485`` + + named-checkzone and named-checkconf -z silently skipped the + missing-glue check for any NS name that had already triggered an + extra-AAAA-glue warning, so zones missing required A glue could pass + validation and be deployed with broken delegations. :gl:`!11899` + +- Glues from different parent are rejected. ``48d098467f`` + + The changes making BIND 9 parent-centric !11621 introduced an issue + where it could be possible, when processing a referral, to use the + glue to a nameserver which has a different parent than the zonecut. + For instance: + + AUTHORITY test.example. NS ns.test.example. + test.example. NS ns.foo.example. test.example. + NS ns.bar. ADDITIONAL ns.bar. + A 1.2.3.4 ns.foo.example. A 5.6.7.8 + ns.test.example. A 9.8.7.6 + + In such situation, only the glues for `ns.foo.example.` and + `ns.test.example.` should be used, and the glue from `ns.bar.` must be + ignored as this is not either a sub-domain or a sibling domain, the + parent is different (`bar.` instead of `example.`). This is now fixed. + + Sibling glue and cyclic sibling glues are defined in RFC 9471 section + 2.2 and section 2.3. :gl:`!11873` + +- Harden dig's EDNS option parsing against malformed replies. + ``7b87ab0236`` + + dig's parser for EDNS options in a DNS reply now stops cleanly when an + option declares a length that runs past the end of the option data, + rather than trusting the upstream OPT-record validator to reject the + reply first. This is a defensive change; behavior is unchanged in + practice. :gl:`!11937` + +- Implement seamless outgoing TCP connection reuse. ``a61427e8ee`` + + The resolver can and will reuse outgoing TCP connections to the same + host, as recommended by RFC 7766. This prevents a whole class of + attacks that abuse the fact that establishing a TCP connection is + expensive and it is fairly easy to deplete the outgoing TCP ports by + putting them into TIME_WAIT state. + + The number of pipelined queries per connection is capped at 256 to + limit the impact of a connection drop. :gl:`!11845` + +- Include according by checking in meson. ``f27aba4d7d`` + + The header has existed in macOS since around ~26. This + causes the `htobeNN`/`htoleNN` macros to be redefined in + in terms of when other system + headers include . + + Fix this issue by using checking for the existence of + in meson and including it according to the probe result. :gl:`!11751` + +- Pass empty string instead of NULL to ns_client_dumpmessage() + ``a0084190b4`` + + Pass "" instead of NULL to ns_client_dumpmessage() to get the log + message printed. + +- Possible crash when a resolver validate a static-stub zone. + ``b8dcabbd72`` + + A NULL pointer dereference could be made in some circumstances when + resolving and validating a name under a `static-stub` zone. This is + now fixed. :gl:`!11788` + +- Prevent excessive priming queries to the root servers. ``5f8624df76`` + + BIND was sending a priming query to the root servers on nearly every + recursive lookup instead of only when the cached root information + expired. Priming now rearms only after the TTL of the fetched records + elapses, and the refreshed root NS set is used for query routing until + the next cycle. :gl:`!11847` + +- Reject record sets too large to serve in DNS. ``a925af7ce6`` + + When BIND was asked to store a record set whose total size exceeds + what fits in a DNS message, it would allocate memory and build the + structure, then fail later at response time. Such oversized record + sets are now rejected at the time of storage with an error, avoiding + wasted work on data that can never be served. :gl:`!11963` + +- Remove deadcode in `query_addbestns()` ``43c58dafcc`` + + The local variable `zfname` was released in the cleanup part of the + function if not NULL, but it turns out it is now always NULL at that + point. + + The flow can get to that part only in two cases: either `zfname` is + not NULL, and then it's ownership is moved to a different variable + (thus, it is now NULL), or `zfname` is already NULL. + + Removing the bit of deadcode releasing it. :gl:`!11790` + +- Remove unneeded options in dns_zonefetch. ``a43a6cfba4`` + + In the `dns_zonefetch` mechanism, some option flags for + `dns_resolver_createfetch()` were used for all fetches, but were + actually only needed by the `DNSKEY` refresh fetches. + + (Specifially, these options were `DNS_FETCHOPT_UNSHARED` and + `DNS_FETCHOPT_NOCACHED`, which were used along with + `DNS_FETCHOPT_NOVALIDATE` to ensure we get a new copy of the DNSKEY as + it is currently published by the authority, without prior validation. + Those conditions are needed for RFC 5011 trust anchor maintenace, but + not when looking up parent-`NS` or `DSYNC` RRsets.) :gl:`!11866` + +- Stop rndc-confgen from following symlinks when writing the keyfile. + ``468b09feb2`` + + When rndc-confgen -a (re)created the rndc control key, it followed a + symbolic link if one happened to exist at the keyfile path: the + existence check looked through the link, then the file was truncated, + its ownership changed, and the key contents written into whatever file + the link pointed at. rndc-confgen now refuses to follow symbolic links + at the keyfile path and fails with an error instead, so the wrong file + can no longer be overwritten by accident. :gl:`!11902` + +- Validate -l and -L numeric arguments in named-checkzone. + ``1064d11af2`` + + named-checkzone and named-compilezone parsed the -l (max TTL) and -L + (source serial) arguments with strtol(), so a negative value such as + -1 silently became UINT32_MAX and out-of-range values were truncated + to 32 bits without warning; -l in particular appeared to cap TTLs but + no longer enforced anything. Both flags now go through + isc_parse_uint32() and reject any value that is not a valid 32-bit + unsigned integer. :gl:`!11900` + + From 631bede2ffa91a34e502462a75fb14aeb2f02c19 Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Thu, 7 May 2026 14:13:00 +0200 Subject: [PATCH 34/36] Prepare release notes for BIND 9.21.22 --- doc/arm/notes.rst | 1 + doc/notes/notes-9.21.22.rst | 402 ++++++++++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 doc/notes/notes-9.21.22.rst diff --git a/doc/arm/notes.rst b/doc/arm/notes.rst index fa13bbe88e..a434d7b276 100644 --- a/doc/arm/notes.rst +++ b/doc/arm/notes.rst @@ -47,6 +47,7 @@ The list of known issues affecting the latest version in the 9.21 branch can be found at https://gitlab.isc.org/isc-projects/bind9/-/wikis/Known-Issues-in-BIND-9.21 +.. include:: ../notes/notes-9.21.22.rst .. include:: ../notes/notes-9.21.21.rst .. include:: ../notes/notes-9.21.20.rst .. include:: ../notes/notes-9.21.19.rst diff --git a/doc/notes/notes-9.21.22.rst b/doc/notes/notes-9.21.22.rst new file mode 100644 index 0000000000..02caf692f8 --- /dev/null +++ b/doc/notes/notes-9.21.22.rst @@ -0,0 +1,402 @@ +.. 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. + +Notes for BIND 9.21.22 +---------------------- + +Security Fixes +~~~~~~~~~~~~~~ + +- Fix outgoing zone transfers' quota issue. + + Unauthorized clients could consume outgoing zone transfers quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` + +- [CVE-2026-3592] 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. :gl:`#5641` + +- [CVE-2026-3039] Fix GSS-API resource leak. + + Fixed a memory leak where each GSS-API TKEY negotiation leaked a + security context inside the GSS library. An unauthenticated attacker + could exhaust server memory by sending repeated TKEY queries to a + server with tkey-gssapi-keytab configured. The leaked memory was + allocated by the GSS library, bypassing BIND's memory accounting. + + Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now + rejected, as BIND never supported it correctly and Kerberos/SPNEGO + completes in a single round. :gl:`#5752` + +- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN + views. + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with recursion yes in a non-IN view will log a warning at + startup, and named-checkconf flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. :gl:`#5784` + +- [CVE-2026-5950] Avoid unbounded recursion loop. + + A bug during bad server handling could cause the resolver to enter an + infinite loop, continuously sending queries to an upstream server with + no exit condition, until the resolver query timeout was hit. This has + been fixed. + + ISC would like to thank Billy Baraja (BielraX) for bringing this issue + to our attention. :gl:`#5804` + +- [CVE-2026-5947] Fix crash in resolver when SIG(0)-signed responses are + received under load. + + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. :gl:`#5819` + +- Fix race condition in getsigningtime() + + Compute qpzone_get_lock(elem->node) into a local variable while the + heap lock is still held, rather than dereferencing the stale elem + pointer after releasing the lock. A concurrent thread running + setsigningtime() (e.g. via IXFR apply on a worker thread) could free + the top-of-heap element between the heap lock release and the + dereference, causing a use-after-free. :gl:`#5883` + +- [CVE-2026-3593] Fix use-after-free in DNS-over-HTTPS when processing + HTTP/2 SETTINGS frames. + + A use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause named to crash when a client sends a flood of HTTP/2 + SETTINGS frames while a DoH response is being written. This affects + servers with DoH (DNS-over-HTTPS) enabled. + + ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting + this. + + For: #5755 + +Feature Changes +~~~~~~~~~~~~~~~ + +- Fix CPU spikes and slow queries when cache approaches memory limit. + + Spread cache cleanup probabilistically to avoid CPU usage spikes and a + drop in query throughput. :gl:`#5891` + +- Document that named-checkzone must not run on untrusted input. + + The zone-file parser implements $INCLUDE by opening whatever local + path the zone text names, and fragments of the included file leak + through parser error messages. There is no safe way to validate + untrusted zone text with named-checkzone or named-compilezone, so the + manual pages for both tools now warn against doing so. + +- Implement RFC 3645 Section 4.1.1 key expiry check in TKEY. + + Check for existing TSIG keys before accepting a new GSS-API + negotiation and delete the key if it has expired. Previously, an + expired GSS key would permanently block re-negotiation for that name + until the server was restarted. + +- Reduce memory footprint by actively returning unused memory to the OS. + + Previously, :iscman:`named` relied on the default allocator settings + for releasing unused memory back to the operating system, which could + result in unnecessarily high resident memory usage. :iscman:`named` + now actively manages memory page purging. On systems using jemalloc, + background cleanup threads are enabled and the dirty page decay time + is reduced from 10 seconds to 5 seconds. Additionally, a volume-based + decay pass is triggered after every 16 MiB of freed memory. On + glibc-based systems, a similar volume-based mechanism using + malloc_trim() is used instead. + +Bug Fixes +~~~~~~~~~ + +- Use the zone file's basename as origin in DNSSEC tools. + + In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not + specified using the `-o` parameter, the default behavior is to try to + sign using the zone's file name as the origin. So, for example, + `dnssec-signzone -S example.com` will work, so long as the file name + matches the zone name. + + This now also works if the zone is in a different directory. For + example, `dnssec-signzone -S zones/example.com` will set the origin + value to `example.com`. :gl:`#5678` + +- Fix a possible race condition during zone transfers. + + The :iscman:`named` process could terminate unexpectedly when + processing an IXFR message during a zone transfer. This has been + fixed. :gl:`#5767` + +- Do not resend query after BADCOOKIE answer on TCP. + + When an upstream server answers BADCOOKIE, no matter which transport + is used, the resolver resends the query using TCP. However, if the + upstream server responded with BADCOOKIE again over TCP, the resolver + would keep resending until the maximum query count was reached. + + This is now fixed by no longer resending once the query has already + been sent over TCP. :gl:`#5804` + +- Fix named crash when processing SIG records in dynamic updates. + + Previously, :iscman:`named` could abort if a client sent a dynamic + update containing a SIG record (the legacy signature type) to a zone + configured with an update-policy. The function `dns_db_findrdataset` + had an incorrect requirements prerequisite that prevented SIG records + being looked up, which was triggered as part of processing an UPDATE + request and could be triggered remotely by any client permitted to + send updates. This has been fixed by ensuring that SIG records are + handled consistently with RRSIG records during update processing. + :gl:`#5818` + +- Fix wrong NSEC proof for empty non-terminals after IXFR. + + When a secondary received an IXFR that transitioned a zone from + unsigned to NSEC-signed, queries for empty non-terminal names returned + the zone apex NSEC record instead of the NSEC that actually covers the + queried name. The issue only occurred with incremental transfers; a + full AXFR or a server restart resolved it. :gl:`#5824` + +- Fix rndc modzone behavior for a zone in named.conf. + + If a zone was present in the configuration file and not originally + added by `rndc addzone`, `rndc modzone` for that zone would succeed + once but subsequent `modzone` attempts would fail. This has been + fixed. :gl:`#5826` + +- Fix zone verification of NSEC3 signed zones. + + Previously, when computing the compressed bitmap during verification + of an NSEC3-signed zone, an undersized buffer was used that resulted + in an out-of-bounds write if there were too many active windows in the + bitmap. This impacted mirror zones which are NSEC3-signed, + `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + :gl:`#5834` + +- Fix 'rndc modzone' issue with non-existing zones. + + The :iscman:`named` process could terminate unexpectedly or become + subject to undefined behavior when issued an :option:`rndc modzone` + operation for a non-existing zone. This has been fixed. :gl:`#5848` + +- Fix zone filename token-parsing bug. + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog member zone containing special characters like + '%' or '$' which could be interpreted as zone filename tokens and + trigger a case-sensitivity bug in the token-parsing code. This has + been fixed. :gl:`#5849` + +- Prevent a crash when using both dns64 and filter-aaaa. + + An assertion failure could be triggered if both `dns64` and the + `filter-aaaa` plugin were in use simultaneously. This happened if the + plugin triggered a second recursion process, which then attempted to + store DNS64 state information in a pointer that had already been set + by the original recursion process. This has been fixed. :gl:`#5854` + +- Remove unnecessary dns_name_free call. + + When processing a catalog zone member's primaries definition and there + is a TXT record containing an invalid name TSIG key name, + dns_name_free was incorrectly called triggering an assertion. This has + been fixed. :gl:`#5858` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + + A DNSSEC-signed zone could publish a DNSKEY with an unusually large + RSA public exponent and force any validator resolving names in that + zone to spend disproportionate CPU verifying signatures. The + validator now rejects such DNSKEYs, matching the limit already applied + to keys read from files or HSMs. :gl:`#5881` + +- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. + + `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + range of 1..1024, but any value above 512 aborted on hardened builds + instead of producing a key. The full advertised range now works. + :gl:`#5903` + +- Validate key names in rndc-confgen, tsig-keygen, ddns-confgen. + + The three tools embedded the key-name argument verbatim into the + generated `named.conf` block, so a name containing characters like + `"`, `{`, or `;` produced output that did not match the intended `key` + clause. Key names are now restricted to letters, digits, dots, + hyphens, and underscores. :gl:`#5904` + +- Prevent crafted queries from degrading RRL performance. + + With response rate limiting enabled, an attacker sending queries from + many spoofed source addresses could steer entries into the same slot + of the internal rate-limit table and slow down query processing on the + affected server. The table now uses a per-process keyed hash so the + placement of entries cannot be predicted or influenced from the + network. :gl:`#5906` + +- Prevent rare named crash when notifies are cancelled. + + Under heavy load, named could occasionally crash when a queued + outbound notify or zone refresh was cancelled at the moment it was + being sent — for example, while a zone was being reloaded or removed. + The race that caused the crash is now prevented. :gl:`#5915` + +- Stop delv from aborting on a malformed query name. + + delv aborts with SIGABRT instead of exiting cleanly when given a query + name that fails wire-format conversion (e.g. a label longer than 63 + octets). After this change delv prints the parse error and exits with + a normal failure code. :gl:`#5916` + +- Fix dig -x crash on excessively long arguments. + + dig -x crashed with a segmentation fault rather than printing an error + when given an argument with thousands of dot-separated components. dig + -x now rejects such inputs cleanly with "Invalid IP address". + :gl:`#5917` + +- Reject negative and out-of-range TTLs in dnssec-* tools. + + The dnssec-* tools accepted negative and out-of-range values for TTL + flags such as dnssec-keygen -L, dnssec-signzone -t and dnssec-settime + -L, silently turning them into TTLs of around 136 years in the + resulting key or zone files. The flag values are now validated and + rejected with a clear "TTL must be non-negative" or "TTL out of range" + error. :gl:`#5923` + +- Fix a crash when reconfiguring while an NTA is being rechecked. + + When named was reconfigured or shut down while a negative trust anchor + was being rechecked against authoritative servers, the in-flight + recheck could outlive the view that owned it and cause `named` to + crash. This has been fixed. :gl:`#5938` + +- Fix a bug in allow-query/allow-transfer catalog zone custom + properties. + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog zone with an invalid ``allow-query`` or + ``allow-transfer`` custom property (i.e. having a non-APL type) + coexisting with the valid property. This has been fixed. :gl:`#5941` + +- Fix a memory leak issue in the catalog zones. + + The :iscman:`named` process could leak small amounts of memory when + processing a catalog zone entry which had defined custom primary + servers with TSIG keys using both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) at the + same time. This has been fixed. :gl:`#5943` + +- Avoid extra round trips for DS lookups when the parent delegation is + already cached. + + DS queries could take two unnecessary extra round trips when the + resolver sent them to the child zone instead of the parent. The child + responds with NODATA, forcing a recovery path to rediscover the parent + delegation even though it was already cached. The resolver now + consults its delegation cache before starting DS fetches, sending + queries directly to the correct parent nameservers and eliminating the + extra latency. + +- Fix suppressed missing-glue check in named-checkzone. + + named-checkzone and named-checkconf -z silently skipped the + missing-glue check for any NS name that had already triggered an + extra-AAAA-glue warning, so zones missing required A glue could pass + validation and be deployed with broken delegations. + +- Glues from different parent are rejected. + + The changes making BIND 9 parent-centric !11621 introduced an issue + where it could be possible, when processing a referral, to use the + glue to a nameserver which has a different parent than the zonecut. + For instance: + + AUTHORITY test.example. NS ns.test.example. + test.example. NS ns.foo.example. test.example. + NS ns.bar. ADDITIONAL ns.bar. + A 1.2.3.4 ns.foo.example. A 5.6.7.8 + ns.test.example. A 9.8.7.6 + + In such situation, only the glues for `ns.foo.example.` and + `ns.test.example.` should be used, and the glue from `ns.bar.` must be + ignored as this is not either a sub-domain or a sibling domain, the + parent is different (`bar.` instead of `example.`). This is now fixed. + + Sibling glue and cyclic sibling glues are defined in RFC 9471 section + 2.2 and section 2.3. + +- Implement seamless outgoing TCP connection reuse. + + The resolver can and will reuse outgoing TCP connections to the same + host, as recommended by RFC 7766. This prevents a whole class of + attacks that abuse the fact that establishing a TCP connection is + expensive and it is fairly easy to deplete the outgoing TCP ports by + putting them into TIME_WAIT state. + + The number of pipelined queries per connection is capped at 256 to + limit the impact of a connection drop. + +- Possible crash when a resolver validate a static-stub zone. + + A NULL pointer dereference could be made in some circumstances when + resolving and validating a name under a `static-stub` zone. This is + now fixed. + +- Prevent excessive priming queries to the root servers. + + BIND was sending a priming query to the root servers on nearly every + recursive lookup instead of only when the cached root information + expired. Priming now rearms only after the TTL of the fetched records + elapses, and the refreshed root NS set is used for query routing until + the next cycle. + +- Reject record sets too large to serve in DNS. + + When BIND was asked to store a record set whose total size exceeds + what fits in a DNS message, it would allocate memory and build the + structure, then fail later at response time. Such oversized record + sets are now rejected at the time of storage with an error, avoiding + wasted work on data that can never be served. + +- Stop rndc-confgen from following symlinks when writing the keyfile. + + When rndc-confgen -a (re)created the rndc control key, it followed a + symbolic link if one happened to exist at the keyfile path: the + existence check looked through the link, then the file was truncated, + its ownership changed, and the key contents written into whatever file + the link pointed at. rndc-confgen now refuses to follow symbolic links + at the keyfile path and fails with an error instead, so the wrong file + can no longer be overwritten by accident. + + From 0f31172f9332474f46e3b4135f9c95adf94a075f Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Thu, 7 May 2026 16:06:18 +0200 Subject: [PATCH 35/36] Tweak and reword release notes --- doc/notes/notes-9.21.22.rst | 292 ++++++++++++++++++------------------ 1 file changed, 146 insertions(+), 146 deletions(-) diff --git a/doc/notes/notes-9.21.22.rst b/doc/notes/notes-9.21.22.rst index 02caf692f8..048207b073 100644 --- a/doc/notes/notes-9.21.22.rst +++ b/doc/notes/notes-9.21.22.rst @@ -15,45 +15,46 @@ Notes for BIND 9.21.22 Security Fixes ~~~~~~~~~~~~~~ -- Fix outgoing zone transfers' quota issue. +- Limit resolver server list size. :cve:`2026-3592` - Unauthorized clients could consume outgoing zone transfers quota and - block authorized zone transfer clients. This has been fixed. - :gl:`#3589` - -- [CVE-2026-3592] Limit resolver server list size. - - When resolving a domain with many nameservers that share overlapping + When resolving a domain with many nameservers that shared 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 + addresses and build up excessively large server lists. Addresses in + the resolver's server list are now deduplicated 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. :gl:`#5641` + point to it. The number of addresses stored per nameserver name + is also now capped at six (combined A and AAAA), preventing memory and CPU overhead from + domains with unusually large NS/glue sets. -- [CVE-2026-3039] Fix GSS-API resource leak. + ISC would like to thank Shuhan Zhang from Tsinghua University for + reporting this issue. :gl:`#5641` - Fixed a memory leak where each GSS-API TKEY negotiation leaked a +- Fix GSS-API resource leak. :cve:`2026-3039` + + A memory leak was fixed where each GSS-API TKEY negotiation leaked a security context inside the GSS library. An unauthenticated attacker could exhaust server memory by sending repeated TKEY queries to a - server with tkey-gssapi-keytab configured. The leaked memory was + server with :any:`tkey-gssapi-keytab` configured. The leaked memory was allocated by the GSS library, bypassing BIND's memory accounting. Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now rejected, as BIND never supported it correctly and Kerberos/SPNEGO - completes in a single round. :gl:`#5752` + completes in a single round. -- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN - views. + ISC would like to thank Vitaly Simonovich for bringing this + vulnerability to our attention. :gl:`#5752` + +- Disable recursion, UPDATE, and NOTIFY for non-IN views. + :cve:`2026-5946` Recursion, dynamic updates (UPDATE), and zone change notifications (NOTIFY) are now disabled for views with a class other than IN (such as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. version.bind in class CHAOS) continues to work as before. Servers - configured with recursion yes in a non-IN view will log a warning at - startup, and named-checkconf flags the same condition. UPDATE and + configured with :namedconf:ref:`recursion yes; ` + in a non-IN view log a warning at + startup, and :iscman:`named-checkconf` flags the same condition. UPDATE and NOTIFY messages that specify the meta-classes ANY or NONE in the question section are now rejected with FORMERR. @@ -61,7 +62,7 @@ Security Fixes identified as CVE-2026-5946. ISC would like to thank Mcsky23 for bringing these issues to our attention. :gl:`#5784` -- [CVE-2026-5950] Avoid unbounded recursion loop. +- Avoid unbounded recursion loop. :cve:`2026-5950` A bug during bad server handling could cause the resolver to enter an infinite loop, continuously sending queries to an upstream server with @@ -71,58 +72,49 @@ Security Fixes ISC would like to thank Billy Baraja (BielraX) for bringing this issue to our attention. :gl:`#5804` -- [CVE-2026-5947] Fix crash in resolver when SIG(0)-signed responses are - received under load. +- Fix crash in resolver when SIG(0)-signed responses are received under + load. :cve:`2026-5947` A resolver could crash when handling a SIG(0)-signed response if the matching client query was cancelled while signature verification was still in progress — for example, when the recursive-clients quota was - exhausted. This has been fixed. :gl:`#5819` + exhausted. This has been fixed. -- Fix race condition in getsigningtime() + ISC would like to thank Naoki Wakamatsu for bringing this + vulnerability to our attention. :gl:`#5819` - Compute qpzone_get_lock(elem->node) into a local variable while the - heap lock is still held, rather than dereferencing the stale elem - pointer after releasing the lock. A concurrent thread running - setsigningtime() (e.g. via IXFR apply on a worker thread) could free - the top-of-heap element between the heap lock release and the - dereference, causing a use-after-free. :gl:`#5883` +- Fix use-after-free error in DNS-over-HTTPS when processing HTTP/2 + SETTINGS frames. :cve:`2026-3593` -- [CVE-2026-3593] Fix use-after-free in DNS-over-HTTPS when processing - HTTP/2 SETTINGS frames. - - A use-after-free vulnerability in the DNS-over-HTTPS implementation - could cause named to crash when a client sends a flood of HTTP/2 - SETTINGS frames while a DoH response is being written. This affects - servers with DoH (DNS-over-HTTPS) enabled. + Previously, a use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause :iscman:`named` to crash when a client sent a flood of HTTP/2 + SETTINGS frames while a DoH response was being written. This affected + servers with DoH (DNS-over-HTTPS) enabled and has been fixed. ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting - this. + this. :gl:`#5755` + +- Fix outgoing zone transfers' quota issue. + + Unauthorized clients could consume the entire outgoing zone-transfer quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` - For: #5755 Feature Changes ~~~~~~~~~~~~~~~ - Fix CPU spikes and slow queries when cache approaches memory limit. - Spread cache cleanup probabilistically to avoid CPU usage spikes and a + Cache cleanup is now spread probabilistically to avoid CPU usage spikes and a drop in query throughput. :gl:`#5891` -- Document that named-checkzone must not run on untrusted input. +- Implement :rfc:`3645` Section 4.1.1 key expiry check in TKEY. - The zone-file parser implements $INCLUDE by opening whatever local - path the zone text names, and fragments of the included file leak - through parser error messages. There is no safe way to validate - untrusted zone text with named-checkzone or named-compilezone, so the - manual pages for both tools now warn against doing so. - -- Implement RFC 3645 Section 4.1.1 key expiry check in TKEY. - - Check for existing TSIG keys before accepting a new GSS-API - negotiation and delete the key if it has expired. Previously, an + BIND now checks for existing TSIG keys before accepting a new GSS-API + negotiation, and deletes the key if it has expired. Previously, an expired GSS key would permanently block re-negotiation for that name - until the server was restarted. + until the server was restarted. :gl:`!11713` - Reduce memory footprint by actively returning unused memory to the OS. @@ -134,22 +126,22 @@ Feature Changes is reduced from 10 seconds to 5 seconds. Additionally, a volume-based decay pass is triggered after every 16 MiB of freed memory. On glibc-based systems, a similar volume-based mechanism using - malloc_trim() is used instead. + ``malloc_trim()`` is used instead. :gl:`!11761` Bug Fixes ~~~~~~~~~ - Use the zone file's basename as origin in DNSSEC tools. - In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not - specified using the `-o` parameter, the default behavior is to try to + In :iscman:`dnssec-signzone` and :iscman:`dnssec-verify`, when the zone origin is not + specified using the ``-o`` parameter, the default behavior is to try to sign using the zone's file name as the origin. So, for example, - `dnssec-signzone -S example.com` will work, so long as the file name + ``dnssec-signzone -S example.com`` will work, so long as the file name matches the zone name. This now also works if the zone is in a different directory. For - example, `dnssec-signzone -S zones/example.com` will set the origin - value to `example.com`. :gl:`#5678` + example, ``dnssec-signzone -S zones/example.com`` will set the origin + value to ``example.com``. :gl:`#5678` - Fix a possible race condition during zone transfers. @@ -157,23 +149,23 @@ Bug Fixes processing an IXFR message during a zone transfer. This has been fixed. :gl:`#5767` -- Do not resend query after BADCOOKIE answer on TCP. +- Do not resend query after ``BADCOOKIE`` answer on TCP. - When an upstream server answers BADCOOKIE, no matter which transport - is used, the resolver resends the query using TCP. However, if the - upstream server responded with BADCOOKIE again over TCP, the resolver + When an upstream server answered ``BADCOOKIE``, no matter which transport + was used, the resolver resent the query using TCP. However, if the + upstream server responded with ``BADCOOKIE`` again over TCP, the resolver would keep resending until the maximum query count was reached. This is now fixed by no longer resending once the query has already been sent over TCP. :gl:`#5804` -- Fix named crash when processing SIG records in dynamic updates. +- Fix :iscman:`named` crash when processing SIG records in dynamic updates. Previously, :iscman:`named` could abort if a client sent a dynamic update containing a SIG record (the legacy signature type) to a zone configured with an update-policy. The function `dns_db_findrdataset` had an incorrect requirements prerequisite that prevented SIG records - being looked up, which was triggered as part of processing an UPDATE + from being looked up, which was triggered as part of processing an UPDATE request and could be triggered remotely by any client permitted to send updates. This has been fixed by ensuring that SIG records are handled consistently with RRSIG records during update processing. @@ -183,15 +175,15 @@ Bug Fixes When a secondary received an IXFR that transitioned a zone from unsigned to NSEC-signed, queries for empty non-terminal names returned - the zone apex NSEC record instead of the NSEC that actually covers the + the zone apex NSEC record instead of the NSEC that actually covered the queried name. The issue only occurred with incremental transfers; a full AXFR or a server restart resolved it. :gl:`#5824` -- Fix rndc modzone behavior for a zone in named.conf. +- Fix :option:`rndc modzone` behavior for a zone in named.conf. If a zone was present in the configuration file and not originally - added by `rndc addzone`, `rndc modzone` for that zone would succeed - once but subsequent `modzone` attempts would fail. This has been + added by :option:`rndc addzone`, :option:`rndc modzone` for that zone would succeed + once but subsequent :option:`rndc modzone` attempts would fail. This has been fixed. :gl:`#5826` - Fix zone verification of NSEC3 signed zones. @@ -199,11 +191,11 @@ Bug Fixes Previously, when computing the compressed bitmap during verification of an NSEC3-signed zone, an undersized buffer was used that resulted in an out-of-bounds write if there were too many active windows in the - bitmap. This impacted mirror zones which are NSEC3-signed, - `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + bitmap. This impacted the mirror zones which are NSEC3-signed, + :iscman:`dnssec-signzone` and :iscman:`dnssec-verify`. This has been fixed. :gl:`#5834` -- Fix 'rndc modzone' issue with non-existing zones. +- Fix :option:`rndc modzone` issue with non-existing zones. The :iscman:`named` process could terminate unexpectedly or become subject to undefined behavior when issued an :option:`rndc modzone` @@ -213,23 +205,23 @@ Bug Fixes The :iscman:`named` process could terminate unexpectedly when processing a catalog member zone containing special characters like - '%' or '$' which could be interpreted as zone filename tokens and + "%" or "$", which could be interpreted as zone filename tokens and trigger a case-sensitivity bug in the token-parsing code. This has been fixed. :gl:`#5849` -- Prevent a crash when using both dns64 and filter-aaaa. +- Prevent a crash when using both :any:`dns64` and :any:`filter-aaaa`. - An assertion failure could be triggered if both `dns64` and the - `filter-aaaa` plugin were in use simultaneously. This happened if the + An assertion failure could be triggered if both :any:`dns64` and the + :any:`filter-aaaa` plugin were in use simultaneously. This happened if the plugin triggered a second recursion process, which then attempted to store DNS64 state information in a pointer that had already been set by the original recursion process. This has been fixed. :gl:`#5854` -- Remove unnecessary dns_name_free call. +- Fixed an assertion failure when processing catalog zones. - When processing a catalog zone member's primaries definition and there - is a TXT record containing an invalid name TSIG key name, - dns_name_free was incorrectly called triggering an assertion. This has + If a TXT record containing an invalid name TSIG key name was found + when processing a catalog zone member's primaries definition, + ``dns_name_free`` was incorrectly called, triggering an assertion. This has been fixed. :gl:`#5858` - Prevent malicious DNSSEC zones from exhausting validator CPU. @@ -240,14 +232,15 @@ Bug Fixes validator now rejects such DNSKEYs, matching the limit already applied to keys read from files or HSMs. :gl:`#5881` -- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. +- Fix :iscman:`rndc-confgen` aborting on HMAC-SHA-384/512 keys above 512 bits. - `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + :iscman:`rndc-confgen` (with either ``-A hmac-sha384`` or + ``-A hmac-sha512``) previously documented a ``-b`` range of 1..1024, but any value above 512 aborted on hardened builds instead of producing a key. The full advertised range now works. :gl:`#5903` -- Validate key names in rndc-confgen, tsig-keygen, ddns-confgen. +- Validate key names in :iscman:`rndc-confgen`, :iscman:`tsig-keygen`, and :iscman:`ddns-confgen`. The three tools embedded the key-name argument verbatim into the generated `named.conf` block, so a name containing characters like @@ -264,114 +257,120 @@ Bug Fixes placement of entries cannot be predicted or influenced from the network. :gl:`#5906` -- Prevent rare named crash when notifies are cancelled. +- Prevent rare :iscman:`named` crash when notifies are cancelled. - Under heavy load, named could occasionally crash when a queued + Under heavy load, :iscman:`named` could occasionally crash when a queued outbound notify or zone refresh was cancelled at the moment it was being sent — for example, while a zone was being reloaded or removed. The race that caused the crash is now prevented. :gl:`#5915` -- Stop delv from aborting on a malformed query name. +- Stop :iscman:`delv` from aborting on a malformed query name. - delv aborts with SIGABRT instead of exiting cleanly when given a query - name that fails wire-format conversion (e.g. a label longer than 63 - octets). After this change delv prints the parse error and exits with + :iscman:`delv` previously aborted with SIGABRT instead of exiting cleanly when given a query + name that failed wire-format conversion (e.g. a label longer than 63 + octets). After this change :iscman:`delv` prints the parse error and exits with a normal failure code. :gl:`#5916` -- Fix dig -x crash on excessively long arguments. +- Fix :option:`dig -x` crash on excessively long arguments. - dig -x crashed with a segmentation fault rather than printing an error - when given an argument with thousands of dot-separated components. dig - -x now rejects such inputs cleanly with "Invalid IP address". + Previously, :option:`dig -x` crashed with a segmentation fault rather than printing + an error when given an argument with thousands of dot-separated components. + :option:`dig -x` now rejects such inputs cleanly with "Invalid IP address". :gl:`#5917` -- Reject negative and out-of-range TTLs in dnssec-* tools. +- Reject negative and out-of-range TTLs in ``dnssec-*`` tools. - The dnssec-* tools accepted negative and out-of-range values for TTL - flags such as dnssec-keygen -L, dnssec-signzone -t and dnssec-settime - -L, silently turning them into TTLs of around 136 years in the + The ``dnssec-*`` tools previously accepted negative and out-of-range values for TTL + flags such as :option:`dnssec-keygen -L`, :option:`dnssec-signzone -t` + and :option:`dnssec-settime -L`, + silently turning them into TTLs of around 136 years in the resulting key or zone files. The flag values are now validated and rejected with a clear "TTL must be non-negative" or "TTL out of range" error. :gl:`#5923` - Fix a crash when reconfiguring while an NTA is being rechecked. - When named was reconfigured or shut down while a negative trust anchor + Previously, if :iscman:`named` was reconfigured or shut down while a negative trust anchor was being rechecked against authoritative servers, the in-flight - recheck could outlive the view that owned it and cause `named` to + recheck could outlive the view that owned it and cause :iscman:`named` to crash. This has been fixed. :gl:`#5938` -- Fix a bug in allow-query/allow-transfer catalog zone custom +- Fix a bug in :any:`allow-query`/:any:`allow-transfer` catalog zone custom properties. The :iscman:`named` process could terminate unexpectedly when - processing a catalog zone with an invalid ``allow-query`` or - ``allow-transfer`` custom property (i.e. having a non-APL type) + processing a catalog zone with an invalid :any:`allow-query` or + :any:`allow-transfer` custom property (i.e. having a non-APL type) coexisting with the valid property. This has been fixed. :gl:`#5941` -- Fix a memory leak issue in the catalog zones. +- Fix a memory leak issue in catalog zones. The :iscman:`named` process could leak small amounts of memory when processing a catalog zone entry which had defined custom primary - servers with TSIG keys using both the regular ``primaries`` custom - property syntax and the legacy alternative syntax (``masters``) at the + servers with TSIG keys, if both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) were used at the same time. This has been fixed. :gl:`#5943` - Avoid extra round trips for DS lookups when the parent delegation is already cached. - DS queries could take two unnecessary extra round trips when the + Previously, DS queries could take two unnecessary extra round trips when the resolver sent them to the child zone instead of the parent. The child - responds with NODATA, forcing a recovery path to rediscover the parent - delegation even though it was already cached. The resolver now + would respond with NODATA, forcing a recovery path to rediscover the parent + delegation even though it was already cached. The resolver now consults its delegation cache before starting DS fetches, sending queries directly to the correct parent nameservers and eliminating the - extra latency. + extra latency. :gl:`!11835` -- Fix suppressed missing-glue check in named-checkzone. +- Fix suppressed missing-glue check in :iscman:`named-checkzone`. - named-checkzone and named-checkconf -z silently skipped the - missing-glue check for any NS name that had already triggered an - extra-AAAA-glue warning, so zones missing required A glue could pass - validation and be deployed with broken delegations. + :iscman:`named-checkzone` and :option:`named-checkconf -z` silently + skipped the missing-glue check for any NS name that had already + triggered an extra-AAAA-glue warning, so zones missing required A glue + could pass validation and be deployed with broken delegations. + :gl:`!11899` -- Glues from different parent are rejected. +- Reject glues from different parents. - The changes making BIND 9 parent-centric !11621 introduced an issue + The changes that made BIND 9 parent-centric (!11621) introduced an issue where it could be possible, when processing a referral, to use the - glue to a nameserver which has a different parent than the zonecut. - For instance: + glue to direct queries to a nameserver which had a different parent than the zonecut. + For instance::: - AUTHORITY test.example. NS ns.test.example. - test.example. NS ns.foo.example. test.example. - NS ns.bar. ADDITIONAL ns.bar. - A 1.2.3.4 ns.foo.example. A 5.6.7.8 - ns.test.example. A 9.8.7.6 + AUTHORITY + test.example. NS ns.test.example. + test.example. NS ns.foo.example. + test.example. NS ns.bar. - In such situation, only the glues for `ns.foo.example.` and - `ns.test.example.` should be used, and the glue from `ns.bar.` must be - ignored as this is not either a sub-domain or a sibling domain, the - parent is different (`bar.` instead of `example.`). This is now fixed. + ADDITIONAL + ns.bar. A 1.2.3.4 + ns.foo.example. A 5.6.7.8 + ns.test.example. A 9.8.7.6 - Sibling glue and cyclic sibling glues are defined in RFC 9471 section - 2.2 and section 2.3. + In such a situation, only the glues for ``ns.foo.example.`` and + ``ns.test.example.`` should have been used; the glue from ``ns.bar.`` must be + ignored as this is neither a sub-domain nor a sibling domain, and the + parent is different (``bar.`` instead of ``example.``). This is now fixed. + + Sibling glue and cyclic sibling glues are defined in :rfc:`9471` section + 2.2 and section 2.3. :gl:`!11873` - Implement seamless outgoing TCP connection reuse. The resolver can and will reuse outgoing TCP connections to the same - host, as recommended by RFC 7766. This prevents a whole class of + host, as recommended by :rfc:`7766`. This prevents a whole class of attacks that abuse the fact that establishing a TCP connection is expensive and it is fairly easy to deplete the outgoing TCP ports by - putting them into TIME_WAIT state. + putting them into ``TIME_WAIT`` state. The number of pipelined queries per connection is capped at 256 to - limit the impact of a connection drop. + limit the impact of a connection drop. :gl:`!11845` -- Possible crash when a resolver validate a static-stub zone. +- Prevent possible crash when a resolver validates a static-stub zone. A NULL pointer dereference could be made in some circumstances when resolving and validating a name under a `static-stub` zone. This is - now fixed. + now fixed. :gl:`!11788` - Prevent excessive priming queries to the root servers. @@ -379,24 +378,25 @@ Bug Fixes recursive lookup instead of only when the cached root information expired. Priming now rearms only after the TTL of the fetched records elapses, and the refreshed root NS set is used for query routing until - the next cycle. + the next cycle. :gl:`!11847` - Reject record sets too large to serve in DNS. - When BIND was asked to store a record set whose total size exceeds - what fits in a DNS message, it would allocate memory and build the + When BIND was asked to store a record set whose total size exceeded + what fit in a DNS message, it would allocate memory and build the structure, then fail later at response time. Such oversized record sets are now rejected at the time of storage with an error, avoiding - wasted work on data that can never be served. + wasted work on data that can never be served. :gl:`!11963` -- Stop rndc-confgen from following symlinks when writing the keyfile. +- Stop :iscman:`rndc-confgen` from following symlinks when writing the + keyfile. - When rndc-confgen -a (re)created the rndc control key, it followed a - symbolic link if one happened to exist at the keyfile path: the - existence check looked through the link, then the file was truncated, + When :option:`rndc-confgen -a` (re)created the rndc control key, it + followed a symbolic link if one happened to exist at the keyfile path; + the existence check looked through the link, then the file was truncated, its ownership changed, and the key contents written into whatever file - the link pointed at. rndc-confgen now refuses to follow symbolic links - at the keyfile path and fails with an error instead, so the wrong file - can no longer be overwritten by accident. + the link pointed at. :iscman:`rndc-confgen` now refuses to follow symbolic + links at the keyfile path and fails with an error instead, so the wrong + file can no longer be overwritten by accident. :gl:`!11902` From 02a7b2c8a549b79cf885d1a0cda28b74de22d197 Mon Sep 17 00:00:00 2001 From: Andoni Duarte Pintado Date: Fri, 8 May 2026 16:37:49 +0200 Subject: [PATCH 36/36] Update BIND version for release --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 81e608cfbd..8a853dae98 100644 --- a/meson.build +++ b/meson.build @@ -12,7 +12,7 @@ project( 'bind', ['c'], - version: '9.21.22-dev', + version: '9.21.22', meson_version: '>=1.3.0', license: 'MPL-2.0', default_options: {