Commit 099d6d7ee7 for bind

commit 099d6d7ee7cc1935ffc39451cdfe49f69e1d24cb
Author: Evan Hunt <each@isc.org>
Date:   Thu Sep 18 20:49:42 2025 -0700

    Authoritative DELEG support

    Support for the DELEG type has been added to the load-time zone
    integrity checks, DNSSEC code that identifies zone cuts and closest
    enclosers, dnssec-signzone, and the qpzone database.

    If a DELEG RRset is present in a zone, then a query with DE=1 for
    data below its owner name will be answered with a DELEG referral.

    If an NS referral is present and DELEG is not, an NS referral is
    returned, along with a proof of DELEG nonexistence if the zone is
    signed.

    If both NS and DELEG are present, NS is used if DE=0, and DELEG if
    DE=1.

    Several new special cases have been added to the handling of queries
    for data *at* a zone cut; the correct response depends on whether the
    delegation record is NS, DELEG, or both, and whether the DE flag is
    set.  qpzone_find() and qpzone_check_zonecut() have been refactored
    and revised to handle all of these cases correctly:

    - For query type KEY, NSEC, or DS, or query type DELEG if DE=1, or
      if the GLUEOK flag is set, we treat the node as if it were not a
      zone cut and search for a normal answer.

    - For query type DELEG if DE=0, if NS is present, we return an NS
      referral.  If NS is not present, we treat the node as i it were
      not a zone cut and search for a normal answer.

    - For any other query type:
      - If DELEG is present and DE=1: return DELEG referral
      - If NS is not present, but DELEG is, and DE=0: return NXRRSET
      - If NS is present, and either DELEG is not present *or* DE=0:
        return NS referral.

diff --git a/bin/dnssec/dnssec-signzone.c b/bin/dnssec/dnssec-signzone.c
index da3a2fb848..98eb28daf7 100644
--- a/bin/dnssec/dnssec-signzone.c
+++ b/bin/dnssec/dnssec-signzone.c
@@ -96,6 +96,14 @@

 typedef struct hashlist hashlist_t;

+/* Delegation types: NS, DELEG, or both */
+typedef enum {
+	DEL_NONE = 0,
+	DEL_NS = 1,
+	DEL_DELEG = 2,
+	DEL_BOTH = 3,
+} deltype_t;
+
 static int nsec_datatype = dns_rdatatype_nsec;

 #define check_dns_dbiterator_current(result)                               \
@@ -1183,25 +1191,33 @@ secure(dns_name_t *name, dns_dbnode_t *node) {
 	return result == ISC_R_SUCCESS;
 }

