From 68832829296eb01fa49a4c7271e7dff1f95a7ce8 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Fri, 31 Oct 2025 12:12:20 +0100 Subject: [PATCH 1/5] add dns_zone_isexpired API Introduce the `dns_zone_isexpired()` API which returns `true` when a secondary, mirror, etc. zone is expired. This internally use the `DNS_ZONEFLG_EXPIRED` which was already set when the zone gets expired, but never used. The flag `DNS_ZONEFLG_EXPIRED` is also now cleared when the expiration time of the zone is updated and in the future. --- lib/dns/include/dns/zone.h | 9 +++++++++ lib/dns/zone.c | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h index b45c842c9a..70b79471a4 100644 --- a/lib/dns/include/dns/zone.h +++ b/lib/dns/include/dns/zone.h @@ -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__) diff --git a/lib/dns/zone.c b/lib/dns/zone.c index d0bc8b65c4..a196d5b96c 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -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); +} From 90b4f256a70e78ac9c095d3c21c3f45183fa6b32 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Fri, 31 Oct 2025 12:15:00 +0100 Subject: [PATCH 2/5] query_getzonedb can returns DNS_R_EXPIRED If `query_getzonedb()` finds a zone but the zone is expired it immediately returns `DNS_R_EXPIRED` and doesn't attempt to get the zone DB (which would be NULL in this case). This enable caller to have a more precise reason of why getting the DB has failed. --- lib/ns/query.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/ns/query.c b/lib/ns/query.c index 9f59e9a577..b9bd2b892a 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -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); } From 59f116fbc94f4fea01bd659d3e79502108cb55ab Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Fri, 31 Oct 2025 12:16:54 +0100 Subject: [PATCH 3/5] support EDE 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. --- lib/ns/query.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/ns/query.c b/lib/ns/query.c index b9bd2b892a..2a8f587bbf 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -5635,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); From 082e1aa83444aad446ae1fe21bb463c052074fcb Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 29 Oct 2025 17:32:29 +0100 Subject: [PATCH 4/5] add tests for EDE 24 support Add system test covering EDE 24 being added in the response in both common cases: when the server has not loaded the DB of a zone and when the zone has expired (secondary). --- bin/tests/system/ede24/ns1/foo.fr.db | 21 +++++++ bin/tests/system/ede24/ns1/named.conf.j2 | 36 ++++++++++++ bin/tests/system/ede24/ns2/named.conf.j2 | 35 ++++++++++++ bin/tests/system/ede24/tests_ede24.py | 71 ++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 bin/tests/system/ede24/ns1/foo.fr.db create mode 100644 bin/tests/system/ede24/ns1/named.conf.j2 create mode 100644 bin/tests/system/ede24/ns2/named.conf.j2 create mode 100644 bin/tests/system/ede24/tests_ede24.py diff --git a/bin/tests/system/ede24/ns1/foo.fr.db b/bin/tests/system/ede24/ns1/foo.fr.db new file mode 100644 index 0000000000..f3937c043c --- /dev/null +++ b/bin/tests/system/ede24/ns1/foo.fr.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 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 diff --git a/bin/tests/system/ede24/ns1/named.conf.j2 b/bin/tests/system/ede24/ns1/named.conf.j2 new file mode 100644 index 0000000000..d32cf6665d --- /dev/null +++ b/bin/tests/system/ede24/ns1/named.conf.j2 @@ -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; }; +}; diff --git a/bin/tests/system/ede24/ns2/named.conf.j2 b/bin/tests/system/ede24/ns2/named.conf.j2 new file mode 100644 index 0000000000..18dc1236bb --- /dev/null +++ b/bin/tests/system/ede24/ns2/named.conf.j2 @@ -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; }; +}; diff --git a/bin/tests/system/ede24/tests_ede24.py b/bin/tests/system/ede24/tests_ede24.py new file mode 100644 index 0000000000..7c5771715e --- /dev/null +++ b/bin/tests/system/ede24/tests_ede24.py @@ -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() From e4851fcdb7d8de44857c722fd852b740290583f5 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Mon, 3 Nov 2025 17:31:26 +0100 Subject: [PATCH 5/5] log that a zone is expired after setting the flag When a (secondary) zone is expired, the log message ` expired` is printed and the flag `DNS_ZONEFLG_EXPIRED` is set. Change the order by setting the expired flag first, then printing the log. This should fixes (rare but persistent) timing-related CI error when the EDE 24 tests expect the zone to be expired (from the log) and immediately after request and expect an EDE 24 error. (In some rare cases, the server was still answering the response). --- lib/dns/zone.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dns/zone.c b/lib/dns/zone.c index a196d5b96c..fdba710f47 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -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