]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Introduce max-delegation-servers configuration option
authorOndřej Surý <ondrej@isc.org>
Thu, 26 Feb 2026 09:21:13 +0000 (10:21 +0100)
committerOndřej Surý <ondrej@sury.org>
Wed, 4 Mar 2026 15:13:49 +0000 (16:13 +0100)
Make the maximum number of processed delegation nameservers configurable
via the new 'max-delegation-servers' option (default: 13), replacing the
hardcoded NS_PROCESSING_LIMIT (20).

The default is reduced to 13 to precisely match the maximum number of
root servers that can fit into a classic 512-byte UDP payload.  This
provides a natural, historically sound cap that mitigates resource
exhaustion and amplification attacks from artificially inflated or
misconfigured delegations.

The configuration option is strictly bounded between 1 and 100 to ensure
resolver stability.

bin/include/defaultconfig.h
bin/named/server.c
bin/tests/system/nsprocessinglimit/ns4/named.conf.j2
bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py
doc/arm/reference.rst
doc/misc/options
lib/dns/include/dns/view.h
lib/dns/resolver.c
lib/dns/view.c
lib/isccfg/namedconf.c

index c92aeaea054d01c06158c81a2611af4c2a4ce1ed..8df5172bd7ccd80048901dad6c45881225ab7c59 100644 (file)
@@ -145,6 +145,7 @@ options {\n\
                                            "   max-cache-size default;\n\
        max-cache-ttl 604800; /* 1 week */\n\
        max-clients-per-query 100;\n\
+       max-delegation-servers 13;\n\
        max-ncache-ttl 10800; /* 3 hours */\n\
        max-recursion-depth 7;\n\
        max-recursion-queries 50;\n\
index 160e3d5990e68a4d56c48e5d54add41cc44be675..25eb888e7ad9b2cd8e41dd98782b97502a6348b3 100644 (file)
@@ -4945,6 +4945,11 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config,
        INSIST(result == ISC_R_SUCCESS);
        dns_view_setmaxqueries(view, cfg_obj_asuint32(obj));
 
+       obj = NULL;
+       result = named_config_get(maps, "max-delegation-servers", &obj);
+       INSIST(result == ISC_R_SUCCESS);
+       CHECK(dns_view_setmaxdelegationservers(view, cfg_obj_asuint32(obj)));
+
        obj = NULL;
        result = named_config_get(maps, "max-validations-per-fetch", &obj);
        if (result == ISC_R_SUCCESS) {
index 8f1aea151be856db8005b00dd93eed33dd6dd402..71492116ddc8694e2a50ed490456189687bb883f 100644 (file)
@@ -11,6 +11,8 @@
  * information regarding copyright ownership.
  */
 
+{% set maxdelegationservers = maxdelegationservers | default(None) %}
+
 options {
        query-source address 10.53.0.4;
        notify-source 10.53.0.4;
@@ -22,6 +24,9 @@ options {
        dnssec-validation no;
        dnstap { resolver query; };
        dnstap-output file "dnstap.out";
+       {% if maxdelegationservers %}
+        @maxdelegationservers@
+       {% endif %}
 };
 
 zone "." {
index 3577c894606d634d81429a5cedbeb7df403086d7..5ed2a77955f96b83f0d8014a91f894bde87593f5 100644 (file)
@@ -52,14 +52,14 @@ def expect_next_ip_and_query(expected_ips_and_queries, ips_and_queries):
         assert query == expected_query
 
 
-def test_selfpointedglue_nslimit(ns4):
+def check_nsprocessinglimit(ns, queries_count):
     msg = isctest.query.create("a.sub.example.tld.", "A")
-    res = isctest.query.tcp(msg, ns4.ip)
+    res = isctest.query.tcp(msg, ns.ip)
     isctest.check.servfail(res)
 
-    # The 4 formers lines are request to find sub.example2.tld NSs.
-    # The latest 20 are queries to sub.example2.tld NSs.
-    ips_and_queries = extract_dnstap(ns4, 24)
+    # The 4 formers lines are request to find sub.example.tld NSs.
+    # The latest are queries to sub.example.tld NSs.
+    ips_and_queries = extract_dnstap(ns, queries_count)
 
     # Checking the begining of the resulution
     expect_next_ip_and_query(
@@ -71,4 +71,52 @@ def test_selfpointedglue_nslimit(ns4):
         ],
         ips_and_queries,
     )
-    expect_query("a.sub.example.tld/IN/A", 20, ips_and_queries)
+    expect_query("a.sub.example.tld/IN/A", queries_count - 4, ips_and_queries)
+
+
+def test_nsprocessinglimit_default(ns4):
+    check_nsprocessinglimit(ns4, 17)
+
+
+def reconfig_maxdelegationservers(ns, templates, count):
+    templates.render(
+        "ns4/named.conf", {"maxdelegationservers": f"max-delegation-servers {count};"}
+    )
+    with ns.watch_log_from_here() as watcher:
+        ns.rndc("flush")
+        ns.rndc("reload")
+        watcher.wait_for_line("running")
+
+
+def reconfig_maxdelegationservers_failure(ns, templates, count):
+    templates.render(
+        "ns4/named.conf", {"maxdelegationservers": f"max-delegation-servers {count};"}
+    )
+    with ns.watch_log_from_here() as watcher:
+        # Reload will fail, so do not raise the exception so the config line
+        # can be checked.
+        ns.rndc("reload", raise_on_exception=False)
+        watcher.wait_for_line("reloading configuration failed: out of range")
+
+
+def test_nsprocessinglimit_13ns(ns4, templates):
+    reconfig_maxdelegationservers(ns4, templates, 13)
+    check_nsprocessinglimit(ns4, 17)
+
+
+def test_nsprocessinglimit_5ns(ns4, templates):
+    reconfig_maxdelegationservers(ns4, templates, 5)
+    check_nsprocessinglimit(ns4, 9)
+
+
+def test_nsprocessinglimit_20ns(ns4, templates):
+    reconfig_maxdelegationservers(ns4, templates, 20)
+    check_nsprocessinglimit(ns4, 24)
+
+
+def test_nsprocessinglimit_lower(ns4, templates):
+    reconfig_maxdelegationservers_failure(ns4, templates, 0)
+
+
+def test_nsprocessinglimit_upper(ns4, templates):
+    reconfig_maxdelegationservers_failure(ns4, templates, 101)
index 341f1b3483ee37d03e56c17c3dcee653f400d84b..5c09f64a3fce83ca928cdfcb2bc72d98893157a6 100644 (file)
@@ -4174,6 +4174,37 @@ Tuning
    exceed 90 seconds and is truncated to 90 seconds if set to a greater
    value.
 
+.. namedconf:statement:: max-delegation-servers
+   :tags: server
+   :short: Configure the maximum number of nameserver names 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
+   :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.
+
+   A limited number of outgoing DNS queries (fetches) are then sent to resolve
+   those shuffled nameserver names. The number of concurrent fetches starts at
+   3 for fetch depth 0 (the initial level where remote DNS queries begin) and
+   decreases with each deeper level. At depth 4 and below, :iscman:`named` only
+   initiates a remote fetch on an as-needed basis. This means that a new
+   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 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
+   attacker cannot force the resolver to perform excessive, wasteful queries.
+
+   :any:`max-delegation-servers` cannot be set to less than ``1``, as this would
+   break DNS resolution. The maximum permitted value is ``100``. Any configured
+   value outside of those bounds is rejected.
+
 .. namedconf:statement:: max-ncache-ttl
    :tags: server
    :short: Specifies the maximum retention time (in seconds) for storage of negative answers in the server's cache.
index b5701e1f597599ddb39d9283f2a7c39345b7325a..677c518124867a4d808285a9b771206841f63225 100644 (file)
@@ -173,6 +173,7 @@ options {
        max-cache-size ( default | unlimited | <sizeval> | <percentage> );
        max-cache-ttl <duration>;
        max-clients-per-query <integer>;
+       max-delegation-servers <integer>; // experimental
        max-ixfr-ratio ( unlimited | <percentage> );
        max-journal-size ( default | unlimited | <sizeval> );
        max-ncache-ttl <duration>;
@@ -566,6 +567,7 @@ view <string> [ <class> ] {
        max-cache-size ( default | unlimited | <sizeval> | <percentage> );
        max-cache-ttl <duration>;
        max-clients-per-query <integer>;
+       max-delegation-servers <integer>; // experimental
        max-ixfr-ratio ( unlimited | <percentage> );
        max-journal-size ( default | unlimited | <sizeval> );
        max-ncache-ttl <duration>;
index 9edad1852fcbc390ae96b0ac03953dc5b28bab17..b7a3f1b0dbbfe791e3fb0d2819737c9c433f191f 100644 (file)
@@ -191,6 +191,7 @@ struct dns_view {
        uint32_t              sig0message_checks_limit;
        uint32_t              maxrrperset;
        uint32_t              maxtypepername;
+       uint32_t              max_delegation_servers;
        uint16_t              max_queries;
        uint8_t               max_restarts;
 
@@ -254,6 +255,12 @@ struct dns_view {
 #define DNS_VIEWATTR_ADBSHUTDOWN 0x02
 #define DNS_VIEWATTR_REQSHUTDOWN 0x04
 
+/*
+ * Maximum number of the max-delegation-servers, this defines the size of the
+ * thread-local storage.
+ */
+#define MAX_DELEGATION_SERVERS 100
+
 #ifdef HAVE_LMDB
 #define DNS_LMDB_COMMON_FLAGS (MDB_CREATE | MDB_NOSUBDIR | MDB_NOLOCK)
 #ifndef __OpenBSD__
@@ -1285,3 +1292,10 @@ dns_view_setmaxqueries(dns_view_t *view, uint16_t max_queries);
  *\li  'view' is valid;
  *\li  'max_queries' is greater than 0.
  */
+
+isc_result_t
+dns_view_setmaxdelegationservers(dns_view_t *view, uint32_t max_servers);
+/*%
+ * Set the maximum number of delegation nameservers processed when looking up
+ * their IP addresses.
+ */
index 3dc51cdb8bf5f7e3958f9cc1cc23c15db780e053..e15243274c27dde9276c3f1aae4539c9301c24cc 100644 (file)
@@ -77,6 +77,8 @@
 #include <dns/validator.h>
 #include <dns/zone.h>
 
+#include "dns/view.h"
+
 #ifdef WANT_QUERYTRACE
 #define RTRACE(m)                                                       \
        isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, \
 #define DEFAULT_MAX_QUERIES 50
 #endif /* ifndef DEFAULT_MAX_QUERIES */
 
-/*
- * IP address lookups are performed for at most NS_PROCESSING_LIMIT NS RRs in
- * any NS RRset encountered, to avoid excessive resource use while processing
- * large delegations.
- */
-#define NS_PROCESSING_LIMIT 20
-
 /* Hash table for zone counters */
 #ifndef RES_DOMAIN_HASH_BITS
 #define RES_DOMAIN_HASH_BITS 12
@@ -3665,8 +3660,9 @@ fctx_getaddresses_nameservers(fetchctx_t *fctx, isc_stdtime_t now,
        dns_rdata_ns_t ns;
        bool have_address = false;
        unsigned int ns_processed = 0;
-       dns_rdata_t nameservers_s[NS_PROCESSING_LIMIT];
-       dns_rdata_t *nameservers[NS_PROCESSING_LIMIT];
+       uint32_t ns_processing_limit = fctx->res->view->max_delegation_servers;
+       static thread_local dns_rdata_t nameservers_s[MAX_DELEGATION_SERVERS];
+       static thread_local dns_rdata_t *nameservers[MAX_DELEGATION_SERVERS];
 
        DNS_RDATASET_FOREACH(&fctx->nameservers) {
                dns_rdata_t *rdata = nameservers[ns_processed] =
@@ -3676,7 +3672,7 @@ fctx_getaddresses_nameservers(fetchctx_t *fctx, isc_stdtime_t now,
 
                dns_rdataset_current(&fctx->nameservers, rdata);
 
-               if (++ns_processed >= NS_PROCESSING_LIMIT) {
+               if (++ns_processed >= ns_processing_limit) {
                        break;
                }
        }
index 217a46faacaed6b58581a497a7a48681445876d5..5ec7269d52cb25ae03bd0428951d85aff6ef12e7 100644 (file)
@@ -2120,3 +2120,16 @@ dns_view_setmaxqueries(dns_view_t *view, uint16_t max_queries) {
 
        view->max_queries = max_queries;
 }
+
+isc_result_t
+dns_view_setmaxdelegationservers(dns_view_t *view, uint32_t max_servers) {
+       REQUIRE(DNS_VIEW_VALID(view));
+
+       if (max_servers < 1 || max_servers > MAX_DELEGATION_SERVERS) {
+               return ISC_R_RANGE;
+       }
+
+       view->max_delegation_servers = max_servers;
+
+       return ISC_R_SUCCESS;
+}
index d8392755a15015e2dadbec551d7017263cc4e566..e6c9d5ce23bedec080aeb35d9b408ec85eaed519 100644 (file)
@@ -2398,6 +2398,8 @@ static cfg_clausedef_t view_clauses[] = {
        { "max-cache-size", &cfg_type_maxcachesize, 0, NULL },
        { "max-cache-ttl", &cfg_type_duration, 0, NULL },
        { "max-clients-per-query", &cfg_type_uint32, 0, NULL },
+       { "max-delegation-servers", &cfg_type_uint32,
+         CFG_CLAUSEFLAG_EXPERIMENTAL, NULL },
        { "max-ncache-ttl", &cfg_type_duration, 0, NULL },
        { "max-recursion-depth", &cfg_type_uint32, 0, NULL },
        { "max-recursion-queries", &cfg_type_uint32, 0, NULL },