summaryrefslogtreecommitdiffstats
path: root/bin/tests/system/chain
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:59:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:59:48 +0000
commit3b9b6d0b8e7f798023c9d109c490449d528fde80 (patch)
tree2e1c188dd7b8d7475cd163de9ae02c428343669b /bin/tests/system/chain
parentInitial commit. (diff)
downloadbind9-3b9b6d0b8e7f798023c9d109c490449d528fde80.tar.xz
bind9-3b9b6d0b8e7f798023c9d109c490449d528fde80.zip
Adding upstream version 1:9.18.19.upstream/1%9.18.19
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--bin/tests/system/chain/README22
-rw-r--r--bin/tests/system/chain/ans3/ans.pl131
-rw-r--r--bin/tests/system/chain/ans4/README.anspy24
-rwxr-xr-xbin/tests/system/chain/ans4/ans.py386
-rwxr-xr-xbin/tests/system/chain/clean.sh18
-rw-r--r--bin/tests/system/chain/ns1/named.conf.in27
-rw-r--r--bin/tests/system/chain/ns1/root.db51
-rw-r--r--bin/tests/system/chain/ns2/example.db69
-rw-r--r--bin/tests/system/chain/ns2/generic.db22
-rw-r--r--bin/tests/system/chain/ns2/named.conf.in74
-rw-r--r--bin/tests/system/chain/ns2/sign.sh54
-rw-r--r--bin/tests/system/chain/ns2/sub.db26
-rw-r--r--bin/tests/system/chain/ns2/wildcard-secure.db29
-rw-r--r--bin/tests/system/chain/ns2/wildcard.db28
-rw-r--r--bin/tests/system/chain/ns5/named.conf.in42
-rw-r--r--bin/tests/system/chain/ns5/sub.db26
-rw-r--r--bin/tests/system/chain/ns7/named.conf.in45
-rw-r--r--bin/tests/system/chain/ns7/root.hint14
-rw-r--r--bin/tests/system/chain/setup.sh22
-rw-r--r--bin/tests/system/chain/tests.sh630
-rw-r--r--bin/tests/system/chain/tests_sh_chain.py14
21 files changed, 1754 insertions, 0 deletions
diff --git a/bin/tests/system/chain/README b/bin/tests/system/chain/README
new file mode 100644
index 0000000..649142e
--- /dev/null
+++ b/bin/tests/system/chain/README
@@ -0,0 +1,22 @@
+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 is the root server.
+
+ns2 and ns5 are both authoritative servers.
+
+ans3 is a mock authoritative server that can return various broken
+responses.
+
+ans4 is a mock authoritative server that can return CNAME or DNAME
+responses of arbitrary size in arbitrary order.
+
+ns7 is the resolver under test.
diff --git a/bin/tests/system/chain/ans3/ans.pl b/bin/tests/system/chain/ans3/ans.pl
new file mode 100644
index 0000000..271b2a4
--- /dev/null
+++ b/bin/tests/system/chain/ans3/ans.pl
@@ -0,0 +1,131 @@
+#!/usr/bin/env perl
+
+# 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.
+
+use strict;
+use warnings;
+
+use IO::File;
+use Getopt::Long;
+use Net::DNS::Nameserver;
+
+my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!";
+print $pidf "$$\n" or die "cannot write pid file: $!";
+$pidf->close or die "cannot close pid file: $!";
+sub rmpid { unlink "ans.pid"; exit 1; };
+
+$SIG{INT} = \&rmpid;
+$SIG{TERM} = \&rmpid;
+
+my $localaddr = "10.53.0.3";
+
+my $localport = int($ENV{'PORT'});
+if (!$localport) { $localport = 5300; }
+
+my $verbose = 0;
+my $ttl = 60;
+my $zone = "example.broken";
+my $nsname = "ns3.$zone";
+my $synth = "synth-then-dname.$zone";
+my $synth2 = "synth2-then-dname.$zone";
+
+sub reply_handler {
+ my ($qname, $qclass, $qtype, $peerhost, $query, $conn) = @_;
+ my ($rcode, @ans, @auth, @add);
+
+ print ("request: $qname/$qtype\n");
+ STDOUT->flush();
+
+ if ($qname eq "example.broken") {
+ if ($qtype eq "SOA") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass SOA . . 0 0 0 0 0");
+ push @ans, $rr;
+ } elsif ($qtype eq "NS") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass NS $nsname");
+ push @ans, $rr;
+ $rr = new Net::DNS::RR("$nsname $ttl $qclass A $localaddr");
+ push @add, $rr;
+ }
+ $rcode = "NOERROR";
+ } elsif ($qname eq "cname-to-$synth2") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name.$synth2");
+ push @ans, $rr;
+ $rr = new Net::DNS::RR("name.$synth2 $ttl $qclass CNAME name");
+ push @ans, $rr;
+ $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME .");
+ push @ans, $rr;
+ $rcode = "NOERROR";
+ } elsif ($qname eq "$synth" || $qname eq "$synth2") {
+ if ($qtype eq "DNAME") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME .");
+ push @ans, $rr;
+ }
+ $rcode = "NOERROR";
+ } elsif ($qname eq "name.$synth") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name.");
+ push @ans, $rr;
+ $rr = new Net::DNS::RR("$synth $ttl $qclass DNAME .");
+ push @ans, $rr;
+ $rcode = "NOERROR";
+ } elsif ($qname eq "name.$synth2") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name.");
+ push @ans, $rr;
+ $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME .");
+ push @ans, $rr;
+ $rcode = "NOERROR";
+ # The following three code branches referring to the "example.dname"
+ # zone are necessary for the resolver variant of the CVE-2021-25215
+ # regression test to work. A named instance cannot be used for
+ # serving the DNAME records below as a version of BIND vulnerable to
+ # CVE-2021-25215 would crash while answering the queries asked by
+ # the tested resolver.
+ } elsif ($qname eq "ns3.example.dname") {
+ if ($qtype eq "A") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass A 10.53.0.3");
+ push @ans, $rr;
+ }
+ if ($qtype eq "AAAA") {
+ my $rr = new Net::DNS::RR("example.dname. $ttl $qclass SOA . . 0 0 0 0 $ttl");
+ push @auth, $rr;
+ }
+ $rcode = "NOERROR";
+ } elsif ($qname eq "self.example.self.example.dname") {
+ my $rr = new Net::DNS::RR("self.example.dname. $ttl $qclass DNAME dname.");
+ push @ans, $rr;
+ $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME self.example.dname.");
+ push @ans, $rr;
+ $rcode = "NOERROR";
+ } elsif ($qname eq "self.example.dname") {
+ if ($qtype eq "DNAME") {
+ my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME dname.");
+ push @ans, $rr;
+ }
+ $rcode = "NOERROR";
+ } else {
+ $rcode = "REFUSED";
+ }
+ return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
+}
+
+GetOptions(
+ 'port=i' => \$localport,
+ 'verbose!' => \$verbose,
+);
+
+my $ns = Net::DNS::Nameserver->new(
+ LocalAddr => $localaddr,
+ LocalPort => $localport,
+ ReplyHandler => \&reply_handler,
+ Verbose => $verbose,
+);
+
+$ns->main_loop;
diff --git a/bin/tests/system/chain/ans4/README.anspy b/bin/tests/system/chain/ans4/README.anspy
new file mode 100644
index 0000000..7cb0bf0
--- /dev/null
+++ b/bin/tests/system/chain/ans4/README.anspy
@@ -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.
+
+REQUIREMENTS
+ans.py requires at least dnspython 1.12.0.
+
+"ans.py" is a fairly simple Python script that will respond as an
+authoritative server to DNS queries. It opens a UDP socket on 10.53.0.4
+and fd92:7065:b8e:ffff::8, port 5300 (or PORT) (these are for DNS queries)
+and a TCP socket addresses on 10.53.0.4 at port 5301 (or EXTRAPORT1)
+(this is the control channel).
+
+Please note that all functionality and formatting are subject to change as
+we determine what features the tool will need.
+
+"ans.py" will respond to queries as follows: TBD
diff --git a/bin/tests/system/chain/ans4/ans.py b/bin/tests/system/chain/ans4/ans.py
new file mode 100755
index 0000000..839067f
--- /dev/null
+++ b/bin/tests/system/chain/ans4/ans.py
@@ -0,0 +1,386 @@
+# 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.
+
+############################################################################
+# ans.py: See README.anspy for details.
+############################################################################
+
+from __future__ import print_function
+import os
+import sys
+import signal
+import socket
+import select
+from datetime import datetime, timedelta
+import functools
+
+import dns, dns.message, dns.query
+from dns.rdatatype import *
+from dns.rdataclass import *
+from dns.rcode import *
+from dns.name import *
+
+############################################################################
+# set up the RRs to be returned in the next answer
+#
+# the message contains up to two pipe-separated ('|') fields.
+#
+# the first field of the message is a comma-separated list
+# of actions indicating what to put into the answer set
+# (e.g., a dname, a cname, another cname, etc)
+#
+# supported actions:
+# - cname (cname from the current name to a new one in the same domain)
+# - dname (dname to a new domain, plus a synthesized cname)
+# - xname ("external" cname, to a new name in a new domain)
+#
+# example: xname, dname, cname represents a CNAME to an external
+# domain which is then answered by a DNAME and synthesized
+# CNAME pointing to yet another domain, which is then answered
+# by a CNAME within the same domain, and finally an answer
+# to the query. each RR in the answer set has a corresponding
+# RRSIG. these signatures are not valid, but will exercise the
+# response parser.
+#
+# the second field is a comma-separated list of which RRs in the
+# answer set to include in the answer, in which order. if prepended
+# with 's', the number indicates which signature to include.
+#
+# examples: for the answer set "cname, cname, cname", an rr set
+# '1, s1, 2, s2, 3, s3, 4, s4' indicates that all four RRs should
+# be included in the answer, with siagntures, in the original
+# order, while 4, s4, 3, s3, 2, s2, 1, s1' indicates the order
+# should be reversed, 's3, s3, s3, s3' indicates that the third
+# RRSIG should be repeated four times and everything else should
+# be omitted, and so on.
+#
+# if there is no second field (i.e., no pipe symbol appears in
+# the line) , the default is to send all answers and signatures.
+# if a pipe symbol exists but the second field is empty, then
+# nothing is sent at all.
+############################################################################
+actions = []
+rrs = []
+
+
+def ctl_channel(msg):
+ global actions, rrs
+
+ msg = msg.splitlines().pop(0)
+ print("received control message: %s" % msg)
+
+ msg = msg.split(b"|")
+ if len(msg) == 0:
+ return
+
+ actions = [x.strip() for x in msg[0].split(b",")]
+ n = functools.reduce(
+ lambda n, act: (n + (2 if act == b"dname" else 1)), [0] + actions
+ )
+
+ if len(msg) == 1:
+ rrs = []
+ for i in range(n):
+ for b in [False, True]:
+ rrs.append((i, b))
+ return
+
+ rlist = [x.strip() for x in msg[1].split(b",")]
+ rrs = []
+ for item in rlist:
+ if item[0] == b"s"[0]:
+ i = int(item[1:].strip()) - 1
+ if i > n:
+ print("invalid index %d" + (i + 1))
+ continue
+ rrs.append((int(item[1:]) - 1, True))
+ else:
+ i = int(item) - 1
+ if i > n:
+ print("invalid index %d" % (i + 1))
+ continue
+ rrs.append((i, False))
+
+
+############################################################################
+# Respond to a DNS query.
+############################################################################
+def create_response(msg):
+ m = dns.message.from_wire(msg)
+ qname = m.question[0].name.to_text()
+ labels = qname.lower().split(".")
+ wantsigs = True if m.ednsflags & dns.flags.DO else False
+
+ # get qtype
+ rrtype = m.question[0].rdtype
+ typename = dns.rdatatype.to_text(rrtype)
+
+ # for 'www.example.com.'...
+ # - name is 'www'
+ # - domain is 'example.com.'
+ # - sld is 'example'
+ # - tld is 'com.'
+ name = labels.pop(0)
+ domain = ".".join(labels)
+ sld = labels.pop(0)
+ tld = ".".join(labels)
+
+ print("query: " + qname + "/" + typename)
+ print("domain: " + domain)
+
+ # default answers, depending on QTYPE.
+ # currently only A, AAAA, TXT and NS are supported.
+ ttl = 86400
+ additionalA = "10.53.0.4"
+ additionalAAAA = "fd92:7065:b8e:ffff::4"
+ if typename == "A":
+ final = "10.53.0.4"
+ elif typename == "AAAA":
+ final = "fd92:7065:b8e:ffff::4"
+ elif typename == "TXT":
+ final = "Some\ text\ here"
+ elif typename == "NS":
+ domain = qname
+ final = "ns1.%s" % domain
+ else:
+ final = None
+
+ # RRSIG rdata - won't validate but will exercise response parsing
+ t = datetime.now()
+ delta = timedelta(30)
+ t1 = t - delta
+ t2 = t + delta
+ inception = t1.strftime("%Y%m%d000000")
+ expiry = t2.strftime("%Y%m%d000000")
+ sigdata = "OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko Tso="
+
+ # construct answer set.
+ answers = []
+ sigs = []
+ curdom = domain
+ curname = name
+ i = 0
+
+ for action in actions:
+ if name != "test":
+ continue
+ if action == b"xname":
+ owner = curname + "." + curdom
+ newname = "cname%d" % i
+ i += 1
+ newdom = "domain%d.%s" % (i, tld)
+ i += 1
+ target = newname + "." + newdom
+ print("add external CNAME %s to %s" % (owner, target))
+ answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
+ rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
+ ttl,
+ expiry,
+ inception,
+ domain,
+ sigdata,
+ )
+ print("add external RRISG(CNAME) %s to %s" % (owner, target))
+ sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
+ curname = newname
+ curdom = newdom
+ continue
+
+ if action == b"cname":
+ owner = curname + "." + curdom
+ newname = "cname%d" % i
+ target = newname + "." + curdom
+ i += 1
+ print("add CNAME %s to %s" % (owner, target))
+ answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
+ rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
+ ttl,
+ expiry,
+ inception,
+ domain,
+ sigdata,
+ )
+ print("add RRSIG(CNAME) %s to %s" % (owner, target))
+ sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
+ curname = newname
+ continue
+
+ if action == b"dname":
+ owner = curdom
+ newdom = "domain%d.%s" % (i, tld)
+ i += 1
+ print("add DNAME %s to %s" % (owner, newdom))
+ answers.append(dns.rrset.from_text(owner, ttl, IN, DNAME, newdom))
+ rrsig = "DNAME 5 3 %d %s %s 12345 %s %s" % (
+ ttl,
+ expiry,
+ inception,
+ domain,
+ sigdata,
+ )
+ print("add RRSIG(DNAME) %s to %s" % (owner, newdom))
+ sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
+ owner = curname + "." + curdom
+ target = curname + "." + newdom
+ print("add synthesized CNAME %s to %s" % (owner, target))
+ answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
+ rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
+ ttl,
+ expiry,
+ inception,
+ domain,
+ sigdata,
+ )
+ print("add synthesized RRSIG(CNAME) %s to %s" % (owner, target))
+ sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
+ curdom = newdom
+ continue
+
+ # now add the final answer
+ owner = curname + "." + curdom
+ answers.append(dns.rrset.from_text(owner, ttl, IN, rrtype, final))
+ rrsig = "%s 5 3 %d %s %s 12345 %s %s" % (
+ typename,
+ ttl,
+ expiry,
+ inception,
+ domain,
+ sigdata,
+ )
+ sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
+
+ # prepare the response and convert to wire format
+ r = dns.message.make_response(m)
+
+ if name != "test":
+ r.answer.append(answers[-1])
+ if wantsigs:
+ r.answer.append(sigs[-1])
+ else:
+ for i, sig in rrs:
+ if sig and not wantsigs:
+ continue
+ elif sig:
+ r.answer.append(sigs[i])
+ else:
+ r.answer.append(answers[i])
+
+ if typename != "NS":
+ r.authority.append(
+ dns.rrset.from_text(domain, ttl, IN, "NS", ("ns1.%s" % domain))
+ )
+ r.additional.append(
+ dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, A, additionalA)
+ )
+ r.additional.append(
+ dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, AAAA, additionalAAAA)
+ )
+
+ r.flags |= dns.flags.AA
+ r.use_edns()
+ return r.to_wire()
+
+
+def sigterm(signum, frame):
+ print("Shutting down now...")
+ os.remove("ans.pid")
+ running = False
+ sys.exit(0)
+
+
+############################################################################
+# Main
+#
+# Set up responder and control channel, open the pid file, and start
+# the main loop, listening for queries on the query channel or commands
+# on the control channel and acting on them.
+############################################################################
+ip4 = "10.53.0.4"
+ip6 = "fd92:7065:b8e:ffff::4"
+
+try:
+ port = int(os.environ["PORT"])
+except:
+ port = 5300
+
+try:
+ ctrlport = int(os.environ["EXTRAPORT1"])
+except:
+ ctrlport = 5300
+
+query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+query4_socket.bind((ip4, port))
+
+havev6 = True
+try:
+ query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
+ try:
+ query6_socket.bind((ip6, port))
+ except:
+ query6_socket.close()
+ havev6 = False
+except:
+ havev6 = False
+
+ctrl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ctrl_socket.bind((ip4, ctrlport))
+ctrl_socket.listen(5)
+
+signal.signal(signal.SIGTERM, sigterm)
+
+f = open("ans.pid", "w")
+pid = os.getpid()
+print(pid, file=f)
+f.close()
+
+running = True
+
+print("Listening on %s port %d" % (ip4, port))
+if havev6:
+ print("Listening on %s port %d" % (ip6, port))
+print("Control channel on %s port %d" % (ip4, ctrlport))
+print("Ctrl-c to quit")
+
+if havev6:
+ input = [query4_socket, query6_socket, ctrl_socket]
+else:
+ input = [query4_socket, ctrl_socket]
+
+while running:
+ try:
+ inputready, outputready, exceptready = select.select(input, [], [])
+ except select.error as e:
+ break
+ except socket.error as e:
+ break
+ except KeyboardInterrupt:
+ break
+
+ for s in inputready:
+ if s == ctrl_socket:
+ # Handle control channel input
+ conn, addr = s.accept()
+ print("Control channel connected")
+ while True:
+ msg = conn.recv(65535)
+ if not msg:
+ break
+ ctl_channel(msg)
+ conn.close()
+ if s == query4_socket or s == query6_socket:
+ print("Query received on %s" % (ip4 if s == query4_socket else ip6))
+ # Handle incoming queries
+ msg = s.recvfrom(65535)
+ rsp = create_response(msg[0])
+ if rsp:
+ s.sendto(rsp, msg[1])
+ if not running:
+ break
diff --git a/bin/tests/system/chain/clean.sh b/bin/tests/system/chain/clean.sh
new file mode 100755
index 0000000..57b05a7
--- /dev/null
+++ b/bin/tests/system/chain/clean.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# 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.
+
+rm -f dig.out.* named*.pid
+rm -f ns*/named.conf
+rm -f */named.memstats */named.recursing */named.lock */named.run */ans.run
+rm -f ns2/K* ns2/dsset-* ns2/*.db.signed
+rm -f ns*/managed-keys.bind*
diff --git a/bin/tests/system/chain/ns1/named.conf.in b/bin/tests/system/chain/ns1/named.conf.in
new file mode 100644
index 0000000..5504261
--- /dev/null
+++ b/bin/tests/system/chain/ns1/named.conf.in
@@ -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.
+ */
+
+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;
+ dnssec-validation yes;
+ notify yes;
+};
+
+zone "." { type primary; file "root.db"; };
diff --git a/bin/tests/system/chain/ns1/root.db b/bin/tests/system/chain/ns1/root.db
new file mode 100644
index 0000000..3469fb5
--- /dev/null
+++ b/bin/tests/system/chain/ns1/root.db
@@ -0,0 +1,51 @@
+; 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 root.domain.nil a.root.servers.nil. (
+ 2016012800 ; 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
+
+example.broken. NS ns3.example.broken.
+ns3.example.broken. A 10.53.0.3
+
+; for the resolver variant of the CVE-2021-25215 regression test
+example.dname. NS ns3.example.dname.
+ns3.example.dname. A 10.53.0.3
+
+domain0.nil. NS ns2.domain0.nil
+domain1.nil. NS ns2.domain0.nil
+domain2.nil. NS ns2.domain0.nil
+domain3.nil. NS ns2.domain0.nil
+domain4.nil. NS ns2.domain0.nil
+domain5.nil. NS ns2.domain0.nil
+domain6.nil. NS ns2.domain0.nil
+domain7.nil. NS ns2.domain0.nil
+domain8.nil. NS ns2.domain0.nil
+domain9.nil. NS ns2.domain0.nil
+ns2.domain0.nil. A 10.53.0.2
+ns2.domain0.nil. AAAA fd92:7065:b8e:ffff::2
+
+domain.nil. NS ns4.domain.nil
+ns4.domain.nil. A 10.53.0.4
+ns4.domain.nil. AAAA fd92:7065:b8e:ffff::4
+
+domain. NS ns4.domain.
+ns4.domain. A 10.53.0.4
diff --git a/bin/tests/system/chain/ns2/example.db b/bin/tests/system/chain/ns2/example.db
new file mode 100644
index 0000000..c13f2d2
--- /dev/null
+++ b/bin/tests/system/chain/ns2/example.db
@@ -0,0 +1,69 @@
+; 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
+ns2 A 10.53.0.2
+
+a.short A 10.0.0.1
+short-dname DNAME short
+a.longlonglonglonglonglonglonglonglonglonglonglonglong A 10.0.0.2
+long-dname DNAME longlonglonglonglonglonglonglonglonglonglonglonglong
+toolong-dname DNAME longlonglonglonglonglonglonglonglonglonglonglonglong
+cname CNAME a.cnamedname
+cnamedname DNAME target
+a.target A 10.0.0.3
+
+; CNAME to delegation
+; (unsigned delegations, external and internal)
+sub5 NS ns5.sub5
+ns5.sub5 A 10.53.0.5
+a CNAME a.sub5
+sub2 NS ns2.sub2
+ns2.sub2 A 10.53.0.2
+b CNAME b.sub2
+
+; (signed delegation, external and internal)
+; note: these DS records are fake and will not validate; we're only
+; testing that the resolver handles their presence in a reply correctly
+signed-sub5 NS ns5.sub5
+signed-sub5 DS 44137 8 2 1CB4F54E0B4F4F85109143113A3C679716A2377D86EB0907846A03FB 0C0A3927
+c CNAME c.signed-sub5
+signed-sub2 NS ns2.sub2
+signed-sub2 DS 44137 8 2 1CB4F54E0B4F4F85109143113A3C679716A2377D86EB0907846A03FB 0C0A3927
+d CNAME d.signed-sub2
+
+; long CNAME loop
+loop CNAME goop
+goop CNAME boop
+boop CNAME soup
+soup CNAME gump
+gump CNAME bump
+bump CNAME lump
+lump CNAME rump
+rump CNAME romp
+romp CNAME bomp
+bomp CNAME stomp
+stomp CNAME clomp
+clomp CNAME clump
+clump CNAME hunk
+hunk CNAME hank
+hank CNAME bank
+bank CNAME wank
+wank CNAME woop
+woop CNAME loop
diff --git a/bin/tests/system/chain/ns2/generic.db b/bin/tests/system/chain/ns2/generic.db
new file mode 100644
index 0000000..9d59378
--- /dev/null
+++ b/bin/tests/system/chain/ns2/generic.db
@@ -0,0 +1,22 @@
+; 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.
+
+@ 86400 SOA ns2.domain0.nil. hostmaster.ns2.nil. 0 1 1 1 1
+@ 86400 NS ns2.domain0.nil.
+ns2 86400 A 10.53.0.2
+ns2 86400 AAAA fd92:7065:b8e:ffff::2
+
+@ 86400 A 1.2.3.4
+@ 86400 AAAA 1:2:3::4
+* 86400 A 1.2.3.4
+* 86400 AAAA 1:2:3::4
+; CVE-2021-25215 regression test data
+self 86400 DNAME nil.
diff --git a/bin/tests/system/chain/ns2/named.conf.in b/bin/tests/system/chain/ns2/named.conf.in
new file mode 100644
index 0000000..922d2fa
--- /dev/null
+++ b/bin/tests/system/chain/ns2/named.conf.in
@@ -0,0 +1,74 @@
+/*
+ * 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; };
+ recursion no;
+ dnssec-validation no;
+ notify yes;
+};
+
+zone "example" {
+ type primary;
+ file "example.db.signed";
+ allow-update { any; };
+};
+
+zone "sub2.example" {
+ type primary;
+ file "sub.db";
+};
+
+zone "signed-sub2.example" {
+ type primary;
+ file "sub.db";
+};
+
+zone "wildcard-secure.example" {
+ type primary;
+ file "wildcard-secure.example.db.signed";
+};
+
+zone "wildcard-nsec.example" {
+ type primary;
+ file "wildcard-nsec.example.db.signed";
+};
+
+zone "wildcard-nsec3.example" {
+ type primary;
+ file "wildcard-nsec3.example.db.signed";
+};
+
+zone "wildcard-nsec3-optout.example" {
+ type primary;
+ file "wildcard-nsec3-optout.example.db.signed";
+};
+
+zone "domain0.nil" { type primary; file "generic.db"; };
+zone "domain1.nil" { type primary; file "generic.db"; };
+zone "domain2.nil" { type primary; file "generic.db"; };
+zone "domain3.nil" { type primary; file "generic.db"; };
+zone "domain4.nil" { type primary; file "generic.db"; };
+zone "domain5.nil" { type primary; file "generic.db"; };
+zone "domain6.nil" { type primary; file "generic.db"; };
+zone "domain7.nil" { type primary; file "generic.db"; };
+zone "domain8.nil" { type primary; file "generic.db"; };
+zone "domain9.nil" { type primary; file "generic.db"; };
diff --git a/bin/tests/system/chain/ns2/sign.sh b/bin/tests/system/chain/ns2/sign.sh
new file mode 100644
index 0000000..90d1912
--- /dev/null
+++ b/bin/tests/system/chain/ns2/sign.sh
@@ -0,0 +1,54 @@
+#!/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.
+
+. ../../conf.sh
+
+zone=example.
+zonefile=example.db
+signedfile=example.db.signed
+
+ksk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} -fk $zone)
+zsk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} $zone)
+$SIGNER -S -o $zone -f $signedfile $zonefile > /dev/null
+
+zone=wildcard-secure.example.
+zonefile=wildcard-secure.db
+signedfile=wildcard-secure.example.db.signed
+
+ksk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} -fk $zone)
+zsk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} $zone)
+$SIGNER -S -o $zone -f $signedfile $zonefile > /dev/null
+
+zone=wildcard-nsec.example.
+zonefile=wildcard.db
+signedfile=wildcard-nsec.example.db.signed
+
+ksk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} -fk $zone)
+zsk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} $zone)
+$SIGNER -S -o $zone -f $signedfile $zonefile > /dev/null
+
+zone=wildcard-nsec3.example.
+zonefile=wildcard.db
+signedfile=wildcard-nsec3.example.db.signed
+
+ksk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} -fk $zone)
+zsk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} $zone)
+$SIGNER -S -3 - -H 0 -o $zone -f $signedfile $zonefile > /dev/null
+
+zone=wildcard-nsec3-optout.example.
+zonefile=wildcard.db
+signedfile=wildcard-nsec3-optout.example.db.signed
+
+ksk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} -fk $zone)
+zsk=$($KEYGEN -q -a ${DEFAULT_ALGORITHM} -b ${DEFAULT_BITS} $zone)
+$SIGNER -S -3 - -H 0 -A -o $zone -f $signedfile $zonefile > /dev/null
diff --git a/bin/tests/system/chain/ns2/sub.db b/bin/tests/system/chain/ns2/sub.db
new file mode 100644
index 0000000..ad03165
--- /dev/null
+++ b/bin/tests/system/chain/ns2/sub.db
@@ -0,0 +1,26 @@
+; 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. . (
+ 2017031001 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+ NS ns2
+ns2 A 10.53.0.2
+
+a A 10.0.0.1
+b A 10.0.0.2
+c A 10.0.0.3
+d A 10.0.0.4
diff --git a/bin/tests/system/chain/ns2/wildcard-secure.db b/bin/tests/system/chain/ns2/wildcard-secure.db
new file mode 100644
index 0000000..e39237a
--- /dev/null
+++ b/bin/tests/system/chain/ns2/wildcard-secure.db
@@ -0,0 +1,29 @@
+; 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. . (
+ 2021051901 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+ NS localhost.
+
+delegation NS localhost.
+ DS 12345 13 2 0000000000000000000000000000000000000000000000000000000000000000
+
+; CNAME pointing into a child zone
+cname CNAME delegation
+
+; wildcard CNAME pointing at a CNAME pointing into a child zone
+* CNAME cname
diff --git a/bin/tests/system/chain/ns2/wildcard.db b/bin/tests/system/chain/ns2/wildcard.db
new file mode 100644
index 0000000..cc39e9c
--- /dev/null
+++ b/bin/tests/system/chain/ns2/wildcard.db
@@ -0,0 +1,28 @@
+; 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. . (
+ 2021051901 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+ NS localhost.
+
+delegation NS localhost.
+
+; CNAME pointing into a child zone
+cname CNAME delegation
+
+; wildcard CNAME pointing at a CNAME pointing into a child zone
+* CNAME cname
diff --git a/bin/tests/system/chain/ns5/named.conf.in b/bin/tests/system/chain/ns5/named.conf.in
new file mode 100644
index 0000000..86bbf26
--- /dev/null
+++ b/bin/tests/system/chain/ns5/named.conf.in
@@ -0,0 +1,42 @@
+/*
+ * 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.5;
+ notify-source 10.53.0.5;
+ transfer-source 10.53.0.5;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.5; };
+ listen-on-v6 { none; };
+ recursion no;
+ dnssec-validation no;
+ notify yes;
+};
+
+zone "." {
+ type hint;
+ file "../../common/root.hint";
+};
+
+zone "sub5.example" {
+ type primary;
+ file "sub.db";
+};
+
+zone "signed-sub5.example" {
+ type primary;
+ file "sub.db";
+};
diff --git a/bin/tests/system/chain/ns5/sub.db b/bin/tests/system/chain/ns5/sub.db
new file mode 100644
index 0000000..df571fb
--- /dev/null
+++ b/bin/tests/system/chain/ns5/sub.db
@@ -0,0 +1,26 @@
+; 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. . (
+ 2017031001 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+ NS ns5
+ns5 A 10.53.0.5
+
+a A 10.0.0.1
+b A 10.0.0.2
+c A 10.0.0.3
+d A 10.0.0.4
diff --git a/bin/tests/system/chain/ns7/named.conf.in b/bin/tests/system/chain/ns7/named.conf.in
new file mode 100644
index 0000000..32c9b5f
--- /dev/null
+++ b/bin/tests/system/chain/ns7/named.conf.in
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+ directory ".";
+ query-source address 10.53.0.7;
+ notify-source 10.53.0.7;
+ transfer-source 10.53.0.7;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.7; };
+ listen-on-v6 { fd92:7065:b8e:ffff::7; };
+ recursion yes;
+ allow-recursion { any; };
+ dnssec-validation yes;
+ deny-answer-aliases {
+ "example";
+ } except-from {
+ "example";
+ };
+};
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+ inet 10.53.0.7 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "." {
+ type hint;
+ file "root.hint";
+};
diff --git a/bin/tests/system/chain/ns7/root.hint b/bin/tests/system/chain/ns7/root.hint
new file mode 100644
index 0000000..4f3f48b
--- /dev/null
+++ b/bin/tests/system/chain/ns7/root.hint
@@ -0,0 +1,14 @@
+; 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 999999
+. IN NS a.root-servers.nil.
+a.root-servers.nil. IN A 10.53.0.1
diff --git a/bin/tests/system/chain/setup.sh b/bin/tests/system/chain/setup.sh
new file mode 100644
index 0000000..6f52e65
--- /dev/null
+++ b/bin/tests/system/chain/setup.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+# 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.
+
+. ../conf.sh
+
+copy_setports ns1/named.conf.in ns1/named.conf
+copy_setports ns2/named.conf.in ns2/named.conf
+copy_setports ns5/named.conf.in ns5/named.conf
+copy_setports ns7/named.conf.in ns7/named.conf
+
+cd ns2
+$SHELL sign.sh
diff --git a/bin/tests/system/chain/tests.sh b/bin/tests/system/chain/tests.sh
new file mode 100644
index 0000000..3ad8e31
--- /dev/null
+++ b/bin/tests/system/chain/tests.sh
@@ -0,0 +1,630 @@
+#!/bin/sh
+
+# 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.
+
+set -e
+
+. ../conf.sh
+
+DIGOPTS="-p ${PORT}"
+RNDCCMD="$RNDC -c ../common/rndc.conf -p ${CONTROLPORT} -s"
+
+sendcmd() {
+ send 10.53.0.4 "${EXTRAPORT1}"
+}
+
+status=0
+n=0
+
+n=$((n + 1))
+echo_i "checking short DNAME from authoritative ($n)"
+ret=0
+$DIG $DIGOPTS a.short-dname.example @10.53.0.2 a > dig.out.ns2.short || ret=1
+grep "status: NOERROR" dig.out.ns2.short > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking short DNAME from recursive ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS a.short-dname.example @10.53.0.7 a > dig.out.ns4.short || ret=1
+grep "status: NOERROR" dig.out.ns4.short > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking long DNAME from authoritative ($n)"
+ret=0
+$DIG $DIGOPTS a.long-dname.example @10.53.0.2 a > dig.out.ns2.long || ret=1
+grep "status: NOERROR" dig.out.ns2.long > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking long DNAME from recursive ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS a.long-dname.example @10.53.0.7 a > dig.out.ns4.long || ret=1
+grep "status: NOERROR" dig.out.ns4.long > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking (too) long DNAME from authoritative ($n)"
+ret=0
+$DIG $DIGOPTS 01234567890123456789012345678901234567890123456789.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.long-dname.example @10.53.0.2 a > dig.out.ns2.toolong || ret=1
+grep "status: YXDOMAIN" dig.out.ns2.toolong > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking (too) long DNAME from recursive with cached DNAME ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS 01234567890123456789012345678901234567890123456789.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.long-dname.example @10.53.0.7 a > dig.out.ns4.cachedtoolong || ret=1
+grep "status: YXDOMAIN" dig.out.ns4.cachedtoolong > /dev/null || ret=1
+grep '^long-dname\.example\..*DNAME.*long' dig.out.ns4.cachedtoolong > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking (too) long DNAME from recursive without cached DNAME ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS 01234567890123456789012345678901234567890123456789.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglong.toolong-dname.example @10.53.0.7 a > dig.out.ns4.uncachedtoolong || ret=1
+grep "status: YXDOMAIN" dig.out.ns4.uncachedtoolong > /dev/null || ret=1
+grep '^toolong-dname\.example\..*DNAME.*long' dig.out.ns4.uncachedtoolong > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+find_records() {
+ owner_name="$1"
+ rr_type="$2"
+ file="$3"
+ awk '$1 == "'"$owner_name"'" && $4 == "'"$rr_type"'" { print }' < "$file"
+}
+
+count_records() {
+ owner_name="$1"
+ rr_type="$2"
+ file="$3"
+ find_records "$owner_name" "$rr_type" "$file" | wc -l
+}
+
+exactly_one_record_exists_for() {
+ owner_name="$1"
+ rr_type="$2"
+ file="$3"
+ test "$(count_records "$owner_name" "$rr_type" "$file")" -eq 1
+}
+
+no_records_exist_for() {
+ owner_name="$1"
+ rr_type="$2"
+ file="$3"
+ test "$(count_records "$owner_name" "$rr_type" "$file")" -eq 0
+}
+
+ensure_no_ds_in_bitmap() {
+ owner_name="$1"
+ rr_type="$2"
+ file="$3"
+ case "$rr_type" in
+ NSEC) start_index=6 ;;
+ NSEC3) start_index=10 ;;
+ *) exit 1 ;;
+ esac
+ find_records "$owner_name" "$rr_type" "$file" | awk '{ for (i='"$start_index"'; i<=NF; i++) if ($i == "DS") exit 1 }'
+}
+
+n=$((n + 1))
+echo_i "checking secure delegation prepared using CNAME chaining ($n)"
+ret=0
+# QNAME exists, so the AUTHORITY section should only contain an NS RRset and a
+# DS RRset.
+$DIG $DIGOPTS @10.53.0.2 cname.wildcard-secure.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains the expected NS and DS RRsets.
+exactly_one_record_exists_for "delegation.wildcard-secure.example." NS dig.out.2.$n || ret=1
+exactly_one_record_exists_for "delegation.wildcard-secure.example." DS dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking secure delegation prepared using wildcard expansion + CNAME chaining ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset, an
+# NSEC record proving nonexistence of QNAME, and a DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 a-nonexistent-name.wildcard-secure.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains the expected NS and DS RRsets.
+exactly_one_record_exists_for "delegation.wildcard-secure.example." NS dig.out.2.$n || ret=1
+exactly_one_record_exists_for "delegation.wildcard-secure.example." DS dig.out.2.$n || ret=1
+# Check NSEC records in the AUTHORITY section.
+no_records_exist_for "wildcard-secure.example." NSEC dig.out.2.$n || ret=1
+exactly_one_record_exists_for "*.wildcard-secure.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "cname.wildcard-secure.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-secure.example." NSEC dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using CNAME chaining, NSEC ($n)"
+ret=0
+# QNAME exists, so the AUTHORITY section should only contain an NS RRset and a
+# single NSEC record proving nonexistence of a DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 cname.wildcard-nsec.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec.example." DS dig.out.2.$n || ret=1
+# Check NSEC records in the AUTHORITY section.
+no_records_exist_for "wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "*.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "cname.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+exactly_one_record_exists_for "delegation.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+# Ensure the NSEC record for the zone cut does not have the DS bit set in the
+# type bit map.
+ensure_no_ds_in_bitmap "delegation.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using wildcard expansion + CNAME chaining, NSEC, QNAME #1 ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset and
+# NSEC records proving nonexistence of both QNAME and a DS RRset at the zone
+# cut. In this test case, these two NSEC records are different.
+$DIG $DIGOPTS @10.53.0.2 a-nonexistent-name.wildcard-nsec.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec.example." DS dig.out.2.$n || ret=1
+# Check NSEC records in the AUTHORITY section.
+no_records_exist_for "wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+exactly_one_record_exists_for "*.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "cname.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+exactly_one_record_exists_for "delegation.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+# Ensure the NSEC record for the zone cut does not have the DS bit set in the
+# type bit map.
+ensure_no_ds_in_bitmap "delegation.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using wildcard expansion + CNAME chaining, NSEC, QNAME #2 ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset and
+# NSEC records proving nonexistence of both QNAME and a DS RRset at the zone
+# cut. In this test case, the same NSEC record proves nonexistence of both the
+# QNAME and the DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 z-nonexistent-name.wildcard-nsec.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec.example." DS dig.out.2.$n || ret=1
+# Check NSEC records in the AUTHORITY section.
+no_records_exist_for "wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "*.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+no_records_exist_for "cname.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+exactly_one_record_exists_for "delegation.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+# Ensure the NSEC record for the zone cut does not have the DS bit set in the
+# type bit map.
+ensure_no_ds_in_bitmap "delegation.wildcard-nsec.example." NSEC dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+# Relevant NSEC3 hashes:
+#
+# - existing names:
+#
+# $ nsec3hash - 1 0 wildcard-nsec3.example.
+# 38IVP9CN0LBISO6H3V5REQCKMTHLI5AN (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 cname.wildcard-nsec3.example.
+# 3DV6GNNVR0O8LA4DC4CHL2JTVNHT8Q1D (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 delegation.wildcard-nsec3.example.
+# AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 *.wildcard-nsec3.example.
+# Q64D8L8HLSB3L98S59PM8OSSMI7SMQA2 (salt=-, hash=1, iterations=0)
+#
+# - nonexistent names:
+#
+# $ nsec3hash - 1 0 a-nonexistent-name.wildcard-nsec3.example.
+# PST9IH6M0DG3M139CO3G12NUP4ER88SH (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 z-nonexistent-name.wildcard-nsec3.example.
+# SG2DEHEAOGCKP7FTNQAUVC3I3TIPJH0J (salt=-, hash=1, iterations=0)
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using CNAME chaining, NSEC3 ($n)"
+ret=0
+# QNAME exists, so the AUTHORITY section should only contain an NS RRset and a
+# single NSEC3 record proving nonexistence of a DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 cname.wildcard-nsec3.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec3.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec3.example." DS dig.out.2.$n || ret=1
+# Check NSEC3 records in the AUTHORITY section.
+no_records_exist_for "38IVP9CN0LBISO6H3V5REQCKMTHLI5AN.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "3DV6GNNVR0O8LA4DC4CHL2JTVNHT8Q1D.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "Q64D8L8HLSB3L98S59PM8OSSMI7SMQA2.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+# Ensure the NSEC3 record matching the zone cut does not have the DS bit set in
+# the type bit map.
+ensure_no_ds_in_bitmap "AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using wildcard expansion + CNAME chaining, NSEC3, QNAME #1 ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset and
+# NSEC3 records proving nonexistence of both QNAME and a DS RRset at the zone
+# cut. In this test case, these two NSEC3 records are different.
+$DIG $DIGOPTS @10.53.0.2 z-nonexistent-name.wildcard-nsec3.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec3.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec3.example." DS dig.out.2.$n || ret=1
+# Check NSEC3 records in the AUTHORITY section.
+no_records_exist_for "38IVP9CN0LBISO6H3V5REQCKMTHLI5AN.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "3DV6GNNVR0O8LA4DC4CHL2JTVNHT8Q1D.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "Q64D8L8HLSB3L98S59PM8OSSMI7SMQA2.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+# Ensure the NSEC3 record matching the zone cut does not have the DS bit set in
+# the type bit map.
+ensure_no_ds_in_bitmap "AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using wildcard expansion + CNAME chaining, NSEC3, QNAME #2 ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset and
+# NSEC3 records proving nonexistence of both QNAME and a DS RRset at the zone
+# cut. In this test case, the same NSEC3 record proves nonexistence of both the
+# QNAME and the DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 a-nonexistent-name.wildcard-nsec3.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec3.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec3.example." DS dig.out.2.$n || ret=1
+# Check NSEC3 records in the AUTHORITY section.
+no_records_exist_for "38IVP9CN0LBISO6H3V5REQCKMTHLI5AN.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "3DV6GNNVR0O8LA4DC4CHL2JTVNHT8Q1D.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "Q64D8L8HLSB3L98S59PM8OSSMI7SMQA2.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+# Ensure the NSEC3 record matching the zone cut does not have the DS bit set in
+# the type bit map.
+ensure_no_ds_in_bitmap "AVKOGGGVJHFSLQA68TILKFKJ94AV4MNC.wildcard-nsec3.example." NSEC3 dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+# Relevant NSEC3 hashes:
+#
+# - existing names with corresponding NSEC3 records:
+#
+# $ nsec3hash - 1 0 *.wildcard-nsec3-optout.example.
+# 2JGSPT59VJ7R9SQB5B9P6HPM5JBATOOO (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 cname.wildcard-nsec3-optout.example.
+# OKRFKC9SS1O60E8U2980UD62MUSMKGUG (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 wildcard-nsec3-optout.example.
+# SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI (salt=-, hash=1, iterations=0)
+#
+# - existing name with no corresponding NSEC3 record due to opt-out:
+#
+# $ nsec3hash - 1 0 delegation.wildcard-nsec3-optout.example.
+# UFP8PVECFTD57HU5PUD2HE0ES37QEOAP (salt=-, hash=1, iterations=0)
+#
+# - nonexistent names:
+#
+# $ nsec3hash - 1 0 b-nonexistent-name.wildcard-nsec3-optout.example.
+# 3J38JE2OU0O7B4CE2ADMBBKJ5HT994S5 (salt=-, hash=1, iterations=0)
+# $ nsec3hash - 1 0 z-nonexistent-name.wildcard-nsec3-optout.example.
+# V7OTS4791T9SU0HKVL93EVNAJ9JH2CH3 (salt=-, hash=1, iterations=0)
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using CNAME chaining, NSEC3 with opt-out ($n)"
+ret=0
+# QNAME exists, so the AUTHORITY section should only contain an NS RRset and a
+# single NSEC3 record proving nonexistence of a DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 cname.wildcard-nsec3-optout.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec3-optout.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec3-optout.example." DS dig.out.2.$n || ret=1
+# Check NSEC3 records in the AUTHORITY section.
+no_records_exist_for "2JGSPT59VJ7R9SQB5B9P6HPM5JBATOOO.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "OKRFKC9SS1O60E8U2980UD62MUSMKGUG.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+# Ensure the NSEC3 record covering the zone cut does not have the DS bit set in
+# the type bit map.
+ensure_no_ds_in_bitmap "SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using wildcard expansion + CNAME chaining, NSEC3 with opt-out, QNAME #1 ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset and
+# NSEC3 records proving nonexistence of both QNAME and a DS RRset at the zone
+# cut. In this test case, these two NSEC3 records are different.
+$DIG $DIGOPTS @10.53.0.2 b-nonexistent-name.wildcard-nsec3-optout.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec3-optout.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec3-optout.example." DS dig.out.2.$n || ret=1
+# Check NSEC3 records in the AUTHORITY section.
+exactly_one_record_exists_for "2JGSPT59VJ7R9SQB5B9P6HPM5JBATOOO.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "OKRFKC9SS1O60E8U2980UD62MUSMKGUG.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+# Ensure the NSEC3 record covering the zone cut does not have the DS bit set in
+# the type bit map.
+ensure_no_ds_in_bitmap "SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking insecure delegation prepared using wildcard expansion + CNAME chaining, NSEC3 with opt-out, QNAME #2 ($n)"
+ret=0
+# QNAME does not exist, so the AUTHORITY section should contain an NS RRset and
+# NSEC3 records proving nonexistence of both QNAME and a DS RRset at the zone
+# cut. In this test case, the same NSEC3 record proves nonexistence of both the
+# QNAME and the DS RRset at the zone cut.
+$DIG $DIGOPTS @10.53.0.2 z-nonexistent-name.wildcard-nsec3-optout.example A +norec +dnssec > dig.out.2.$n 2>&1 || ret=1
+# Ensure that the AUTHORITY section contains an NS RRset without an associated
+# DS RRset.
+exactly_one_record_exists_for "delegation.wildcard-nsec3-optout.example." NS dig.out.2.$n || ret=1
+no_records_exist_for "delegation.wildcard-nsec3-optout.example." DS dig.out.2.$n || ret=1
+# Check NSEC3 records in the AUTHORITY section.
+no_records_exist_for "2JGSPT59VJ7R9SQB5B9P6HPM5JBATOOO.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+no_records_exist_for "OKRFKC9SS1O60E8U2980UD62MUSMKGUG.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+exactly_one_record_exists_for "SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+# Ensure the NSEC3 record covering the zone cut does not have the DS bit set in
+# the type bit map.
+ensure_no_ds_in_bitmap "SS5M1RUBSGMANEQ1VLRDDEC6SOAT7HNI.wildcard-nsec3-optout.example." NSEC3 dig.out.2.$n || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME to DNAME from authoritative ($n)"
+ret=0
+$DIG $DIGOPTS cname.example @10.53.0.2 a > dig.out.ns2.cname
+grep "status: NOERROR" dig.out.ns2.cname > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME to DNAME from recursive"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS cname.example @10.53.0.7 a > dig.out.ns4.cname
+grep "status: NOERROR" dig.out.ns4.cname > /dev/null || ret=1
+grep '^cname.example.' dig.out.ns4.cname > /dev/null || ret=1
+grep '^cnamedname.example.' dig.out.ns4.cname > /dev/null || ret=1
+grep '^a.cnamedname.example.' dig.out.ns4.cname > /dev/null || ret=1
+grep '^a.target.example.' dig.out.ns4.cname > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking DNAME is returned with synthesized CNAME before DNAME ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 name.synth-then-dname.example.broken A > dig.out.test$n
+grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1
+grep '^name.synth-then-dname\.example\.broken\..*CNAME.*name.$' dig.out.test$n > /dev/null || ret=1
+grep '^synth-then-dname\.example\.broken\..*DNAME.*\.$' dig.out.test$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking DNAME is returned with CNAME to synthesized CNAME before DNAME ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 cname-to-synth2-then-dname.example.broken A > dig.out.test$n
+grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1
+grep '^cname-to-synth2-then-dname\.example\.broken\..*CNAME.*name\.synth2-then-dname\.example\.broken.$' dig.out.test$n > /dev/null || ret=1
+grep '^name\.synth2-then-dname\.example\.broken\..*CNAME.*name.$' dig.out.test$n > /dev/null || ret=1
+grep '^synth2-then-dname\.example\.broken\..*DNAME.*\.$' dig.out.test$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME loops are detected ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 loop.example > dig.out.test$n
+grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1
+grep "ANSWER: 17" dig.out.test$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME to external delegated zones is handled ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 a.example > dig.out.test$n
+grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1
+grep "ANSWER: 2" dig.out.test$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME to internal delegated zones is handled ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 b.example > dig.out.test$n
+grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1
+grep "ANSWER: 2" dig.out.test$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME to signed external delegation is handled ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 c.example > dig.out.$n
+grep "status: NOERROR" dig.out.$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME to signed internal delegation is handled ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 d.example > dig.out.$n
+grep "status: NOERROR" dig.out.$n > /dev/null || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking CNAME chains in various orders ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n - step 1 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "cname,cname,cname|1,2,3,4,s1,s2,s3,s4" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.1.$n 2>&1
+grep 'status: NOERROR' dig.out.1.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.1.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 2 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "cname,cname,cname|1,1,2,2,3,4,s4,s3,s1" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.2.$n 2>&1
+grep 'status: NOERROR' dig.out.2.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.2.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 3 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "cname,cname,cname|2,1,3,4,s3,s1,s2,s4" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.3.$n 2>&1
+grep 'status: NOERROR' dig.out.3.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.3.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 4 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "cname,cname,cname|4,3,2,1,s4,s3,s2,s1" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.4.$n 2>&1
+grep 'status: NOERROR' dig.out.4.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.4.$n > /dev/null 2>&1 || ret=1
+echo "cname,cname,cname|4,3,2,1,s4,s3,s2,s1" | sendcmd
+$RNDCCMD 10.53.0.7 null --- start test$n - step 5 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.5.$n 2>&1
+grep 'status: NOERROR' dig.out.5.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.5.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 6 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "cname,cname,cname|4,3,3,3,s1,s1,1,3,4" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.6.$n 2>&1
+grep 'status: NOERROR' dig.out.6.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.6.$n > /dev/null 2>&1 || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking that only the initial CNAME is cached ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "cname,cname,cname|1,2,3,4,s1,s2,s3,s4" | sendcmd
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.1.$n 2>&1
+sleep 1
+$DIG $DIGOPTS +noall +answer @10.53.0.7 cname1.domain.nil > dig.out.2.$n 2>&1
+ttl=$(awk '{print $2}' dig.out.2.$n)
+[ "$ttl" -eq 86400 ] || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking DNAME chains in various orders ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n - step 1 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "dname,dname|5,4,3,2,1,s5,s4,s3,s2,s1" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.1.$n 2>&1
+grep 'status: NOERROR' dig.out.1.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 3' dig.out.1.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 2 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "dname,dname|5,4,3,2,1,s5,s4,s3,s2,s1" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.2.$n 2>&1
+grep 'status: NOERROR' dig.out.2.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 3' dig.out.2.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 3 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "dname,dname|2,3,s1,s2,s3,s4,1" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.3.$n 2>&1
+grep 'status: NOERROR' dig.out.3.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 3' dig.out.3.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking external CNAME/DNAME chains in various orders ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n - step 1 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "xname,dname|1,2,3,4,s1,s2,s3,s4" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.1.$n 2>&1
+grep 'status: NOERROR' dig.out.1.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.1.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 2 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "xname,dname|s2,2,s1,1,4,s4,3" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.2.$n 2>&1
+grep 'status: NOERROR' dig.out.2.$n > /dev/null 2>&1 || ret=1
+grep 'ANSWER: 2' dig.out.2.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 null --- start test$n - step 3 --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+echo "xname,dname|s2,2,2,2" | sendcmd
+$DIG $DIGOPTS @10.53.0.7 test.domain.nil > dig.out.3.$n 2>&1
+grep 'status: SERVFAIL' dig.out.3.$n > /dev/null 2>&1 || ret=1
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking explicit DNAME query ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 dname short-dname.example > dig.out.7.$n 2>&1
+grep 'status: NOERROR' dig.out.7.$n > /dev/null 2>&1 || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+n=$((n + 1))
+echo_i "checking DNAME via ANY query ($n)"
+ret=0
+$RNDCCMD 10.53.0.7 null --- start test$n --- 2>&1 | sed 's/^/ns7 /' | cat_i
+$RNDCCMD 10.53.0.7 flush 2>&1 | sed 's/^/ns7 /' | cat_i
+$DIG $DIGOPTS @10.53.0.7 any short-dname.example > dig.out.7.$n 2>&1
+grep 'status: NOERROR' dig.out.7.$n > /dev/null 2>&1 || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+# Regression test for CVE-2021-25215 (authoritative server).
+n=$((n + 1))
+echo_i "checking DNAME resolution via itself (authoritative) ($n)"
+ret=0
+$DIG $DIGOPTS @10.53.0.2 DNAME self.domain0.self.domain0.nil. > dig.out.2.$n 2>&1
+grep 'status: NOERROR' dig.out.2.$n > /dev/null 2>&1 || ret=1
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=$((status + ret))
+
+# Regression test for CVE-2021-25215 (recursive resolver).
+n=$((n + 1))
+echo_i "checking DNAME resolution via itself (recursive) ($n)"
+ret=0
+$DIG $DIGOPTS @10.53.0.7 DNAME self.example.self.example.dname. > dig.out.7.$n 2>&1
+grep 'status: NOERROR' dig.out.7.$n > /dev/null 2>&1 || ret=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/chain/tests_sh_chain.py b/bin/tests/system/chain/tests_sh_chain.py
new file mode 100644
index 0000000..ca3c057
--- /dev/null
+++ b/bin/tests/system/chain/tests_sh_chain.py
@@ -0,0 +1,14 @@
+# 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.
+
+
+def test_chain(run_tests_sh):
+ run_tests_sh()