2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-30 05:57:52 +00:00

new: usr: Implement the min-transfer-rate-in configuration option

A new option 'min-transfer-rate-in <bytes> <minutes>' has been added
to the view and zone configurations. It can abort incoming zone
transfers which run very slowly due to network related issues, for
example. The default value is set to 10240 bytes in 5 minutes.

Closes #3914

Merge branch '3914-detect-and-restart-stalled-zone-transfers' into 'main'

See merge request isc-projects/bind9!9098
This commit is contained in:
Arаm Sаrgsyаn 2025-02-20 10:31:47 +00:00
commit a282f1ba3f
20 changed files with 250 additions and 5 deletions

View File

@ -931,6 +931,7 @@
<th>Messages Received</th>
<th>Records Received</th>
<th>Bytes Received</th>
<th>Transfer Rate (B/s)</th>
</tr>
</thead>
<tbody>
@ -959,6 +960,7 @@
<td><xsl:value-of select="nmsg"/></td>
<td><xsl:value-of select="nrecs"/></td>
<td><xsl:value-of select="nbytes"/></td>
<td><xsl:value-of select="rate"/></td>
</tr>
</xsl:for-each>
</tbody>

View File

@ -232,6 +232,7 @@ options {\n\
max-transfer-time-out 120;\n\
min-refresh-time 300;\n\
min-retry-time 500;\n\
min-transfer-rate-in 10240 5;\n\
multi-master no;\n\
notify yes;\n\
notify-delay 5;\n\

View File

@ -1488,6 +1488,7 @@ xfrin_xmlrender(dns_zone_t *zone, void *arg) {
unsigned int nmsg = 0;
unsigned int nrecs = 0;
uint64_t nbytes = 0;
uint64_t rate = 0;
statlevel = dns_zone_getstatlevel(zone);
if (statlevel == dns_zonestat_none) {
@ -1701,7 +1702,7 @@ xfrin_xmlrender(dns_zone_t *zone, void *arg) {
TRY0(xmlTextWriterEndElement(writer));
if (is_running) {
dns_xfrin_getstats(xfr, &nmsg, &nrecs, &nbytes);
dns_xfrin_getstats(xfr, &nmsg, &nrecs, &nbytes, &rate);
}
TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "nmsg"));
TRY0(xmlTextWriterWriteFormatString(writer, "%u", nmsg));
@ -1712,6 +1713,9 @@ xfrin_xmlrender(dns_zone_t *zone, void *arg) {
TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "nbytes"));
TRY0(xmlTextWriterWriteFormatString(writer, "%" PRIu64, nbytes));
TRY0(xmlTextWriterEndElement(writer));
TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "rate"));
TRY0(xmlTextWriterWriteFormatString(writer, "%" PRIu64, rate));
TRY0(xmlTextWriterEndElement(writer));
TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "ixfr"));
if (is_running && is_first_data_received) {
@ -2559,6 +2563,7 @@ xfrin_jsonrender(dns_zone_t *zone, void *arg) {
unsigned int nmsg = 0;
unsigned int nrecs = 0;
uint64_t nbytes = 0;
uint64_t rate = 0;
statlevel = dns_zone_getstatlevel(zone);
if (statlevel == dns_zonestat_none) {
@ -2756,7 +2761,7 @@ xfrin_jsonrender(dns_zone_t *zone, void *arg) {
}
if (is_running) {
dns_xfrin_getstats(xfr, &nmsg, &nrecs, &nbytes);
dns_xfrin_getstats(xfr, &nmsg, &nrecs, &nbytes, &rate);
}
json_object_object_add(xfrinobj, "nmsg",
json_object_new_int64((int64_t)nmsg));
@ -2766,6 +2771,10 @@ xfrin_jsonrender(dns_zone_t *zone, void *arg) {
xfrinobj, "nbytes",
json_object_new_int64(nbytes > INT64_MAX ? INT64_MAX
: (int64_t)nbytes));
json_object_object_add(xfrinobj, "rate",
json_object_new_int64(rate > INT64_MAX
? INT64_MAX
: (int64_t)rate));
if (is_running && is_first_data_received) {
json_object_object_add(

View File

@ -1874,6 +1874,33 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig,
}
dns_zone_setoption(mayberaw, DNS_ZONEOPT_MULTIMASTER, multi);
obj = NULL;
result = named_config_get(maps, "min-transfer-rate-in", &obj);
INSIST(result == ISC_R_SUCCESS && obj != NULL);
uint32_t traffic_bytes =
cfg_obj_asuint32(cfg_tuple_get(obj, "traffic_bytes"));
uint32_t time_minutes =
cfg_obj_asuint32(cfg_tuple_get(obj, "time_minutes"));
if (traffic_bytes == 0) {
cfg_obj_log(obj, ISC_LOG_ERROR,
"zone '%s': 'min-transfer-rate-in' bytes"
"value can not be '0'",
zname);
CHECK(ISC_R_FAILURE);
}
/* Max. 28 days (in minutes). */
const unsigned int time_minutes_max = 28 * 24 * 60;
if (time_minutes < 1 || time_minutes > time_minutes_max) {
cfg_obj_log(obj, ISC_LOG_ERROR,
"zone '%s': 'min-transfer-rate-in' minutes"
"value is out of range (1..%u)",
zname, time_minutes_max);
CHECK(ISC_R_FAILURE);
}
dns_zone_setminxfrratein(mayberaw, traffic_bytes,
transferinsecs ? time_minutes
: time_minutes * 60);
obj = NULL;
result = named_config_get(maps, "max-transfer-time-in", &obj);
INSIST(result == ISC_R_SUCCESS && obj != NULL);

View File

@ -0,0 +1,15 @@
; 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 3600
@ IN SOA . . 0 0 0 0 0
@ IN NS .
$GENERATE 1-5000 host$ TXT data-$

View File

@ -47,6 +47,10 @@ zone "edns-expire" {
file "edns-expire.db";
};
zone "axfr-min-transfer-rate" {
type primary;
file "axfr-min-transfer-rate.db";
};
zone "axfr-max-transfer-time" {
type primary;

View File

@ -36,6 +36,11 @@ zone "." {
file "root.db";
};
zone "axfr-min-transfer-rate" {
type primary;
file "axfr-min-transfer-rate.db";
};
zone "axfr-max-transfer-time" {
type primary;
file "axfr-max-transfer-time.db";

View File

@ -32,6 +32,7 @@ options {
ixfr-from-differences primary;
check-integrity no;
tcp-idle-timeout 600;
min-transfer-rate-in 10240 300; # this is tested as seconds, when used with '-T transferinsecs' (i.e. convert the default '10240 5' back so that it doesn't interfere with other tests)
};
zone "." {
@ -57,6 +58,13 @@ zone "edns-expire" {
file "edns-expire.bk";
};
zone "axfr-min-transfer-rate" {
type secondary;
min-transfer-rate-in 100000 5; # this is tested as seconds, when used with '-T transferinsecs' (i.e. 100000 bytes in 5 seconds)
primaries { 10.53.0.1; };
file "axfr-min-transfer-rate.bk";
};
zone "axfr-max-transfer-time" {
type secondary;
max-transfer-time-in 1; # this is tested as seconds, when used with '-T transferinsecs'

View File

@ -709,11 +709,22 @@ status=$((status + tmp))
nextpart ns6/named.run >/dev/null
n=$((n + 1))
echo_i "test min-transfer-rate-in with 5 seconds timeout ($n)"
$RNDCCMD 10.53.0.6 retransfer axfr-min-transfer-rate 2>&1 | sed 's/^/ns6 /' | cat_i
tmp=0
retry_quiet 10 wait_for_message "minimum transfer rate reached: timed out" || tmp=1
if test $tmp != 0; then echo_i "failed"; fi
status=$((status + tmp))
nextpart ns6/named.run >/dev/null
n=$((n + 1))
echo_i "test max-transfer-time-in with 1 second timeout ($n)"
$RNDCCMD 10.53.0.6 retransfer axfr-max-transfer-time 2>&1 | sed 's/^/ns6 /' | cat_i
tmp=0
retry_quiet 10 wait_for_message "maximum transfer time exceeded: timed out" || tmp=1
if test $tmp != 0; then echo_i "failed"; fi
status=$((status + tmp))
# Restart ns1 with -T transferstuck
@ -730,6 +741,7 @@ start=$(date +%s)
$RNDCCMD 10.53.0.6 retransfer axfr-max-idle-time 2>&1 | sed 's/^/ns6 /' | cat_i
tmp=0
retry_quiet 60 wait_for_message "maximum idle time exceeded: timed out" || tmp=1
if test $tmp != 0; then echo_i "failed"; fi
if [ $tmp -eq 0 ]; then
now=$(date +%s)
diff=$((now - start))

View File

@ -41,6 +41,7 @@ pytestmark = pytest.mark.extra_artifacts(
"ns4/root.db",
"ns6/axfr-max-idle-time.bk",
"ns6/axfr-max-transfer-time.bk",
"ns6/axfr-min-transfer-rate.bk",
"ns6/axfr-rndc-retransfer-force.bk",
"ns6/edns-expire.bk",
"ns6/ixfr-too-big.bk",

View File

@ -3324,6 +3324,16 @@ options apply to zone transfers.
global :any:`also-notify` list are not sent NOTIFY messages for that
zone. The default is the empty list (no global notification list).
.. namedconf:statement:: min-transfer-rate-in
:tags: transfer
:short: Specifies the minimum traffic rate below which inbound zone transfers are terminated.
Inbound zone transfers running slower than the given amount of bytes in the
given amount of minutes are terminated. This option takes two non-zero integer values.
A check is performed periodically every time the configured time interval
passes. The default value is ``10240 5``, i.e. 10240 bytes in 5 minutes.
The maximum time value is 28 days (40320 minutes).
.. namedconf:statement:: max-transfer-time-in
:tags: transfer
:short: Specifies the number of minutes after which inbound zone transfers are terminated.
@ -7068,6 +7078,9 @@ Zone Options
:any:`max-records`
See the description of :any:`max-records` in :ref:`server_resource_limits`.
:any:`min-transfer-rate-in`
See the description of :any:`min-transfer-rate-in` in :ref:`zone_transfers`.
:any:`max-transfer-time-in`
See the description of :any:`max-transfer-time-in` in :ref:`zone_transfers`.
@ -7822,6 +7835,12 @@ Incoming Zone Transfers
64-bit unsigned Integer. This is the number of usable bytes
of DNS data. It does not include transport overhead.
``Transfer Rate (B/s)`` (``rate``)
64 bit unsigned Integer. This is the average zone transfer rate in
bytes-per-second during the latest full interval that is configured by the
:any:`min-transfer-rate-in` configuration option. If no such interval
has passed yet, then the overall average rate is reported instead.
.. note::
Depending on the current state of the transfer, some of the
values may be empty or set to ``-`` (meaning "not available").

View File

@ -26,6 +26,7 @@ zone <string> [ <class> ] {
max-types-per-name <integer>;
min-refresh-time <integer>;
min-retry-time <integer>;
min-transfer-rate-in <integer> <integer>;
multi-master <boolean>;
notify ( explicit | master-only | primary-only | <boolean> );
notify-delay <integer>;

View File

@ -202,6 +202,7 @@ options {
min-ncache-ttl <duration>;
min-refresh-time <integer>;
min-retry-time <integer>;
min-transfer-rate-in <integer> <integer>;
minimal-any <boolean>;
minimal-responses ( no-auth | no-auth-recursive | <boolean> );
multi-master <boolean>;
@ -484,6 +485,7 @@ view <string> [ <class> ] {
min-ncache-ttl <duration>;
min-refresh-time <integer>;
min-retry-time <integer>;
min-transfer-rate-in <integer> <integer>;
minimal-any <boolean>;
minimal-responses ( no-auth | no-auth-recursive | <boolean> );
multi-master <boolean>;

View File

@ -38,6 +38,7 @@ zone <string> [ <class> ] {
max-types-per-name <integer>;
min-refresh-time <integer>;
min-retry-time <integer>;
min-transfer-rate-in <integer> <integer>;
multi-master <boolean>;
notify ( explicit | master-only | primary-only | <boolean> );
notify-delay <integer>;

View File

@ -18,6 +18,7 @@ zone <string> [ <class> ] {
max-types-per-name <integer>;
min-refresh-time <integer>;
min-retry-time <integer>;
min-transfer-rate-in <integer> <integer>;
multi-master <boolean>;
primaries [ port <integer> ] [ source ( <ipv4_address> | * ) ] [ source-v6 ( <ipv6_address> | * ) ] { ( <server-list> | <ipv4_address> [ port <integer> ] | <ipv6_address> [ port <integer> ] ) [ key <string> ] [ tls <string> ]; ... };
transfer-source ( <ipv4_address> | * );

View File

@ -140,10 +140,13 @@ dns_xfrin_getendserial(dns_xfrin_t *xfr);
void
dns_xfrin_getstats(dns_xfrin_t *xfr, unsigned int *nmsgp, unsigned int *nrecsp,
uint64_t *nbytesp);
uint64_t *nbytesp, uint64_t *ratep);
/*%<
* Get various statistics values of the xfrin object: number of the received
* messages, number of the received records, number of the received bytes.
* messages, number of the received records, number of the received bytes,
* and the average transfer rate (in bytes-per-second) during the last full
* 'min-transfer-rate-in <bytes> <minutes>' minutes interval. If no such
* interval has passed yet, then the overall average rate is reported instead.
*
* Requires:
*\li 'xfr' is a valid dns_xfrin_t.

View File

@ -1233,6 +1233,36 @@ dns_zone_notifyreceive(dns_zone_t *zone, isc_sockaddr_t *from,
*\li DNS_R_SUCCESS
*/
void
dns_zone_setminxfrratein(dns_zone_t *zone, uint32_t bytes, uint32_t seconds);
/*%<
* Set the minumum traffic rate (in bytes per seconds) that a zone transfer in
* (AXFR/IXFR) of this zone will use before being aborted.
*
* Requires:
* \li 'zone' to be valid initialised zone.
*/
uint32_t
dns_zone_getminxfrratebytesin(dns_zone_t *zone);
/*%<
* Returns the 'bytes' portion of the minimum traffic rate for the transfer in
* for this zone.
*
* Requires:
*\li 'zone' to be valid initialised zone.
*/
uint32_t
dns_zone_getminxfrratesecondsin(dns_zone_t *zone);
/*%<
* Returns the 'seconds' portion of the minimum traffic rate for the transfer in
* for this zone.
*
* Requires:
*\li 'zone' to be valid initialised zone.
*/
void
dns_zone_setmaxxfrin(dns_zone_t *zone, uint32_t maxxfrin);
/*%<

View File

@ -154,11 +154,13 @@ struct dns_xfrin {
atomic_uint nrecs; /*%< Number of records recvd */
atomic_uint_fast64_t nbytes; /*%< Number of bytes received */
_Atomic(isc_time_t) start; /*%< Start time of the transfer */
atomic_uint_fast64_t rate_bytes_per_second;
_Atomic(dns_transport_type_t) soa_transport_type;
atomic_uint_fast32_t end_serial;
unsigned int maxrecords; /*%< The maximum number of
* records set for the zone */
uint64_t nbytes_saved; /*%< For enforcing the minimum transfer rate */
dns_tsigkey_t *tsigkey; /*%< Key used to create TSIG */
isc_buffer_t *lasttsig; /*%< The last TSIG */
@ -192,6 +194,7 @@ struct dns_xfrin {
isc_loop_t *loop;
isc_timer_t *min_rate_timer;
isc_timer_t *max_time_timer;
isc_timer_t *max_idle_timer;
@ -269,6 +272,8 @@ xfrin_timedout(void *);
static void
xfrin_idledout(void *);
static void
xfrin_minratecheck(void *);
static void
xfrin_fail(dns_xfrin_t *xfr, isc_result_t result, const char *msg);
static isc_result_t
render(dns_message_t *msg, isc_mem_t *mctx, isc_buffer_t *buf);
@ -963,6 +968,32 @@ xfrin_idledout(void *xfr) {
xfrin_fail(xfr, ISC_R_TIMEDOUT, "maximum idle time exceeded");
}
static void
xfrin_minratecheck(void *arg) {
dns_xfrin_t *xfr = arg;
REQUIRE(VALID_XFRIN(xfr));
const uint64_t nbytes = atomic_load_relaxed(&xfr->nbytes);
const uint64_t min = dns_zone_getminxfrratebytesin(xfr->zone);
uint64_t rate = nbytes - xfr->nbytes_saved;
if (rate < min) {
isc_timer_stop(xfr->min_rate_timer);
xfrin_fail(xfr, ISC_R_TIMEDOUT,
"minimum transfer rate reached");
} else {
xfr->nbytes_saved = nbytes;
/*
* Calculate and store for the statistics channel the transfer
* rate in bytes-per-second for the latest interval.
*/
rate /= dns_zone_getminxfrratesecondsin(xfr->zone);
atomic_store_relaxed(&xfr->rate_bytes_per_second, rate);
}
}
isc_time_t
dns_xfrin_getstarttime(dns_xfrin_t *xfr) {
REQUIRE(VALID_XFRIN(xfr));
@ -1024,13 +1055,29 @@ dns_xfrin_getendserial(dns_xfrin_t *xfr) {
void
dns_xfrin_getstats(dns_xfrin_t *xfr, unsigned int *nmsgp, unsigned int *nrecsp,
uint64_t *nbytesp) {
uint64_t *nbytesp, uint64_t *ratep) {
REQUIRE(VALID_XFRIN(xfr));
REQUIRE(nmsgp != NULL && nrecsp != NULL && nbytesp != NULL);
uint64_t rate = atomic_load_relaxed(&xfr->rate_bytes_per_second);
if (rate == 0) {
/*
* Likely the first 'min-transfer-rate-in <bytes> <minutes>'
* minutes interval hasn't passed yet. Calculate the overall
* average transfer rate instead.
*/
isc_time_t now = isc_time_now();
isc_time_t start = atomic_load_relaxed(&xfr->start);
uint64_t sec = isc_time_microdiff(&now, &start) / US_PER_SEC;
if (sec > 0) {
rate = atomic_load_relaxed(&xfr->nbytes) / sec;
}
}
SET_IF_NOT_NULL(nmsgp, atomic_load_relaxed(&xfr->nmsg));
SET_IF_NOT_NULL(nrecsp, atomic_load_relaxed(&xfr->nrecs));
SET_IF_NOT_NULL(nbytesp, atomic_load_relaxed(&xfr->nbytes));
SET_IF_NOT_NULL(ratep, rate);
}
const isc_sockaddr_t *
@ -1326,6 +1373,15 @@ xfrin_start(dns_xfrin_t *xfr) {
isc_interval_set(&interval, dns_zone_getidlein(xfr->zone), 0);
isc_timer_start(xfr->max_idle_timer, isc_timertype_once, &interval);
/* Set the minimum transfer rate checking timer */
if (xfr->min_rate_timer == NULL) {
isc_timer_create(dns_zone_getloop(xfr->zone),
xfrin_minratecheck, xfr, &xfr->min_rate_timer);
}
isc_interval_set(&interval, dns_zone_getminxfrratesecondsin(xfr->zone),
0);
isc_timer_start(xfr->min_rate_timer, isc_timertype_ticker, &interval);
/*
* The connect has to be the last thing that is called before returning,
* as it can end synchronously and destroy the xfr object.
@ -1606,6 +1662,8 @@ xfrin_send_request(dns_xfrin_t *xfr) {
atomic_store_relaxed(&xfr->nbytes, 0);
atomic_store_relaxed(&xfr->start, isc_time_now());
xfr->nbytes_saved = 0;
msg->id = xfr->id;
if (xfr->tsigctx != NULL) {
dst_context_destroy(&xfr->tsigctx);
@ -1724,6 +1782,10 @@ xfrin_end(dns_xfrin_t *xfr, isc_result_t result) {
isc_timer_stop(xfr->max_idle_timer);
isc_timer_destroy(&xfr->max_idle_timer);
}
if (xfr->min_rate_timer != NULL) {
isc_timer_stop(xfr->min_rate_timer);
isc_timer_destroy(&xfr->min_rate_timer);
}
if (xfr->shutdown_result == ISC_R_UNSET) {
xfr->shutdown_result = result;
@ -2017,6 +2079,7 @@ xfrin_recv_done(isc_result_t result, isc_region_t *region, void *arg) {
case XFRST_AXFR_END:
case XFRST_IXFR_END:
/* We are at the end, cancel the timers and IO */
isc_timer_stop(xfr->min_rate_timer);
isc_timer_stop(xfr->max_idle_timer);
isc_timer_stop(xfr->max_time_timer);
xfrin_cancelio(xfr);
@ -2182,6 +2245,7 @@ xfrin_destroy(dns_xfrin_t *xfr) {
INSIST(xfr->max_time_timer == NULL);
INSIST(xfr->max_idle_timer == NULL);
INSIST(xfr->min_rate_timer == NULL);
isc_loop_detach(&xfr->loop);

View File

@ -361,6 +361,8 @@ struct dns_zone {
dns_request_t *request;
dns_loadctx_t *loadctx;
dns_dumpctx_t *dumpctx;
uint32_t minxfrratebytesin;
uint32_t minxfrratesecondsin;
uint32_t maxxfrin;
uint32_t maxxfrout;
uint32_t idlein;
@ -16154,6 +16156,28 @@ message_count(dns_message_t *msg, dns_section_t section, dns_rdatatype_t type) {
return count;
}
void
dns_zone_setminxfrratein(dns_zone_t *zone, uint32_t bytes, uint32_t seconds) {
REQUIRE(DNS_ZONE_VALID(zone));
zone->minxfrratebytesin = bytes;
zone->minxfrratesecondsin = seconds;
}
uint32_t
dns_zone_getminxfrratebytesin(dns_zone_t *zone) {
REQUIRE(DNS_ZONE_VALID(zone));
return zone->minxfrratebytesin;
}
uint32_t
dns_zone_getminxfrratesecondsin(dns_zone_t *zone) {
REQUIRE(DNS_ZONE_VALID(zone));
return zone->minxfrratesecondsin;
}
void
dns_zone_setmaxxfrin(dns_zone_t *zone, uint32_t maxxfrin) {
REQUIRE(DNS_ZONE_VALID(zone));

View File

@ -2211,6 +2211,20 @@ static cfg_clausedef_t dnssecpolicy_clauses[] = {
{ NULL, NULL, 0 }
};
/*
* For min-transfer-rate-in.
*/
static cfg_tuplefielddef_t min_transfer_rate_fields[] = {
{ "traffic_bytes", &cfg_type_uint32, 0 },
{ "time_minutes", &cfg_type_uint32, 0 },
{ NULL, NULL, 0 }
};
static cfg_type_t cfg_type_min_transfer_rate_in = {
"min-transfer-rate-in", cfg_parse_tuple, cfg_print_tuple,
cfg_doc_tuple, &cfg_rep_tuple, min_transfer_rate_fields
};
/*%
* Clauses that can be found in a 'zone' statement,
* with defaults in the 'view' or 'options' statement.
@ -2304,6 +2318,8 @@ static cfg_clausedef_t zone_clauses[] = {
CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR | CFG_ZONE_STUB },
{ "max-retry-time", &cfg_type_uint32,
CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR | CFG_ZONE_STUB },
{ "min-transfer-rate-in", &cfg_type_min_transfer_rate_in,
CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR | CFG_ZONE_STUB },
{ "max-transfer-idle-in", &cfg_type_uint32,
CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR | CFG_ZONE_STUB },
{ "max-transfer-idle-out", &cfg_type_uint32,