]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Backport the whole NTA test suite
authorAram Sargsyan <aram@isc.org>
Tue, 24 Feb 2026 17:24:38 +0000 (17:24 +0000)
committerOndřej Surý <ondrej@isc.org>
Fri, 20 Mar 2026 02:24:56 +0000 (03:24 +0100)
The 9.20 branch was missing the new NTA test suite.  Backport it.

15 files changed:
bin/tests/system/nta/ns1/named.conf.j2 [new file with mode: 0644]
bin/tests/system/nta/ns1/root.db.in [new file with mode: 0644]
bin/tests/system/nta/ns1/sign.sh [new file with mode: 0644]
bin/tests/system/nta/ns2/corp.db [new file with mode: 0644]
bin/tests/system/nta/ns2/example.db.in [new file with mode: 0644]
bin/tests/system/nta/ns2/named.conf.j2 [new file with mode: 0644]
bin/tests/system/nta/ns2/sign.sh [new file with mode: 0644]
bin/tests/system/nta/ns3/bogus.example.db.in [new file with mode: 0644]
bin/tests/system/nta/ns3/named.conf.j2 [new file with mode: 0644]
bin/tests/system/nta/ns3/secure.example.db.in [new file with mode: 0644]
bin/tests/system/nta/ns3/sign.sh [new file with mode: 0644]
bin/tests/system/nta/ns4/named.conf.j2 [new file with mode: 0644]
bin/tests/system/nta/ns9/named.conf.j2 [new file with mode: 0644]
bin/tests/system/nta/setup.sh [new file with mode: 0644]
bin/tests/system/nta/tests_nta.py [new file with mode: 0644]

