Commit f1e50a0d93 for bind

commit f1e50a0d93c603ad178d8e16e4a4a769a76922e5
Author: Evan Hunt <each@isc.org>
Date:   Wed Sep 17 21:53:25 2025 -0700

    Support for DELEG/DELEGPARAM rrtypes

    Add support for the DELEG and DELEGPARAM rdata types, as
    specified in https://datatracker.ietf.org/doc/draft-ietf-deleg/.
    DELEG is assigned to type 61440, DELEGPARAM to 54. Both types
    are class-IN specific.

    In order to correctly handle parsing and printing of server-name
    lists containing commas, we use commatxt_fromtext() for parsing, and
    add a comma_name_totext() function to escape commas and backslashes
    correctly when converting DELEG rdata to text.

diff --git a/bin/tests/system/checkzone/tests.sh b/bin/tests/system/checkzone/tests.sh
index 10aaae0dad..115bffdb52 100644
--- a/bin/tests/system/checkzone/tests.sh
+++ b/bin/tests/system/checkzone/tests.sh
@@ -31,6 +31,9 @@ for db in zones/good*.db; do
     zones/good-rpz-prefix.db)
       $CHECKZONE -P -i local db $db >test.out.$n 2>&1 || ret=1
       ;;
+    zones/good-deleg-root.db)
+      $CHECKZONE -i local . $db >test.out.$n 2>&1 || ret=1
+      ;;
     *)
       $CHECKZONE -i local example $db >test.out.$n 2>&1 || ret=1
       ;;
@@ -134,7 +137,7 @@ for db in zones/bad*.db; do
       ;;
     zones/bad-svcb-mandatory.db)
       $CHECKZONE -P -i local db $db >test.out.$n 2>&1 || v=$?
-      grep "zones/bad-svcb-mandatory.db:6: near eol: disallowed (by application policy)" test.out.$n >/dev/null || ret=1
+      grep "zones/bad-svcb-mandatory.db:6: near eol: disallowed" test.out.$n >/dev/null || ret=1
       ;;
     zones/bad-svcb-servername.db)
       $CHECKZONE -k fail -i local 0.0.0.0.in-addr.arpa $db >test.out.$n 2>&1 || v=$?
diff --git a/bin/tests/system/checkzone/zones/bad-cname-and-type54.db b/bin/tests/system/checkzone/zones/bad-cname-and-type-540.db
similarity index 84%
rename from bin/tests/system/checkzone/zones/bad-cname-and-type54.db
rename to bin/tests/system/checkzone/zones/bad-cname-and-type-540.db
index c75ea512c4..d046b63a6f 100644
--- a/bin/tests/system/checkzone/zones/bad-cname-and-type54.db
+++ b/bin/tests/system/checkzone/zones/bad-cname-and-type-540.db
@@ -2,5 +2,5 @@ $TTL 600
 @		SOA	ns hostmaster 2011012708 3600 1200 604800 1200
 		NS	ns
 ns		A	192.0.2.1
-bad		TYPE54	\# 0
+bad		TYPE540	\# 0
 bad		CNAME	@
diff --git a/bin/tests/system/checkzone/zones/good-deleg-root.db b/bin/tests/system/checkzone/zones/good-deleg-root.db
new file mode 100644
index 0000000000..3730080ab1
--- /dev/null
+++ b/bin/tests/system/checkzone/zones/good-deleg-root.db
@@ -0,0 +1,33 @@
+; 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
+.			86400	IN	SOA	a.root-servers.nil.  hostmaster.root-servers.nil. 2025091700 1800 900 604800 86400
+.			240748	IN	NS	a.root-servers.nil.
+a.root-servers.nil. A 10.53.0.1
+
+example.   DELEG server-ipv4=192.0.2.1 server-ipv6=2001:DB8::1
+example.   DELEG server-name=ns2.example.net.,ns3.example.org.
+example.   DELEG include-delegparam=other.
+
+example.   NS    a.example.
+example.   NS    b.example.net.
+example.   NS    c.example.org.
+
+example.   DS    44444 13 2 FD03B2312C8F0FE72C1751EFA1007D743C94EC91594FF0047C23C37CE119BA0C
+
+; unsigned glue for legacy (NS) delegation
+a.example. A     192.0.2.1
+a.example. AAAA  2001:DB8::1
+
+other. DELEGPARAM server-ipv4=192.0.2.2
+other. DELEGPARAM server-ipv6=2001:DB8::2
+other. DELEGPARAM key123=foo
diff --git a/bin/tests/system/doth/example.axfr.good b/bin/tests/system/doth/example.axfr.good
index 1c67f07baf..cbd76c8737 100644
--- a/bin/tests/system/doth/example.axfr.good
+++ b/bin/tests/system/doth/example.axfr.good
@@ -2536,6 +2536,13 @@ cname02.example.	3600	IN	CNAME	cname-target.example.
 cname03.example.	3600	IN	CNAME	.
 csync01.example.	3600	IN	CSYNC	0 0 A NS AAAA
 csync02.example.	3600	IN	CSYNC	0 0
+deleg.example.		3600	IN	DELEG	server-ipv4=192.0.2.1 server-ipv6=2001:db8::1
+deleg.example.		3600	IN	DELEG   mandatory=server-ipv4 server-ipv4=192.0.2.2
+deleg.example.		3600	IN	DELEG	server-name=ns1.example.net.,ns2.example.org.
+deleg.example.		3600	IN	DELEG	key12345=abc
+delegparam.example.		3600	IN	DELEGPARAM	server-ipv4=192.0.2.1 server-ipv6=2001:db8::1
+delegparam.example.		3600	IN	DELEGPARAM	server-name=ns1.example.net.,ns2.example.org.
+delegparam.example.		3600	IN	DELEGPARAM	key321=xyz
 dhcid01.example.	3600	IN	DHCID	AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=
 dhcid02.example.	3600	IN	DHCID	AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=
 dhcid03.example.	3600	IN	DHCID	AAABxLmlskllE0MVjd57zHcWmEH3pCQ6VytcKD//7es/deY=
diff --git a/bin/tests/system/doth/example8.axfr.good b/bin/tests/system/doth/example8.axfr.good
index a474711cd5..effc3d84b0 100644
--- a/bin/tests/system/doth/example8.axfr.good
+++ b/bin/tests/system/doth/example8.axfr.good
@@ -2536,6 +2536,13 @@ cname02.example8.	3600	IN	CNAME	cname-target.example8.
 cname03.example8.	3600	IN	CNAME	.
 csync01.example8.	3600	IN	CSYNC	0 0 A NS AAAA
 csync02.example8.	3600	IN	CSYNC	0 0
+deleg.example8.		3600	IN	DELEG   mandatory=server-ipv4 server-ipv4=192.0.2.2
+deleg.example8.		3600	IN	DELEG	server-ipv4=192.0.2.1 server-ipv6=2001:db8::1
+deleg.example8.		3600	IN	DELEG	server-name=ns1.example.net.,ns2.example.org.
+deleg.example8.		3600	IN	DELEG	key12345=abc
+delegparam.example8.	3600	IN	DELEGPARAM	server-ipv4=192.0.2.1 server-ipv6=2001:db8::1
+delegparam.example8.	3600	IN	DELEGPARAM	server-name=ns1.example.net.,ns2.example.org.
+delegparam.example8.	3600	IN	DELEGPARAM	key321=xyz
 dhcid01.example8.	3600	IN	DHCID	AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=
 dhcid02.example8.	3600	IN	DHCID	AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=
 dhcid03.example8.	3600	IN	DHCID	AAABxLmlskllE0MVjd57zHcWmEH3pCQ6VytcKD//7es/deY=
diff --git a/bin/tests/system/genzone.sh b/bin/tests/system/genzone.sh
index d244f103ba..bc87435620 100644
--- a/bin/tests/system/genzone.sh
+++ b/bin/tests/system/genzone.sh
@@ -524,6 +524,15 @@ keydata			TYPE65533	\# 0
 keydata			TYPE65533	\# 6 010203040506
 keydata			TYPE65533	\# 18 010203040506010203040506010203040506

+; deleg and delegparam (61440 and 54)
+deleg			DELEG	server-name=ns1.example.net.,ns2.example.org.
+deleg			DELEG	server-ipv4=192.0.2.1 server-ipv6=2001:DB8::1
+deleg                   DELEG   server-ipv4=192.0.2.2 mandatory=server-ipv4
+deleg			DELEG	key12345=abc
+delegparam			DELEGPARAM	server-name=ns1.example.net.,ns2.example.org.
+delegparam			DELEGPARAM	server-ipv4=192.0.2.1 server-ipv6=2001:DB8::1
+delegparam			DELEGPARAM	key321=xyz
+
 ; type 65535 (reserved)

 EOF
diff --git a/bin/tests/system/masterformat/setup.sh b/bin/tests/system/masterformat/setup.sh
index 2737abc7ed..d76a648cbb 100755
--- a/bin/tests/system/masterformat/setup.sh
+++ b/bin/tests/system/masterformat/setup.sh
@@ -47,7 +47,7 @@ awk 'END {
 }' </dev/null >>ns1/above-limit.db

 cp ns1/empty.db.in ns1/255types.db
-for ntype in $(seq 65280 65534); do
+for ntype in $(seq 65280 65431) $(seq 65434 65534); do
   echo "m TYPE${ntype} \# 0"
 done >>ns1/255types.db
 echo "m TXT bunny" >>ns1/255types.db
