From 1ba67b6055860df8c6b93b468cba6f52ecce55ec Mon Sep 17 00:00:00 2001 From: Alessio Podda Date: Mon, 13 Apr 2026 15:55:38 +0200 Subject: [PATCH 1/2] Add xfr quota starvation system test Add a starvation test that tries to starve the XFR quota with unautorized requests. (cherry picked from commit 53135592b7ff6c272b6577b2e7747258628442e3) --- bin/tests/system/xferquota/ns1/named.conf.in | 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.in b/bin/tests/system/xferquota/ns1/named.conf.in index e868318ae6..922e6ab3a6 100644 --- a/bin/tests/system/xferquota/ns1/named.conf.in +++ b/bin/tests/system/xferquota/ns1/named.conf.in @@ -22,6 +22,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 230135cd52..bc5e23caf7 100644 --- a/bin/tests/system/xferquota/tests_xferquota.py +++ b/bin/tests/system/xferquota/tests_xferquota.py @@ -10,6 +10,7 @@ # information regarding copyright ownership. import glob +import multiprocessing import os import re from re import compile as Re @@ -18,6 +19,8 @@ import signal import time import dns.message +import dns.query +import dns.zone import pytest import isctest @@ -59,6 +62,9 @@ def test_xferquota(named_port, servers): 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") @@ -79,3 +85,39 @@ def test_xferquota(named_port, servers): with servers["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 4a893eacf3ed2dc22518619f0230a6184113d6ec Mon Sep 17 00:00:00 2001 From: Aram Sargsyan Date: Tue, 31 Mar 2026 13:00:00 +0000 Subject: [PATCH 2/2] 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. (cherry picked from commit 5615e6c47a2cd00d82d48b568cc55a4b89daa330) --- lib/ns/xfrout.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/ns/xfrout.c b/lib/ns/xfrout.c index d022a68999..bc936e1ed0 100644 --- a/lib/ns/xfrout.c +++ b/lib/ns/xfrout.c @@ -757,16 +757,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_attach(&client->sctx->xfroutquota, "a); - if (result != ISC_R_SUCCESS) { - isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, - "%s request denied: %s", mnemonic, - isc_result_totext(result)); - goto cleanup; - } /* * Interpret the question section. @@ -938,6 +928,18 @@ 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_attach(&client->sctx->xfroutquota, "a); + if (result != ISC_R_SUCCESS) { + isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, + "%s request denied: %s", mnemonic, + isc_result_totext(result)); + goto cleanup; + } + /* * Look up the requesting server in the peer table. */ @@ -1194,6 +1196,7 @@ cleanup: } /* XXX kludge */ if (xfr != NULL) { + /* The quota will be released in xfrout_ctx_destroy(). */ xfrout_fail(xfr, result, "setting up zone transfer"); } else if (result != ISC_R_SUCCESS) { ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT,