chg: usr: Limit the number of glue records cached from a referral

When a delegation response contained many glue addresses per listed
nameserver, all of them were cached without a per-nameserver bound,
inflating resolver cache memory beyond what resolution could ever use.
The cache now keeps at most 20 IPv4 and 20 IPv6 glue addresses per
nameserver from a delegation.

Closes #5701

Merge branch '5701-limit-the-number-of-GLUE-records-9.20' into 'bind-9.20'

See merge request isc-projects/bind9!11972
This commit is contained in:
Ondřej Surý 2026-05-12 16:19:05 +02:00
commit eb401f6b92
8 changed files with 222 additions and 34 deletions

View file

@ -0,0 +1,15 @@
options {
query-source address 10.53.0.1;
notify-source 10.53.0.1;
transfer-source 10.53.0.1;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.1; };
recursion no;
dnssec-validation no;
};
zone "." {
type primary;
file "root.db";
};

View file

@ -0,0 +1,13 @@
$TTL 300
. IN SOA dnshoster.root-servers.nil. a.root.servers.nil. (
2010 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
. NS a.root-servers.nil.
a.root-servers.nil. A 10.53.0.1
tld. NS ns.tld.
ns.tld. A 10.53.0.2

View file

@ -0,0 +1,15 @@
options {
query-source address 10.53.0.2;
notify-source 10.53.0.2;
transfer-source 10.53.0.2;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.2; };
recursion no;
dnssec-validation no;
};
zone "tld" {
type primary;
file "tld.db";
};

View file

@ -0,0 +1,63 @@
$TTL 300
@ IN SOA hoster.tld. ns.tld. (
2010 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
NS ns
ns A 10.52.0.2
example NS ns.example
ns.example A 10.53.0.20
ns.example A 10.53.0.21
ns.example A 10.53.0.22
ns.example A 10.53.0.23
ns.example A 10.53.0.24
ns.example A 10.53.0.25
ns.example A 10.53.0.26
ns.example A 10.53.0.27
ns.example A 10.53.0.28
ns.example A 10.53.0.29
ns.example A 10.53.0.30
ns.example A 10.53.0.31
ns.example A 10.53.0.32
ns.example A 10.53.0.33
ns.example A 10.53.0.34
ns.example A 10.53.0.35
ns.example A 10.53.0.36
ns.example A 10.53.0.37
ns.example A 10.53.0.38
ns.example A 10.53.0.39
ns.example A 10.53.0.40
ns.example A 10.53.0.41
ns.example A 10.53.0.42
ns.example A 10.53.0.43
ns.example AAAA 2001:db8::20
ns.example AAAA 2001:db8::21
ns.example AAAA 2001:db8::22
ns.example AAAA 2001:db8::23
ns.example AAAA 2001:db8::24
ns.example AAAA 2001:db8::25
ns.example AAAA 2001:db8::26
ns.example AAAA 2001:db8::27
ns.example AAAA 2001:db8::28
ns.example AAAA 2001:db8::29
ns.example AAAA 2001:db8::30
ns.example AAAA 2001:db8::31
ns.example AAAA 2001:db8::32
ns.example AAAA 2001:db8::33
ns.example AAAA 2001:db8::34
ns.example AAAA 2001:db8::35
ns.example AAAA 2001:db8::36
ns.example AAAA 2001:db8::37
ns.example AAAA 2001:db8::38
ns.example AAAA 2001:db8::39
ns.example AAAA 2001:db8::40
ns.example AAAA 2001:db8::41
ns.example AAAA 2001:db8::42
ns.example AAAA 2001:db8::43

View file

@ -0,0 +1,31 @@
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 yes;
dnssec-validation no;
};
server 10.53.0.2 {
// Avoid truncation of the additional section without TC, because some
// mandatory glues would already be in the additional section, thus the
// resolver wouldn't try again using TCP by itself.
tcp-only yes;
};
zone "." {
type hint;
file "root.hint";
};
key rndc_key {
secret "1234abcd8765";
algorithm @DEFAULT_HMAC@;
};
controls {
inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};

View file

@ -0,0 +1,3 @@
$TTL 999999
. IN NS a.root-servers.nil.
a.root-servers.nil. IN A 10.53.0.1

View file

@ -0,0 +1,33 @@
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
import isctest
def test_cap_glues(ns3):
msg = isctest.query.create("example.tld.", "A")
isctest.query.udp(msg, ns3.ip)
with ns3.watch_log_from_here() as watcher:
ns3.rndc("dumpdb -cache")
watcher.wait_for_line("dumpdb complete")
db = isctest.text.TextFile(f"{ns3.identifier}/named_dump.db")
allowed_suffixes = range(20, 40)
skipped_suffixes = range(40, 44)
for n in allowed_suffixes:
assert len(db.grep(f"10.53.0.{n}")) >= 1
assert len(db.grep(f"2001:db8::{n}")) >= 1
for n in skipped_suffixes:
assert len(db.grep(f"10.53.0.{n}")) == 0
assert len(db.grep(f"2001:db8::{n}")) == 0