diff --git a/bin/tests/system/reclimit/tests.sh b/bin/tests/system/reclimit/tests.sh
index 674c5c335a..d9cda0aa12 100644
--- a/bin/tests/system/reclimit/tests.sh
+++ b/bin/tests/system/reclimit/tests.sh
@@ -474,7 +474,7 @@ for ntype in $(seq 65280 65534); do
 done
 # Wait at least one second
 sleep 1
-for ntype in $(seq 65280 65534); do
+for ntype in $(seq 65280 65432) $(seq 65434 65534); do
   check_manytypes 2 manytypes.big "TYPE${ntype}" NOERROR manytypes.big "TYPE${ntype}" "" 120 || ret=1
 done

diff --git a/bin/tests/system/rrchecker/tests_rrchecker.py b/bin/tests/system/rrchecker/tests_rrchecker.py
index f3734d926f..8fc4a32cf6 100644
--- a/bin/tests/system/rrchecker/tests_rrchecker.py
+++ b/bin/tests/system/rrchecker/tests_rrchecker.py
@@ -44,6 +44,8 @@ pytestmark = pytest.mark.extra_artifacts(
                 "CERT",
                 "CNAME",
                 "CSYNC",
+                "DELEG",
+                "DELEGPARAM",
                 "DHCID",
                 "DLV",
                 "DNAME",
@@ -137,7 +139,7 @@ def run_rrchecker(option, rr_class, rr_type, rr_rest):

 @pytest.mark.parametrize(
     "option",
-    ["-p", "-u", "multi-line at class", " multi-line at type", "multi-line at data"],
+    ["-p", "-u", "multi-line at class", "multi-line at type", "multi-line at data"],
 )
 def test_rrchecker_conversions(option):
     tempzone_file = "tempzone"
diff --git a/bin/tests/system/xfer/response1.good b/bin/tests/system/xfer/response1.good
index 5d82b40405..e031deaad2 100644
--- a/bin/tests/system/xfer/response1.good
+++ b/bin/tests/system/xfer/response1.good
@@ -35,6 +35,13 @@ cname02.example. 3600 IN CNAME cname-target.example.
 cname03.example. 3600 IN CNAME .
 csync01.example. 3600 IN CSYNC 0 0 A NS AAAA
 csync02.example. 3600 IN CSYNC 0 0
+deleg.example. 3600 IN TYPE61440 \# 14 00000002000100010004c0000202
+deleg.example. 3600 IN TYPE61440 \# 28 00010004c00002010002001020010db8 000000000000000000000001
+deleg.example. 3600 IN TYPE61440 \# 38 00030022036e7331076578616d706c65 036e657400036e7332076578616d706c 65036f726700
+deleg.example. 3600 IN TYPE61440 \# 7 30390003616263
+delegparam.example. 3600 IN TYPE54 \# 28 00010004c00002010002001020010db8 000000000000000000000001
+delegparam.example. 3600 IN TYPE54 \# 38 00030022036e7331076578616d706c65 036e657400036e7332076578616d706c 65036f726700
+delegparam.example. 3600 IN TYPE54 \# 7 0141000378797a
 dhcid01.example. 3600 IN DHCID AAIBY2/AuCccgoJbsaxcQc9TUapptP69 lOjxfNuVAA2kjEA=
 dhcid02.example. 3600 IN DHCID AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQd WL3b/NaiUDlW2No=
 dhcid03.example. 3600 IN DHCID AAABxLmlskllE0MVjd57zHcWmEH3pCQ6 VytcKD//7es/deY=
diff --git a/bin/tests/system/xfer/response2.good b/bin/tests/system/xfer/response2.good
index 4ffb43a728..ac3b7ea47b 100644
--- a/bin/tests/system/xfer/response2.good
+++ b/bin/tests/system/xfer/response2.good
@@ -35,6 +35,13 @@ cname02.example. 3600 IN CNAME cname-target.example.
 cname03.example. 3600 IN CNAME .
 csync01.example. 3600 IN CSYNC 0 0 A NS AAAA
 csync02.example. 3600 IN CSYNC 0 0
+deleg.example. 3600 IN TYPE61440 \# 14 00000002000100010004c0000202
+deleg.example. 3600 IN TYPE61440 \# 28 00010004c00002010002001020010db8 000000000000000000000001
+deleg.example. 3600 IN TYPE61440 \# 38 00030022036e7331076578616d706c65 036e657400036e7332076578616d706c 65036f726700
+deleg.example. 3600 IN TYPE61440 \# 7 30390003616263
+delegparam.example. 3600 IN TYPE54 \# 28 00010004c00002010002001020010db8 000000000000000000000001
+delegparam.example. 3600 IN TYPE54 \# 38 00030022036e7331076578616d706c65 036e657400036e7332076578616d706c 65036f726700
+delegparam.example. 3600 IN TYPE54 \# 7 0141000378797a
 dhcid01.example. 3600 IN DHCID AAIBY2/AuCccgoJbsaxcQc9TUapptP69 lOjxfNuVAA2kjEA=
 dhcid02.example. 3600 IN DHCID AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQd WL3b/NaiUDlW2No=
 dhcid03.example. 3600 IN DHCID AAABxLmlskllE0MVjd57zHcWmEH3pCQ6 VytcKD//7es/deY=
diff --git a/lib/dns/db_p.h b/lib/dns/db_p.h
index a77acd0fda..f5b5b5ded5 100644
--- a/lib/dns/db_p.h
+++ b/lib/dns/db_p.h
@@ -109,6 +109,8 @@ prio_type(dns_typepair_t type) {
 	case DNS_SIGTYPEPAIR(dns_rdatatype_dname):
 	case dns_rdatatype_svcb:
 	case DNS_SIGTYPEPAIR(dns_rdatatype_svcb):
+	case dns_rdatatype_deleg:
+	case DNS_SIGTYPEPAIR(dns_rdatatype_deleg):
 	case dns_rdatatype_https:
 	case DNS_SIGTYPEPAIR(dns_rdatatype_https):
 	case dns_rdatatype_dnskey:
diff --git a/lib/dns/rdata.c b/lib/dns/rdata.c
index 13241a8ef6..d4dc6d4493 100644
--- a/lib/dns/rdata.c
+++ b/lib/dns/rdata.c
@@ -75,7 +75,8 @@
 #define ARGS_FROMTEXT                                           \
 	int rdclass, dns_rdatatype_t type, isc_lex_t *lexer,    \
 		const dns_name_t *origin, unsigned int options, \
-		isc_buffer_t *target, dns_rdatacallbacks_t *callbacks
+		isc_buffer_t *target,                           \
+		dns_rdatacallbacks_t *callbacks ISC_ATTR_UNUSED

 #define CALL_FROMTEXT rdclass, type, lexer, origin, options, target, callbacks

@@ -113,9 +114,9 @@

 #define CALL_DIGEST rdata, digest, arg

-#define ARGS_CHECKOWNER                                   \
-	const dns_name_t *name, dns_rdataclass_t rdclass, \
-		dns_rdatatype_t type, bool wildcard
+#define ARGS_CHECKOWNER                                                   \
+	const dns_name_t *name ISC_ATTR_UNUSED, dns_rdataclass_t rdclass, \
+		dns_rdatatype_t type, bool wildcard ISC_ATTR_UNUSED

 #define CALL_CHECKOWNER name, rdclass, type, wildcard

@@ -146,14 +147,15 @@ static isc_result_t
 txt_fromwire(isc_buffer_t *source, isc_buffer_t *target);

 static isc_result_t
-commatxt_fromtext(isc_textregion_t *source, bool comma, isc_buffer_t *target);
+commatxt_fromtext(isc_textregion_t *source, bool comma, bool isname,
+		  isc_buffer_t *target);

 static isc_result_t
 commatxt_totext(isc_region_t *source, bool quote, bool comma,
 		isc_buffer_t *target);

 static isc_result_t
-multitxt_totext(isc_region_t *source, isc_buffer_t *target);
+multitxt_totext(isc_region_t *source, bool quote, isc_buffer_t *target);

 static isc_result_t
 multitxt_fromtext(isc_textregion_t *source, isc_buffer_t *target);
@@ -303,6 +305,19 @@ generic_rdata_in_svcb_next(dns_rdata_in_svcb_t *);
 static void
 generic_rdata_in_svcb_current(dns_rdata_in_svcb_t *, isc_region_t *);

+static isc_result_t generic_fromtext_in_deleg(ARGS_FROMTEXT);
+static isc_result_t generic_totext_in_deleg(ARGS_TOTEXT);
+static isc_result_t generic_fromwire_in_deleg(ARGS_FROMWIRE);
+static isc_result_t generic_fromstruct_in_deleg(ARGS_FROMSTRUCT);
+static isc_result_t generic_tostruct_in_deleg(ARGS_TOSTRUCT);
+static bool generic_checknames_in_deleg(ARGS_CHECKNAMES);
+static isc_result_t
+generic_rdata_in_deleg_first(dns_rdata_in_deleg_t *);
+static isc_result_t
+generic_rdata_in_deleg_next(dns_rdata_in_deleg_t *);
+static void
+generic_rdata_in_deleg_current(dns_rdata_in_deleg_t *, isc_region_t *);
+
 /*% INT16 Size */
 #define NS_INT16SZ 2
 /*% IPv6 Address Size */
@@ -1617,7 +1632,8 @@ txt_totext(isc_region_t *source, bool quote, isc_buffer_t *target) {
 }

 static isc_result_t
-commatxt_fromtext(isc_textregion_t *source, bool comma, isc_buffer_t *target) {
+commatxt_fromtext(isc_textregion_t *source, bool comma, bool isname,
+		  isc_buffer_t *target) {
 	isc_region_t tregion;
 	bool escape = false, comma_escape = false, seen_comma = false;
 	unsigned int n, nrem;
@@ -1685,19 +1701,26 @@ commatxt_fromtext(isc_textregion_t *source, bool comma, isc_buffer_t *target) {
 		 * h1\\h2   =>	h1\h2   =>	h1h2
 		 * h1\\\\h2 =>	h1\\h2  =>	h1\h2
 		 */
-		if (comma && !comma_escape && c == ',') {
-			seen_comma = true;
-			break;
-		}
-		if (comma && !comma_escape && c == '\\') {
-			comma_escape = true;
-			continue;
+		if (comma && !comma_escape) {
+			if (c == ',') {
+				seen_comma = true;
+				break;
+			}
+			if (c == '\\') {
+				comma_escape = true;
+				continue;
+			}
 		}
 		comma_escape = false;
-		if (nrem == 0) {
+
+		if (nrem == (isname && c == '\\') ? 1 : 0) {
 			return (tregion.length <= 256U) ? ISC_R_NOSPACE
 							: DNS_R_SYNTAX;
 		}
+		if (isname && c == '\\') {
+			*t++ = c;
+			nrem--;
+		}
 		*t++ = c;
 		nrem--;
 	}
@@ -1738,7 +1761,7 @@ commatxt_fromtext(isc_textregion_t *source, bool comma, isc_buffer_t *target) {

 static isc_result_t
 txt_fromtext(isc_textregion_t *source, isc_buffer_t *target) {
-	return commatxt_fromtext(source, false, target);
+	return commatxt_fromtext(source, false, false, target);
 }

 static isc_result_t
@@ -1773,7 +1796,7 @@ txt_fromwire(isc_buffer_t *source, isc_buffer_t *target) {
  * Conversion of TXT-like rdata fields without length limits.
  */
 static isc_result_t
-multitxt_totext(isc_region_t *source, isc_buffer_t *target) {
+multitxt_totext(isc_region_t *source, bool quote, isc_buffer_t *target) {
 	unsigned int tl;
 	unsigned int n0, n;
 	unsigned char *sp;
@@ -1788,8 +1811,10 @@ multitxt_totext(isc_region_t *source, isc_buffer_t *target) {
 	if (tl < 1) {
 		return ISC_R_NOSPACE;
 	}
-	*tp++ = '"';
-	tl--;
+	if (quote) {
+		*tp++ = '"';
+		tl--;
+	}
 	do {
 		n = source->length;
 		n0 = source->length - 1;
@@ -1826,8 +1851,10 @@ multitxt_totext(isc_region_t *source, isc_buffer_t *target) {
 	if (tl < 1) {
 		return ISC_R_NOSPACE;
 	}
-	*tp++ = '"';
-	tl--;
+	if (quote) {
+		*tp++ = '"';
+		tl--;
+	}
 	POST(tl);
 	isc_buffer_add(target, (unsigned int)(tp - (char *)region.base));
 	return ISC_R_SUCCESS;
diff --git a/lib/dns/rdata/generic/caa_257.c b/lib/dns/rdata/generic/caa_257.c
index 1ec6522659..9d77603564 100644
--- a/lib/dns/rdata/generic/caa_257.c
+++ b/lib/dns/rdata/generic/caa_257.c
@@ -357,7 +357,7 @@ totext_caa(ARGS_TOTEXT) {
 	/*
 	 * Value
 	 */
-	RETERR(multitxt_totext(&region, target));
+	RETERR(multitxt_totext(&region, true, target));
 	return ISC_R_SUCCESS;
 }

diff --git a/lib/dns/rdata/generic/uri_256.c b/lib/dns/rdata/generic/uri_256.c
index 4aa55107ba..85e73a66a1 100644
--- a/lib/dns/rdata/generic/uri_256.c
+++ b/lib/dns/rdata/generic/uri_256.c
@@ -94,7 +94,7 @@ totext_uri(ARGS_TOTEXT) {
 	/*
 	 * Target URI
 	 */
-	RETERR(multitxt_totext(&region, target));
+	RETERR(multitxt_totext(&region, true, target));
 	return ISC_R_SUCCESS;
 }

diff --git a/lib/dns/rdata/in_1/deleg_61440.c b/lib/dns/rdata/in_1/deleg_61440.c
new file mode 100644
index 0000000000..14d6d1c03b
--- /dev/null
+++ b/lib/dns/rdata/in_1/deleg_61440.c
@@ -0,0 +1,1113 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include <isc/result.h>
+
+#define RRTYPE_DELEG_ATTRIBUTES                                            \
+	(DNS_RDATATYPEATTR_FOLLOWADDITIONAL | DNS_RDATATYPEATTR_ATPARENT | \
+	 DNS_RDATATYPEATTR_ZONECUTAUTH)
+
+/*
+ * Delegation Information Registry
+ */
+enum deleg_encoding {
+	deleg_unknown,
+	deleg_namelist,
+	deleg_ipv4,
+	deleg_ipv6,
+	deleg_keylist
+};
+
+enum deleg_type {
+	delegkey_mandatory = 0,
+	delegkey_ipv4 = 1,
+	delegkey_ipv6 = 2,
+	delegkey_name = 3,
+	delegkey_include = 4,
+};
+
+static const struct {
+	const char *name;
+	unsigned int value;
+	enum deleg_encoding encoding;
+	bool initial; /* Part of the first defined set of encodings. */
+} direg[] = {
+	{ "mandatory", delegkey_mandatory, deleg_keylist, true },
+	{ "server-ipv4", delegkey_ipv4, deleg_ipv4, true },
+	{ "server-ipv6", delegkey_ipv6, deleg_ipv6, true },
+	{ "server-name", delegkey_name, deleg_namelist, true },
+	{ "include-delegparam", delegkey_include, deleg_namelist, true },
+};
+
+static int
+deleg_keycmp(const void *a1, const void *a2) {
+	const unsigned char *u1 = a1, *u2 = a2;
+	if (*u1 != *u2) {
+		return *u1 - *u2;
+	}
+	return *(++u1) - *(++u2);
+}
+
+static isc_result_t
+deleg_sortkeylist(isc_buffer_t *target, unsigned int used) {
+	isc_region_t region;
+
+	isc_buffer_usedregion(target, &region);
+	isc_region_consume(&region, used);
+	INSIST(region.length > 0U);
+	qsort(region.base, region.length / 2, 2, deleg_keycmp);
+	/* Reject duplicates. */
+	while (region.length >= 4) {
+		if (region.base[0] == region.base[2] &&
+		    region.base[1] == region.base[3])
+		{
+			return DNS_R_SYNTAX;
+		}
+		isc_region_consume(&region, 2);
+	}
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+deleg_validate(uint16_t key, isc_region_t *region) {
+	size_t i, j;
+
+	for (i = 0; i < ARRAY_SIZE(direg); i++) {
+		if (direg[i].value == key) {
+			switch (direg[i].encoding) {
+			case deleg_ipv4:
+				if ((region->length % 4) != 0 ||
+				    region->length == 0)
+				{
+					return DNS_R_FORMERR;
+				}
+				break;
+			case deleg_ipv6:
+				if ((region->length % 16) != 0 ||
+				    region->length == 0)
+				{
+					return DNS_R_FORMERR;
+				}
+				break;
+			case deleg_namelist:
+				if (region->length == 0) {
+					return DNS_R_FORMERR;
+				}
+				break;
+			case deleg_keylist: {
+				if ((region->length % 2) != 0 ||
+				    region->length == 0)
+				{
+					return DNS_R_FORMERR;
+				}
+				/* In order? */
+				while (region->length >= 4) {
+					if (region->base[0] > region->base[2] ||
+					    (region->base[0] ==
+						     region->base[2] &&
+					     region->base[1] >=
+						     region->base[3]))
+					{
+						return DNS_R_FORMERR;
+					}
+					isc_region_consume(region, 2);
+				}
+				break;
+			}
+			case deleg_unknown:
+				for (j = 0; j < region->length; j++) {
+					if (!islower(region->base[j]) &&
+					    !isdigit(region->base[j]) &&
+					    region->base[j] != '-')
+					{
+						return DNS_R_FORMERR;
+					}
+				}
+				break;
+			default:
+				UNREACHABLE();
+			}
+		}
+	}
+	return ISC_R_SUCCESS;
+}
+
+/*
+ * Parse keyname from region.
+ */
+static isc_result_t
+deleg_keyfromregion(isc_textregion_t *region, char sep, uint16_t *value,
+		    isc_buffer_t *target) {
+	char *e = NULL;
+	size_t i;
+	unsigned long ul;
+
+	/* Look for known key names.  */
+	for (i = 0; i < ARRAY_SIZE(direg); i++) {
+		size_t len = strlen(direg[i].name);
+		if (strncasecmp(region->base, direg[i].name, len) != 0) {
+			continue;
+		}
+
+		INSIST(region->length >= len);
+		if (region->length != len && region->base[len] != sep) {
+			continue;
+		}
+		isc_textregion_consume(region, len);
+		ul = direg[i].value;
+		goto finish;
+	}
+	/* Handle keyXXXXX form. */
+	if (strncmp(region->base, "key", 3) != 0) {
+		return DNS_R_SYNTAX;
+	}
+	isc_textregion_consume(region, 3);
+	/* Disallow [+-]XXXXX which is allowed by strtoul. */
+	if (region->length == 0 || *region->base == '-' || *region->base == '+')
+	{
+		return DNS_R_SYNTAX;
+	}
+	/* No zero padding. */
+	if (region->length > 1 && *region->base == '0' &&
+	    region->base[1] != '=')
+	{
+		return DNS_R_SYNTAX;
+	}
+	ul = strtoul(region->base, &e, 10);
+	/* Valid number? */
+	if (e == region->base || (*e != sep && *e != 0)) {
+		return DNS_R_SYNTAX;
+	}
+	if (ul > 0xffff) {
+		return ISC_R_RANGE;
+	}
+	isc_textregion_consume(region, e - region->base);
+finish:
+	if (sep == ',' && region->length == 1) {
+		return DNS_R_SYNTAX;
+	}
+	/* Consume separator. */
+	if (region->length != 0) {
+		isc_textregion_consume(region, 1);
+	}
+	RETERR(uint16_tobuffer(ul, target));
+	SET_IF_NOT_NULL(value, ul);
+	return ISC_R_SUCCESS;
+}
+
+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) {
+	isc_textregion_t src = *region;
+
+	if (origin == NULL) {
+		origin = dns_rootname;
+	}
+
+	do {
+		char buf[DNS_NAME_FORMATSIZE];
+		isc_buffer_t b;
+		dns_fixedname_t fn;
+		dns_name_t *name = dns_fixedname_initname(&fn);
+
+		isc_buffer_init(&b, buf, sizeof(buf));
+		RETERR(commatxt_fromtext(&src, true, true, &b));
+
+		/* skip the leading length byte */
+		isc_buffer_forward(&b, 1);
+
+		RETERR(dns_name_fromtext(name, &b, origin, options));
+		RETERR(dns_name_towire(name, NULL, target));
+
+		/* check for valid hostnames */
+		if ((options & DNS_RDATA_CHECKNAMES) == 0 ||
+		    dns_name_ishostname(name, false))
+		{
+			continue;
+		}
+
+		if ((options & DNS_RDATA_CHECKNAMESFAIL) != 0) {
+			RETERR(DNS_R_BADNAME);
+		}
+		if (callbacks != NULL) {
+			warn_badname(name, lexer, callbacks);
+		}
+	} while (src.length != 0);
+
+	isc_textregion_consume(region, region->length);
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+deleg_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) {
+	char *e = NULL;
+	char abuf[16];
+	char tbuf[sizeof("aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:255.255.255.255,")];
+	isc_buffer_t sb;
+	isc_region_t keyregion;
+	size_t len;
+	uint16_t key;
+	unsigned int i;
+	unsigned int used;
+
+	for (i = 0; i < ARRAY_SIZE(direg); i++) {
+		len = strlen(direg[i].name);
+		if (strncmp(region->base, direg[i].name, len) != 0 ||
+		    (region->base[len] != 0 && region->base[len] != '='))
+		{
+			continue;
+		}
+
+		if (region->base[len] == '=') {
+			len++;
+		}
+
+		RETERR(uint16_tobuffer(direg[i].value, target));
+		isc_textregion_consume(region, len);
+
+		sb = *target;
+		RETERR(uint16_tobuffer(0, target)); /* dummy length */
+
+		switch (direg[i].encoding) {
+		case deleg_ipv4:
+			do {
+				snprintf(tbuf, sizeof(tbuf), "%*s",
+					 (int)(region->length), region->base);
+				e = strchr(tbuf, ',');
+				if (e != NULL) {
+					*e++ = 0;
+					isc_textregion_consume(region,
+							       e - tbuf);
+				}
+				if (inet_pton(AF_INET, tbuf, abuf) != 1) {
+					return DNS_R_SYNTAX;
+				}
+				mem_tobuffer(target, abuf, 4);
+			} while (e != NULL);
+			break;
+		case deleg_ipv6:
+			do {
+				snprintf(tbuf, sizeof(tbuf), "%*s",
+					 (int)(region->length), region->base);
+				e = strchr(tbuf, ',');
+				if (e != NULL) {
+					*e++ = 0;
+					isc_textregion_consume(region,
+							       e - tbuf);
+				}
+				if (inet_pton(AF_INET6, tbuf, abuf) != 1) {
+					return DNS_R_SYNTAX;
+				}
+				mem_tobuffer(target, abuf, 16);
+			} while (e != NULL);
+			break;
+		case deleg_namelist:
+			RETERR(servname_fromtxt(region, origin, options, lexer,
+						callbacks, target));
+			break;
+		case deleg_keylist:
+			if (region->length == 0) {
+				return DNS_R_SYNTAX;
+			}
+			used = isc_buffer_usedlength(target);
+			while (region->length != 0) {
+				RETERR(deleg_keyfromregion(region, ',', NULL,
+							   target));
+			}
+			RETERR(deleg_sortkeylist(target, used));
+			break;
+		case deleg_unknown:
+		default:
+			UNREACHABLE();
+		}
+
+		len = isc_buffer_usedlength(target) -
+		      isc_buffer_usedlength(&sb) - 2;
+		RETERR(uint16_tobuffer(len, &sb)); /* actual length */
+		return ISC_R_SUCCESS;
+	}
+
+	RETERR(deleg_keyfromregion(region, '=', &key, target));
+	if (region->length == 0) {
+		RETERR(uint16_tobuffer(0, target)); /* length */
+		/* Sanity check keyXXXXX form. */
+		keyregion.base = isc_buffer_used(target);
+		keyregion.length = 0;
+		return deleg_validate(key, &keyregion);
+	}
+	sb = *target;
+	RETERR(uint16_tobuffer(0, target)); /* dummy length */
+	RETERR(multitxt_fromtext(region, target));
+	len = isc_buffer_usedlength(target) - isc_buffer_usedlength(&sb) - 2;
+	RETERR(uint16_tobuffer(len, &sb)); /* length */
+	/* Sanity check keyXXXXX form. */
+	keyregion.base = isc_buffer_used(&sb);
+	keyregion.length = len;
+	return deleg_validate(key, &keyregion);
+}
+
+static isc_result_t
+deleg_sortkeys(isc_buffer_t *target, unsigned int used) {
+	isc_region_t r1, r2, man = { .base = NULL, .length = 0 };
+	bool have_address = false, have_name = false;
+	unsigned char buf[1024];
+	uint16_t mankey = 0;
+
+	if (isc_buffer_usedlength(target) == used) {
+		return ISC_R_SUCCESS;
+	}
+
+	/*
+	 * Get the parameters into r1.
+	 */
+	isc_buffer_usedregion(target, &r1);
+	isc_region_consume(&r1, used);
+
+	while (1) {
+		uint16_t key1, len1, key2, len2;
+		unsigned char *base1, *base2;
+
+		r2 = r1;
+
+		/*
+		 * Get the first parameter.
+		 */
+		base1 = r1.base;
+		key1 = uint16_fromregion(&r1);
+		isc_region_consume(&r1, 2);
+		len1 = uint16_fromregion(&r1);
+		isc_region_consume(&r1, 2);
+		isc_region_consume(&r1, len1);
+
+		/*
+		 * Was there only one key left?
+		 */
+		if (r1.length == 0) {
+			if (mankey != 0) {
+				/* Is this the last mandatory key? */
+				if (key1 != mankey || man.length != 0) {
+					return DNS_R_INCONSISTENTRR;
+				}
+			} else if (key1 == 0) {
+				/* Lone mandatory field. */
+				return DNS_R_DISALLOWED;
+			}
+
+			/* Both name and address are present. */
+			if ((have_address && key1 == delegkey_name) ||
+			    (have_name &&
+			     (key1 == delegkey_ipv4 || key1 == delegkey_ipv6)))
+			{
+				return DNS_R_DISALLOWED;
+			}
+
+			return ISC_R_SUCCESS;
+		}
+
+		/*
+		 * Find the smallest parameter.
+		 */
+		while (r1.length != 0) {
+			base2 = r1.base;
+			key2 = uint16_fromregion(&r1);
+			isc_region_consume(&r1, 2);
+			len2 = uint16_fromregion(&r1);
+			isc_region_consume(&r1, 2);
+			isc_region_consume(&r1, len2);
+			if (key2 == key1) {
+				return DNS_R_DUPLICATE;
+			}
+			if (key2 < key1) {
+				base1 = base2;
+				key1 = key2;
+				len1 = len2;
+			}
+		}
+
+		/*
+		 * Do we need to move the smallest parameter to the start?
+		 */
+		if (base1 != r2.base) {
+			size_t offset = 0;
+			size_t bytes = len1 + 4;
+			size_t length = base1 - r2.base;
+
+			/*
+			 * Move the smallest parameter to the start.
+			 */
+			while (bytes > 0) {
+				size_t count;
+
+				if (bytes > sizeof(buf)) {
+					count = sizeof(buf);
+				} else {
+					count = bytes;
+				}
+				memmove(buf, base1, count);
+				memmove(r2.base + offset + count,
+					r2.base + offset, length);
+				memmove(r2.base + offset, buf, count);
+				base1 += count;
+				bytes -= count;
+				offset += count;
+			}
+		}
+
+		/*
+		 * Check key against mandatory key list.
+		 */
+		if (mankey != 0) {
+			if (key1 > mankey) {
+				return DNS_R_INCONSISTENTRR;
+			}
+			if (key1 == mankey) {
+				if (man.length >= 2) {
+					mankey = uint16_fromregion(&man);
+					isc_region_consume(&man, 2);
+				} else {
+					mankey = 0;
+				}
+			}
+		}
+
+		/*
+		 * Is this the mandatory key?
+		 */
+		if (key1 == 0) {
+			man = r2;
+			man.length = len1 + 4;
+			isc_region_consume(&man, 4);
+			if (man.length < 2) {
+				return DNS_R_SYNTAX;
+			}
+
+			mankey = uint16_fromregion(&man);
+			isc_region_consume(&man, 2);
+			if (mankey == 0) {
+				return DNS_R_DISALLOWED;
+			}
+		}
+
+		/*
+		 * Remember if this was a name or address field.
+		 */
+		switch (key1) {
+		case delegkey_ipv4:
+		case delegkey_ipv6:
+			have_address = true;
+			break;
+		case delegkey_name:
+			have_name = true;
+			break;
+		default:
+			break;
+		}
+
+		/*
+		 * Consume the smallest parameter.
+		 */
+		isc_region_consume(&r2, len1 + 4);
+		r1 = r2;
+	}
+}
+
+static isc_result_t
+generic_fromtext_in_deleg(ARGS_FROMTEXT) {
+	isc_token_t token;
+	unsigned int used;
+	bool eolok = false;
+
+	UNUSED(type);
+	UNUSED(rdclass);
+
+	/*
+	 * DelegInfos
+	 */
+	used = isc_buffer_usedlength(target);
+	while (1) {
+		RETERR(isc_lex_getmastertoken(lexer, &token,
+					      isc_tokentype_qvpair, eolok));
+		if (token.type == isc_tokentype_eol ||
+		    token.type == isc_tokentype_eof)
+		{
+			isc_lex_ungettoken(lexer, &token);
+			return deleg_sortkeys(target, used);
+		}
+
+		if (token.type != isc_tokentype_string && /* key only */
+		    token.type != isc_tokentype_qvpair &&
+		    token.type != isc_tokentype_vpair)
+		{
+			RETTOK(DNS_R_SYNTAX);
+		}
+		RETTOK(deleg_fromtext(&token.value.as_textregion, origin,
+				      options, lexer, callbacks, target));
+		eolok = true;
+	}
+}
+
+static isc_result_t
+comma_name_totext(const dns_name_t *name, isc_buffer_t *target) {
+	char in[DNS_NAME_FORMATSIZE];
+	size_t len = isc_buffer_availablelength(target);
+
+	dns_name_format(name, in, sizeof(in));
+
+	for (char *sp = in; *sp != '\0'; sp++) {
+		if (*sp == ',') {
+			if (len < 3) {
+				return ISC_R_NOSPACE;
+			}
+			isc_buffer_putstr(target, "\\\\,");
+		} else if (*sp == '\\') {
+			if (len < 2) {
+				return ISC_R_NOSPACE;
+			}
+			isc_buffer_putstr(target, "\\\\");
+		} else {
+			if (len < 1) {
+				return ISC_R_NOSPACE;
+			}
+			isc_buffer_putuint8(target, *sp);
+		}
+		len--;
+	}
+
+	if (len < 1) {
+		return ISC_R_NOSPACE;
+	}
+	isc_buffer_putuint8(target, '.');
+
+	return ISC_R_SUCCESS;
+}
+
+static inline int
+param_index(unsigned short value) {
+	for (size_t i = 0; i < ARRAY_SIZE(direg); i++) {
+		if (direg[i].value == value) {
+			return (int)i;
+		}
+	}
+	return -1;
+}
+
+static inline enum deleg_encoding
+param_encoding(int i) {
+	if (i >= 0) {
+		return direg[i].encoding;
+	}
+	return deleg_unknown;
+}
+
+static const char *
+deleg_paramkey(unsigned short value, char *buf, size_t len) {
+	int i = param_index(value);
+
+	if (i >= 0) {
+		return direg[i].name;
+	} else {
+		int n = snprintf(buf, len, "key%u", value);
+		INSIST(n > 0 && (unsigned int)n < len);
+		return buf;
+	}
+}
+
+static isc_result_t
+generic_totext_in_deleg(ARGS_TOTEXT) {
+	isc_region_t region;
+	char buf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255")];
+	bool compat = (tctx->flags & DNS_STYLEFLAG_SVCPARAMKEYCOMPAT) != 0;
+	bool first = true;
+
+	REQUIRE(rdata->length != 0);
+
+	dns_rdata_toregion(rdata, &region);
+
+	while (region.length > 0) {
+		enum deleg_encoding encoding = deleg_unknown;
+		uint16_t num;
+		isc_region_t r;
+
+		if (!first) {
+			RETERR(str_totext(" ", target));
+		}
+		first = false;
+
+		INSIST(region.length >= 2);
+		num = uint16_fromregion(&region);
+		isc_region_consume(&region, 2);
+
+		int i = param_index(num);
+		if (i >= 0 && ((direg[i].initial) || !compat)) {
+			encoding = param_encoding(i);
+		}
+		RETERR(str_totext(deleg_paramkey(num, buf, sizeof(buf)),
+				  target));
+
+		INSIST(region.length >= 2);
+		num = uint16_fromregion(&region);
+		isc_region_consume(&region, 2);
+
+		INSIST(region.length >= num);
+		r = region;
+		r.length = num;
+		isc_region_consume(&region, num);
+		if (num == 0) {
+			continue;
+		}
+		RETERR(str_totext("=", target));
+		switch (encoding) {
+		case deleg_ipv4:
+			while (r.length > 0U) {
+				INSIST(r.length >= 4U);
+				inet_ntop(AF_INET, r.base, buf, sizeof(buf));
+				RETERR(str_totext(buf, target));
+				isc_region_consume(&r, 4);
+				if (r.length != 0U) {
+					RETERR(str_totext(",", target));
+				}
+			}
+			break;
+		case deleg_ipv6:
+			while (r.length > 0U) {
+				INSIST(r.length >= 16U);
+				inet_ntop(AF_INET6, r.base, buf, sizeof(buf));
+				RETERR(str_totext(buf, target));
+				isc_region_consume(&r, 16);
+				if (r.length != 0U) {
+					RETERR(str_totext(",", target));
+				}
+			}
+			break;
+		case deleg_namelist:
+			while (r.length > 0U) {
+				dns_fixedname_t fn;
+				dns_name_t *name = dns_fixedname_initname(&fn);
+				isc_buffer_t b;
+
+				isc_buffer_init(&b, r.base, r.length);
+				isc_buffer_add(&b, r.length);
+				isc_buffer_setactive(&b, r.length);
+				RETERR(dns_name_fromwire(name, &b, NULL, NULL));
+				RETERR(comma_name_totext(name, target));
+
+				isc_region_consume(
+					&r, isc_buffer_consumedlength(&b));
+				if (r.length != 0U) {
+					RETERR(str_totext(",", target));
+				}
+			}
+			break;
+		case deleg_keylist:
+			while (r.length > 0) {
+				num = uint16_fromregion(&r);
+				isc_region_consume(&r, 2);
+				RETERR(str_totext(
+					deleg_paramkey(num, buf, sizeof(buf)),
+					target));
+				if (r.length != 0) {
+					RETERR(str_totext(",", target));
+				}
+			}
+			break;
+		case deleg_unknown:
+			RETERR(multitxt_totext(&r, false, target));
+			break;
+		default:
+			UNREACHABLE();
+		}
+	}
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+generic_fromwire_in_deleg(ARGS_FROMWIRE) {
+	isc_region_t region, man = { .base = NULL, .length = 0 };
+	uint16_t lastkey = 0, mankey = 0;
+	bool first = true;
+
+	UNUSED(type);
+	UNUSED(rdclass);
+	UNUSED(dctx);
+
+	/*
+	 * DelegInfos
+	 */
+	isc_buffer_activeregion(source, &region);
+	if (region.length < 2) {
+		return ISC_R_UNEXPECTEDEND;
+	}
+
+	while (region.length > 0U) {
+		isc_region_t keyregion;
+		uint16_t key, len;
+
+		/*
+		 * DelegInfoKey
+		 */
+		if (region.length < 2U) {
+			return ISC_R_UNEXPECTEDEND;
+		}
+		RETERR(mem_tobuffer(target, region.base, 2));
+		key = uint16_fromregion(&region);
+		isc_region_consume(&region, 2);
+
+		/*
+		 * Keys must be unique and in order.
+		 */
+		if (!first && key <= lastkey) {
+			return DNS_R_FORMERR;
+		}
+
+		first = false;
+		lastkey = key;
+
+		/*
+		 * Check mandatory keys.
+		 */
+		if (mankey != 0) {
+			/* Missing mandatory key? */
+			if (key > mankey) {
+				return DNS_R_FORMERR;
+			}
+			if (key == mankey) {
+				/* Get next mandatory key. */
+				if (man.length >= 2) {
+					mankey = uint16_fromregion(&man);
+					isc_region_consume(&man, 2);
+				} else {
+					mankey = 0;
+				}
+			}
+		}
+
+		/*
+		 * DelegInfoValue length.
+		 */
+		if (region.length < 2U) {
+			return ISC_R_UNEXPECTEDEND;
+		}
+		RETERR(mem_tobuffer(target, region.base, 2));
+		len = uint16_fromregion(&region);
+		isc_region_consume(&region, 2);
+
+		/*
+		 * DelegInfoValue.
+		 */
+		if (region.length < len) {
+			return ISC_R_UNEXPECTEDEND;
+		}
+
+		/*
+		 * Remember manatory key.
+		 */
+		if (key == delegkey_mandatory) {
+			man = region;
+			man.length = len;
+			/* Get first mandatory key */
+			if (man.length >= 2) {
+				mankey = uint16_fromregion(&man);
+				isc_region_consume(&man, 2);
+				if (mankey == delegkey_mandatory) {
+					return DNS_R_FORMERR;
+				}
+			} else {
+				return DNS_R_FORMERR;
+			}
+		}
+
+		keyregion = region;
+		keyregion.length = len;
+		RETERR(deleg_validate(key, &keyregion));
+		RETERR(mem_tobuffer(target, region.base, len));
+		isc_region_consume(&region, len);
+		isc_buffer_forward(source, len + 4);
+	}
+
+	/*
+	 * Do we have an outstanding mandatory key?
+	 */
+	if (mankey != 0) {
+		return DNS_R_FORMERR;
+	}
+
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+generic_fromstruct_in_deleg(ARGS_FROMSTRUCT) {
+	dns_rdata_in_deleg_t *deleg = source;
+
+	REQUIRE(deleg != NULL);
+	REQUIRE(deleg->common.rdtype == type);
+	REQUIRE(deleg->common.rdclass == rdclass);
+
+	UNUSED(type);
+	UNUSED(rdclass);
+
+	return mem_tobuffer(target, deleg->dinfo, deleg->dinfolen);
+}
+
+static isc_result_t
+generic_tostruct_in_deleg(ARGS_TOSTRUCT) {
+	isc_region_t region;
+	dns_rdata_in_deleg_t *deleg = target;
+
+	REQUIRE(deleg != NULL);
+	REQUIRE(rdata->length != 0);
+
+	DNS_RDATACOMMON_INIT(deleg, rdata->type, rdata->rdclass);
+
+	dns_rdata_toregion(rdata, &region);
+
+	deleg->dinfolen = region.length;
+	deleg->dinfo = region.base;
+
+	deleg->offset = 0;
+
+	return ISC_R_SUCCESS;
+}
+
+static bool
+generic_checknames_in_deleg(ARGS_CHECKNAMES) {
+	isc_region_t region;
+
+	dns_rdata_toregion(rdata, &region);
+	while (region.length > 0) {
+		uint16_t key = uint16_fromregion(&region);
+
+		isc_region_consume(&region, 2);
+
+		/* get the DelegInfo lenth */
+		uint16_t len = uint16_fromregion(&region);
+		isc_region_consume(&region, 2);
+
+		isc_region_t r = { .base = region.base, .length = len };
+		isc_region_consume(&region, len);
+
+		if (param_encoding(param_index(key)) != deleg_namelist) {
+			continue;
+		}
+
+		while (r.length > 0U) {
+			dns_name_t name = DNS_NAME_INITEMPTY;
+
+			dns_name_fromregion(&name, &r);
+			isc_region_consume(&r, name.length);
+
+			if (bad != NULL) {
+				dns_name_clone(&name, bad);
+			}
+			if (!dns_name_ishostname(&name, false)) {
+				return false;
+			}
+			if (dns_name_issubdomain(&name, owner)) {
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
+static isc_result_t
+generic_rdata_in_deleg_first(dns_rdata_in_deleg_t *deleg) {
+	if (deleg->dinfolen == 0) {
+		return ISC_R_NOMORE;
+	}
+	deleg->offset = 0;
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+generic_rdata_in_deleg_next(dns_rdata_in_deleg_t *deleg) {
+	isc_region_t region;
+	size_t len;
+
+	if (deleg->offset >= deleg->dinfolen) {
+		return ISC_R_NOMORE;
+	}
+
+	region.base = deleg->dinfo + deleg->offset;
+	region.length = deleg->dinfolen - deleg->offset;
+	INSIST(region.length >= 4);
+	isc_region_consume(&region, 2);
+	len = uint16_fromregion(&region);
+	INSIST(region.length >= len + 2);
+	deleg->offset += len + 4;
+	return deleg->offset >= deleg->dinfolen ? ISC_R_NOMORE : ISC_R_SUCCESS;
+}
+
+static void
+generic_rdata_in_deleg_current(dns_rdata_in_deleg_t *deleg,
+			       isc_region_t *region) {
+	size_t len;
+
+	INSIST(deleg->offset <= deleg->dinfolen);
+
+	region->base = deleg->dinfo + deleg->offset;
+	region->length = deleg->dinfolen - deleg->offset;
+	INSIST(region->length >= 4);
+	isc_region_consume(region, 2);
+	len = uint16_fromregion(region);
+	INSIST(region->length >= len + 2);
+	region->base = deleg->dinfo + deleg->offset;
+	region->length = len + 4;
+}
+
+static isc_result_t
+fromtext_in_deleg(ARGS_FROMTEXT) {
+	REQUIRE(type == dns_rdatatype_deleg);
+	REQUIRE(rdclass == dns_rdataclass_in);
+
+	return generic_fromtext_in_deleg(CALL_FROMTEXT);
+}
+
+static isc_result_t
+totext_in_deleg(ARGS_TOTEXT) {
+	REQUIRE(rdata->type == dns_rdatatype_deleg);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+	REQUIRE(rdata->length != 0);
+
+	return generic_totext_in_deleg(CALL_TOTEXT);
+}
+
+static isc_result_t
+fromwire_in_deleg(ARGS_FROMWIRE) {
+	REQUIRE(type == dns_rdatatype_deleg);
+	REQUIRE(rdclass == dns_rdataclass_in);
+
+	return generic_fromwire_in_deleg(CALL_FROMWIRE);
+}
+
+static int
+compare_in_deleg(ARGS_COMPARE) {
+	isc_region_t region1;
+	isc_region_t region2;
+
+	REQUIRE(rdata1->type == rdata2->type);
+	REQUIRE(rdata1->rdclass == rdata2->rdclass);
+	REQUIRE(rdata1->type == dns_rdatatype_deleg);
+	REQUIRE(rdata1->rdclass == dns_rdataclass_in);
+	REQUIRE(rdata1->length != 0);
+	REQUIRE(rdata2->length != 0);
+
+	dns_rdata_toregion(rdata1, &region1);
+	dns_rdata_toregion(rdata2, &region2);
+
+	return isc_region_compare(&region1, &region2);
+}
+
+static isc_result_t
+fromstruct_in_deleg(ARGS_FROMSTRUCT) {
+	dns_rdata_in_deleg_t *deleg = source;
+
+	REQUIRE(type == dns_rdatatype_deleg);
+	REQUIRE(rdclass == dns_rdataclass_in);
+	REQUIRE(deleg != NULL);
+	REQUIRE(deleg->common.rdtype == type);
+	REQUIRE(deleg->common.rdclass == rdclass);
+
+	return generic_fromstruct_in_deleg(CALL_FROMSTRUCT);
+}
+
+static isc_result_t
+tostruct_in_deleg(ARGS_TOSTRUCT) {
+	dns_rdata_in_deleg_t *deleg = target;
+
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+	REQUIRE(rdata->type == dns_rdatatype_deleg);
+	REQUIRE(deleg != NULL);
+	REQUIRE(rdata->length != 0);
+
+	return generic_tostruct_in_deleg(CALL_TOSTRUCT);
+}
+
+static isc_result_t
+additionaldata_in_deleg(ARGS_ADDLDATA) {
+	REQUIRE(rdata->type == dns_rdatatype_deleg);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+
+	UNUSED(rdata);
+	UNUSED(owner);
+	UNUSED(add);
+	UNUSED(arg);
+
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+digest_in_deleg(ARGS_DIGEST) {
+	isc_region_t region1;
+
+	REQUIRE(rdata->type == dns_rdatatype_deleg);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+
+	dns_rdata_toregion(rdata, &region1);
+	return (digest)(arg, &region1);
+}
+
+static bool
+checkowner_in_deleg(ARGS_CHECKOWNER) {
+	REQUIRE(type == dns_rdatatype_deleg);
+	REQUIRE(rdclass == dns_rdataclass_in);
+
+	return true;
+}
+
+static bool
+checknames_in_deleg(ARGS_CHECKNAMES) {
+	REQUIRE(rdata->type == dns_rdatatype_deleg);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+
+	return generic_checknames_in_deleg(CALL_CHECKNAMES);
+}
+
+static int
+casecompare_in_deleg(ARGS_COMPARE) {
+	return compare_in_deleg(rdata1, rdata2);
+}
+
+isc_result_t
+dns_rdata_in_deleg_first(dns_rdata_in_deleg_t *deleg) {
+	REQUIRE(deleg != NULL);
+	REQUIRE(deleg->common.rdtype == dns_rdatatype_deleg);
+	REQUIRE(deleg->common.rdclass == dns_rdataclass_in);
+
+	return generic_rdata_in_deleg_first(deleg);
+}
+
+isc_result_t
+dns_rdata_in_deleg_next(dns_rdata_in_deleg_t *deleg) {
+	REQUIRE(deleg != NULL);
+	REQUIRE(deleg->common.rdtype == dns_rdatatype_deleg);
+	REQUIRE(deleg->common.rdclass == dns_rdataclass_in);
+
+	return generic_rdata_in_deleg_next(deleg);
+}
+
+void
+dns_rdata_in_deleg_current(dns_rdata_in_deleg_t *deleg, isc_region_t *region) {
+	REQUIRE(deleg != NULL);
+	REQUIRE(deleg->common.rdtype == dns_rdatatype_deleg);
+	REQUIRE(deleg->common.rdclass == dns_rdataclass_in);
+	REQUIRE(region != NULL);
+
+	generic_rdata_in_deleg_current(deleg, region);
+}
diff --git a/lib/dns/rdata/in_1/deleg_61440.h b/lib/dns/rdata/in_1/deleg_61440.h
new file mode 100644
index 0000000000..0070b5600f
--- /dev/null
+++ b/lib/dns/rdata/in_1/deleg_61440.h
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/*!
+ *  \brief Per draft-wesplaap-deleg-01
+ */
+
+#include <isc/region.h>
+#include <isc/result.h>
+
+typedef struct dns_rdata_in_deleg {
+	dns_rdatacommon_t common;
+	isc_mem_t *mctx;
+	unsigned char *dinfo;
+	uint16_t dinfolen;
+	uint16_t offset;
+} dns_rdata_in_deleg_t;
+
+isc_result_t
+dns_rdata_in_deleg_first(dns_rdata_in_deleg_t *);
+
+isc_result_t
+dns_rdata_in_deleg_next(dns_rdata_in_deleg_t *);
+
+void
+dns_rdata_in_deleg_current(dns_rdata_in_deleg_t *, isc_region_t *);
diff --git a/lib/dns/rdata/in_1/delegparam_54.c b/lib/dns/rdata/in_1/delegparam_54.c
new file mode 100644
index 0000000000..e54d09ab6f
--- /dev/null
+++ b/lib/dns/rdata/in_1/delegparam_54.c
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include <isc/result.h>
+
+#define RRTYPE_DELEGPARAM_ATTRIBUTES (DNS_RDATATYPEATTR_FOLLOWADDITIONAL)
+
+static isc_result_t
+fromtext_in_delegparam(ARGS_FROMTEXT) {
+	REQUIRE(type == dns_rdatatype_delegparam);
+	REQUIRE(rdclass == dns_rdataclass_in);
+
+	return generic_fromtext_in_deleg(CALL_FROMTEXT);
+}
+
+static isc_result_t
+totext_in_delegparam(ARGS_TOTEXT) {
+	REQUIRE(rdata->type == dns_rdatatype_delegparam);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+	REQUIRE(rdata->length != 0);
+
+	return generic_totext_in_deleg(CALL_TOTEXT);
+}
+
+static isc_result_t
+fromwire_in_delegparam(ARGS_FROMWIRE) {
+	REQUIRE(type == dns_rdatatype_delegparam);
+	REQUIRE(rdclass == dns_rdataclass_in);
+
+	return generic_fromwire_in_deleg(CALL_FROMWIRE);
+}
+
+static int
+compare_in_delegparam(ARGS_COMPARE) {
+	isc_region_t region1;
+	isc_region_t region2;
+
+	REQUIRE(rdata1->type == rdata2->type);
+	REQUIRE(rdata1->rdclass == rdata2->rdclass);
+	REQUIRE(rdata1->type == dns_rdatatype_delegparam);
+	REQUIRE(rdata1->rdclass == dns_rdataclass_in);
+	REQUIRE(rdata1->length != 0);
+	REQUIRE(rdata2->length != 0);
+
+	dns_rdata_toregion(rdata1, &region1);
+	dns_rdata_toregion(rdata2, &region2);
+
+	return isc_region_compare(&region1, &region2);
+}
+
+static isc_result_t
+fromstruct_in_delegparam(ARGS_FROMSTRUCT) {
+	dns_rdata_in_delegparam_t *delegparam = source;
+
+	REQUIRE(type == dns_rdatatype_delegparam);
+	REQUIRE(rdclass == dns_rdataclass_in);
+	REQUIRE(delegparam != NULL);
+	REQUIRE(delegparam->common.rdtype == type);
+	REQUIRE(delegparam->common.rdclass == rdclass);
+
+	return generic_fromstruct_in_deleg(CALL_FROMSTRUCT);
+}
+
+static isc_result_t
+tostruct_in_delegparam(ARGS_TOSTRUCT) {
+	dns_rdata_in_delegparam_t *delegparam = target;
+
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+	REQUIRE(rdata->type == dns_rdatatype_delegparam);
+	REQUIRE(delegparam != NULL);
+	REQUIRE(rdata->length != 0);
+
+	return generic_tostruct_in_deleg(CALL_TOSTRUCT);
+}
+
+static isc_result_t
+additionaldata_in_delegparam(ARGS_ADDLDATA) {
+	REQUIRE(rdata->type == dns_rdatatype_delegparam);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+
+	UNUSED(rdata);
+	UNUSED(owner);
+	UNUSED(add);
+	UNUSED(arg);
+
+	return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+digest_in_delegparam(ARGS_DIGEST) {
+	isc_region_t region1;
+
+	REQUIRE(rdata->type == dns_rdatatype_delegparam);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+
+	dns_rdata_toregion(rdata, &region1);
+	return (digest)(arg, &region1);
+}
+
+static bool
+checkowner_in_delegparam(ARGS_CHECKOWNER) {
+	REQUIRE(type == dns_rdatatype_delegparam);
+	REQUIRE(rdclass == dns_rdataclass_in);
+
+	return true;
+}
+
+static bool
+checknames_in_delegparam(ARGS_CHECKNAMES) {
+	REQUIRE(rdata->type == dns_rdatatype_delegparam);
+	REQUIRE(rdata->rdclass == dns_rdataclass_in);
+
+	return generic_checknames_in_deleg(CALL_CHECKNAMES);
+}
+
+static int
+casecompare_in_delegparam(ARGS_COMPARE) {
+	return compare_in_delegparam(rdata1, rdata2);
+}
+
+isc_result_t
+dns_rdata_in_delegparam_first(dns_rdata_in_delegparam_t *delegparam) {
+	REQUIRE(delegparam != NULL);
+	REQUIRE(delegparam->common.rdtype == dns_rdatatype_delegparam);
+	REQUIRE(delegparam->common.rdclass == dns_rdataclass_in);
+
+	return generic_rdata_in_deleg_first(delegparam);
+}
+
+isc_result_t
+dns_rdata_in_delegparam_next(dns_rdata_in_delegparam_t *delegparam) {
+	REQUIRE(delegparam != NULL);
+	REQUIRE(delegparam->common.rdtype == dns_rdatatype_delegparam);
+	REQUIRE(delegparam->common.rdclass == dns_rdataclass_in);
+
+	return generic_rdata_in_deleg_next(delegparam);
+}
+
+void
+dns_rdata_in_delegparam_current(dns_rdata_in_delegparam_t *delegparam,
+				isc_region_t *region) {
+	REQUIRE(delegparam != NULL);
+	REQUIRE(delegparam->common.rdtype == dns_rdatatype_delegparam);
+	REQUIRE(delegparam->common.rdclass == dns_rdataclass_in);
+	REQUIRE(region != NULL);
+
+	generic_rdata_in_deleg_current(delegparam, region);
+}
diff --git a/lib/dns/rdata/in_1/delegparam_54.h b/lib/dns/rdata/in_1/delegparam_54.h
new file mode 100644
index 0000000000..f8b8b6129a
--- /dev/null
+++ b/lib/dns/rdata/in_1/delegparam_54.h
@@ -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.
+ */
+
+#pragma once
+
+#include <isc/region.h>
+#include <isc/result.h>
+
+typedef struct dns_rdata_in_deleg dns_rdata_in_delegparam_t;
+
+isc_result_t
+dns_rdata_in_delegparam_first(dns_rdata_in_delegparam_t *);
+
+isc_result_t
+dns_rdata_in_delegparam_next(dns_rdata_in_delegparam_t *);
+
+void
+dns_rdata_in_delegparam_current(dns_rdata_in_delegparam_t *, isc_region_t *);
diff --git a/lib/dns/rdata/in_1/meson.build b/lib/dns/rdata/in_1/meson.build
index 8e2341286a..4964b16fa2 100644
--- a/lib/dns/rdata/in_1/meson.build
+++ b/lib/dns/rdata/in_1/meson.build
@@ -20,6 +20,10 @@ dns_header_depfiles += files(
     'apl_42.h',
     'atma_34.c',
     'atma_34.h',
+    'deleg_61440.c',
+    'deleg_61440.h',
+    'delegparam_54.c',
+    'delegparam_54.h',
     'dhcid_49.c',
     'dhcid_49.h',
     'eid_31.c',
diff --git a/lib/dns/rdata/in_1/svcb_64.c b/lib/dns/rdata/in_1/svcb_64.c
index c11afacc43..c556439645 100644
--- a/lib/dns/rdata/in_1/svcb_64.c
+++ b/lib/dns/rdata/in_1/svcb_64.c
@@ -58,7 +58,7 @@ static isc_result_t
 alpn_fromtxt(isc_textregion_t *source, isc_buffer_t *target) {
 	isc_textregion_t source0 = *source;
 	do {
-		RETERR(commatxt_fromtext(&source0, true, target));
+		RETERR(commatxt_fromtext(&source0, true, false, target));
 	} while (source0.length != 0);
 	return ISC_R_SUCCESS;
 }
@@ -715,7 +715,7 @@ generic_totext_in_svcb(ARGS_TOTEXT) {
 		switch (encoding) {
 		case sbpr_text:
 		case sbpr_dohpath:
-			RETERR(multitxt_totext(&r, target));
+			RETERR(multitxt_totext(&r, true, target));
 			break;
 		case sbpr_port:
 			num = uint16_fromregion(&r);
diff --git a/lib/isc/result.c b/lib/isc/result.c
index fcbfdcce48..65c9fa884a 100644
--- a/lib/isc/result.c
+++ b/lib/isc/result.c
@@ -98,7 +98,7 @@ static const char *description[ISC_R_NRESULTS] = {
 	[DNS_R_UNKNOWN] = "unknown class/type",
 	[DNS_R_BADLABELTYPE] = "bad label type",
 	[DNS_R_BADPOINTER] = "bad compression pointer",
-	[DNS_R_DISALLOWED] = "disallowed (by application policy)",
+	[DNS_R_DISALLOWED] = "disallowed",
 	[DNS_R_EXTRATOKEN] = "extra input text",
 	[DNS_R_EXTRADATA] = "extra input data",
 	[DNS_R_TEXTTOOLONG] = "text too long",
@@ -183,7 +183,7 @@ static const char *description[ISC_R_NRESULTS] = {
 	[DNS_R_UNKNOWNCOMMAND] = "unknown command",
 	[DNS_R_COVERINGNSEC] = "covering NSEC record returned",
 	[DNS_R_MXISADDRESS] = "MX is an address",
-	[DNS_R_DUPLICATE] = "duplicate query",
+	[DNS_R_DUPLICATE] = "duplicate",
 	[DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name",
 	[DNS_R_NOTPRIMARY] = "not primary",
 	[DNS_R_BROKENCHAIN] = "broken trust chain",
diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c
index c7e3c1a5df..af5271dc45 100644
--- a/tests/dns/rdata_test.c
+++ b/tests/dns/rdata_test.c
@@ -3897,6 +3897,135 @@ ISC_RUN_TEST_IMPL(decompression_hop_limit) {
 	}
 }

+ISC_RUN_TEST_IMPL(deleg) {
+	/*
+	 * Known keys: mandatory, server-ipv4, server-ipv6,
+	 *             server-name, include-delegparam
+	 */
+	text_ok_t text_ok[] = {
+		/* unknown key invalid */
+		TEXT_INVALID("unknown="),
+		/* minimal record */
+		TEXT_INVALID(""),
+		/* server-ipv4 */
+		TEXT_VALID("server-ipv4=1.2.3.4"),
+		TEXT_VALID("server-ipv4=1.2.3.4,2.3.4.5,3.4.5.6"),
+		TEXT_INVALID("server-ipv4=dead::beef"),
+		TEXT_INVALID("server-ipv4=xxxxx"), TEXT_INVALID("server-ipv4="),
+		/* server-ipv6 */
+		TEXT_VALID("server-ipv6=dead::beef"),
+		TEXT_VALID("server-ipv6=dead::beef,beef::dead"),
+		TEXT_INVALID("server-ipv6=1.2.3.4"),
+		TEXT_INVALID("server-ipv6=xxxxx"), TEXT_INVALID("server-ipv6="),
+		/* both */
+		TEXT_VALID("server-ipv4=1.2.3.4 server-ipv6=dead::beef"),
+		TEXT_VALID_CHANGED("server-ipv6=dead::beef server-ipv4=1.2.3.4",
+				   "server-ipv4=1.2.3.4 "
+				   "server-ipv6=dead::beef"),
+		TEXT_VALID("server-ipv4=1.2.3.4,4.3.2.1 "
+			   "server-ipv6=dead::beef,beef::dead"),
+		TEXT_INVALID("server-ipv6=1.2.3.4 server-ipv4=dead::beef"),
+		/* server-name */
+		TEXT_VALID("server-name=ns.example.com."),
+		TEXT_VALID_CHANGED("server-name=ns.example.com",
+				   "server-name=ns.example.com."),
+		TEXT_INVALID("server-name=...."), TEXT_VALID("server-name===."),
+		TEXT_VALID_CHANGED(
+			"server-name=\"part1,part2,part3\\\\,part4\\\\\\\\\"",
+			"server-name=part1.,part2.,part3\\\\,part4\\\\\\\\."),
+		/* server-name mixed with server addresses */
+		TEXT_INVALID("server-ipv4=1.2.3.4 server-name=example."),
+		TEXT_INVALID("server-name=example. server-ipv4=1.2.3.4"),
+		/* include-delegparam */
+		TEXT_VALID("include-delegparam=foo."),
+		TEXT_VALID("server-ipv4=1.2.3.4 include-delegparam=foo."),
+		TEXT_VALID("server-ipv6=dead::beef include-delegparam=foo."),
+		TEXT_VALID("server-name=ns.foo. include-delegparam=bar."),
+		TEXT_VALID_CHANGED("include-delegparam=bar. "
+				   "server-name=ns.foo.",
+				   "server-name=ns.foo. "
+				   "include-delegparam=bar."),
+		/* mandatory */
+		TEXT_VALID("mandatory=server-ipv4 server-ipv4=1.2.3.4"),
+		TEXT_VALID_CHANGED("server-ipv4=1.2.3.4 mandatory=server-ipv4",
+				   "mandatory=server-ipv4 server-ipv4=1.2.3.4"),
+		TEXT_INVALID("mandatory=server-ipv6 server-ipv4=1.2.3.4"),
+		TEXT_INVALID("mandatory=server-ipv4 server-name=example."),
+		TEXT_INVALID("mandatory=mandatory"),
+		/* unknown keys, including key-only forms */
+		TEXT_VALID("key123=foo"), TEXT_VALID("key123"),
+		TEXT_VALID("mandatory=key123 key122 key123"),
+		TEXT_VALID("mandatory=key123 key123=foo"),
+		TEXT_VALID("mandatory=key123 key123"),
+		TEXT_INVALID("mandatory=key1234 key123=foo"),
+		TEXT_INVALID("mandatory=key1234 key123"), TEXT_SENTINEL()
+	};
+	wire_ok_t wire_ok[] = {
+		/* too short */
+		WIRE_INVALID(0x00, 0x00),
+		/* mandatory=mandatory*/
+		WIRE_INVALID(0x00, 0x00, 0x00, 0x02, 0x00, 0x00),
+		/* mandatory=key32 key32 */
+		WIRE_VALID(0x00, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x20, 0x00,
+			   0x00),
+		/* mandatory=key32 key33 */
+		WIRE_INVALID(0x00, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x21,
+			     0x00, 0x00),
+		/* mandatory=key32 */
+		WIRE_INVALID(0x00, 0x00, 0x00, 0x02, 0x00, 0x20),
+		/* mandatory=key32 key31 key32 */
+		WIRE_VALID(0x00, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x19, 0x00,
+			   0x00, 0x00, 0x20, 0x00, 0x00),
+		/*
+		 * mandatory=key10,key11,key12
+		 * key10=1 key11=2 key12=3
+		 */
+		WIRE_VALID(0x00, 0x00, 0x00, 0x06, 0x00, 0x0A, 0x00, 0x0B, 0x00,
+			   0x0C, 0x00, 0x0A, 0x00, 0x01, 0x31, 0x00, 0x0B, 0x00,
+			   0x01, 0x32, 0x00, 0x0C, 0x00, 0x01, 0x33),
+		/*
+		 * mandatory=key10,key11,key12
+		 * key11=2 key12=3
+		 */
+		WIRE_INVALID(0x00, 0x00, 0x00, 0x06, 0x00, 0x0A, 0x00, 0x0B,
+			     0x00, 0x0C, 0x00, 0x0B, 0x00, 0x01, 0x32, 0x00,
+			     0x0C, 0x00, 0x01, 0x33),
+		/*
+		 * mandatory=key10,key11,key12
+		 * key10=1 key12=3
+		 */
+		WIRE_INVALID(0x00, 0x00, 0x00, 0x06, 0x00, 0x0A, 0x00, 0x0B,
+			     0x00, 0x0C, 0x00, 0x0A, 0x00, 0x01, 0x31, 0x00,
+			     0x0C, 0x00, 0x01, 0x33),
+		/*
+		 * mandatory=key10,key11,key12
+		 * key10=1 key11=2
+		 */
+		WIRE_INVALID(0x00, 0x00, 0x00, 0x06, 0x00, 0x0A, 0x00, 0x0B,
+			     0x00, 0x0C, 0x00, 0x0A, 0x00, 0x01, 0x31, 0x00,
+			     0x0B, 0x00, 0x01, 0x32),
+
+		WIRE_SENTINEL()
+	};
+	textvsunknown_t textvsunknown[] = {
+		/* server-ipv4=1.2.3.4 */
+		{ "server-ipv4=1.2.3.4", "\\# 8 0001000401020304" },
+
+		{ NULL, NULL }
+	};
+
+	check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in,
+		    dns_rdatatype_deleg, sizeof(dns_rdata_in_deleg_t));
+	check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in,
+		    dns_rdatatype_delegparam,
+		    sizeof(dns_rdata_in_delegparam_t));
+
+	check_textvsunknown(textvsunknown, dns_rdataclass_in,
+			    dns_rdatatype_deleg);
+	check_textvsunknown(textvsunknown, dns_rdataclass_in,
+			    dns_rdatatype_delegparam);
+}
+
 ISC_RUN_TEST_IMPL(atcname) {
 	unsigned int i;

@@ -3930,6 +4059,7 @@ ISC_RUN_TEST_IMPL(atparent) {
 		bool tf = dns_rdatatype_atparent((dns_rdatatype_t)i);
 		switch (i) {
 		case dns_rdatatype_ds:
+		case dns_rdatatype_deleg:
 			if (!tf) {
 				print_message(UNR, i);
 			}
@@ -3954,6 +4084,7 @@ ISC_RUN_TEST_IMPL(iszonecutauth) {
 		switch (i) {
 		case dns_rdatatype_ns:
 		case dns_rdatatype_ds:
+		case dns_rdatatype_deleg:
 		case dns_rdatatype_nsec:
 		case dns_rdatatype_rrsig:
 			if (!tf) {
@@ -4094,6 +4225,7 @@ ISC_TEST_ENTRY(uri)
 ISC_TEST_ENTRY(wallet)
 ISC_TEST_ENTRY(wks)
 ISC_TEST_ENTRY(zonemd)
+ISC_TEST_ENTRY(deleg)

 /* other tests */
 ISC_TEST_ENTRY(edns_client_subnet)