From: Ondřej Surý Date: Thu, 26 Feb 2026 09:21:13 +0000 (+0100) Subject: Introduce max-delegation-servers configuration option X-Git-Tag: v9.21.20~18^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c1ba80169c05ce5250cc54ef2cb34b1b7d3c5930;p=thirdparty%2Fbind9.git Introduce max-delegation-servers configuration option 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. --- diff --git a/bin/include/defaultconfig.h b/bin/include/defaultconfig.h index c92aeaea054..8df5172bd7c 100644 --- a/bin/include/defaultconfig.h +++ b/bin/include/defaultconfig.h @@ -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\ diff --git a/bin/named/server.c b/bin/named/server.c index 160e3d5990e..25eb888e7ad 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -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) { diff --git a/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 b/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 index 8f1aea151be..71492116ddc 100644 --- a/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 +++ b/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 @@ -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 "." { diff --git a/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py b/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py index 3577c894606..5ed2a77955f 100644 --- a/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py +++ b/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py @@ -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) diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index 341f1b3483e..5c09f64a3fc 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -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. diff --git a/doc/misc/options b/doc/misc/options index b5701e1f597..677c5181248 100644 --- a/doc/misc/options +++ b/doc/misc/options @@ -173,6 +173,7 @@ options { max-cache-size ( default | unlimited | | ); max-cache-ttl ; max-clients-per-query ; + max-delegation-servers ; // experimental max-ixfr-ratio ( unlimited | ); max-journal-size ( default | unlimited | ); max-ncache-ttl ; @@ -566,6 +567,7 @@ view [ ] { max-cache-size ( default | unlimited | | ); max-cache-ttl ; max-clients-per-query ; + max-delegation-servers ; // experimental max-ixfr-ratio ( unlimited | ); max-journal-size ( default | unlimited | ); max-ncache-ttl ; diff --git a/lib/dns/include/dns/view.h b/lib/dns/include/dns/view.h index 9edad1852fc..b7a3f1b0dbb 100644 --- a/lib/dns/include/dns/view.h +++ b/lib/dns/include/dns/view.h @@ -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. + */ diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 3dc51cdb8bf..e15243274c2 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -77,6 +77,8 @@ #include #include +#include "dns/view.h" + #ifdef WANT_QUERYTRACE #define RTRACE(m) \ isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, \ @@ -227,13 +229,6 @@ #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; } } diff --git a/lib/dns/view.c b/lib/dns/view.c index 217a46faaca..5ec7269d52c 100644 --- a/lib/dns/view.c +++ b/lib/dns/view.c @@ -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; +} diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index d8392755a15..e6c9d5ce23b 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -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 },