View file

@ -236,6 +236,15 @@
*/
#define NS_PROCESSING_LIMIT 20
/*
* Cap on the number of glue addresses cached per NS owner from a referral.
* The resolver only ever tries a handful of addresses per NS, so accepting
* more than this from a single referral is wasted memory. Each NS owner
* may contribute at most DELEG_MAX_GLUES_PER_NS A and DELEG_MAX_GLUES_PER_NS
* AAAA glue records.
*/
#define DELEG_MAX_GLUES_PER_NS 20
/* Hash table for zone counters */
#ifndef RES_DOMAIN_HASH_BITS
#define RES_DOMAIN_HASH_BITS 12
@ -6762,12 +6771,52 @@ unlock:
return result;
}
/*
* Truncate 'rdataset' to at most 'max' rdata, by unlinking the trailing
* rdata from the underlying rdatalist. The rdataset must be backed by a
* dns_rdatalist, which is the case for rdatasets parsed from a message.
*/
static void
truncate_rdataset(dns_rdataset_t *rdataset, unsigned int max) {
dns_rdatalist_t *rdatalist = NULL;
dns_rdata_t *keep = NULL;
dns_rdata_t *next = NULL;
unsigned int i;
REQUIRE(max > 0);
if (dns_rdataset_count(rdataset) <= max) {
return;
}
dns_rdatalist_fromrdataset(rdataset, &rdatalist);
keep = ISC_LIST_HEAD(rdatalist->rdata);
for (i = 1; i < max && keep != NULL; i++) {
keep = ISC_LIST_NEXT(keep, link);
}
INSIST(keep != NULL);
next = ISC_LIST_NEXT(keep, link);
while (next != NULL) {
dns_rdata_t *unlinked = next;
next = ISC_LIST_NEXT(next, link);
ISC_LIST_UNLINK(rdatalist->rdata, unlinked, link);
}
}
static void
mark_related(dns_name_t *name, dns_rdataset_t *rdataset, bool external,
bool gluing) {
name->attributes.cache = true;
if (gluing) {
rdataset->trust = dns_trust_glue;
if (rdataset->type == dns_rdatatype_a ||
rdataset->type == dns_rdatatype_aaaa)
{
truncate_rdataset(rdataset, DELEG_MAX_GLUES_PER_NS);
}
/*
* Glue with 0 TTL causes problems. We force the TTL to
* 1 second to prevent this.
@ -6901,12 +6950,6 @@ check_section(void *arg, const dns_name_t *addname, dns_rdatatype_t type,
REQUIRE(VALID_FCTX(fctx));
#if CHECK_FOR_GLUE_IN_ANSWER
if (section == DNS_SECTION_ANSWER && type != dns_rdatatype_a) {
return ISC_R_SUCCESS;
}
#endif /* if CHECK_FOR_GLUE_IN_ANSWER */
gluing = (GLUING(fctx) || (fctx->type == dns_rdatatype_ns &&
dns_name_equal(fctx->name, dns_rootname)));
@ -6962,18 +7005,6 @@ check_related(void *arg, const dns_name_t *addname, dns_rdatatype_t type,
return check_section(arg, addname, type, found, DNS_SECTION_ADDITIONAL);
}
#ifndef CHECK_FOR_GLUE_IN_ANSWER
#define CHECK_FOR_GLUE_IN_ANSWER 0
#endif /* ifndef CHECK_FOR_GLUE_IN_ANSWER */
#if CHECK_FOR_GLUE_IN_ANSWER
static isc_result_t
check_answer(void *arg, const dns_name_t *addname, dns_rdatatype_t type,
dns_rdataset_t *found) {
return check_section(arg, addname, type, found, DNS_SECTION_ANSWER);
}
#endif /* if CHECK_FOR_GLUE_IN_ANSWER */
static bool
is_answeraddress_allowed(dns_view_t *view, dns_name_t *name,
dns_rdataset_t *rdataset) {
@ -9518,22 +9549,6 @@ rctx_referral(respctx_t *rctx) {
*/
(void)dns_rdataset_additionaldata(rctx->ns_rdataset, rctx->ns_name,
check_related, rctx, 0);
#if CHECK_FOR_GLUE_IN_ANSWER
/*
* Look in the answer section for "glue" that is incorrectly
* returned as a answer. This is needed if the server also
* minimizes the response size by not adding records to the
* additional section that are in the answer section or if
* the record gets dropped due to message size constraints.
*/
if (rctx->glue_in_answer &&
(fctx->type == dns_rdatatype_aaaa || fctx->type == dns_rdatatype_a))
{
(void)dns_rdataset_additionaldata(rctx->ns_rdataset,
rctx->ns_name, check_answer,
fctx, 0);
}
#endif /* if CHECK_FOR_GLUE_IN_ANSWER */
FCTX_ATTR_CLR(fctx, FCTX_ATTR_GLUING);
/*