new: usr: Add support for Extended DNS Error 24 (Invalid Data)

Extended DNS Error 24 (Invalid Data) is returned when the server cannot answer data for a zone it is configured for. This occurs typically when an authoritative server does not have loaded the DB of a configured zone, or a secondary server zone is expired.

See RFC 8914 section 4.25.

See #1836

Merge branch 'colin/ede24' into 'main'

See merge request isc-projects/bind9!11169
This commit is contained in:
Colin Vidal 2025-11-03 18:25:38 +01:00
commit 4941d33a8a
7 changed files with 198 additions and 2 deletions

View file

@ -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 1
foo.fr. IN SOA ns.foo.fr. op.foo.fr. (
3 ; serial
1 ; refresh
1 ; retry
1 ; expire
60 ; minimum
)
foo.fr. NS ns.foo.fr.
ns.foo.fr. A 10.53.0.1

View file

@ -0,0 +1,36 @@
/*
* 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 {
listen-on port @PORT@ { 10.53.0.1; };
transfer-source 10.53.0.1;
pid-file "named.pid";
recursion no;
also-notify { 10.53.0.2 port @PORT@; };
notify-source 10.53.0.1;
};
zone "foo.fr" {
type primary;
allow-transfer{ 10.53.0.2; };
file "foo.fr.db";
};
key rndc_key {
secret "1234abcd8765";
algorithm @DEFAULT_HMAC@;
};
controls {
inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};

View file

@ -0,0 +1,35 @@
/*
* 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 {
listen-on port @PORT@ { 10.53.0.2; };
transfer-source 10.53.0.2;
pid-file "named.pid";
recursion no;
};
zone "foo.fr" {
min-refresh-time 1;
min-retry-time 1;
type secondary;
primaries { 10.53.0.1 port @PORT@; };
};
key rndc_key {
secret "1234abcd8765";
algorithm @DEFAULT_HMAC@;
};
controls {
inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};

View file

@ -0,0 +1,71 @@
# 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 os
import isctest
def check_soa_noerror():
msg = isctest.query.create("foo.fr", "SOA")
res = isctest.query.udp(msg, "10.53.0.2")
isctest.check.noerror(res)
def check_soa_servfail_ede24(edemsg):
msg = isctest.query.create("foo.fr", "SOA")
res = isctest.query.udp(msg, "10.53.0.2")
isctest.check.servfail(res)
# Few CI machines uses old version of dnspython which doesn't supports
# EDNS, so we effectively bypass the check for those one. (It's fine, a
# bunch of other CI machines _does_ have recent version of dnspython).
if hasattr(res, "extended_errors"):
assert len(res.extended_errors()) == 1
assert res.extended_errors()[0].to_text() == f"EDE 24 (Invalid Data): {edemsg}"
def test_ede24_noloaded(ns1, ns2):
# Sanity check that everything works first
check_soa_noerror()
# Stop all servers, and we'll restart only ns2.
ns1.stop()
ns2.stop()
with ns2.watch_log_from_here() as watcher:
ns2.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
watcher.wait_for_line("failure trying primary 10.53.0.1")
# ns2 attempts an XFR but ns1 since is off the zone DB can't be loaded.
check_soa_servfail_ede24("zone not loaded")
def test_ede24_expired(ns1, ns2):
# Restart ns1 then checks the server notify the zone in ns2 and ns2 serves
# the zone again.
with ns2.watch_log_from_here() as watcher:
ns1.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
watcher.wait_for_line("Transfer status: success")
check_soa_noerror()
# Stop the primary and wait for expiration of the zone in the secondary.
with ns2.watch_log_from_here() as watcher:
ns1.stop()
watcher.wait_for_line(" zone foo.fr/IN: expired")
# ns2 can't answer anymore.
check_soa_servfail_ede24("zone expired")
# Restart the primary and wait for the zone to be back up again.
with ns2.watch_log_from_here() as watcher:
ns1.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
watcher.wait_for_line("Transfer status: success")
check_soa_noerror()

View file

@ -2792,6 +2792,15 @@ dns_zone_getcfg(dns_zone_t *zone);
* \li 'zone' to be a valid zone.
*/
bool
dns_zone_isexpired(dns_zone_t *zone);
/*%<
* Return true if a (secondary, mirror, etc.) zone is expired
*
* Requires:
* \li 'zone\ to be a valid zone.
*/
#if DNS_ZONE_TRACE
#define dns_zone_ref(ptr) dns_zone__ref(ptr, __func__, __FILE__, __LINE__)
#define dns_zone_unref(ptr) dns_zone__unref(ptr, __func__, __FILE__, __LINE__)

View file

@ -11848,13 +11848,13 @@ zone_expire(dns_zone_t *zone) {
REQUIRE(LOCKED_ZONE(zone));
dns_zone_log(zone, ISC_LOG_WARNING, "expired");
DNS_ZONE_SETFLAG(zone, DNS_ZONEFLG_EXPIRED);
zone->refresh = DNS_ZONE_DEFAULTREFRESH;
zone->retry = DNS_ZONE_DEFAULTRETRY;
DNS_ZONE_CLRFLAG(zone, DNS_ZONEFLG_HAVETIMERS);
dns_zone_log(zone, ISC_LOG_WARNING, "expired");
/*
* An RPZ zone has expired; before unloading it, we must
* first remove it from the RPZ summary database. The
@ -17617,6 +17617,7 @@ again:
isc_time_compare(&expiretime, &zone->expiretime) > 0)
{
zone->expiretime = expiretime;
DNS_ZONE_CLRFLAG(zone, DNS_ZONEFLG_EXPIRED);
}
/*
@ -24249,3 +24250,10 @@ dns_zone_getcfg(dns_zone_t *zone) {
return zone->cfg;
}
bool
dns_zone_isexpired(dns_zone_t *zone) {
REQUIRE(DNS_ZONE_VALID(zone));
return DNS_ZONE_FLAG(zone, DNS_ZONEFLG_EXPIRED);
}

View file

@ -1250,6 +1250,11 @@ query_getzonedb(ns_client_t *client, const dns_name_t *name,
partial = true;
}
if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) {
if (dns_zone_isexpired(zone)) {
result = DNS_R_EXPIRED;
goto fail;
}
result = dns_zone_getdb(zone, &db);
}
@ -5630,8 +5635,19 @@ ns__query_start(query_ctx_t *qctx) {
QUERY_ERROR(qctx, DNS_R_REFUSED);
}
} else {
const char *edemsg = NULL;
CCTRACE(ISC_LOG_ERROR, "ns__query_start: query_getdb "
"failed");
if (result == DNS_R_NOTLOADED) {
edemsg = "zone not loaded";
} else if (result == DNS_R_EXPIRED) {
edemsg = "zone expired";
}
dns_ede_add(&qctx->client->edectx, DNS_EDE_INVALIDDATA,
edemsg);
QUERY_ERROR(qctx, result);
}
return ns_query_done(qctx);