diff --git a/bin/tests/system/nta/ns1/named.conf.j2 b/bin/tests/system/nta/ns1/named.conf.j2
new file mode 100644 (file)
index 0000000..bd1ccc4
--- /dev/null
@@ -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.
+ */
+
+// NS1
+
+options {
+       query-source address 10.53.0.1;
+       notify-source 10.53.0.1;
+       transfer-source 10.53.0.1;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.1; };
+       listen-on-v6 { none; };
+       recursion no;
+       notify yes;
+       dnssec-validation yes;
+       /* test that we can turn off trust-anchor-telemetry */
+       trust-anchor-telemetry no;
+};
+
+zone "." {
+       type primary;
+       file "root.db.signed";
+};
+
+include "trusted.conf";
diff --git a/bin/tests/system/nta/ns1/root.db.in b/bin/tests/system/nta/ns1/root.db.in
new file mode 100644 (file)
index 0000000..34f7773
--- /dev/null
@@ -0,0 +1,24 @@
+; 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  gson.nominum.com. a.root.servers.nil. (
+                               2000042100      ; serial
+                               600             ; refresh
+                               600             ; retry
+                               1200            ; expire
+                               600             ; minimum
+                               )
+.                      NS      a.root-servers.nil.
+a.root-servers.nil.    A       10.53.0.1
+
+example.               NS      ns2.example.
+ns2.example.           A       10.53.0.2
diff --git a/bin/tests/system/nta/ns1/sign.sh b/bin/tests/system/nta/ns1/sign.sh
new file mode 100644 (file)
index 0000000..243503d
--- /dev/null
@@ -0,0 +1,37 @@
+#!/bin/sh -e
+
+# 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.
+
+# shellcheck source=conf.sh
+. ../../conf.sh
+
+set -e
+
+zone=.
+infile=root.db.in
+zonefile=root.db
+
+(cd ../ns2 && $SHELL sign.sh)
+
+cp "../ns2/dsset-example." .
+
+ksk=$("$KEYGEN" -q -fk -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+
+cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
+
+"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1
+
+# Configure the resolving server with a static key.
+keyfile_to_static_ds "$ksk" >trusted.conf
+cp trusted.conf ../ns4/trusted.conf
+cp trusted.conf ../ns9/trusted.conf
diff --git a/bin/tests/system/nta/ns2/corp.db b/bin/tests/system/nta/ns2/corp.db
new file mode 100644 (file)
index 0000000..b2912bc
--- /dev/null
@@ -0,0 +1,23 @@
+; 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 30        ; 5 minutes
+@                      IN SOA  mname1. . (
+                               2000042407 ; serial
+                               20         ; refresh (20 seconds)
+                               20         ; retry (20 seconds)
+                               1814400    ; expire (3 weeks)
+                               30       ; minimum (1 hour)
+                               )
+                       NS      ns2
+ns2                    A       10.53.0.2
+
+www                    A       10.0.0.1
diff --git a/bin/tests/system/nta/ns2/example.db.in b/bin/tests/system/nta/ns2/example.db.in
new file mode 100644 (file)
index 0000000..f72258f
--- /dev/null
@@ -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.
+
+$TTL 300       ; 5 minutes
+@                      IN SOA  mname1. . (
+                               2000042407 ; serial
+                               20         ; refresh (20 seconds)
+                               20         ; retry (20 seconds)
+                               1814400    ; expire (3 weeks)
+                               3600       ; minimum (1 hour)
+                               )
+                       NS      ns2
+                       NS      ns3
+ns2                    A       10.53.0.2
+ns3                    A       10.53.0.3
+
+; A secure subdomain
+secure                 NS      ns3.secure
+ns3.secure             A       10.53.0.3
+
+; A secure subdomain we're going to inject bogus data into
+bogus                  NS      ns.bogus
+ns.bogus               A       10.53.0.3
+
+; A subdomain with a corrupt DS
+badds                  NS      ns.badds
+ns.badds               A       10.53.0.3
diff --git a/bin/tests/system/nta/ns2/named.conf.j2 b/bin/tests/system/nta/ns2/named.conf.j2
new file mode 100644 (file)
index 0000000..9bfbcde
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+// NS2
+
+options {
+       query-source address 10.53.0.2;
+       notify-source 10.53.0.2;
+       transfer-source 10.53.0.2;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.2; };
+       listen-on-v6 { none; };
+       allow-transfer { any; };
+       recursion no;
+       notify yes;
+        notify-delay 1;
+       dnssec-validation no;
+       minimal-responses no;
+};
+
+key rndc_key {
+        secret "1234abcd8765";
+        algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+        inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example" {
+       type primary;
+       file "example.db.signed";
+       allow-update { any; };
+};
+
+zone "corp" {
+       type primary;
+       file "corp.db";
+};
diff --git a/bin/tests/system/nta/ns2/sign.sh b/bin/tests/system/nta/ns2/sign.sh
new file mode 100644 (file)
index 0000000..5eb698e
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/sh -e
+
+# 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.
+
+# shellcheck source=conf.sh
+. ../../conf.sh
+
+set -e
+
+# Sign child zones (served by ns3).
+(cd ../ns3 && $SHELL sign.sh)
+
+# The "example." zone.
+zone=example.
+infile=example.db.in
+zonefile=example.db
+
+# Get the DS records for the "example." zone.
+for subdomain in bogus badds secure; do
+  cp "../ns3/dsset-$subdomain.example." .
+done
+
+# Sign the "example." zone.
+keyname1=$("$KEYGEN" -q -a "$ALTERNATIVE_ALGORITHM" -b "$ALTERNATIVE_BITS" -f KSK "$zone")
+keyname2=$("$KEYGEN" -q -a "$ALTERNATIVE_ALGORITHM" -b "$ALTERNATIVE_BITS" "$zone")
+
+cat "$infile" "$keyname1.key" "$keyname2.key" >"$zonefile"
+
+"$SIGNER" -g -o "$zone" -k "$keyname1" "$zonefile" "$keyname2" >/dev/null 2>&1
diff --git a/bin/tests/system/nta/ns3/bogus.example.db.in b/bin/tests/system/nta/ns3/bogus.example.db.in
new file mode 100644 (file)
index 0000000..0feb441
--- /dev/null
@@ -0,0 +1,27 @@
+; 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       ; 5 minutes
+@                      IN SOA  mname1. . (
+                               2000042407 ; serial
+                               20         ; refresh (20 seconds)
+                               20         ; retry (20 seconds)
+                               1814400    ; expire (3 weeks)
+                               3600       ; minimum (1 hour)
+                               )
+                       NS      ns
+ns                     A       10.53.0.3
+
+a                      A       10.0.0.1
+b                      A       10.0.0.2
+c                      A       10.0.0.3
+d                      A       10.0.0.4
+z                      A       10.0.0.26
diff --git a/bin/tests/system/nta/ns3/named.conf.j2 b/bin/tests/system/nta/ns3/named.conf.j2
new file mode 100644 (file)
index 0000000..3c3f256
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+// NS3
+
+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; };
+       allow-transfer { any; };
+       recursion no;
+       notify yes;
+       dnssec-validation no;
+       minimal-responses no;
+};
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example" {
+       type secondary;
+       primaries { 10.53.0.2; };
+       file "example.bk";
+};
+
+zone "secure.example" {
+       type primary;
+       file "secure.example.db.signed";
+       allow-update { any; };
+};
+
+zone "bogus.example" {
+       type primary;
+       file "bogus.example.db.signed";
+       allow-update { any; };
+};
+
+zone "badds.example" {
+       type primary;
+       file "badds.example.db.signed";
+       allow-update { any; };
+};
diff --git a/bin/tests/system/nta/ns3/secure.example.db.in b/bin/tests/system/nta/ns3/secure.example.db.in
new file mode 100644 (file)
index 0000000..182329b
--- /dev/null
@@ -0,0 +1,30 @@
+; 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       ; 5 minutes
+@                      IN SOA  mname1. . (
+                               2000042407 ; serial
+                               20         ; refresh (20 seconds)
+                               20         ; retry (20 seconds)
+                               1814400    ; expire (3 weeks)
+                               3600       ; minimum (1 hour)
+                               )
+                       NS      ns3
+ns3                    A       10.53.0.3
+
+a                      A       10.0.0.1
+b                      A       10.0.0.2
+c                      A       10.0.0.3
+d                      A       10.0.0.4
+e                      A       10.0.0.5
+f                      A       10.0.0.6
+g                      A       10.0.0.7
+z                      A       10.0.0.26
diff --git a/bin/tests/system/nta/ns3/sign.sh b/bin/tests/system/nta/ns3/sign.sh
new file mode 100644 (file)
index 0000000..5e5405a
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh -e
+
+# 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.
+
+# shellcheck source=conf.sh
+. ../../conf.sh
+
+set -e
+
+# a validly signed zone
+zone=secure.example.
+infile=secure.example.db.in
+zonefile=secure.example.db
+
+keyname=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+
+cat "$infile" "$keyname.key" >"$zonefile"
+
+"$SIGNER" -z -D -o "$zone" "$zonefile" >/dev/null
+cat "$zonefile" "$zonefile".signed >"$zonefile".tmp
+mv "$zonefile".tmp "$zonefile".signed
+
+# a zone that we'll add bogus data to
+zone=bogus.example.
+infile=bogus.example.db.in
+zonefile=bogus.example.db
+
+keyname=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+
+cat "$infile" "$keyname.key" >"$zonefile"
+
+"$SIGNER" -z -o "$zone" "$zonefile" >/dev/null
+
+{
+  echo "a.bogus.example.       A       10.0.0.22"
+  echo "b.bogus.example.       A       10.0.0.23"
+  echo "c.bogus.example.       A       10.0.0.23"
+} >>bogus.example.db.signed
+
+#
+# A zone with a bad DS in the parent
+# (sourced from bogus.example.db.in)
+#
+zone=badds.example.
+infile=bogus.example.db.in
+zonefile=badds.example.db
+
+keyname=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+
+cat "$infile" "$keyname.key" >"$zonefile"
+
+"$SIGNER" -P -o "$zone" "$zonefile" >/dev/null
+sed -e 's/bogus/badds/g' <dsset-bogus.example. >dsset-badds.example.
diff --git a/bin/tests/system/nta/ns4/named.conf.j2 b/bin/tests/system/nta/ns4/named.conf.j2
new file mode 100644 (file)
index 0000000..87f70e2
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+// NS4
+
+options {
+       query-source address 10.53.0.4;
+       notify-source 10.53.0.4;
+       transfer-source 10.53.0.4;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.4; };
+       listen-on-v6 { none; };
+       recursion yes;
+       minimal-responses no;
+
+       nta-lifetime 12s;
+       nta-recheck 9s;
+       validate-except { corp; };
+
+       dnssec-validation yes;
+};
+
+include "trusted.conf";
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "." {
+       type hint;
+       file "../../_common/root.hint";
+};
+
+zone "corp" {
+       type static-stub;
+       server-addresses { 10.53.0.2; };
+};
diff --git a/bin/tests/system/nta/ns9/named.conf.j2 b/bin/tests/system/nta/ns9/named.conf.j2
new file mode 100644 (file)
index 0000000..cdbe7ec
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+// NS9
+
+options {
+       query-source address 10.53.0.9;
+       notify-source 10.53.0.9;
+       transfer-source 10.53.0.9;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.9; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation yes;
+       forward only;
+       forwarders { 10.53.0.4; };
+       servfail-ttl 0;
+};
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.9 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+include "trusted.conf";
diff --git a/bin/tests/system/nta/setup.sh b/bin/tests/system/nta/setup.sh
new file mode 100644 (file)
index 0000000..4a4db2d
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+
+# 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.
+
+# shellcheck source=conf.sh
+. ../conf.sh
+
+set -e
+
+(
+  cd ns1
+  $SHELL sign.sh
+)
diff --git a/bin/tests/system/nta/tests_nta.py b/bin/tests/system/nta/tests_nta.py
new file mode 100644 (file)
index 0000000..ece8db6
--- /dev/null
@@ -0,0 +1,420 @@
+# 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.
+
+from re import compile as Re
+
+import os
+import time
+
+import isctest
+
+
+def active(blob):
+    return len([x for x in blob.splitlines() if " expiry" in x])
+
+
+# global start-time variable
+# pylint: disable=global-statement
+START = 0
+
+
+def test_initial():
+    m = isctest.query.create("a.bogus.example.", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.servfail(res)
+
+    m = isctest.query.create("badds.example.", "SOA")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.servfail(res)
+
+    m = isctest.query.create("a.secure.example.", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+
+def test_nta_validate_except(servers):
+    ns4 = servers["ns4"]
+    response = ns4.rndc("secroots -")
+    assert Re("^corp: permanent") in response.out
+
+    # check insecure local domain works with validate-except
+    m = isctest.query.create("www.corp", "NS")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+
+def test_nta_bogus_lifetimes(servers):
+    ns4 = servers["ns4"]
+
+    # no nta lifetime specified:
+    response = ns4.rndc("nta -l '' foo", raise_on_exception=False)
+    assert "'nta' failed: bad ttl" in response.err
+
+    # bad nta lifetime:
+    response = ns4.rndc("nta -l garbage foo", raise_on_exception=False)
+    assert "'nta' failed: bad ttl" in response.err
+
+    # excessive nta lifetime:
+    response = ns4.rndc("nta -l 7d1h foo", raise_on_exception=False)
+    assert "'nta' failed: out of range" in response.err
+
+
+def test_nta_install(servers):
+    global START
+
+    ns4 = servers["ns4"]
+    ns4.rndc("nta -f -l 20s bogus.example")
+    ns4.rndc("nta badds.example")
+
+    # NTAs should persist after reconfig
+    ns4.reconfigure()
+
+    response = ns4.rndc("nta -d")
+    assert len(response.out.splitlines()) == 3
+
+    ns4.rndc("nta secure.example")
+    ns4.rndc("nta fakenode.secure.example")
+    with ns4.watch_log_from_here() as watcher:
+        ns4.rndc("reload")
+        watcher.wait_for_line("all zones loaded")
+
+    response = ns4.rndc("nta -d")
+    assert len(response.out.splitlines()) == 5
+
+    START = time.time()
+
+
+def test_nta_behavior(servers):
+    assert START, "test_nta_behavior must be run as part of the full NTA test"
+
+    m = isctest.query.create("a.bogus.example.", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    m = isctest.query.create("badds.example.", "SOA")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    m = isctest.query.create("a.secure.example.", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    m = isctest.query.create("a.fakenode.secure.example.", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noadflag(res)
+
+    ns4 = servers["ns4"]
+    response = ns4.rndc("secroots -")
+    assert Re("^bogus.example: expiry") in response.out
+    assert Re("^badds.example: expiry") in response.out
+    assert Re("^secure.example: expiry") in response.out
+    assert Re("^fakenode.secure.example: expiry") in response.out
+
+    # secure.example and badds.example used the default nta-duration
+    # (configured as 12s in ns4/named1.conf), but the nta recheck interval
+    # is configured to 9s, so at t=10 the NTAs for secure.example and
+    # fakenode.secure.example should both be lifted, while badds.example
+    # should still be going.
+    delay = START + 10 - time.time()
+    if delay > 0:
+        time.sleep(delay)
+
+    m = isctest.query.create("b.secure.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+    m = isctest.query.create("b.fakenode.secure.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.nxdomain(res)
+    isctest.check.adflag(res)
+
+    m = isctest.query.create("badds.example.", "SOA")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    # bogus.example was set to expire in 20s, so at t=13
+    # it should still be NTA'd, but badds.example used the default
+    # lifetime of 12s, so it should revert to SERVFAIL now.
+    delay = START + 13 - time.time()
+    if delay > 0:
+        time.sleep(delay)
+
+    response = ns4.rndc("nta -d")
+    assert active(response.out) <= 2
+
+    response = ns4.rndc("secroots -")
+    assert Re("bogus.example: expiry") in response.out
+    assert Re("badds.example: expiry") not in response.out
+
+    m = isctest.query.create("b.bogus.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+
+    m = isctest.query.create("a.badds.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.servfail(res)
+    isctest.check.noadflag(res)
+
+    m = isctest.query.create("c.secure.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+    # at t=21, all the NTAs should have expired.
+    delay = START + 21 - time.time()
+    if delay > 0:
+        time.sleep(delay)
+
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 0
+
+    m = isctest.query.create("d.secure.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+    m = isctest.query.create("c.bogus.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.servfail(res)
+    isctest.check.noadflag(res)
+
+
+def test_nta_removals(servers):
+    ns4 = servers["ns4"]
+    ns4.rndc("nta badds.example")
+
+    response = ns4.rndc("nta -d")
+    assert Re("^badds.example/_default: expiry") in response.out
+
+    m = isctest.query.create("a.badds.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    response = ns4.rndc("nta -remove badds.example")
+    assert "Negative trust anchor removed: badds.example" in response.out
+
+    response = ns4.rndc("nta -d")
+    assert Re("^badds.example/_default: expiry") not in response.out
+
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.servfail(res)
+    isctest.check.noadflag(res)
+
+    # remove non-existent NTA three times
+    ns4.rndc("nta -r foo")
+    ns4.rndc("nta -remove foo")
+    response = ns4.rndc("nta -r foo")
+    assert "not found" in response.out
+
+
+def test_nta_restarts(servers):
+    global START
+    assert START, "test_nta_restarts must be run as part of the full NTA test"
+
+    # test NTA persistence across restarts
+    ns4 = servers["ns4"]
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 0
+
+    START = time.time()
+    ns4.rndc("nta -f -l 30s bogus.example")
+    ns4.rndc("nta -f -l 10s badds.example")
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 2
+
+    # stop the server
+    ns4.stop()
+
+    # wait 14s before restarting. badds.example's NTA (lifetime=10s) should
+    # have expired, and bogus.example should still be running.
+    delay = START + 14 - time.time()
+    if delay > 0:
+        time.sleep(delay)
+    ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
+
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 1
+    assert Re("^bogus.example/_default: expiry") in response.out
+
+    m = isctest.query.create("a.badds.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.servfail(res)
+
+    m = isctest.query.create("a.bogus.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    ns4.rndc("nta -r bogus.example")
+
+
+def test_nta_regular(servers):
+    global START
+    assert START, "test_nta_regular must be run as part of the full NTA test"
+
+    # check "regular" attribute in NTA file
+    ns4 = servers["ns4"]
+
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 0
+
+    # secure.example validates with AD=1
+    m = isctest.query.create("a.secure.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+    # stop the server, update _default.nta, restart
+    ns4.stop()
+    now = time.localtime()
+    future = str(now.tm_year + 20) + "0101010000"
+    with open("ns4/_default.nta", "w", encoding="utf-8") as f:
+        f.write(f"secure.example. regular {future}")
+
+    ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
+
+    # NTA active; secure.example. should now return an AD=0 answer.
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    # nta-recheck is configured as 9s, so at t=12 the NTA for
+    # secure.example. should be lifted as it is not a "forced" NTA.
+    START = time.mktime(now)
+    delay = START + 12 - time.time()
+    if delay > 0:
+        time.sleep(delay)
+
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 0
+
+    # NTA lifted; secure.example. flush the cache to trigger a new query,
+    # and it should now return an AD=1 answer.
+    ns4.rndc("flushtree secure.example")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+
+def test_nta_forced(servers):
+    global START
+    assert START, "test_nta_regular must be run as part of the full NTA test"
+
+    # check "forced" attribute in NTA file
+    ns4 = servers["ns4"]
+
+    # just to be certain, clean up any existing NTA first
+    ns4.rndc("nta -r secure.example")
+
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 0
+
+    # secure.example validates with AD=1
+    m = isctest.query.create("a.secure.example", "A")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.adflag(res)
+
+    # stop the server, update _default.nta, restart
+    ns4.stop()
+    now = time.localtime()
+    future = str(now.tm_year + 20) + "0101010000"
+    with open("ns4/_default.nta", "w", encoding="utf-8") as f:
+        f.write(f"secure.example. forced {future}")
+
+    ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
+
+    # NTA active; secure.example. should now return an AD=0 answer
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+    # nta-recheck is configured as 9s. at t=12 the NTA for
+    # secure.example. should NOT be lifted as it is "forced".
+    START = time.mktime(now)
+    delay = START + 12 - time.time()
+    if delay > 0:
+        time.sleep(delay)
+
+    # NTA lifted; secure.example. should still return an AD=0 answer
+    ns4.rndc("flushtree secure.example")
+    res = isctest.query.tcp(m, "10.53.0.4")
+    isctest.check.noerror(res)
+    isctest.check.noadflag(res)
+
+
+def test_nta_clamping(servers):
+    ns4 = servers["ns4"]
+
+    # clean up any existing NTA
+    ns4.rndc("nta -r secure.example")
+
+    # stop the server, update _default.nta, restart
+    ns4.stop()
+    now = time.localtime()
+    future = str(now.tm_year + 20) + "0101010000"
+    with open("ns4/_default.nta", "w", encoding="utf-8") as f:
+        f.write(f"secure.example. forced {future}")
+
+    ns4.start(["--noclean", "--restart", "--port", os.environ["PORT"]])
+
+    # check that NTA lifetime read from file is clamped to 1 week.
+    response = ns4.rndc("nta -d")
+    assert active(response.out) == 1
+
+    nta = next((s for s in response.out.splitlines() if " expiry" in s), None)
+    assert nta is not None
+
+    nta = nta.split(" ")
+    expiry = f"{nta[2]} {nta[3]}"
+    then = time.mktime(time.strptime(expiry, "%d-%b-%Y %H:%M:%S.000"))
+    nextweek = time.mktime(now) + (86400 * 7)
+
+    # normally there's no more than a few seconds difference between the
+    # clamped expiration date and the calculated date for next week,
+    # but add a 3600 second fudge factor to allow for daylight savings
+    # changes.
+    assert abs(nextweek - then < 3610)
+
+    # remove the NTA
+    ns4.rndc("nta -r secure.example")
+
+
+def test_nta_forward(servers):
+    ns9 = servers["ns9"]
+
+    m = isctest.query.create("badds.example", "SOA")
+    res = isctest.query.tcp(m, "10.53.0.9")
+    isctest.check.servfail(res)
+    isctest.check.empty_answer(res)
+    isctest.check.noadflag(res)
+
+    # add NTA and expect resolution to succeed
+    ns9.rndc("nta badds.example")
+    res = isctest.query.tcp(m, "10.53.0.9")
+    isctest.check.noerror(res)
+    isctest.check.rr_count_eq(res.answer, 2)
+    isctest.check.noadflag(res)
+
+    # remove NTA and expect resolution to fail again
+    ns9.rndc("nta -remove badds.example")
+    res = isctest.query.tcp(m, "10.53.0.9")
+    isctest.check.servfail(res)
+    isctest.check.empty_answer(res)
+    isctest.check.noadflag(res)