Commit 4eba717192 for bind
commit 4eba7171927d1620b8c0a91dbbc2b08f939685af
Author: Evan Hunt <each@isc.org>
Date: Wed Mar 25 22:17:37 2026 -0700
NS checks are no longer fatal by default
A zone which is delegated using DELEG is not required to have
an NS RRset at its apex. The absence of such an RRset now generates
a warning by default, rather than a hard failure.
This behavior can be controlled using the new "check-ns" option, which
can be set to fail, warn, or ignore.
diff --git a/bin/check/check-tool.c b/bin/check/check-tool.c
index 7932f577cd..4301183a74 100644
--- a/bin/check/check-tool.c
+++ b/bin/check/check-tool.c
@@ -79,10 +79,9 @@ bool dochecksrv = false;
bool docheckns = false;
#endif /* if CHECK_LOCAL */
bool docheckrpz = false;
-dns_zoneopt_t zone_options = DNS_ZONEOPT_CHECKNS | DNS_ZONEOPT_FATALNS |
- DNS_ZONEOPT_CHECKMX | DNS_ZONEOPT_CHECKDUPRR |
- DNS_ZONEOPT_CHECKSPF | DNS_ZONEOPT_MANYERRORS |
- DNS_ZONEOPT_CHECKNAMES |
+dns_zoneopt_t zone_options = DNS_ZONEOPT_CHECKNS | DNS_ZONEOPT_CHECKMX |
+ DNS_ZONEOPT_CHECKDUPRR | DNS_ZONEOPT_CHECKSPF |
+ DNS_ZONEOPT_MANYERRORS | DNS_ZONEOPT_CHECKNAMES |
DNS_ZONEOPT_CHECKINTEGRITY |
#if CHECK_SIBLING
DNS_ZONEOPT_CHECKSIBLING |
diff --git a/bin/include/defaultconfig.h b/bin/include/defaultconfig.h
index e8d474b1f1..655e57a16f 100644
--- a/bin/include/defaultconfig.h
+++ b/bin/include/defaultconfig.h
@@ -127,6 +127,7 @@ options {\n\
check-names primary fail;\n\
check-names response ignore;\n\
check-names secondary warn;\n\
+ check-ns warn;\n\
check-spf warn;\n\
check-svcb yes;\n\
clients-per-query 10;\n\
diff --git a/bin/named/server.c b/bin/named/server.c
index 143ff506a2..fb8ac9f908 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -5925,6 +5925,33 @@ create_view(const cfg_obj_t *vconfig, dns_viewlist_t *viewlist,
return ISC_R_SUCCESS;
}
+static void
+configure_zone_nscheck(const cfg_obj_t *options, const cfg_obj_t *vconfig,
+ dns_zone_t *zone) {
+ const cfg_obj_t *modeobj = NULL;
+ const cfg_obj_t *voptions = NULL;
+ const char *mode;
+
+ if (vconfig) {
+ voptions = cfg_tuple_get(vconfig, "options");
+ }
+ (void)named_config_findopt(voptions, options, "check-ns", &modeobj);
+ mode = cfg_obj_asstring(modeobj);
+
+ if (strcmp(mode, "fail") == 0) {
+ dns_zone_setoption(zone, DNS_ZONEOPT_CHECKNS, true);
+ dns_zone_setoption(zone, DNS_ZONEOPT_FATALNS, true);
+ } else if (strcmp(mode, "warn") == 0) {
+ dns_zone_setoption(zone, DNS_ZONEOPT_CHECKNS, true);
+ dns_zone_setoption(zone, DNS_ZONEOPT_FATALNS, false);
+ } else if (strcmp(mode, "ignore") == 0) {
+ dns_zone_setoption(zone, DNS_ZONEOPT_CHECKNS, false);
+ dns_zone_setoption(zone, DNS_ZONEOPT_FATALNS, false);
+ } else {
+ UNREACHABLE();
+ }
+}
+
/*
* Configure or reconfigure a zone.
*/
@@ -6235,8 +6262,7 @@ configure_zone(const cfg_obj_t *config, const cfg_obj_t *zconfig,
}
}
- dns_zone_setoption(zone, DNS_ZONEOPT_CHECKNS, true);
- dns_zone_setoption(zone, DNS_ZONEOPT_FATALNS, true);
+ configure_zone_nscheck(options, vconfig, zone);
if (zone_is_catz) {
dns_zone_catz_enable(zone, view->catzs);
diff --git a/bin/tests/system/checkns/ns1/example.db b/bin/tests/system/checkns/ns1/example.db
new file mode 100644
index 0000000000..1fd7600f66
--- /dev/null
+++ b/bin/tests/system/checkns/ns1/example.db
@@ -0,0 +1,2 @@
+@ 30 IN SOA ns1 hostmaster 2017052201 3600 600 604800 30
+a 300 IN A 10.53.0.5
diff --git a/bin/tests/system/checkns/ns1/named.conf.j2 b/bin/tests/system/checkns/ns1/named.conf.j2
new file mode 100644
index 0000000000..ecec846b0e
--- /dev/null
+++ b/bin/tests/system/checkns/ns1/named.conf.j2
@@ -0,0 +1,31 @@
+options {
+ {% include_indented "_common/options.conf.j2" %}
+};
+
+{% include "_common/controls.conf.j2" %}
+
+view vwarn {
+ match-clients { @ns.ip@; };
+ zone "example" {
+ type primary;
+ file "example.db";
+ };
+};
+
+view vignore {
+ match-clients { 10.53.0.2; };
+ check-ns ignore;
+ zone "example2" {
+ type primary;
+ file "example.db";
+ };
+};
+
+view vfail {
+ match-clients { 10.53.0.3; };
+ check-ns fail;
+ zone "example3" {
+ type primary;
+ file "example.db";
+ };
+};
diff --git a/bin/tests/system/checkns/tests_checkns.py b/bin/tests/system/checkns/tests_checkns.py
new file mode 100644
index 0000000000..252f1f7f9f
--- /dev/null
+++ b/bin/tests/system/checkns/tests_checkns.py
@@ -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.
+
+
+import isctest
+
+
+def test_checkns(ns1):
+ with ns1.watch_log_from_start() as watcher:
+ watcher.wait_for_line("all zones loaded")
+
+ assert "zone example/IN/vwarn: has no NS records" in ns1.log
+ assert "example/IN/vwarn: loaded" in ns1.log
+ msg = isctest.query.create("a.example", "A")
+ res = isctest.query.udp(msg, ns1.ip, source=ns1.ip)
+ isctest.check.noerror(res)
+
+ assert "example2/IN/vignore: loaded" in ns1.log
+ assert "has no NS records (example2)" not in ns1.log
+ msg = isctest.query.create("a.example2", "A")
+ res = isctest.query.udp(msg, ns1.ip, source="10.53.0.2")
+ isctest.check.noerror(res)
+
+ assert "zone example3/IN/vfail: has no NS records" in ns1.log
+ msg = isctest.query.create("a.example3", "A")
+ res = isctest.query.udp(msg, ns1.ip, source="10.53.0.3")
+ isctest.check.servfail(res)
diff --git a/bin/tests/system/checkzone/tests.sh b/bin/tests/system/checkzone/tests.sh
index 115bffdb52..9d380e0519 100644
--- a/bin/tests/system/checkzone/tests.sh
+++ b/bin/tests/system/checkzone/tests.sh
@@ -444,5 +444,15 @@ n=$((n + 1))
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
+echo_i "Checking for no NS records warning ($n)"
+ret=0
+$CHECKZONE example zones/warn.no-apex-ns.db >test.out.$n || ret=1
+grep "zone example/IN: has no NS records" test.out.$n >/dev/null || ret=1
+grep "OK" test.out.$n >/dev/null || ret=1
+$CHECKZONE -n fail example zones/warn.no-apex-ns.db >/dev/null && ret=1
+n=$((n + 1))
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
echo_i "exit status: $status"
[ $status -eq 0 ] || exit 1
diff --git a/bin/tests/system/checkzone/zones/warn.no-apex-ns.db b/bin/tests/system/checkzone/zones/warn.no-apex-ns.db
new file mode 100644
index 0000000000..8451e14bdd
--- /dev/null
+++ b/bin/tests/system/checkzone/zones/warn.no-apex-ns.db
@@ -0,0 +1,18 @@
+; 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.
+
+@ 30 IN SOA ns1 hostmaster 2017052201 3600 600 604800 30
+ns1 3600 IN AAAA fd92:7065:b8e:ffff::1
+ns2 3600 IN AAAA fd92:7065:b8e:ffff::2
+ns3 3600 IN AAAA fd92:7065:b8e:ffff::4
+ns4 3600 IN AAAA fd92:7065:b8e:ffff::4
+dualstack 300 IN AAAA 2001:db8::1
+dualstack 300 IN A 10.53.0.5
diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst
index 115685a3d9..50c0c90776 100644
--- a/doc/arm/reference.rst
+++ b/doc/arm/reference.rst
@@ -2654,6 +2654,14 @@ Boolean Options
default is to ``warn``. Other possible values are ``fail`` and
``ignore``.
+.. namedconf:statement:: check-ns
+ :tags: zone
+ :short: Checks existence and correctness of NS records.
+
+ This checks whether a zone has an NS record at its apex, and whether
+ NS records appear to refer to IP addresses. The default is to ``warn``.
+ Other possible values are ``fail`` and ``ignore``.
+
.. namedconf:statement:: check-wildcard
:tags: zone
:short: Checks for non-terminal wildcards.
diff --git a/doc/misc/hint.zoneopt b/doc/misc/hint.zoneopt
index 260db7fb5f..dba0106bb5 100644
--- a/doc/misc/hint.zoneopt
+++ b/doc/misc/hint.zoneopt
@@ -1,6 +1,7 @@
zone <string> [ <class> ] {
type hint;
check-names ( fail | warn | ignore );
+ check-ns ( fail | warn | ignore );
file <quoted_string>;
template <string>;
};
diff --git a/doc/misc/mirror.zoneopt b/doc/misc/mirror.zoneopt
index 78ce7a5044..0e976ae38f 100644
--- a/doc/misc/mirror.zoneopt
+++ b/doc/misc/mirror.zoneopt
@@ -7,6 +7,7 @@ zone <string> [ <class> ] {
allow-update-forwarding { <address_match_element>; ... };
also-notify [ port <integer> ] [ source ( <ipv4_address> | * ) ] [ source-v6 ( <ipv6_address> | * ) ] { ( <server-list> | <ipv4_address> [ port <integer> ] | <ipv6_address> [ port <integer> ] ) [ key <string> ] [ tls <string> ]; ... };
check-names ( fail | warn | ignore );
+ check-ns ( fail | warn | ignore );
database <string>;
file <quoted_string>;
ixfr-from-differences <boolean>;
diff --git a/doc/misc/options b/doc/misc/options
index eadf175850..0885933ced 100644
--- a/doc/misc/options
+++ b/doc/misc/options
@@ -91,6 +91,7 @@ options {
check-mx ( fail | warn | ignore );
check-mx-cname ( fail | warn | ignore );
check-names ( primary | master | secondary | slave | response ) ( fail | warn | ignore ); // may occur multiple times
+ check-ns ( fail | warn | ignore );
check-sibling <boolean>;
check-spf ( warn | ignore );
check-srv-cname ( fail | warn | ignore );
@@ -384,6 +385,7 @@ template <string> {
check-mx ( fail | warn | ignore );
check-mx-cname ( fail | warn | ignore );
check-names ( fail | warn | ignore );
+ check-ns ( fail | warn | ignore );
check-sibling <boolean>;
check-spf ( warn | ignore );
check-srv-cname ( fail | warn | ignore );
@@ -504,6 +506,7 @@ view <string> [ <class> ] {
check-mx ( fail | warn | ignore );
check-mx-cname ( fail | warn | ignore );
check-names ( primary | master | secondary | slave | response ) ( fail | warn | ignore ); // may occur multiple times
+ check-ns ( fail | warn | ignore );
check-sibling <boolean>;
check-spf ( warn | ignore );
check-srv-cname ( fail | warn | ignore );
diff --git a/doc/misc/primary.zoneopt b/doc/misc/primary.zoneopt
index 3bdbf9ee65..c9224553de 100644
--- a/doc/misc/primary.zoneopt
+++ b/doc/misc/primary.zoneopt
@@ -10,6 +10,7 @@ zone <string> [ <class> ] {
check-mx ( fail | warn | ignore );
check-mx-cname ( fail | warn | ignore );
check-names ( fail | warn | ignore );
+ check-ns ( fail | warn | ignore );
check-sibling <boolean>;
check-spf ( warn | ignore );
check-srv-cname ( fail | warn | ignore );
diff --git a/doc/misc/secondary.zoneopt b/doc/misc/secondary.zoneopt
index 631ee731c3..c6a91e91f5 100644
--- a/doc/misc/secondary.zoneopt
+++ b/doc/misc/secondary.zoneopt
@@ -7,6 +7,7 @@ zone <string> [ <class> ] {
allow-update-forwarding { <address_match_element>; ... };
also-notify [ port <integer> ] [ source ( <ipv4_address> | * ) ] [ source-v6 ( <ipv6_address> | * ) ] { ( <server-list> | <ipv4_address> [ port <integer> ] | <ipv6_address> [ port <integer> ] ) [ key <string> ] [ tls <string> ]; ... };
check-names ( fail | warn | ignore );
+ check-ns ( fail | warn | ignore );
checkds ( explicit | <boolean> );
database <string>;
dlz <string>;
diff --git a/doc/misc/stub.zoneopt b/doc/misc/stub.zoneopt
index 4d25095484..8df5d77306 100644
--- a/doc/misc/stub.zoneopt
+++ b/doc/misc/stub.zoneopt
@@ -3,6 +3,7 @@ zone <string> [ <class> ] {
allow-query { <address_match_element>; ... };
allow-query-on { <address_match_element>; ... };
check-names ( fail | warn | ignore );
+ check-ns ( fail | warn | ignore );
database <string>;
file <quoted_string>;
forward ( first | only );
diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c
index 446bf4e8f9..7331c7b870 100644
--- a/lib/isccfg/namedconf.c
+++ b/lib/isccfg/namedconf.c
@@ -2369,6 +2369,7 @@ static cfg_clausedef_t view_clauses[] = {
{ "catalog-zones", &cfg_type_catz, 0, NULL },
{ "check-names", &cfg_type_checknames, CFG_CLAUSEFLAG_MULTI,
checknames_merge },
+ { "check-ns", &cfg_type_checkmode, 0, NULL },
{ "cleaning-interval", NULL, CFG_CLAUSEFLAG_ANCIENT, NULL },
{ "clients-per-query", &cfg_type_uint32, 0, NULL },
{ "deny-answer-addresses", &cfg_type_denyaddresses, 0, NULL },
@@ -2799,6 +2800,10 @@ static cfg_clausedef_t zone_only_clauses[] = {
CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR |
CFG_ZONE_HINT | CFG_ZONE_STUB,
NULL },
+ { "check-ns", &cfg_type_checkmode,
+ CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR |
+ CFG_ZONE_HINT | CFG_ZONE_STUB,
+ NULL },
{ "checkds", &cfg_type_checkdstype,
CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY, NULL },
{ "database", &cfg_type_astring,