-static bool
-is_delegation(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *origin,
-	      dns_name_t *name, dns_dbnode_t *node, uint32_t *ttlp) {
-	dns_rdataset_t nsset;
-	isc_result_t result;
+static deltype_t
+delegation_type(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *origin,
+		dns_name_t *name, dns_dbnode_t *node, uint32_t *ttlp) {
+	dns_rdataset_t rdataset = DNS_RDATASET_INIT;
+	deltype_t deltype = DEL_NONE;

 	if (dns_name_equal(name, origin)) {
-		return false;
+		return DEL_NONE;
 	}

-	dns_rdataset_init(&nsset);
-	result = dns_db_findrdataset(db, node, ver, dns_rdatatype_ns, 0, 0,
-				     &nsset, NULL);
-	if (dns_rdataset_isassociated(&nsset)) {
-		SET_IF_NOT_NULL(ttlp, nsset.ttl);
-		dns_rdataset_disassociate(&nsset);
+	(void)dns_db_findrdataset(db, node, ver, dns_rdatatype_ns, 0, 0,
+				  &rdataset, NULL);
+	if (dns_rdataset_isassociated(&rdataset)) {
+		deltype |= DEL_NS;
+		SET_IF_NOT_NULL(ttlp, rdataset.ttl);
+		dns_rdataset_disassociate(&rdataset);
 	}

-	return result == ISC_R_SUCCESS;
+	(void)dns_db_findrdataset(db, node, ver, dns_rdatatype_deleg, 0, 0,
+				  &rdataset, NULL);
+	if (dns_rdataset_isassociated(&rdataset)) {
+		deltype |= DEL_DELEG;
+		SET_IF_NOT_NULL(ttlp, rdataset.ttl);
+		dns_rdataset_disassociate(&rdataset);
+	}
+
+	return deltype;
 }

 /*%
@@ -1229,9 +1245,9 @@ signname(dns_dbnode_t *node, bool apex, dns_name_t *name) {
 	isc_result_t result;
 	dns_rdataset_t rdataset;
 	dns_rdatasetiter_t *rdsiter;
-	bool isdelegation = false;
 	dns_diff_t del, add;
 	char namestr[DNS_NAME_FORMATSIZE];
+	deltype_t deltype;

 	dns_rdataset_init(&rdataset);
 	dns_name_format(name, namestr, sizeof(namestr));
@@ -1239,9 +1255,7 @@ signname(dns_dbnode_t *node, bool apex, dns_name_t *name) {
 	/*
 	 * Determine if this is a delegation point.
 	 */
-	if (is_delegation(gdb, gversion, gorigin, name, node, NULL)) {
-		isdelegation = true;
-	}
+	deltype = delegation_type(gdb, gversion, gorigin, name, node, NULL);

 	/*
 	 * Now iterate through the rdatasets.
@@ -1262,11 +1276,11 @@ signname(dns_dbnode_t *node, bool apex, dns_name_t *name) {
 		/*
 		 * If this name is a delegation point, skip all records
 		 * except NSEC and DS sets.  Otherwise check that there
-		 * isn't a DS record.
+		 * isn't a DS or DELEG record.
 		 */
-		if (isdelegation) {
+		if (deltype != DEL_NONE) {
 			if (rdataset.type != nsec_datatype &&
-			    rdataset.type != dns_rdatatype_ds)
+			    !dns_rdatatype_atparent(rdataset.type))
 			{
 				goto skip;
 			}
@@ -1641,11 +1655,13 @@ assignwork(void *arg) {
 			    (zonecut == NULL ||
 			     !dns_name_issubdomain(name, zonecut)))
 			{
-				if (is_delegation(gdb, gversion, gorigin, name,
-						  node, NULL))
-				{
+				deltype_t deltype =
+					delegation_type(gdb, gversion, gorigin,
+							name, node, NULL);
+				if (deltype != DEL_NONE) {
 					zonecut = savezonecut(&fzonecut, name);
 					if (!OPTOUT(nsec3flags) ||
+					    (deltype & DEL_DELEG) != 0 ||
 					    secure(name, node))
 					{
 						found = true;
@@ -1867,6 +1883,8 @@ nsecify(void) {
 	check_result(result, "dns_dbiterator_first()");

 	while (!done) {
+		deltype_t deltype;
+
 		result = dns_dbiterator_current(dbiter, &node, name);
 		check_dns_dbiterator_current(result);
 		/*
@@ -1889,7 +1907,9 @@ nsecify(void) {
 			(void)active_node(node);
 		}

-		if (is_delegation(gdb, gversion, gorigin, name, node, &nsttl)) {
+		deltype = delegation_type(gdb, gversion, gorigin, name, node,
+					  &nsttl);
+		if (deltype != DEL_NONE) {
 			zonecut = savezonecut(&fzonecut, name);
 			remove_sigs(node, true, 0);
 			if (generateds) {
@@ -2316,6 +2336,7 @@ nsec3ify(unsigned int hashalg, dns_iterations_t iterations,
 		result = dns_dbiterator_next(dbiter);
 		nextnode = NULL;
 		while (result == ISC_R_SUCCESS) {
+			deltype_t deltype;
 			result = dns_dbiterator_current(dbiter, &nextnode,
 							nextname);
 			check_dns_dbiterator_current(result);
@@ -2334,15 +2355,16 @@ nsec3ify(unsigned int hashalg, dns_iterations_t iterations,
 				result = dns_dbiterator_next(dbiter);
 				continue;
 			}
-			if (is_delegation(gdb, gversion, gorigin, nextname,
-					  nextnode, &nsttl))
-			{
+			deltype = delegation_type(gdb, gversion, gorigin,
+						  nextname, nextnode, &nsttl);
+			if (deltype != DEL_NONE) {
 				zonecut = savezonecut(&fzonecut, nextname);
 				remove_sigs(nextnode, true, 0);
 				if (generateds) {
 					add_ds(nextname, nextnode, nsttl);
 				}
 				if (OPTOUT(nsec3flags) &&
+				    (deltype & DEL_DELEG) == 0 &&
 				    !secure(nextname, nextnode))
 				{
 					dns_db_detachnode(&nextnode);
@@ -2458,6 +2480,7 @@ nsec3ify(unsigned int hashalg, dns_iterations_t iterations,
 		result = dns_dbiterator_next(dbiter);
 		nextnode = NULL;
 		while (result == ISC_R_SUCCESS) {
+			deltype_t deltype;
 			result = dns_dbiterator_current(dbiter, &nextnode,
 							nextname);
 			check_dns_dbiterator_current(result);
@@ -2475,11 +2498,12 @@ nsec3ify(unsigned int hashalg, dns_iterations_t iterations,
 				result = dns_dbiterator_next(dbiter);
 				continue;
 			}
-			if (is_delegation(gdb, gversion, gorigin, nextname,
-					  nextnode, NULL))
-			{
+			deltype = delegation_type(gdb, gversion, gorigin,
+						  nextname, nextnode, NULL);
+			if (deltype != DEL_NONE) {
 				zonecut = savezonecut(&fzonecut, nextname);
 				if (OPTOUT(nsec3flags) &&
+				    (deltype & DEL_DELEG) == 0 &&
 				    !secure(nextname, nextnode))
 				{
 					dns_db_detachnode(&nextnode);
diff --git a/bin/tests/system/dnssectools/signer/general/test14.zone b/bin/tests/system/dnssectools/signer/general/test14.zone
new file mode 100644
index 0000000000..4ec0cbfdb3
--- /dev/null
+++ b/bin/tests/system/dnssectools/signer/general/test14.zone
@@ -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.
+
+;	This is a zone which has two DNSKEY records, both of which have
+; existing private key files available.  They should be loaded automatically
+; and the zone correctly signed.
+;
+$TTL 3600
+example.com.	IN	SOA ns hostmaster 00090000 1200 3600 604800 300
+$include Kexample.com.+010+18240.key
+$include Kexample.com.+010+28633.key
+
+deleg-only	IN	DELEG	server-ipv4=1.2.3.4
+
+ns-only		IN	NS	ns.ns-only
+ns.ns-only	IN	A	1.2.3.4
+
+mixed		IN	NS	ns.ns-only
+mixed		IN	DELEG	server-ipv4=1.2.3.4
diff --git a/bin/tests/system/dnssectools/tests.sh b/bin/tests/system/dnssectools/tests.sh
index c23cd3604d..4e9ae18084 100644
--- a/bin/tests/system/dnssectools/tests.sh
+++ b/bin/tests/system/dnssectools/tests.sh
@@ -237,6 +237,22 @@ n=$((n + 1))
 test "$ret" -eq 0 || echo_i "failed"
 status=$((status + ret))

+echo_ic "check that dnssec-signzone correctly handles DELEG ($n)"
+ret=0
+(
+  cd signer/general || exit 1
+  rm -f signed.zone
+  $SIGNER -O full -f signed.zone -o example.com. test14.zone >signer.out.$n
+  grep -q "^mixed.*IN RRSIG.*DELEG" signed.zone || exit 1
+  grep -q "^mixed.*IN NSEC.*NS .*DELEG" signed.zone || exit 1
+  grep -q "^deleg-only.*IN NSEC.*DELEG" signed.zone || exit 1
+  grep -q "^deleg-only.*IN NSEC.*NS " signed.zone && exit 1
+  test -f signed.zone || exit 1
+) || ret=1
+n=$((n + 1))
+test "$ret" -eq 0 || echo_i "failed"
+status=$((status + ret))
+
 get_default_algorithm_key_ids_from_sigs() {
   zone=$1

diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst
index d51a64a843..115685a3d9 100644
--- a/doc/arm/reference.rst
+++ b/doc/arm/reference.rst
@@ -2625,8 +2625,8 @@ Boolean Options
    :rfc:`952` and :rfc:`821` as modified by :rfc:`1123`.

    :any:`check-names` applies to the owner names of A, AAAA, and MX records.
-   It also applies to the domain names in the RDATA of NS, SOA, MX, and
-   SRV records. It further applies to the RDATA of PTR records where the
+   It also applies to the domain names in the RDATA of NS, SOA, MX, SRV,
+   and DELEG records. It further applies to the RDATA of PTR records where the
    owner name indicates that it is a reverse lookup of a hostname (the
    owner name ends in IN-ADDR.ARPA, IP6.ARPA, or IP6.INT).

@@ -2675,6 +2675,8 @@ Boolean Options
    use :iscman:`named-checkzone`). For NS records, only names below top-of-zone
    are checked (for out-of-zone names and glue consistency checks, use
    :iscman:`named-checkzone`). DS records not at delegations are rejected.
+   DELEG records containing "server-name" or "include-delegparam"
+   parameters which point to names below the zone cut are rejected.
    The default is ``yes``.

    The use of the SPF record to publish Sender Policy Framework is
diff --git a/lib/dns/clientinfo.c b/lib/dns/clientinfo.c
index 1ea5e7d69d..17913fc758 100644
--- a/lib/dns/clientinfo.c
+++ b/lib/dns/clientinfo.c
@@ -25,10 +25,12 @@ dns_clientinfomethods_init(dns_clientinfomethods_t *methods,
 }

 void
-dns_clientinfo_init(dns_clientinfo_t *ci, void *data, void *versionp) {
+dns_clientinfo_init(dns_clientinfo_t *ci, void *data, void *versionp,
+		    dns_edectx_t *edectx) {
 	ci->version = DNS_CLIENTINFO_VERSION;
 	ci->data = data;
 	ci->dbversion = versionp;
+	ci->edectx = edectx;
 	dns_ecs_init(&ci->ecs);
 }

diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
index 4117145bf5..c8dbdf6ce8 100644
--- a/lib/dns/dnssec.c
+++ b/lib/dns/dnssec.c
@@ -448,7 +448,7 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
 	 * NS, SOA and DNSKEY records are signed by their owners.
 	 * NSEC3 records are signed by the apex, exactly one level up
 	 * from their owner names.
-	 * DS records are signed by the parent zone.
+	 * DS and DELEG records are signed by the parent.
 	 */
 	switch (set->type) {
 	case dns_rdatatype_nsec3: {
@@ -473,6 +473,7 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
 		}
 		break;
 	case dns_rdatatype_ds:
+	case dns_rdatatype_deleg:
 		if (dns_name_equal(name, &sig.signer)) {
 			inc_stat(dns_dnssecstats_fail);
 			return DNS_R_SIGINVALID;
diff --git a/lib/dns/include/dns/clientinfo.h b/lib/dns/include/dns/clientinfo.h
index f25c4925ce..f2f7fcff0c 100644
--- a/lib/dns/include/dns/clientinfo.h
+++ b/lib/dns/include/dns/clientinfo.h
@@ -43,21 +43,23 @@
 #include <isc/types.h>

 #include <dns/ecs.h>
+#include <dns/ede.h>

 /*****
 ***** Types
 *****/

-#define DNS_CLIENTINFO_VERSION 3
+#define DNS_CLIENTINFO_VERSION 4
 /*
  * Any updates to this structure should also be applied in
  * https://gitlab.isc.org/isc-projects/dlz-modules/-/raw/main/modules/include/dlz_minimal.h
  */
 typedef struct dns_clientinfo {
-	uint16_t  version;
-	void	 *data;
-	void	 *dbversion;
-	dns_ecs_t ecs;
+	uint16_t      version;
+	void	     *data;
+	void	     *dbversion;
+	dns_ecs_t     ecs;
+	dns_edectx_t *edectx;
 } dns_clientinfo_t;

 typedef isc_result_t (*dns_clientinfo_sourceip_t)(dns_clientinfo_t *client,
@@ -84,10 +86,12 @@ dns_clientinfomethods_init(dns_clientinfomethods_t  *methods,
 			   dns_clientinfo_sourceip_t sourceip);

 void
-dns_clientinfo_init(dns_clientinfo_t *ci, void *data, void *versionp);
+dns_clientinfo_init(dns_clientinfo_t *ci, void *data, void *versionp,
+		    dns_edectx_t *edectx);
 /*%<
- * Initialize a clientinfo object, setting the data to 'data' and the
- * database version to 'versionp'. ECS data is initialized to 0/0/0.
+ * Initialize a clientinfo object, setting the data to 'data', the
+ * database version to 'versionp', and the EDE context to 'edectx'.
+ * ECS data is initialized to 0/0/0.
  */

 void
diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h
index ba9a7b2d7b..0eb0d2fbe8 100644
--- a/lib/dns/include/dns/db.h
+++ b/lib/dns/include/dns/db.h
@@ -291,6 +291,7 @@ enum {
 	 * stale-refresh-time window.
 	 */
 	DNS_DBFIND_STALESTART = 1 << 12,
+	DNS_DBFIND_DELEGOK = 1 << 13,
 };
 /*@}*/

@@ -856,6 +857,11 @@ dns__db_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version,
  *	pending data will be returned.  This option is only meaningful for
  *	cache databases.
  *
+ * \li	If 'options' does not have #DNS_DBFIND_DELEGOK set, then only
+ * 	NS referrals will be returned. This allows different delegation
+ * 	answers to be sent depending on whether a client has set the
+ * 	DE (DELEG-OK) bit.
+ *
  * \li	If the #DNS_DBFIND_NOWILD option is set, then wildcard matching will
  *	be disabled.  This option is only meaningful for zone databases.
  *
diff --git a/lib/dns/include/dns/rdata.h b/lib/dns/include/dns/rdata.h
index 71fbac4ab9..0187197ca4 100644
--- a/lib/dns/include/dns/rdata.h
+++ b/lib/dns/include/dns/rdata.h
@@ -143,6 +143,8 @@ enum {
 	DNS_RDATATYPEATTR_ATCNAME = 1 << 10,
 	/*% Follow additional */
 	DNS_RDATATYPEATTR_FOLLOWADDITIONAL = 1 << 11,
+	/*% Delegation type */
+	DNS_RDATATYPEATTR_DELEGATION = 1 << 12,
 };

 #define DNS_RDATA_INIT                        \
@@ -642,6 +644,22 @@ dns_rdatatype_atparent(dns_rdatatype_t type) {
 	       0;
 }

+/*%
+ * Special case of dns_rdatatype_atparent(), to be used when DELEG might or
+ * might not count as an at-parent type, depending on whether DE=1.
+ *
+ * Requires:
+ * \li	'type' is a valid rdata type.
+ *
+ */
+static inline bool
+dns_rdatatype_atparent_deleg(dns_rdatatype_t type, bool de) {
+	if (type == dns_rdatatype_deleg) {
+		return de;
+	}
+	return dns_rdatatype_atparent(type);
+}
+
 /*%
  * Return true if adding a record of type 'type' to the ADDITIONAL section
  * of a message can itself trigger the addition of still more data to the
@@ -751,6 +769,20 @@ dns_rdatatype_isalias(dns_rdatatype_t type) {
 	return type == dns_rdatatype_cname || type == dns_rdatatype_dname;
 }

+/*%
+ * Return true iff rdata of type 'type' is a delegating type: either
+ * NS or DELEG.
+ *
+ * Requires:
+ * \li	'type' is a valid rdata type.
+ *
+ */
+static inline bool
+dns_rdatatype_isdelegation(dns_rdatatype_t type) {
+	return (dns_rdatatype_attributes(type) &
+		DNS_RDATATYPEATTR_DELEGATION) != 0;
+}
+
 /*%
  * Return true iff the rdata class 'rdclass' is a meta-class
  * like ANY or NONE.
diff --git a/lib/dns/nsec.c b/lib/dns/nsec.c
index 4e211abe20..337f2694e7 100644
--- a/lib/dns/nsec.c
+++ b/lib/dns/nsec.c
@@ -143,8 +143,9 @@ dns_nsec_buildrdata(dns_db_t *db, dns_dbversion_t *version, dns_dbnode_t *node,
 	/*
 	 * At zone cuts, deny the existence of glue in the parent zone.
 	 */
-	if (dns_nsec_isset(bm, dns_rdatatype_ns) &&
-	    !dns_nsec_isset(bm, dns_rdatatype_soa))
+	if (dns_nsec_isset(bm, dns_rdatatype_deleg) ||
+	    (dns_nsec_isset(bm, dns_rdatatype_ns) &&
+	     !dns_nsec_isset(bm, dns_rdatatype_soa)))
 	{
 		for (i = 0; i <= max_type; i++) {
 			if (dns_nsec_isset(bm, i) &&
diff --git a/lib/dns/nsec3.c b/lib/dns/nsec3.c
index 77cfb09c8e..31f8c01080 100644
--- a/lib/dns/nsec3.c
+++ b/lib/dns/nsec3.c
@@ -127,18 +127,20 @@ dns_nsec3_buildrdata(dns_db_t *db, dns_dbversion_t *version, dns_dbnode_t *node,
 			 * Work out if we need to set the RRSIG bit for
 			 * this node.  We set the RRSIG bit if either of
 			 * the following conditions are met:
-			 * 1) We have a SOA or DS then we need to set
-			 *    the RRSIG bit as both always will be signed.
-			 * 2) We set the RRSIG bit if we don't have
-			 *    a NS record but do have other data.
+			 * 1) We have an SOA, DS or DELEG; these will
+			 *    always be signed.
+			 * 2) We don't have an NS, but do have other data.
 			 */
-			if (rdataset.type == dns_rdatatype_soa ||
-			    rdataset.type == dns_rdatatype_ds)
-			{
+			switch (rdataset.type) {
+			case dns_rdatatype_soa:
+			case dns_rdatatype_ds:
+			case dns_rdatatype_deleg:
 				need_rrsig = true;
-			} else if (rdataset.type == dns_rdatatype_ns) {
+				break;
+			case dns_rdatatype_ns:
 				found_ns = true;
-			} else {
+				break;
+			default:
 				found = true;
 			}
 		}
@@ -156,8 +158,9 @@ dns_nsec3_buildrdata(dns_db_t *db, dns_dbversion_t *version, dns_dbnode_t *node,
 	/*
 	 * At zone cuts, deny the existence of glue in the parent zone.
 	 */
-	if (dns_nsec_isset(bm, dns_rdatatype_ns) &&
-	    !dns_nsec_isset(bm, dns_rdatatype_soa))
+	if (dns_nsec_isset(bm, dns_rdatatype_deleg) ||
+	    (dns_nsec_isset(bm, dns_rdatatype_ns) &&
+	     !dns_nsec_isset(bm, dns_rdatatype_soa)))
 	{
 		for (i = 0; i <= max_type; i++) {
 			if (dns_nsec_isset(bm, i) &&
@@ -1934,6 +1937,8 @@ dns__nsec3_noexistnodata(dns_rdatatype_t type, const dns_name_t *name,
 			    (dns_name_empty(closest) ||
 			     dns_name_issubdomain(qname, closest)) &&
 			    !dns_nsec3_typepresent(&rdata, dns_rdatatype_ds) &&
+			    !dns_nsec3_typepresent(&rdata,
+						   dns_rdatatype_deleg) &&
 			    !dns_nsec3_typepresent(&rdata,
 						   dns_rdatatype_dname) &&
 			    (dns_nsec3_typepresent(&rdata, dns_rdatatype_soa) ||
diff --git a/lib/dns/qpzone.c b/lib/dns/qpzone.c
index 69078fb7a3..b0d6b32f7b 100644
--- a/lib/dns/qpzone.c
+++ b/lib/dns/qpzone.c
@@ -1767,6 +1767,7 @@ qpzone_findrdataset(dns_db_t *db, dns_dbnode_t *dbnode,
 static bool
 delegating_type(qpzonedb_t *qpdb, qpznode_t *node, dns_typepair_t typepair) {
 	return typepair == DNS_TYPEPAIR(dns_rdatatype_dname) ||
+	       typepair == DNS_TYPEPAIR(dns_rdatatype_deleg) ||
 	       (typepair == DNS_TYPEPAIR(dns_rdatatype_ns) &&
 		(node != qpdb->origin || IS_STUB(qpdb)));
 }
@@ -3394,43 +3395,69 @@ again:
 }

 static isc_result_t
-qpzone_check_zonecut(qpznode_t *node, void *arg DNS__DB_FLARG) {
+qpzone_check_zonecut(qpznode_t *node, void *arg, bool skipdname DNS__DB_FLARG) {
 	qpz_search_t *search = arg;
 	dns_vecheader_t *dname_header = NULL, *sigdname_header = NULL;
+	dns_vecheader_t *deleg_header = NULL, *sigdeleg_header = NULL;
 	dns_vecheader_t *ns_header = NULL;
-	dns_vecheader_t *found = NULL;
+	dns_vecheader_t *found = NULL, *foundsig = NULL;
 	isc_result_t result = DNS_R_CONTINUE;
 	isc_rwlocktype_t nlocktype = isc_rwlocktype_none;
 	isc_rwlock_t *nlock = qpzone_get_lock(node);
+	bool de = (search->options & DNS_DBFIND_DELEGOK) != 0;

 	NODE_RDLOCK(nlock, &nlocktype);

 	/*
-	 * Look for an NS or DNAME rdataset active in our version.
+	 * The 'delegating' flag will be set for any node below
+	 * the origin that has an NS, DNAME or DELEG record.
+	 * If it's not set and not the origin, we don't need to
+	 * search. We might need to set the 'wild' flag, though.
+	 */
+	if (node != search->qpdb->origin && !node->delegating) {
+		if (node->wild && (search->options & DNS_DBFIND_NOWILD) == 0) {
+			search->wild = true;
+		}
+		CLEANUP(result);
+	}
+
+	/*
+	 * Look for an NS, DELEG or DNAME rdataset active in our version.
 	 */
 	ISC_SLIST_FOREACH(top, node->next_type, next_type) {
-		if (top->typepair == DNS_TYPEPAIR(dns_rdatatype_ns) ||
-		    top->typepair == DNS_TYPEPAIR(dns_rdatatype_dname) ||
-		    top->typepair == DNS_SIGTYPEPAIR(dns_rdatatype_dname))
-		{
+		bool rrsig = (DNS_TYPEPAIR_TYPE(top->typepair) ==
+			      dns_rdatatype_rrsig);
+		bool ns = (top->typepair == DNS_TYPEPAIR(dns_rdatatype_ns));
+		bool deleg =
+			(top->typepair == DNS_TYPEPAIR(dns_rdatatype_deleg)) ||
+			(top->typepair == DNS_SIGTYPEPAIR(dns_rdatatype_deleg));
+		bool dname =
+			(top->typepair == DNS_TYPEPAIR(dns_rdatatype_dname)) ||
+			(top->typepair == DNS_SIGTYPEPAIR(dns_rdatatype_dname));
+
+		if (ns || deleg || (!skipdname && dname)) {
 			dns_vecheader_t *header =
 				first_existing_header(top, search->serial);
 			if (header != NULL) {
-				if (top->typepair ==
-				    DNS_TYPEPAIR(dns_rdatatype_dname))
-				{
-					dname_header = header;
-				} else if (top->typepair ==
-					   DNS_SIGTYPEPAIR(dns_rdatatype_dname))
-				{
+				if (dname && rrsig) {
 					sigdname_header = header;
-				} else if (node != search->qpdb->origin ||
-					   IS_STUB(search->qpdb))
+				} else if (dname) {
+					dname_header = header;
+				} else if (node == search->qpdb->origin &&
+					   !IS_STUB(search->qpdb))
 				{
 					/*
-					 * We've found an NS rdataset that
-					 * isn't at the origin node.
+					 * If we're at the origin node
+					 * then we're not interested in
+					 * the NS, and DELEG isn't allowed,
+					 * so we can skip those.
 					 */
+					continue;
+				} else if (deleg && rrsig) {
+					sigdeleg_header = header;
+				} else if (deleg) {
+					deleg_header = header;
+				} else if (ns) {
 					ns_header = header;
 				}
 			}
@@ -3439,20 +3466,38 @@ qpzone_check_zonecut(qpznode_t *node, void *arg DNS__DB_FLARG) {

 	/*
 	 * Did we find anything?
+	 *
+	 * Note: delegations have precedence over DNAME in normal zones,
+	 * but the reverse is true in stub zones.  DELEG has
+	 * precedence over NS if both exist and DE=1; NS has
+	 * precedence over DELEG if both exist and DE=0.
 	 */
-	if (!IS_STUB(search->qpdb) && ns_header != NULL) {
-		/*
-		 * Note that NS has precedence over DNAME if both exist
-		 * in a zone.  Otherwise DNAME take precedence over NS.
-		 */
-		found = ns_header;
-		search->zonecut_sigheader = NULL;
-	} else if (dname_header != NULL) {
-		found = dname_header;
-		search->zonecut_sigheader = sigdname_header;
-	} else if (ns_header != NULL) {
-		found = ns_header;
-		search->zonecut_sigheader = NULL;
+	if (!IS_STUB(search->qpdb)) {
+		if (de && deleg_header != NULL) {
+			found = deleg_header;
+			foundsig = sigdeleg_header;
+		} else if (ns_header != NULL) {
+			found = ns_header;
+		} else if (deleg_header != NULL) {
+			found = deleg_header;
+			foundsig = sigdeleg_header;
+		} else if (dname_header != NULL) {
+			found = dname_header;
+			foundsig = sigdname_header;
+		}
+	} else {
+		if (dname_header != NULL) {
+			found = dname_header;
+			foundsig = sigdname_header;
+		} else if (de && deleg_header != NULL) {
+			found = deleg_header;
+			foundsig = sigdeleg_header;
+		} else if (ns_header != NULL) {
+			found = ns_header;
+		} else if (deleg_header != NULL) {
+			found = deleg_header;
+			foundsig = sigdeleg_header;
+		}
 	}

 	if (found != NULL) {
@@ -3462,7 +3507,16 @@ qpzone_check_zonecut(qpznode_t *node, void *arg DNS__DB_FLARG) {
 		 */
 		qpznode_acquire(node DNS__DB_FLARG_PASS);
 		search->zonecut = node;
-		search->zonecut_header = found;
+
+		/*
+		 * We set the zonecut node but not the header if this
+		 * is a DELEG-only delegation and DE=0.
+		 */
+		if (de || found != deleg_header) {
+			search->zonecut_header = found;
+			search->zonecut_sigheader = foundsig;
+		}
+
 		/*
 		 * Since we've found a zonecut, anything beneath it is
 		 * glue and is not subject to wildcard matching, so we
@@ -3503,6 +3557,7 @@ qpzone_check_zonecut(qpznode_t *node, void *arg DNS__DB_FLARG) {
 		}
 	}

+cleanup:
 	NODE_UNLOCK(nlock, &nlocktype);

 	return result;
@@ -3571,14 +3626,13 @@ qpzone_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version,
 	    dns_rdatatype_t type, unsigned int options,
 	    isc_stdtime_t now ISC_ATTR_UNUSED, dns_name_t *foundname,
 	    dns_clientinfomethods_t *methods ISC_ATTR_UNUSED,
-	    dns_clientinfo_t *clientinfo ISC_ATTR_UNUSED,
-	    dns_rdataset_t *rdataset,
+	    dns_clientinfo_t *clientinfo, dns_rdataset_t *rdataset,
 	    dns_rdataset_t *sigrdataset DNS__DB_FLARG) {
 	isc_result_t result;
 	qpzonedb_t *qpdb = (qpzonedb_t *)db;
 	qpznode_t *node = NULL;
 	bool cname_ok = true, close_version = false;
-	bool maybe_zonecut = false;
+	bool at_zonecut = false, deleg_only = false;
 	bool wild = false, empty_node = false;
 	bool nsec3 = false;
 	dns_vecheader_t *found = NULL, *nsecheader = NULL;
@@ -3587,6 +3641,8 @@ qpzone_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version,
 	bool active;
 	isc_rwlock_t *nlock = NULL;
 	isc_rwlocktype_t nlocktype = isc_rwlocktype_none;
+	bool de = (options & DNS_DBFIND_DELEGOK) != 0;
+	bool atparent = dns_rdatatype_atparent_deleg(type, de);

 	REQUIRE(VALID_QPZONE((qpzonedb_t *)db));
 	INSIST(version == NULL ||
@@ -3632,8 +3688,9 @@ qpzone_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version,
 	 * Check the QP chain to see if there's a node above us with a
 	 * active DNAME or NS rdatasets.
 	 *
-	 * We're only interested in nodes above QNAME, so if the result
-	 * was success, then we skip the last item in the chain.
+	 * At the moment we're only interested in nodes above QNAME, so if
+	 * the result from dns_qp_lookup() was success, then we skip the
+	 * last item in the chain.
 	 */
 	unsigned int clen = dns_qpchain_length(&search.chain);
 	if (result == ISC_R_SUCCESS) {
@@ -3646,7 +3703,8 @@ qpzone_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version,
 		isc_result_t tresult;

 		dns_qpchain_node(&search.chain, i, (void **)&n, NULL);
-		tresult = qpzone_check_zonecut(n, &search DNS__DB_FLARG_PASS);
+		tresult = qpzone_check_zonecut(n, &search,
+					       false DNS__DB_FLARG_PASS);
 		if (tresult != DNS_R_CONTINUE) {
 			result = tresult;
 			dns_name_copy(&n->name, foundname);
@@ -3654,9 +3712,32 @@ qpzone_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version,
 		}
 	}

-	if (result == DNS_R_PARTIALMATCH) {
+	/*
+	 * If we're beneath a zone cut, we don't want to look for
+	 * CNAMEs because they're not legitimate zone glue. And
+	 * some DNSSEC types (KEY and NSEC) are not subject to CNAME
+	 * matching (see RFC4035, section 2.5, and RFC3007).
+	 */
+	if (search.zonecut != NULL || type == dns_rdatatype_key ||
+	    type == dns_rdatatype_nsec)
+	{
+		cname_ok = false;
+	}
+
+	if (result == ISC_R_SUCCESS && search.zonecut == NULL) {
+		/*
+		 * We found the node we were looking for, and there aren't
+		 * any zone cuts higher in the tree, but we need to call
+		 * qpzone_check_zonecut() on the QNAME node too, to
+		 * populate search.zonecut, .zonecut_header and
+		 * .zonecut_sigheader in case we need to return a
+		 * referral.
+		 */
+		qpzone_check_zonecut(node, &search, true DNS__DB_FLARG_PASS);
+	} else if (result == DNS_R_PARTIALMATCH) {
+		/* No such name, or it exists but has a zone cut above it. */
 	partial_match:
-		if (search.zonecut != NULL) {
+		if (search.zonecut_header != NULL) {
 			result = qpzone_setup_delegation(
 				&search, foundname, rdataset,
 				sigrdataset DNS__DB_FLARG_PASS);
@@ -3718,32 +3799,25 @@ found:
 	 * We have found a node whose name is the desired name, or we
 	 * have matched a wildcard.
 	 */
-
 	nlock = qpzone_get_lock(node);
 	NODE_RDLOCK(nlock, &nlocktype);

-	if (search.zonecut != NULL) {
-		/*
-		 * If we're beneath a zone cut, we don't want to look for
-		 * CNAMEs because they're not legitimate zone glue.
-		 */
-		cname_ok = false;
-	} else {
+	if (search.zonecut == node) {
 		/*
-		 * The node may be a zone cut itself.  If it might be one,
-		 * make sure we check for it later.
-		 *
-		 * DS records live above the zone cut in ordinary zone so
-		 * we want to ignore any referral.
-		 *
-		 * Stub zones don't have anything "above" the delegation so
-		 * we always return a referral.
+		 * This node contains a delegation type (NS or DELEG),
+		 * so it might be a zone cut. If this isn't the zone origin
+		 * (where NS is authoritative, and DELEG is forbidden), or
+		 * if this is a stub zone, then it *is* a zone cut.
 		 */
-		if (node->delegating && ((node != search.qpdb->origin &&
-					  !dns_rdatatype_atparent(type)) ||
-					 IS_STUB(search.qpdb)))
-		{
-			maybe_zonecut = true;
+		if (node != search.qpdb->origin || IS_STUB(search.qpdb)) {
+			at_zonecut = true;
+			/*
+			 * If the delegation record is DELEG-only, with no
+			 * NS, we might need to handle it specially later.
+			 */
+			if (search.zonecut_header == NULL) {
+				deleg_only = true;
+			}
 		}
 	}

@@ -3761,147 +3835,179 @@ found:
 	/*
 	 * We now go looking for rdata...
 	 */
-
 	sigpair = DNS_SIGTYPEPAIR(type);
 	empty_node = true;
 	ISC_SLIST_FOREACH(top, node->next_type, next_type) {
+		bool keep_looking;
+
 		/*
 		 * Look for an active, extant rdataset.
 		 */
 		dns_vecheader_t *header = first_existing_header(top,
 								search.serial);
-		if (header != NULL) {
+		if (header == NULL) {
+			continue;
+		}
+
+		/*
+		 * We now know that there is at least one active rdataset
+		 * at this node.
+		 */
+		empty_node = false;
+
+		if (!at_zonecut || atparent) {
 			/*
-			 * We now know that there is at least one active
-			 * rdataset at this node.
+			 * Either this isn't a zone cut or we're looking
+			 * for a parent-side type. Keep looking for an
+			 * answer.
 			 */
-			empty_node = false;
-
+			keep_looking = true;
+		} else {
 			/*
-			 * Do special zone cut handling, if requested.
+			 * Zone cut. There are several special cases we
+			 * have to handle, depending on query type and
+			 * whether the zone is delegated with NS, DELEG, or
+			 * both:
+			 *
+			 * QTYPE NSEC, or the GLUEOK flag is set:
+			 * - look for the answer as if this were not
+			 *   a zone cut.
+			 *
+			 * QTYPE DELEG:
+			 * - if NS is present and DE=0: return NS referral
+			 * - otherwise, look for the answer
+			 *
+			 * any other QTYPE:
+			 * - if NS is not present but DELEG is, and
+			 *   DE=0: return NXRRSET
+			 * - if NS is present, and either DELEG is not
+			 *   present *or* DE=0: return NS referral
+			 * - if DELEG is present and DE=1: return
+			 *   DELEG referral
 			 */
-			if (maybe_zonecut &&
-			    top->typepair == DNS_TYPEPAIR(dns_rdatatype_ns))
+			if (type == dns_rdatatype_nsec ||
+			    (options & DNS_DBFIND_GLUEOK) != 0)
 			{
-				/*
-				 * We increment the reference count on node to
-				 * ensure that search->zonecut_header will
-				 * still be valid later.
-				 */
-				qpznode_acquire(node DNS__DB_FLARG_PASS);
-				search.zonecut = node;
-				search.zonecut_header = header;
-				search.zonecut_sigheader = NULL;
-				maybe_zonecut = false;
-
-				if ((search.options & DNS_DBFIND_GLUEOK) == 0 &&
-				    type != dns_rdatatype_nsec)
-				{
-					/*
-					 * Glue is not OK, but any answer we
-					 * could return would be glue.  Return
-					 * the delegation.
-					 */
-					found = NULL;
+				keep_looking = true;
+			} else if (type == dns_rdatatype_deleg) {
+				if (!de && !deleg_only) {
+					/* triggers a referral */
 					break;
 				}
-				if (found != NULL && foundsig != NULL) {
-					break;
+				keep_looking = true;
+			} else if (deleg_only && !de) {
+				if (clientinfo != NULL &&
+				    clientinfo->edectx != NULL)
+				{
+					dns_ede_add(clientinfo->edectx,
+						    DNS_EDE_DELEGONLY, NULL);
 				}
+				keep_looking = false;
+			} else {
+				/* triggers a referral */
+				break;
 			}
+		}

+		/*
+		 * If the NSEC3 record doesn't match the chain
+		 * we are using, behave as if it isn't here.
+		 */
+		if (top->typepair == DNS_TYPEPAIR(dns_rdatatype_nsec3) &&
+		    !matchparams(header, &search))
+		{
+			NODE_UNLOCK(nlock, &nlocktype);
+			goto partial_match;
+		}
+
+		/*
+		 * For an ANY query, the caller will be iterating the
+		 * node, so it doesn't matter which header we find, we
+		 * just need one.
+		 */
+		if (type == dns_rdatatype_any) {
+			found = header;
+			break;
+		}
+
+		/*
+		 * If we found a type we were looking for, remember it.
+		 */
+		if (keep_looking &&
+		    (top->typepair == type ||
+		     (top->typepair == DNS_TYPEPAIR(dns_rdatatype_cname) &&
+		      cname_ok)))
+		{
 			/*
-			 * If the NSEC3 record doesn't match the chain
-			 * we are using behave as if it isn't here.
+			 * We've found the answer!
 			 */
+			found = header;
 			if (top->typepair ==
-				    DNS_TYPEPAIR(dns_rdatatype_nsec3) &&
-			    !matchparams(header, &search))
-			{
-				NODE_UNLOCK(nlock, &nlocktype);
-				goto partial_match;
-			}
-			/*
-			 * If we found a type we were looking for,
-			 * remember it.
-			 */
-			if (top->typepair == type ||
-			    type == dns_rdatatype_any ||
-			    (top->typepair ==
-				     DNS_TYPEPAIR(dns_rdatatype_cname) &&
-			     cname_ok))
+				    DNS_TYPEPAIR(dns_rdatatype_cname) &&
+			    cname_ok)
 			{
 				/*
-				 * We've found the answer!
-				 */
-				found = header;
-				if (top->typepair ==
-					    DNS_TYPEPAIR(dns_rdatatype_cname) &&
-				    cname_ok)
-				{
-					/*
-					 * We may be finding a CNAME instead
-					 * of the desired type.
-					 *
-					 * If we've already got the CNAME RRSIG,
-					 * use it, otherwise change sigtype
-					 * so that we find it.
-					 */
-					if (cnamesig != NULL) {
-						foundsig = cnamesig;
-					} else {
-						sigpair = DNS_SIGTYPEPAIR(
-							dns_rdatatype_cname);
-					}
-				}
-				/*
-				 * If we've got all we need, end the search.
+				 * We may be finding a CNAME instead
+				 * of the desired type.
+				 *
+				 * If we've already got the CNAME RRSIG,
+				 * use it, otherwise change sigtype
+				 * so that we find it.
 				 */
-				if (!maybe_zonecut && foundsig != NULL) {
-					break;
+				if (cnamesig != NULL) {
+					foundsig = cnamesig;
+				} else {
+					sigpair = DNS_SIGTYPEPAIR(
+						dns_rdatatype_cname);
 				}
-			} else if (top->typepair == sigpair) {
-				/*
-				 * We've found the RRSIG rdataset for our
-				 * target type.  Remember it.
-				 */
-				foundsig = header;
-				/*
-				 * If we've got all we need, end the search.
-				 */
-				if (!maybe_zonecut && found != NULL) {
-					break;
-				}
-			} else if (top->typepair ==
-					   DNS_TYPEPAIR(dns_rdatatype_nsec) &&
-				   !search.version->havensec3)
-			{
-				/*
-				 * Remember a NSEC rdataset even if we're
-				 * not specifically looking for it, because
-				 * we might need it later.
-				 */
-				nsecheader = header;
-			} else if (top->typepair ==
-					   DNS_SIGTYPEPAIR(
-						   dns_rdatatype_nsec) &&
-				   !search.version->havensec3)
-			{
-				/*
-				 * If we need the NSEC rdataset, we'll also
-				 * need its signature.
-				 */
-				nsecsig = header;
-			} else if (cname_ok &&
-				   top->typepair ==
-					   DNS_SIGTYPEPAIR(dns_rdatatype_cname))
-			{
-				/*
-				 * If we get a CNAME match, we'll also need
-				 * its signature.
-				 */
-				cnamesig = header;
 			}
+
+			/*
+			 * If we've got all we need, end the search.
+			 */
+			if (foundsig != NULL) {
+				break;
+			}
+		} else if (keep_looking && top->typepair == sigpair) {
+			/*
+			 * We've found the RRSIG rdataset for our
+			 * target type.  Remember it.
+			 */
+			foundsig = header;
+
+			/*
+			 * If we've got all we need, end the search.
+			 */
+			if (found != NULL) {
+				break;
+			}
+		} else if (top->typepair == DNS_TYPEPAIR(dns_rdatatype_nsec) &&
+			   !search.version->havensec3)
+		{
+			/*
+			 * Remember a NSEC rdataset even if we're
+			 * not specifically looking for it, because
+			 * we might need it later.
+			 */
+			nsecheader = header;
+		} else if (top->typepair ==
+				   DNS_SIGTYPEPAIR(dns_rdatatype_nsec) &&
+			   !search.version->havensec3)
+		{
+			/*
+			 * If we need the NSEC rdataset, we'll also
+			 * need its signature.
+			 */
+			nsecsig = header;
+		} else if (keep_looking && cname_ok &&
+			   top->typepair ==
+				   DNS_SIGTYPEPAIR(dns_rdatatype_cname))
+		{
+			/*
+			 * If we get a CNAME match, we'll also need
+			 * its signature.
+			 */
+			cnamesig = header;
 		}
 	}

@@ -3930,9 +4036,11 @@ found:
 	 * If we didn't find what we were looking for...
 	 */
 	if (found == NULL) {
-		if (search.zonecut != NULL) {
+		if (search.zonecut_header != NULL &&
+		    ((at_zonecut && !atparent) || search.zonecut != node))
+		{
 			/*
-			 * We were trying to find glue at a node beneath a
+			 * We were trying to find glue at or below a
 			 * zone cut, but didn't.
 			 *
 			 * Return the delegation.
@@ -3946,6 +4054,7 @@ found:
 				sigrdataset DNS__DB_FLARG_PASS);
 			goto tree_exit;
 		}
+
 		/*
 		 * The desired type doesn't exist.
 		 */
@@ -3998,6 +4107,12 @@ found:
 		 * that result to the caller.
 		 */
 		result = DNS_R_CNAME;
+	} else if (search.zonecut != NULL && search.zonecut != node) {
+		/* If we're below a zone cut, the result is glue. */
+		result = DNS_R_GLUE;
+	} else if (search.zonecut != NULL && at_zonecut && atparent) {
+		/* We're at a zone cut, but looking for a parent-side type */
+		result = ISC_R_SUCCESS;
 	} else if (search.zonecut != NULL) {
 		/*
 		 * If we're beneath a zone cut, we must indicate that the
@@ -4005,7 +4120,9 @@ found:
 		 * and the type is NSEC.
 		 */
 		if (search.zonecut == node) {
-			if (dns_rdatatype_isnsec(type)) {
+			if (dns_rdatatype_isnsec(type) ||
+			    type == dns_rdatatype_deleg)
+			{
 				result = ISC_R_SUCCESS;
 			} else if (type == dns_rdatatype_any) {
 				result = DNS_R_ZONECUT;
@@ -5018,7 +5135,7 @@ qpzone_addrdataset_inner(qpzonedb_t *qpdb, qpznode_t *node,
 	}

 	/*
-	 * If we're adding a delegation type (e.g. NS or DNAME),
+	 * If we're adding a delegation type (e.g. NS, DNAME or DELEG),
 	 * then we need to set the callback bit on the node.
 	 */
 	if (result == ISC_R_SUCCESS &&
diff --git a/lib/dns/rdata/generic/ns_2.c b/lib/dns/rdata/generic/ns_2.c
index 856a6bbe39..28262ad3e1 100644
--- a/lib/dns/rdata/generic/ns_2.c
+++ b/lib/dns/rdata/generic/ns_2.c
@@ -14,7 +14,8 @@
 #ifndef RDATA_GENERIC_NS_2_C
 #define RDATA_GENERIC_NS_2_C

-#define RRTYPE_NS_ATTRIBUTES (DNS_RDATATYPEATTR_ZONECUTAUTH)
+#define RRTYPE_NS_ATTRIBUTES \
+	(DNS_RDATATYPEATTR_ZONECUTAUTH | DNS_RDATATYPEATTR_DELEGATION)

 static isc_result_t
 fromtext_ns(ARGS_FROMTEXT) {
diff --git a/lib/dns/rdata/in_1/deleg_61440.c b/lib/dns/rdata/in_1/deleg_61440.c
index 14d6d1c03b..e079c847fb 100644
--- a/lib/dns/rdata/in_1/deleg_61440.c
+++ b/lib/dns/rdata/in_1/deleg_61440.c
@@ -19,7 +19,7 @@

 #define RRTYPE_DELEG_ATTRIBUTES                                            \
 	(DNS_RDATATYPEATTR_FOLLOWADDITIONAL | DNS_RDATATYPEATTR_ATPARENT | \
-	 DNS_RDATATYPEATTR_ZONECUTAUTH)
+	 DNS_RDATATYPEATTR_ZONECUTAUTH | DNS_RDATATYPEATTR_DELEGATION)

 /*
  * Delegation Information Registry
@@ -210,9 +210,9 @@ finish:
 }

 static isc_result_t
-servname_fromtxt(isc_textregion_t *region, const dns_name_t *origin,
-		 unsigned int options, isc_lex_t *lexer,
-		 dns_rdatacallbacks_t *callbacks, isc_buffer_t *target) {
+servname_fromtext(isc_textregion_t *region, const dns_name_t *origin,
+		  unsigned int options, isc_lex_t *lexer,
+		  dns_rdatacallbacks_t *callbacks, isc_buffer_t *target) {
 	isc_textregion_t src = *region;

 	if (origin == NULL) {
@@ -319,8 +319,8 @@ deleg_fromtext(isc_textregion_t *region, const dns_name_t *origin,
 			} while (e != NULL);
 			break;
 		case deleg_namelist:
-			RETERR(servname_fromtxt(region, origin, options, lexer,
-						callbacks, target));
+			RETERR(servname_fromtext(region, origin, options, lexer,
+						 callbacks, target));
 			break;
 		case deleg_keylist:
 			if (region->length == 0) {
@@ -918,7 +918,9 @@ generic_checknames_in_deleg(ARGS_CHECKNAMES) {
 			if (!dns_name_ishostname(&name, false)) {
 				return false;
 			}
-			if (dns_name_issubdomain(&name, owner)) {
+			if (rdata->type == dns_rdatatype_deleg &&
+			    dns_name_issubdomain(&name, owner))
+			{
 				return false;
 			}
 		}
diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
index 89070b0586..5f91c23153 100644
--- a/lib/dns/resolver.c
+++ b/lib/dns/resolver.c
@@ -8061,7 +8061,7 @@ resquery_response_continue(void *arg, isc_result_t result) {
 		result = rctx_answer_none(rctx);
 		switch (result) {
 		case ISC_R_SUCCESS:
-		case DNS_R_CHASEDSSERVERS:
+		case DNS_R_CHASEPARENTSERVERS:
 			break;
 		case DNS_R_DELEGATION:
 			/*
@@ -9167,15 +9167,16 @@ rctx_answer_none(respctx_t *rctx) {
 	}

 	/*
-	 * Trigger lookups for DNS nameservers.
+	 * Trigger lookups for DNS nameservers for types for
+	 * which the parent zone is authoritative (DS, DELEG).
 	 */
 	if (rctx->negative &&
 	    rctx->query->rmessage->rcode == dns_rcode_noerror &&
-	    fctx->type == dns_rdatatype_ds && rctx->soa_name != NULL &&
+	    dns_rdatatype_atparent(fctx->type) && rctx->soa_name != NULL &&
 	    dns_name_equal(rctx->soa_name, fctx->name) &&
 	    !dns_name_isroot(fctx->name))
 	{
-		return DNS_R_CHASEDSSERVERS;
+		return DNS_R_CHASEPARENTSERVERS;
 	}

 	/*
@@ -9870,7 +9871,7 @@ rctx_done(respctx_t *rctx, isc_result_t result) {
 	} else if (rctx->resend) {
 		UNLOCK(&fctx->lock);
 		rctx_resend(rctx, addrinfo);
-	} else if (result == DNS_R_CHASEDSSERVERS) {
+	} else if (result == DNS_R_CHASEPARENTSERVERS) {
 		UNLOCK(&fctx->lock);
 		rctx_chaseds(rctx, message, addrinfo, result);
 	} else if (result == ISC_R_SUCCESS && !HAVE_ANSWER(fctx)) {
diff --git a/lib/dns/update.c b/lib/dns/update.c
index 96617ede86..9e17819ae1 100644
--- a/lib/dns/update.c
+++ b/lib/dns/update.c
@@ -376,7 +376,8 @@ rrset_visible(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name,
 	dns_fixedname_t fixed;

 	dns_fixedname_init(&fixed);
-	result = dns_db_find(db, name, ver, type, DNS_DBFIND_NOWILD,
+	result = dns_db_find(db, name, ver, type,
+			     DNS_DBFIND_NOWILD | DNS_DBFIND_DELEGOK,
 			     (isc_stdtime_t)0, dns_name(&fixed), NULL, NULL);
 	switch (result) {
 	case ISC_R_SUCCESS:
@@ -658,10 +659,10 @@ is_active(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, bool *flag,
 	isc_result_t result;
 	dns_fixedname_t foundname;
 	dns_fixedname_init(&foundname);
-	result = dns_db_find(db, name, ver, dns_rdatatype_any,
-			     DNS_DBFIND_GLUEOK | DNS_DBFIND_NOWILD,
-			     (isc_stdtime_t)0, dns_name(&foundname), NULL,
-			     NULL);
+	result = dns_db_find(
+		db, name, ver, dns_rdatatype_any,
+		DNS_DBFIND_GLUEOK | DNS_DBFIND_DELEGOK | DNS_DBFIND_NOWILD,
+		(isc_stdtime_t)0, dns_name(&foundname), NULL, NULL);
 	if (result == ISC_R_SUCCESS || result == DNS_R_EMPTYNAME) {
 		*flag = true;
 		*cut = false;
@@ -1215,8 +1216,8 @@ add_exposed_sigs(dns_update_log_t *log, dns_zone_t *zone, dns_db_t *db,
 		dns_rdataset_disassociate(&rdataset);

 		/*
-		 * We don't need to sign unsigned NSEC records at the cut
-		 * as they are handled elsewhere.
+		 * We don't need to sign unsigned DS records at the
+		 * zone cut as they are handled elsewhere.
 		 */
 		if ((type == dns_rdatatype_rrsig) ||
 		    (cut && type != dns_rdatatype_ds))
@@ -1594,12 +1595,12 @@ next_state:

 		/*
 		 * Find names potentially affected by delegation changes
-		 * (obscured by adding an NS or DNAME, or unobscured by
-		 * removing one).
+		 * (obscured by adding an NS, DNAME or DELEG, or
+		 * unobscured by removing one).
 		 */
 		ISC_LIST_FOREACH(state->diffnames.tuples, t, link) {
-			bool ns_existed, dname_existed;
-			bool ns_exists, dname_exists;
+			bool ns_existed, dname_existed, deleg_existed;
+			bool ns_exists, dname_exists, deleg_exists;

 			if (oldver != NULL) {
 				CHECK(rrset_exists(db, oldver, &t->name,
@@ -1615,13 +1616,23 @@ next_state:
 			} else {
 				dname_existed = false;
 			}
+			if (oldver != NULL) {
+				CHECK(rrset_exists(db, oldver, &t->name,
+						   dns_rdatatype_deleg, 0,
+						   &deleg_existed));
+			} else {
+				deleg_existed = false;
+			}
 			CHECK(rrset_exists(db, newver, &t->name,
 					   dns_rdatatype_ns, 0, &ns_exists));
 			CHECK(rrset_exists(db, newver, &t->name,
 					   dns_rdatatype_dname, 0,
 					   &dname_exists));
-			if ((ns_exists || dname_exists) ==
-			    (ns_existed || dname_existed))
+			CHECK(rrset_exists(db, newver, &t->name,
+					   dns_rdatatype_deleg, 0,
+					   &deleg_exists));
+			if ((ns_exists || dname_exists || deleg_exists) ==
+			    (ns_existed || dname_existed || deleg_existed))
 			{
 				continue;
 			}
@@ -1807,15 +1818,15 @@ next_state:

 		/*
 		 * Find names potentially affected by delegation changes
-		 * (obscured by adding an NS or DNAME, or unobscured by
-		 * removing one).
+		 * (obscured by adding an NS, DNAME or DELEG, or
+		 * unobscured by removing one).
 		 */
 		tuple = ISC_LIST_HEAD(diff->tuples);
 		while (tuple != NULL) {
 			dns_name_t *name = &tuple->name;

-			bool ns_existed, dname_existed;
-			bool ns_exists, dname_exists;
+			bool ns_existed, dname_existed, deleg_existed;
+			bool ns_exists, dname_exists, deleg_exists;
 			bool exists, existed;

 			if (tuple->rdata.type == dns_rdatatype_nsec ||
@@ -1841,14 +1852,24 @@ next_state:
 			} else {
 				dname_existed = false;
 			}
+			if (oldver != NULL) {
+				CHECK(rrset_exists(db, oldver, name,
+						   dns_rdatatype_deleg, 0,
+						   &deleg_existed));
+			} else {
+				deleg_existed = false;
+			}
 			CHECK(rrset_exists(db, newver, name, dns_rdatatype_ns,
 					   0, &ns_exists));
 			CHECK(rrset_exists(db, newver, name,
 					   dns_rdatatype_dname, 0,
 					   &dname_exists));
+			CHECK(rrset_exists(db, newver, name,
+					   dns_rdatatype_deleg, 0,
+					   &deleg_exists));

-			exists = ns_exists || dname_exists;
-			existed = ns_existed || dname_existed;
+			exists = ns_exists || dname_exists || deleg_exists;
+			existed = ns_existed || dname_existed || deleg_existed;
 			if (exists == existed) {
 				goto nextname;
 			}
diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index e88cbca5f3..f24df3642f 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -2286,33 +2286,30 @@ static bool
 integrity_checks(dns_zone_t *zone, dns_db_t *db) {
 	dns_dbiterator_t *dbiterator = NULL;
 	dns_dbnode_t *node = NULL;
-	dns_rdataset_t rdataset;
+	dns_rdataset_t rdataset = DNS_RDATASET_INIT;
 	dns_fixedname_t fixed;
 	dns_fixedname_t fixedbottom;
+	dns_name_t *name = dns_fixedname_initname(&fixed);
+	dns_name_t *bottom = dns_fixedname_initname(&fixedbottom);
 	dns_rdata_mx_t mx;
 	dns_rdata_ns_t ns;
 	dns_rdata_in_srv_t srv;
-	dns_name_t *name;
-	dns_name_t *bottom;
 	isc_result_t result;
-	bool ok = true, have_spf, have_txt;
-	bool has_a = false;
-	bool has_aaaa = false;
-	int level;
+	bool ok = true;
+	bool has_a = false, has_aaaa = false;
 	char namebuf[DNS_NAME_FORMATSIZE];
 	bool logged_algorithm[DST_MAX_ALGS];
 	bool logged_digest_type[DNS_DSDIGEST_MAX + 1];

-	name = dns_fixedname_initname(&fixed);
-	bottom = dns_fixedname_initname(&fixedbottom);
-	dns_rdataset_init(&rdataset);
-
 	result = dns_db_createiterator(db, 0, &dbiterator);
 	if (result != ISC_R_SUCCESS) {
 		return true;
 	}

 	DNS_DBITERATOR_FOREACH(dbiterator) {
+		bool have_ns = false, have_deleg = false;
+		bool have_spf = false, have_txt = false;
+
 		CHECK(dns_dbiterator_current(dbiterator, &node, name));

 		/*
@@ -2374,23 +2371,25 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {

 	checkforns:
 		/*
-		 * Don't check the NS records at the origin.
+		 * Skip checking NS records at the apex.
 		 */
 		if (dns_name_equal(name, &zone->origin)) {
-			goto checkfords;
+			goto checkfordeleg;
 		}

 		result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_ns,
 					     0, 0, &rdataset, NULL);
 		if (result != ISC_R_SUCCESS) {
-			goto checkfords;
+			goto checkfordeleg;
 		}

 		/*
 		 * Remember bottom of zone due to NS.
 		 */
 		dns_name_copy(name, bottom);
+		have_ns = true;

+		/* Check glue correctness */
 		DNS_RDATASET_FOREACH(&rdataset) {
 			dns_rdata_t rdata = DNS_RDATA_INIT;
 			dns_rdataset_current(&rdataset, &rdata);
@@ -2405,15 +2404,85 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
 		}
 		dns_rdataset_disassociate(&rdataset);

+	checkfordeleg:
+		result = dns_db_findrdataset(db, node, NULL,
+					     dns_rdatatype_deleg, 0, 0,
+					     &rdataset, NULL);
+		if (result != ISC_R_SUCCESS) {
+			goto checkfords;
+		}
+
+		/* DELEG is not allowed at the apex and is class-IN only. */
+		if (dns_name_equal(name, &zone->origin)) {
+			dns_name_format(name, namebuf, sizeof(namebuf));
+			dns_zone_log(zone, ISC_LOG_ERROR,
+				     "DELEG at zone apex (%s)", namebuf);
+			dns_rdataset_disassociate(&rdataset);
+			ok = false;
+			goto checkfords;
+		} else if (zone->rdclass != dns_rdataclass_in) {
+			dns_zone_log(zone, ISC_LOG_ERROR, "DELEG: bad class");
+			ok = false;
+			goto checkfords;
+		}
+
+		/*
+		 * check the DELEG rrset for server-name and
+		 * include-delegparam paramters, and reject if they're
+		 * below the owner name.
+		 */
+		DNS_RDATASET_FOREACH(&rdataset) {
+			dns_rdata_t rdata = DNS_RDATA_INIT;
+			dns_rdataset_current(&rdataset, &rdata);
+			dns_fixedname_t fbad;
+			dns_name_t *bad = dns_fixedname_initname(&fbad);
+
+			if (!dns_rdata_checknames(&rdata, name, bad)) {
+				dns_name_format(bad, namebuf, sizeof(namebuf));
+				dns_zone_log(zone, ISC_LOG_ERROR,
+					     "DELEG: invalid target name '%s'",
+					     namebuf);
+				ok = false;
+				dns_rdataset_disassociate(&rdataset);
+				goto checkfords;
+			}
+		}
+
+		/*
+		 * Remember bottom of zone due to DELEG.
+		 */
+		dns_name_copy(name, bottom);
+		have_deleg = true;
+		dns_rdataset_disassociate(&rdataset);
+
+	checkfords:
 		/*
-		 * Check for deprecated DS digest types.
+		 * DS is only allowed at a delegation point (i.e.,
+		 * coexisting with NS or DELEG and not at the apex).
 		 */
 		result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_ds,
 					     0, 0, &rdataset, NULL);
 		if (result != ISC_R_SUCCESS) {
-			goto next;
+			goto checkfordname;
+		}
+
+		if (!have_ns && !have_deleg) {
+			int level = ISC_LOG_WARNING;
+			if (zone->type == dns_zone_primary) {
+				level = ISC_LOG_ERROR;
+				ok = false;
+			}
+			dns_name_format(name, namebuf, sizeof(namebuf));
+			dns_zone_log(zone, level,
+				     "DS not at delegation point (%s)",
+				     namebuf);
+			dns_rdataset_disassociate(&rdataset);
+			goto checkfordname;
 		}

+		/*
+		 * Check for deprecated DS digest types.
+		 */
 		memset(logged_algorithm, 0, sizeof(logged_algorithm));
 		memset(logged_digest_type, 0, sizeof(logged_digest_type));
 		DNS_RDATASET_FOREACH(&rdataset) {
@@ -2469,38 +2538,21 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
 		}
 		dns_rdataset_disassociate(&rdataset);

-		goto next;
-
-	checkfords:
-		result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_ds,
-					     0, 0, &rdataset, NULL);
-		if (result != ISC_R_SUCCESS) {
-			goto checkfordname;
-		}
-		dns_rdataset_disassociate(&rdataset);
-
-		if (zone->type == dns_zone_primary) {
-			level = ISC_LOG_ERROR;
-			ok = false;
-		} else {
-			level = ISC_LOG_WARNING;
-		}
-		dns_name_format(name, namebuf, sizeof(namebuf));
-		dns_zone_log(zone, level, "DS not at delegation point (%s)",
-			     namebuf);
-
 	checkfordname:
 		result = dns_db_findrdataset(db, node, NULL,
 					     dns_rdatatype_dname, 0, 0,
 					     &rdataset, NULL);
-		if (result == ISC_R_SUCCESS) {
-			/*
-			 * Remember bottom of zone due to DNAME.
-			 */
-			dns_name_copy(name, bottom);
-			dns_rdataset_disassociate(&rdataset);
+		if (result != ISC_R_SUCCESS) {
+			goto checkformx;
 		}

+		/*
+		 * Remember bottom of zone due to DNAME.
+		 */
+		dns_name_copy(name, bottom);
+		dns_rdataset_disassociate(&rdataset);
+
+	checkformx:
 		result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_mx,
 					     0, 0, &rdataset, NULL);
 		if (result != ISC_R_SUCCESS) {
@@ -2520,6 +2572,7 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {

 	checksrv:
 		if (zone->rdclass != dns_rdataclass_in) {
+			/* All the remaining tests are for class IN only */
 			goto next;
 		}
 		result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_srv,
@@ -2541,7 +2594,8 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {

 	checkforaaaa:
 		/*
-		 * Check if there is an A or AAAA RRset in the zone.
+		 * Check if there is an A or AAAA RRset anywhere in
+		 * the zone.
 		 */
 		if (!has_a) {
 			result = dns_db_findrdataset(db, node, NULL,
@@ -2572,7 +2626,7 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
 		if (zone->rdclass != dns_rdataclass_in) {
 			goto next;
 		}
-		have_spf = have_txt = false;
+
 		result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_spf,
 					     0, 0, &rdataset, NULL);
 		if (result == ISC_R_SUCCESS) {
@@ -4142,7 +4196,6 @@ zone_postload(dns_zone_t *zone, dns_db_t *db, isc_time_t loadtime,
 			dns_journal_destroy(&journal);
 		} else {
 			jserial = serial;
-			result = ISC_R_SUCCESS;
 		}

 		if (jserial != serial) {
@@ -4189,7 +4242,7 @@ zone_postload(dns_zone_t *zone, dns_db_t *db, isc_time_t loadtime,
 		if (nscount == 0 && DNS_ZONE_OPTION(zone, DNS_ZONEOPT_CHECKNS))
 		{
 			dns_zone_logc(zone, DNS_LOGCATEGORY_ZONELOAD,
-				      ISC_LOG_ERROR, "has no NS records");
+				      ISC_LOG_WARNING, "has no NS records");
 			if (DNS_ZONE_OPTION(zone, DNS_ZONEOPT_FATALNS)) {
 				RETERR(DNS_R_BADZONE);
 			}
@@ -6310,7 +6363,8 @@ check_if_bottom_of_zone(dns_db_t *db, dns_dbnode_t *node,
 			dns_dbversion_t *version, bool *is_bottom_of_zone) {
 	isc_result_t result;
 	dns_rdatasetiter_t *iterator = NULL;
-	bool seen_soa = false, seen_ns = false, seen_dname = false;
+	bool seen_soa = false, seen_ns = false;
+	bool seen_dname = false, seen_deleg = false;

 	REQUIRE(is_bottom_of_zone != NULL);

@@ -6332,6 +6386,9 @@ check_if_bottom_of_zone(dns_db_t *db, dns_dbnode_t *node,
 		case dns_rdatatype_ns:
 			seen_ns = true;
 			break;
+		case dns_rdatatype_deleg:
+			seen_deleg = true;
+			break;
 		case dns_rdatatype_dname:
 			seen_dname = true;
 			break;
@@ -6339,7 +6396,7 @@ check_if_bottom_of_zone(dns_db_t *db, dns_dbnode_t *node,
 		dns_rdataset_disassociate(&rdataset);
 	}

-	if ((seen_ns && !seen_soa) || seen_dname) {
+	if ((seen_ns && !seen_soa) || seen_dname || seen_deleg) {
 		*is_bottom_of_zone = true;
 	}

@@ -6355,6 +6412,7 @@ typedef struct seen {
 	bool nsec3;
 	bool ds;
 	bool dname;
+	bool deleg;
 } seen_t;

 static isc_result_t
@@ -6388,6 +6446,8 @@ allrdatasets(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version,
 			(*seen).nsec = true;
 		} else if (rdataset.type == dns_rdatatype_nsec3) {
 			(*seen).nsec3 = true;
+		} else if (rdataset.type == dns_rdatatype_deleg) {
+			(*seen).deleg = true;
 		}

 		dns_rdataset_disassociate(&rdataset);
@@ -6487,6 +6547,7 @@ sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name,
 		}

 		if (seen.ns && !seen.soa && rdataset.type != dns_rdatatype_ds &&
+		    rdataset.type != dns_rdatatype_deleg &&
 		    rdataset.type != dns_rdatatype_nsec)
 		{
 			continue;
@@ -15271,7 +15332,8 @@ zone_replacedb(dns_zone_t *zone, dns_db_t *db, bool dump) {
 		}
 		if (nscount == 0 && DNS_ZONE_OPTION(zone, DNS_ZONEOPT_CHECKNS))
 		{
-			dns_zone_log(zone, ISC_LOG_ERROR, "has no NS records");
+			dns_zone_log(zone, ISC_LOG_WARNING,
+				     "has no NS records");
 			if (DNS_ZONE_OPTION(zone, DNS_ZONEOPT_FATALNS)) {
 				RETERR(DNS_R_BADZONE);
 			}
@@ -15541,7 +15603,7 @@ again:
 			    DNS_ZONE_OPTION(zone, DNS_ZONEOPT_CHECKNS))
 			{
 				dns_zone_logc(zone, DNS_LOGCATEGORY_XFER_IN,
-					      ISC_LOG_ERROR,
+					      ISC_LOG_WARNING,
 					      "transferred zone "
 					      "has no NS records");
 				if (DNS_ZONE_OPTION(zone, DNS_ZONEOPT_FATALNS))
@@ -17183,7 +17245,9 @@ checkds_done(void *arg) {
 		}

 		ISC_LIST_FOREACH(name->list, rdataset, link) {
-			if (rdataset->type != dns_rdatatype_ds) {
+			if (rdataset->type != dns_rdatatype_ds &&
+			    rdataset->type != dns_rdatatype_deleg)
+			{
 				goto next;
 			}

diff --git a/lib/dns/zoneverify.c b/lib/dns/zoneverify.c
index 925a5059dc..576a7eec82 100644
--- a/lib/dns/zoneverify.c
+++ b/lib/dns/zoneverify.c
@@ -131,22 +131,31 @@ zoneverify_log_error(const vctx_t *vctx, const char *fmt, ...) {
 static bool
 is_delegation(const vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node,
 	      uint32_t *ttlp) {
-	dns_rdataset_t nsset;
+	dns_rdataset_t rdataset = DNS_RDATASET_INIT;
 	isc_result_t result;

 	if (dns_name_equal(name, vctx->origin)) {
 		return false;
 	}

-	dns_rdataset_init(&nsset);
 	result = dns_db_findrdataset(vctx->db, node, vctx->ver,
-				     dns_rdatatype_ns, 0, 0, &nsset, NULL);
-	if (dns_rdataset_isassociated(&nsset)) {
-		SET_IF_NOT_NULL(ttlp, nsset.ttl);
-		dns_rdataset_disassociate(&nsset);
+				     dns_rdatatype_ns, 0, 0, &rdataset, NULL);
+	if (result == ISC_R_SUCCESS && dns_rdataset_isassociated(&rdataset)) {
+		SET_IF_NOT_NULL(ttlp, rdataset.ttl);
+		dns_rdataset_disassociate(&rdataset);
+		return true;
 	}

-	return result == ISC_R_SUCCESS;
+	result = dns_db_findrdataset(vctx->db, node, vctx->ver,
+				     dns_rdatatype_deleg, 0, 0, &rdataset,
+				     NULL);
+	if (result == ISC_R_SUCCESS && dns_rdataset_isassociated(&rdataset)) {
+		SET_IF_NOT_NULL(ttlp, rdataset.ttl);
+		dns_rdataset_disassociate(&rdataset);
+		return true;
+	}
+
+	return false;
 }

 /*%
@@ -705,6 +714,11 @@ verifynsec3(const vctx_t *vctx, const dns_name_t *name,
 					     dns_rdatatype_nsec3, 0, 0,
 					     &rdataset, NULL);
 	}
+
+	/*
+	 * An OPT-OUT range may cover unsigned delegation points and
+	 * empty nodes, but any other node must have a matching NSEC3.
+	 */
 	if (result != ISC_R_SUCCESS &&
 	    (!delegation || !optout ||
 	     (!empty && dns_nsec_isset(types, dns_rdatatype_ds))))
@@ -918,6 +932,7 @@ verifynode(vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node,
 		 */
 		if (rdataset.type != dns_rdatatype_rrsig &&
 		    (!delegation || rdataset.type == dns_rdatatype_ds ||
+		     rdataset.type == dns_rdatatype_deleg ||
 		     rdataset.type == dns_rdatatype_nsec))
 		{
 			result = verifyset(vctx, &rdataset, name, node, dstkeys,
diff --git a/lib/isc/include/isc/result.h b/lib/isc/include/isc/result.h
index 477956595e..2668a4e7e2 100644
--- a/lib/isc/include/isc/result.h
+++ b/lib/isc/include/isc/result.h
@@ -169,7 +169,7 @@ typedef enum isc_result {
 	DNS_R_LAME,
 	DNS_R_UNEXPECTEDRCODE,
 	DNS_R_UNEXPECTEDOPCODE,
-	DNS_R_CHASEDSSERVERS,
+	DNS_R_CHASEPARENTSERVERS,
 	DNS_R_EMPTYNAME,
 	DNS_R_EMPTYWILD,
 	DNS_R_BADBITMAP,
diff --git a/lib/isc/result.c b/lib/isc/result.c
index 65c9fa884a..4e876382ad 100644
--- a/lib/isc/result.c
+++ b/lib/isc/result.c
@@ -172,7 +172,7 @@ static const char *description[ISC_R_NRESULTS] = {
 	[DNS_R_LAME] = "lame server detected",
 	[DNS_R_UNEXPECTEDRCODE] = "unexpected RCODE",
 	[DNS_R_UNEXPECTEDOPCODE] = "unexpected OPCODE",
-	[DNS_R_CHASEDSSERVERS] = "chase DS servers",
+	[DNS_R_CHASEPARENTSERVERS] = "chase parent servers",
 	[DNS_R_EMPTYNAME] = "empty name",
 	[DNS_R_EMPTYWILD] = "empty wild",
 	[DNS_R_BADBITMAP] = "bad bitmap",
@@ -408,7 +408,7 @@ static const char *identifier[ISC_R_NRESULTS] = {
 	[DNS_R_LAME] = "DNS_R_LAME",
 	[DNS_R_UNEXPECTEDRCODE] = "DNS_R_UNEXPECTEDRCODE",
 	[DNS_R_UNEXPECTEDOPCODE] = "DNS_R_UNEXPECTEDOPCODE",
-	[DNS_R_CHASEDSSERVERS] = "DNS_R_CHASEDSSERVERS",
+	[DNS_R_CHASEPARENTSERVERS] = "DNS_R_CHASEPARENTSERVERS",
 	[DNS_R_EMPTYNAME] = "DNS_R_EMPTYNAME",
 	[DNS_R_EMPTYWILD] = "DNS_R_EMPTYWILD",
 	[DNS_R_BADBITMAP] = "DNS_R_BADBITMAP",
diff --git a/lib/ns/include/ns/client.h b/lib/ns/include/ns/client.h
index d84a60117c..def34e60e3 100644
--- a/lib/ns/include/ns/client.h
+++ b/lib/ns/include/ns/client.h
@@ -179,6 +179,7 @@ struct ns_client {
 						      */
 				bool wantdnssec : 1; /*%< include dnssec records
 						      */
+				bool wantdeleg : 1;  /*%< DELEG ok */
 				bool wantnsid : 1; /*%< include nameserver ID */
 				bool badcookie : 1; /*%< bad/out-of-date cookie
 						     */
diff --git a/lib/ns/query.c b/lib/ns/query.c
index 649bfb0ae8..96b2769fd1 100644
--- a/lib/ns/query.c
+++ b/lib/ns/query.c
@@ -1468,7 +1468,7 @@ query_getdb(ns_client_t *client, dns_name_t *name, dns_rdatatype_t qtype,
 		isc_result_t tresult;

 		dns_clientinfomethods_init(&cm, ns_client_sourceip);
-		dns_clientinfo_init(&ci, client, NULL);
+		dns_clientinfo_init(&ci, client, NULL, &client->edectx);
 		dns_clientinfo_setecs(&ci, &client->inner.ecs);

 		tdbp = NULL;
@@ -1605,7 +1605,7 @@ query_additionalauthfind(dns_db_t *db, dns_dbversion_t *version,
 	isc_result_t result;

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * Since we are looking for authoritative data, we do not set
@@ -1745,7 +1745,7 @@ query_additional_cb(void *arg, const dns_name_t *name, dns_rdatatype_t qtype,
 	}

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * We treat type A additional section processing as if it
@@ -2165,7 +2165,7 @@ query_additional(query_ctx_t *qctx, dns_name_t *name,
 		}

 		dns_clientinfomethods_init(&cm, ns_client_sourceip);
-		dns_clientinfo_init(&ci, client, NULL);
+		dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 		dns_db_addglue(qctx->db, dbversion->version, name, rdataset,
 			       client->message, &cm, &ci);
@@ -2328,10 +2328,12 @@ query_zone_delegation_rrset(query_ctx_t *qctx, dns_linkedname_t **namep,
 		return;
 	}

-	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
-	dns_db_addglue(qctx->db, qctx->version, dns_name(mname), rdataset,
-		       client->message, &cm, &ci);
+	if (rdataset->type == dns_rdatatype_ns) {
+		dns_clientinfomethods_init(&cm, ns_client_sourceip);
+		dns_clientinfo_init(&ci, client, NULL, &client->edectx);
+		dns_db_addglue(qctx->db, qctx->version, dns_name(mname),
+			       rdataset, client->message, &cm, &ci);
+	}

 	CTRACE(ISC_LOG_DEBUG(3), "query_zone_delegation_rrset: done");
 }
@@ -2483,7 +2485,8 @@ stale_refresh_aftermath(ns_client_t *client, isc_result_t result) {
 		qctx_init(client, NULL, 0, &qctx);

 		dns_clientinfomethods_init(&cm, ns_client_sourceip);
-		dns_clientinfo_init(&ci, qctx.client, NULL);
+		dns_clientinfo_init(&ci, qctx.client, NULL,
+				    &qctx.client->edectx);
 		if (qctx.client->inner.haveecs) {
 			dns_clientinfo_setecs(&ci, &qctx.client->inner.ecs);
 		}
@@ -2913,7 +2916,7 @@ rpz_rrset_find(ns_client_t *client, dns_name_t *name, dns_rdatatype_t type,
 	}

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);
 	result = dns_db_findext(*dbp, name, version, type, options,
 				client->inner.now, found, &cm, &ci, *rdatasetp,
 				NULL);
@@ -3069,7 +3072,7 @@ rpz_find_p(ns_client_t *client, dns_name_t *self_name, dns_rdatatype_t qtype,
 	REQUIRE(foundname != NULL);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * Try to find either a CNAME or the type of record demanded by the
@@ -4254,7 +4257,7 @@ query_findclosestnsec3(dns_name_t *qname, dns_db_t *db,
 	dns_name_clone(qname, &name);
 	labels = dns_name_countlabels(&name);
 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * Map unknown algorithm to known value.
@@ -4443,7 +4446,7 @@ redirect(ns_client_t *client, dns_name_t *name, dns_rdataset_t *rdataset,
 	found = dns_fixedname_initname(&fixed);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);
 	dns_clientinfo_setecs(&ci, &client->inner.ecs);

 	if (client->inner.wantdnssec && *dbp != NULL && dns_db_iszone(*dbp) &&
@@ -4580,7 +4583,7 @@ redirect2(ns_client_t *client, dns_name_t *name, dns_rdataset_t *rdataset,
 	found = dns_fixedname_initname(&fixed);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);
 	dns_clientinfo_setecs(&ci, &client->inner.ecs);

 	if (client->inner.wantdnssec && *dbp != NULL && dns_db_iszone(*dbp) &&
@@ -5236,7 +5239,7 @@ ns__query_start(query_ctx_t *qctx) {
 			     qctx->qtype, qctx->options, &qctx->zone, &qctx->db,
 			     &qctx->version, &qctx->is_zone);
 	if ((result != ISC_R_SUCCESS || !qctx->is_zone) &&
-	    qctx->qtype == dns_rdatatype_ds &&
+	    dns_rdatatype_atparent(qctx->qtype) &&
 	    !qctx->client->query.recursionok && qctx->options.noexact)
 	{
 		/*
@@ -5565,7 +5568,7 @@ query_lookup(query_ctx_t *qctx) {
 	CALL_HOOK(NS_QUERY_LOOKUP_BEGIN, qctx);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, qctx->client, NULL);
+	dns_clientinfo_init(&ci, qctx->client, NULL, &qctx->client->edectx);
 	if (qctx->client->inner.haveecs) {
 		dns_clientinfo_setecs(&ci, &qctx->client->inner.ecs);
 	}
@@ -5615,6 +5618,13 @@ query_lookup(query_ctx_t *qctx) {
 		dboptions |= DNS_DBFIND_COVERINGNSEC;
 	}

+	/*
+	 * Client used the DE flag.
+	 */
+	if (qctx->client->inner.wantdeleg) {
+		dboptions |= DNS_DBFIND_DELEGOK;
+	}
+
 	foundname = dns_fixedname_initname(&qctx->foundname);

 	result = dns_db_findext(qctx->db, rpzqname, qctx->version, qctx->type,
@@ -7352,7 +7362,7 @@ query_respond_any(query_ctx_t *qctx) {
 	}

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, qctx->client, NULL);
+	dns_clientinfo_init(&ci, qctx->client, NULL, &qctx->client->edectx);
 	if (qctx->client->inner.haveecs) {
 		dns_clientinfo_setecs(&ci, &qctx->client->inner.ecs);
 	}
@@ -8475,7 +8485,7 @@ query_addds(query_ctx_t *qctx) {
 	lookupname = qctx_has_foundname(qctx) ? qctx_get_foundname(qctx) : name;
 	foundname = dns_fixedname_initname(&foundfixed);
 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);
 	if (client->inner.haveecs) {
 		dns_clientinfo_setecs(&ci, &client->inner.ecs);
 	}
@@ -8515,6 +8525,10 @@ query_addds(query_ctx_t *qctx) {
 	 */
 	MSG_SECTION_FOREACH(client->message, DNS_SECTION_AUTHORITY, rname) {
 		result = dns_message_findtype(rname, dns_rdatatype_ns, 0, NULL);
+		if (result != ISC_R_SUCCESS) {
+			result = dns_message_findtype(
+				rname, dns_rdatatype_deleg, 0, NULL);
+		}
 		if (result == ISC_R_SUCCESS) {
 			/*
 			 * Add the relevant RRset (DS or NSEC) to the
@@ -9473,7 +9487,7 @@ query_coveringnsec(query_ctx_t *qctx) {
 	nowild = dns_fixedname_initname(&fnowild);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, qctx->client, NULL);
+	dns_clientinfo_init(&ci, qctx->client, NULL, &qctx->client->edectx);

 	/*
 	 * All signer names must be the same to accept.
@@ -10186,7 +10200,7 @@ query_addsoa(query_ctx_t *qctx, unsigned int override_ttl,
 	CTRACE(ISC_LOG_DEBUG(3), "query_addsoa");

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * Don't add the SOA record for test which set "-T nosoa".
@@ -10323,7 +10337,7 @@ query_addns(query_ctx_t *qctx) {
 	fname = dns_fixedname_initname(&foundname);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * Get resources and make 'name' be the database origin.
@@ -10407,7 +10421,7 @@ query_addbestns(query_ctx_t *qctx) {
 	CTRACE(ISC_LOG_DEBUG(3), "query_addbestns");

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	dns_name_init(&qname);
 	dns_name_clone(client->query.qname, &qname);
@@ -10593,7 +10607,7 @@ query_addwildcardproof(query_ctx_t *qctx, dns_name_t *name, bool ispositive,
 	REQUIRE(name != NULL);

 	dns_clientinfomethods_init(&cm, ns_client_sourceip);
-	dns_clientinfo_init(&ci, client, NULL);
+	dns_clientinfo_init(&ci, client, NULL, &client->edectx);

 	/*
 	 * Get the NOQNAME proof then if !ispositive
@@ -11253,6 +11267,10 @@ ns_query_start(ns_client_t *client, isc_nmhandle_t *handle) {
 		client->inner.wantdnssec = true;
 	}

+	if ((client->inner.extflags & DNS_MESSAGEEXTFLAG_DE) != 0) {
+		client->inner.wantdeleg = true;
+	}
+
 	switch (client->inner.view->minimalresponses) {
 	case dns_minimal_no:
 		break;
diff --git a/lib/ns/update.c b/lib/ns/update.c
index 53c1525f9d..c9d6889dd9 100644
--- a/lib/ns/update.c
+++ b/lib/ns/update.c
@@ -530,7 +530,7 @@ foreach_rrset(dns_db_t *db, dns_dbversion_t *ver, const dns_name_t *name,
 	 * different from the current version
 	 */
 	dns_db_currentversion(db, &oldver);
-	dns_clientinfo_init(&ci, NULL, (ver != oldver) ? ver : NULL);
+	dns_clientinfo_init(&ci, NULL, (ver != oldver) ? ver : NULL, NULL);
 	dns_db_closeversion(db, &oldver, false);

 	node = NULL;
@@ -612,7 +612,7 @@ foreach_rr(dns_db_t *db, dns_dbversion_t *ver, const dns_name_t *name,
 	 * different from the current version
 	 */
 	dns_db_currentversion(db, &oldver);
-	dns_clientinfo_init(&ci, NULL, (ver != oldver) ? ver : NULL);
+	dns_clientinfo_init(&ci, NULL, (ver != oldver) ? ver : NULL, NULL);
 	dns_db_closeversion(db, &oldver, false);

 	if (type == dns_rdatatype_any) {
diff --git a/tests/dns/qpzone_test.c b/tests/dns/qpzone_test.c
index 58f94ddaee..a079680576 100644
--- a/tests/dns/qpzone_test.c
+++ b/tests/dns/qpzone_test.c
@@ -594,7 +594,7 @@ ISC_RUN_TEST_IMPL(wildcard_foundname) {
 	assert_null(db);
 }

-ISC_RUN_TEST_IMPL(wildcard_delegation_foundname) {
+ISC_RUN_TEST_IMPL(wildcard_no_delegation) {
 	isc_result_t result;
 	dns_db_t *db = NULL;
 	dns_dbversion_t *version = NULL;
@@ -610,6 +610,12 @@ ISC_RUN_TEST_IMPL(wildcard_delegation_foundname) {
 	assert_int_equal(result, ISC_R_SUCCESS);
 	assert_non_null(db);

+	/*
+	 * Create an NS record at a wildcard node, *.example.org, then
+	 * query for host.example.org/A. It should *not* return
+	 * DNS_R_DELEGATION. Delegation types at wildcard nodes
+	 * were previously undefined, but are now outright forbidden.
+	 */
 	dns_test_namefromstring("host.example.org.", &fqname);
 	dns_test_namefromstring("*.example.org.", &fwild);
 	qname = dns_fixedname_name(&fqname);
@@ -633,12 +639,8 @@ ISC_RUN_TEST_IMPL(wildcard_delegation_foundname) {
 	dns_db_currentversion(db, &version);
 	result = dns_db_find(db, qname, version, dns_rdatatype_a, 0, 0, found,
 			     &rdataset, NULL);
-	assert_int_equal(result, DNS_R_DELEGATION);
-	assert_true(dns_name_equal(found, wild));
-	assert_true(found->attributes.wildcard);
-	assert_true(dns_rdataset_isassociated(&rdataset));
+	assert_int_equal(result, DNS_R_NXRRSET);

-	dns_rdataset_disassociate(&rdataset);
 	dns_db_closeversion(db, &version, false);
 	dns_db_detach(&db);
 	assert_null(db);
@@ -909,7 +911,7 @@ ISC_TEST_ENTRY(resign_sooner_values)
 ISC_TEST_ENTRY(unscheduled_resign)
 ISC_TEST_ENTRY(diffop_add_sub)
 ISC_TEST_ENTRY(wildcard_foundname)
-ISC_TEST_ENTRY(wildcard_delegation_foundname)
+ISC_TEST_ENTRY(wildcard_no_delegation)
 ISC_TEST_ENTRY(nodes_outside_zone)
 ISC_TEST_ENTRY(diffop_addresign)
 ISC_TEST_LIST_END