Browse Source

chore(api),feat(api): dnspython upgrade, support L32/L64/LP/NID

Peter Thomassen 3 years ago
parent
commit
f57e5db9ab

+ 10 - 38
api/desecapi/dns.py

@@ -6,7 +6,7 @@ from ipaddress import IPv6Address
 import dns
 import dns
 import dns.name
 import dns.name
 import dns.rdtypes.txtbase, dns.rdtypes.svcbbase
 import dns.rdtypes.txtbase, dns.rdtypes.svcbbase
-import dns.rdtypes.ANY.CDS, dns.rdtypes.ANY.DLV, dns.rdtypes.ANY.DS, dns.rdtypes.ANY.MX, dns.rdtypes.ANY.NS
+import dns.rdtypes.ANY.CERT, dns.rdtypes.ANY.MX, dns.rdtypes.ANY.NS
 import dns.rdtypes.IN.AAAA, dns.rdtypes.IN.SRV
 import dns.rdtypes.IN.AAAA, dns.rdtypes.IN.SRV
 
 
 
 
@@ -25,6 +25,15 @@ dns.rdtypes.svcbbase.MandatoryParam.to_text = _strip_quotes_decorator(dns.rdtype
 dns.rdtypes.svcbbase.PortParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.PortParam.to_text)
 dns.rdtypes.svcbbase.PortParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.PortParam.to_text)
 
 
 
 
+@dns.immutable.immutable
+class CERT(dns.rdtypes.ANY.CERT.CERT):
+    def to_text(self, origin=None, relativize=True, **kw):
+        certificate_type = str(self.certificate_type)  # upstream implementation calls _ctype_to_text
+        return "%s %d %s %s" % (certificate_type, self.key_tag,
+                                dns.dnssec.algorithm_to_text(self.algorithm),
+                                dns.rdata._base64ify(self.certificate, **kw))
+
+
 @dns.immutable.immutable
 @dns.immutable.immutable
 class AAAA(dns.rdtypes.IN.AAAA.AAAA):
 class AAAA(dns.rdtypes.IN.AAAA.AAAA):
     def to_text(self, origin=None, relativize=True, **kw):
     def to_text(self, origin=None, relativize=True, **kw):
@@ -69,43 +78,6 @@ class LongQuotedTXT(dns.rdtypes.txtbase.TXTBase):
                 file.write(s)
                 file.write(s)
 
 
 
 
-# TODO remove when https://github.com/rthalley/dnspython/pull/625 is in the main codebase
-class _DigestLengthMixin:
-    _digest_length_by_type = {  # octets (not hex)
-        0: 1,  # reserved in RFC 3658 Sec. 2.4, but used in RFC 8078 Sec. 4 (DS delete via CDS)
-        1: 20,  # SHA-1, RFC 3658 Sec. 2.4
-        2: 32,  # SHA-256, RFC 4509 Sec. 2.2
-        3: 32,  # GOST R 34.11-94, RFC 5933 Sec. 4 in conjunction with RFC 4490 Sec. 2.1
-        4: 48,  # SHA-384, RFC 6605 Sec. 2
-    }
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        try:
-            expected_len = _DigestLengthMixin._digest_length_by_type[self.digest_type]
-        except KeyError:
-            raise ValueError('unknown digest type')
-        actual_len = len(self.digest)
-        if actual_len != expected_len:
-            raise ValueError(f'invalid digest length {actual_len*2} (expected for this digest type: {expected_len*2}')
-
-
-@dns.immutable.immutable
-class CDS(_DigestLengthMixin, dns.rdtypes.ANY.CDS.CDS):
-    pass
-
-
-@dns.immutable.immutable
-class DLV(_DigestLengthMixin, dns.rdtypes.ANY.DLV.DLV):
-    pass
-
-
-@dns.immutable.immutable
-class DS(_DigestLengthMixin, dns.rdtypes.ANY.DS.DS):
-    pass
-
-
 def _HostnameMixin(name_field, *, allow_root):
 def _HostnameMixin(name_field, *, allow_root):
     # Taken from https://github.com/PowerDNS/pdns/blob/4646277d05f293777a3d2423a3b188ccdf42c6bc/pdns/dnsname.cc#L419
     # Taken from https://github.com/PowerDNS/pdns/blob/4646277d05f293777a3d2423a3b188ccdf42c6bc/pdns/dnsname.cc#L419
     hostname_re = re.compile(r'^(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)\.)+$')
     hostname_re = re.compile(r'^(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)\.)+$')

+ 5 - 7
api/desecapi/models.py

@@ -41,7 +41,7 @@ from rest_framework.exceptions import APIException
 
 
 from desecapi import metrics
 from desecapi import metrics
 from desecapi import pdns
 from desecapi import pdns
-from desecapi.dns import AAAA, CDS, DLV, DS, LongQuotedTXT, MX, NS, SRV
+from desecapi.dns import AAAA, CERT, LongQuotedTXT, MX, NS, SRV
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 psl = psl_dns.PSL(resolver=settings.PSL_RESOLVER, timeout=.5)
 psl = psl_dns.PSL(resolver=settings.PSL_RESOLVER, timeout=.5)
@@ -587,10 +587,10 @@ class Donation(ExportModelOperationsMixin('Donation'), models.Model):
 # known, but unsupported types
 # known, but unsupported types
 RR_SET_TYPES_UNSUPPORTED = {
 RR_SET_TYPES_UNSUPPORTED = {
     'ALIAS',  # Requires signing at the frontend, hence unsupported in desec-stack
     'ALIAS',  # Requires signing at the frontend, hence unsupported in desec-stack
-    'IPSECKEY',  # broken in pdns, https://github.com/PowerDNS/pdns/issues/10589 TODO enable with pdns auth > 4.5.0
+    'IPSECKEY',  # broken in pdns, https://github.com/PowerDNS/pdns/issues/10589 TODO enable with pdns auth >= 4.6.0
     'KEY',  # Application use restricted by RFC 3445, DNSSEC use replaced by DNSKEY and handled automatically
     'KEY',  # Application use restricted by RFC 3445, DNSSEC use replaced by DNSKEY and handled automatically
     'WKS',  # General usage not recommended, "SHOULD NOT" be used in SMTP (RFC 1123)
     'WKS',  # General usage not recommended, "SHOULD NOT" be used in SMTP (RFC 1123)
-} | {'NID', 'L32', 'L64', 'LP'}  # https://github.com/rthalley/dnspython/issues/674
+}
 # restricted types are managed in use by the API, and cannot directly be modified by the API client
 # restricted types are managed in use by the API, and cannot directly be modified by the API client
 RR_SET_TYPES_AUTOMATIC = {
 RR_SET_TYPES_AUTOMATIC = {
     # corresponding functionality is automatically managed:
     # corresponding functionality is automatically managed:
@@ -602,7 +602,7 @@ RR_SET_TYPES_AUTOMATIC = {
 RR_SET_TYPES_BACKEND = pdns.SUPPORTED_RRSET_TYPES
 RR_SET_TYPES_BACKEND = pdns.SUPPORTED_RRSET_TYPES
 # validation types are types supported by the validation backend, currently: dnspython
 # validation types are types supported by the validation backend, currently: dnspython
 RR_SET_TYPES_VALIDATION = set(ANY.__all__) | set(IN.__all__) \
 RR_SET_TYPES_VALIDATION = set(ANY.__all__) | set(IN.__all__) \
-                          | {'HTTPS', 'SVCB'}  # https://github.com/rthalley/dnspython/pull/624
+                          | {'L32', 'L64', 'LP', 'NID'}  # https://github.com/rthalley/dnspython/pull/751
 # manageable types are directly managed by the API client
 # manageable types are directly managed by the API client
 RR_SET_TYPES_MANAGEABLE = \
 RR_SET_TYPES_MANAGEABLE = \
         (RR_SET_TYPES_BACKEND & RR_SET_TYPES_VALIDATION) - RR_SET_TYPES_UNSUPPORTED - RR_SET_TYPES_AUTOMATIC
         (RR_SET_TYPES_BACKEND & RR_SET_TYPES_VALIDATION) - RR_SET_TYPES_UNSUPPORTED - RR_SET_TYPES_AUTOMATIC
@@ -774,9 +774,7 @@ class RR(ExportModelOperationsMixin('RR'), models.Model):
 
 
     _type_map = {
     _type_map = {
         dns.rdatatype.AAAA: AAAA,  # TODO remove when https://github.com/PowerDNS/pdns/issues/8182 is fixed
         dns.rdatatype.AAAA: AAAA,  # TODO remove when https://github.com/PowerDNS/pdns/issues/8182 is fixed
-        dns.rdatatype.CDS: CDS,  # TODO remove when https://github.com/rthalley/dnspython/pull/625 is in main codebase
-        dns.rdatatype.DLV: DLV,  # TODO remove when https://github.com/rthalley/dnspython/pull/625 is in main codebase
-        dns.rdatatype.DS: DS,  # TODO remove when https://github.com/rthalley/dnspython/pull/625 is in main codebase
+        dns.rdatatype.CERT: CERT,  # do DNS name validation the same way as pdns
         dns.rdatatype.MX: MX,  # do DNS name validation the same way as pdns
         dns.rdatatype.MX: MX,  # do DNS name validation the same way as pdns
         dns.rdatatype.NS: NS,  # do DNS name validation the same way as pdns
         dns.rdatatype.NS: NS,  # do DNS name validation the same way as pdns
         dns.rdatatype.SRV: SRV,  # do DNS name validation the same way as pdns
         dns.rdatatype.SRV: SRV,  # do DNS name validation the same way as pdns

+ 2 - 2
api/desecapi/tests/base.py

@@ -660,8 +660,8 @@ class DesecTestCase(MockPDNSTestCase):
                        'kota.aichi.jp', 's3.amazonaws.com', 'wildcard.ck'}
                        'kota.aichi.jp', 's3.amazonaws.com', 'wildcard.ck'}
     SUPPORTED_RR_SET_TYPES = {
     SUPPORTED_RR_SET_TYPES = {
         'A', 'AAAA', 'AFSDB', 'APL', 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', 'CSYNC', 'DHCID', 'DNAME', 'DNSKEY',
         'A', 'AAAA', 'AFSDB', 'APL', 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', 'CSYNC', 'DHCID', 'DNAME', 'DNSKEY',
-        'DLV', 'DS', 'EUI48', 'EUI64', 'HINFO', 'HTTPS', 'KX', 'LOC', 'MX', 'NAPTR', 'NS', 'OPENPGPKEY', 'PTR', 'RP',
-        'SMIMEA', 'SPF', 'SRV', 'SSHFP', 'SVCB', 'TLSA', 'TXT', 'URI',
+        'DLV', 'DS', 'EUI48', 'EUI64', 'HINFO', 'HTTPS', 'KX', 'L32', 'L64', 'LOC', 'LP', 'MX', 'NAPTR',
+        'NID', 'NS', 'OPENPGPKEY', 'PTR', 'RP', 'SMIMEA', 'SPF', 'SRV', 'SSHFP', 'SVCB', 'TLSA', 'TXT', 'URI',
     }
     }
 
 
     admin = None
     admin = None

+ 27 - 15
api/desecapi/tests/test_rrsets.py

@@ -397,18 +397,22 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             ('HINFO', ('"cpu" "os"', '"cpu" "os"')),
             ('HINFO', ('"cpu" "os"', '"cpu" "os"')),
             ('HTTPS', ('01 h3POOL.exaMPLe. aLPn=h2,h3',
             ('HTTPS', ('01 h3POOL.exaMPLe. aLPn=h2,h3',
                        '1 h3POOL.exaMPLe. alpn=h2,h3')),
                        '1 h3POOL.exaMPLe. alpn=h2,h3')),
-            # ('HTTPS', ('01 h3POOL.exaMPLe. aLPn=h2,h3 ECH=MTIzLi4uCg==',  # TODO dnspython > 2.1.0
-            #            '1 h3POOL.exaMPLe. alpn=h2,h3 ech="MTIzLi4uCg=="')),
+            ('HTTPS', ('01 h3POOL.exaMPLe. aLPn=h2,h3 ECH=MTIzLi4uCg==',
+                       '1 h3POOL.exaMPLe. alpn=h2,h3 ech="MTIzLi4uCg=="')),
             # ('IPSECKEY', ('01 00 02 . ASDFAF==', '1 0 2 . ASDFAA==')),
             # ('IPSECKEY', ('01 00 02 . ASDFAF==', '1 0 2 . ASDFAA==')),
             # ('IPSECKEY', ('01 00 02 . 000000==', '1 0 2 . 00000w==')),
             # ('IPSECKEY', ('01 00 02 . 000000==', '1 0 2 . 00000w==')),
             ('KX', ('010 example.com.', '10 example.com.')),
             ('KX', ('010 example.com.', '10 example.com.')),
+            ('L32', ('010  10.1.2.0', '10 10.1.2.0')),
+            ('L64', ('010   2001:0Db8:2140:2000', '10 2001:0db8:2140:2000')),
             ('LOC', ('023 012 59 N 042 022 48.500 W 65.00m 20.00m 10.00m 10.00m',
             ('LOC', ('023 012 59 N 042 022 48.500 W 65.00m 20.00m 10.00m 10.00m',
                      '23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m')),
                      '23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m')),
+            ('LP', ('010   l64-subnet1.example.com.', '10 l64-subnet1.example.com.')),
             ('MX', ('10 010.1.1.1.', '10 010.1.1.1.')),
             ('MX', ('10 010.1.1.1.', '10 010.1.1.1.')),
             ('MX', ('010 010.1.1.2.', '10 010.1.1.2.')),
             ('MX', ('010 010.1.1.2.', '10 010.1.1.2.')),
             ('MX', ('0 .', '0 .')),
             ('MX', ('0 .', '0 .')),
             ('NAPTR', ('100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.',
             ('NAPTR', ('100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.',
                        '100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.')),
                        '100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.')),
+            ('NID', ('010 0014:4fff:ff20:Ee64', '10 0014:4fff:ff20:ee64')),
             ('NS', ('EXaMPLE.COM.', 'example.com.')),
             ('NS', ('EXaMPLE.COM.', 'example.com.')),
             ('OPENPGPKEY', ('mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tS xLFJYhX+uabSgMrhOqUizJhkLx82',
             ('OPENPGPKEY', ('mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tS xLFJYhX+uabSgMrhOqUizJhkLx82',
                             'mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tSxLFJYhX+uabSgMrhOqUizJhkLx82')),
                             'mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tSxLFJYhX+uabSgMrhOqUizJhkLx82')),
@@ -425,8 +429,8 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             ('SSHFP', ('2 2 aabbccEEddff', '2 2 aabbcceeddff')),
             ('SSHFP', ('2 2 aabbccEEddff', '2 2 aabbcceeddff')),
             ('SVCB', ('2 sVc2.example.NET. IPV6hint=2001:db8:00:0::2 port=01234',
             ('SVCB', ('2 sVc2.example.NET. IPV6hint=2001:db8:00:0::2 port=01234',
                       '2 sVc2.example.NET. port=1234 ipv6hint=2001:db8::2')),
                       '2 sVc2.example.NET. port=1234 ipv6hint=2001:db8::2')),
-            # ('SVCB', ('2 sVc2.example.NET. ECH=MjIyLi4uCg== IPV6hint=2001:db8:00:0::2 port=01234',  # TODO dnspython > 2.1.0
-            #           '2 sVc2.example.NET. port=1234 ech="MjIyLi4uCg==" ipv6hint=2001:db8::2')),
+            ('SVCB', ('2 sVc2.example.NET. ECH=MjIyLi4uCg== IPV6hint=2001:db8:00:0::2 port=01234',
+                      '2 sVc2.example.NET. port=1234 ech="MjIyLi4uCg==" ipv6hint=2001:db8::2')),
             ('TLSA', ('3 0001 1 000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '3 1 1 000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')),
             ('TLSA', ('3 0001 1 000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '3 1 1 000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')),
             ('TLSA', ('003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',
             ('TLSA', ('003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',
                       '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd')),
                       '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd')),
@@ -473,7 +477,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '2:FF00::/8 !1:192.168.38.0/28',
                 '2:FF00::/8 !1:192.168.38.0/28',
             ],
             ],
             'CAA': ['128 issue "letsencrypt.org"', '128 iodef "mailto:desec@example.com"', '1 issue "letsencrypt.org"'],
             'CAA': ['128 issue "letsencrypt.org"', '128 iodef "mailto:desec@example.com"', '1 issue "letsencrypt.org"'],
-            'CERT': ['6 0 0 sadfdd=='],
+            'CERT': ['06 0 0 sadfdd==', 'IPGP 0 0 sadfdd=='],
             'CDNSKEY': [
             'CDNSKEY': [
                 '256 3 8 AwEAAday3UX323uVzQqtOMQ7EHQYfD5Ofv4akjQGN2zY5AgB/2jmdR/+ 1PvXFqzKCAGJv4wjABEBNWLLFm7ew1hHMDZEKVL17aml0EBKI6Dsz6Mx t6n7ScvLtHaFRKaxT4i2JxiuVhKdQR9XGMiWAPQKrRM5SLG0P+2F+TLK l3D0L/cD',
                 '256 3 8 AwEAAday3UX323uVzQqtOMQ7EHQYfD5Ofv4akjQGN2zY5AgB/2jmdR/+ 1PvXFqzKCAGJv4wjABEBNWLLFm7ew1hHMDZEKVL17aml0EBKI6Dsz6Mx t6n7ScvLtHaFRKaxT4i2JxiuVhKdQR9XGMiWAPQKrRM5SLG0P+2F+TLK l3D0L/cD',
                 '257 3 8 AwEAAcw5QLr0IjC0wKbGoBPQv4qmeqHy9mvL5qGQTuaG5TSrNqEAR6b/ qvxDx6my4JmEmjUPA1JeEI9YfTUieMr2UZflu7aIbZFLw0vqiYrywCGr CHXLalOrEOmrvAxLvq4vHtuTlH7JIszzYBSes8g1vle6KG7xXiP3U5Ll 96Qiu6bZ31rlMQSPB20xbqJJh6psNSrQs41QvdcXAej+K2Hl1Wd8kPri ec4AgiBEh8sk5Pp8W9ROLQ7PcbqqttFaW2m7N/Wy4qcFU13roWKDEAst bxH5CHPoBfZSbIwK4KM6BK/uDHpSPIbiOvOCW+lvu9TAiZPc0oysY6as lO7jXv16Gws=',
                 '257 3 8 AwEAAcw5QLr0IjC0wKbGoBPQv4qmeqHy9mvL5qGQTuaG5TSrNqEAR6b/ qvxDx6my4JmEmjUPA1JeEI9YfTUieMr2UZflu7aIbZFLw0vqiYrywCGr CHXLalOrEOmrvAxLvq4vHtuTlH7JIszzYBSes8g1vle6KG7xXiP3U5Ll 96Qiu6bZ31rlMQSPB20xbqJJh6psNSrQs41QvdcXAej+K2Hl1Wd8kPri ec4AgiBEh8sk5Pp8W9ROLQ7PcbqqttFaW2m7N/Wy4qcFU13roWKDEAst bxH5CHPoBfZSbIwK4KM6BK/uDHpSPIbiOvOCW+lvu9TAiZPc0oysY6as lO7jXv16Gws=',
@@ -484,6 +488,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
                 '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
                 '62703 13 2 085BF1EE0ADBBC99D4D9328229EBDCAEC5FAB20E38610072AD055474 4C7AF4A0',
                 '62703 13 2 085BF1EE0ADBBC99D4D9328229EBDCAEC5FAB20E38610072AD055474 4C7AF4A0',
                 '61655 13 4 C838A5C66FCBF83B8B6B50C3CEEC3524777FE4AF8A9FE0172ECAD242 48B0CA1A216DD0D538F20C130DD3059538204B04',
                 '61655 13 4 C838A5C66FCBF83B8B6B50C3CEEC3524777FE4AF8A9FE0172ECAD242 48B0CA1A216DD0D538F20C130DD3059538204B04',
+                '6454 8 5 24396E17E36D031F71C354B06A979A67A01F503E',
             ],
             ],
             'CNAME': ['example.com.'],
             'CNAME': ['example.com.'],
             'CSYNC': ['0 0', '66 1 A', '66 2 AAAA', '66 3 A NS AAAA', '66 15 NSEC'],
             'CSYNC': ['0 0', '66 1 A', '66 2 AAAA', '66 3 A NS AAAA', '66 15 NSEC'],
@@ -493,6 +498,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
                 '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
                 '62703 13 2 085BF1EE0ADBBC99D4D9328229EBDCAEC5FAB20E38610072AD055474 4C7AF4A0',
                 '62703 13 2 085BF1EE0ADBBC99D4D9328229EBDCAEC5FAB20E38610072AD055474 4C7AF4A0',
                 '61655 13 4 C838A5C66FCBF83B8B6B50C3CEEC3524777FE4AF8A9FE0172ECAD242 48B0CA1A216DD0D538F20C130DD3059538204B04',
                 '61655 13 4 C838A5C66FCBF83B8B6B50C3CEEC3524777FE4AF8A9FE0172ECAD242 48B0CA1A216DD0D538F20C130DD3059538204B04',
+                '6454 8 5 24396E17E36D031F71C354B06A979A67A01F503E',
             ],
             ],
             'DNAME': ['example.com.'],
             'DNAME': ['example.com.'],
             'DNSKEY': [
             'DNSKEY': [
@@ -505,6 +511,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
                 '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
                 '62703 13 2 085BF1EE0ADBBC99D4D9328229EBDCAEC5FAB20E38610072AD055474 4C7AF4A0',
                 '62703 13 2 085BF1EE0ADBBC99D4D9328229EBDCAEC5FAB20E38610072AD055474 4C7AF4A0',
                 '61655 13 4 C838A5C66FCBF83B8B6B50C3CEEC3524777FE4AF8A9FE0172ECAD242 48B0CA1A216DD0D538F20C130DD3059538204B04',
                 '61655 13 4 C838A5C66FCBF83B8B6B50C3CEEC3524777FE4AF8A9FE0172ECAD242 48B0CA1A216DD0D538F20C130DD3059538204B04',
+                '6454 8 5 24396E17E36D031F71C354B06A979A67A01F503E',
             ],
             ],
             'EUI48': ['aa-bb-cc-dd-ee-ff', 'AA-BB-CC-DD-EE-FF'],
             'EUI48': ['aa-bb-cc-dd-ee-ff', 'AA-BB-CC-DD-EE-FF'],
             'EUI64': ['aa-bb-cc-dd-ee-ff-00-11', 'AA-BB-CC-DD-EE-FF-00-11'],
             'EUI64': ['aa-bb-cc-dd-ee-ff-00-11', 'AA-BB-CC-DD-EE-FF-00-11'],
@@ -513,11 +520,11 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 # from https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-06.html#name-examples, with ech base64'd
                 # from https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-06.html#name-examples, with ech base64'd
                 '1 . alpn=h3',
                 '1 . alpn=h3',
                 '0 pool.svc.example.',
                 '0 pool.svc.example.',
-                # '1 h3pool.example. alpn=h2,h3 ech="MTIzLi4uCg=="',  # TODO dnspython > 2.1.0
-                # '2 .      alpn=h2 ech="YWJjLi4uCg=="',  # TODO dnspython > 2.1.0
+                '1 h3pool.example. alpn=h2,h3 ech="MTIzLi4uCg=="',
+                '2 .      alpn=h2 ech="YWJjLi4uCg=="',
                 # made-up (not from RFC)
                 # made-up (not from RFC)
-                '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',
-                # '2 . ech=... key65333=ex1 key65444=ex2 mandatory=key65444,ech',  # see #section-7  # TODO dnspython > 2.1.0
+                '1 pool.svc.example. no-default-alpn alpn=h2 port=1234 ipv4hint=192.168.123.1',
+                '2 . ech=... key65333=ex1 key65444=ex2 mandatory=key65444,ech',  # see #section-7
             ],
             ],
             # 'IPSECKEY': [
             # 'IPSECKEY': [
             #     '12 0 2 . asdfdf==',
             #     '12 0 2 . asdfdf==',
@@ -526,9 +533,13 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             #     '12 3 01 example.com. asdfdf==',
             #     '12 3 01 example.com. asdfdf==',
             # ],
             # ],
             'KX': ['4 example.com.', '28 io.'],
             'KX': ['4 example.com.', '28 io.'],
+            'L32': ['010   10.1.2.0', '65535 1.2.3.4'],
+            'L64': ['010   2001:0DB8:1140:1000', '10 2001:0DB8:1140:1000'],
             'LOC': ['23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m'],
             'LOC': ['23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m'],
+            'LP': ['10 l64-subnet1.example.com.', '65535 .'],
             'MX': ['10 example.com.', '20 1.1.1.1.'],
             'MX': ['10 example.com.', '20 1.1.1.1.'],
             'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
             'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
+            'NID': ['65535   0014:4fff:ff20:ee64'],
             'NS': ['ns1.example.com.'],
             'NS': ['ns1.example.com.'],
             'OPENPGPKEY': [
             'OPENPGPKEY': [
                 'mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tSxLFJYhX+uabSgMrhOqUizJhkLx82',  # key incomplete
                 'mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tSxLFJYhX+uabSgMrhOqUizJhkLx82',  # key incomplete
@@ -545,7 +556,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             'SVCB': [
             'SVCB': [
                 '0 svc4-baz.example.net.',
                 '0 svc4-baz.example.net.',
                 '1 . key65333=...',
                 '1 . key65333=...',
-                # '2 svc2.example.net. ech="MjIyLi4uCg==" ipv6hint=2001:db8::2 port=1234',  # TODO dnspython > 2.1.0
+                '2 svc2.example.net. ech="MjIyLi4uCg==" ipv6hint=2001:db8::2 port=1234',
             ],
             ],
             'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
             'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
                      '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',
                      '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',
@@ -597,7 +608,6 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '6454 8 c 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 c 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 1 d',
                 '6454 8 1 d',
                 '6454 8 0 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 0 24396E17E36D031F71C354B06A979A67A01F503E',
-                '6454 8 5 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 1 aabbccddeeff',
                 '6454 8 1 aabbccddeeff',
                 '0 0 0 0',
                 '0 0 0 0',
             ],
             ],
@@ -611,7 +621,6 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '6454 8 c 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 c 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 1 d',
                 '6454 8 1 d',
                 '6454 8 0 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 0 24396E17E36D031F71C354B06A979A67A01F503E',
-                '6454 8 5 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 1 aabbccddeeff',
                 '6454 8 1 aabbccddeeff',
             ],
             ],
             'DNAME': ['example.com', '10 example.com.'],
             'DNAME': ['example.com', '10 example.com.'],
@@ -628,7 +637,6 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                 '6454 8 c 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 c 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 1 d',
                 '6454 8 1 d',
                 '6454 8 0 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 0 24396E17E36D031F71C354B06A979A67A01F503E',
-                '6454 8 5 24396E17E36D031F71C354B06A979A67A01F503E',
                 '6454 8 1 aabbccddeeff',
                 '6454 8 1 aabbccddeeff',
             ],
             ],
             'EUI48': ['aa-bb-ccdd-ee-ff', 'AA-BB-CC-DD-EE-GG'],
             'EUI48': ['aa-bb-ccdd-ee-ff', 'AA-BB-CC-DD-EE-GG'],
@@ -636,18 +644,22 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             'HINFO': ['"ARMv8-A"', f'"a" "{"b"*256}"'],
             'HINFO': ['"ARMv8-A"', f'"a" "{"b"*256}"'],
             'HTTPS': [
             'HTTPS': [
                 # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with ech base64'd
                 # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with ech base64'd
-                # '1 h3pool alpn=h2,h3 ech="MTIzLi4uCg=="',  # TODO dnspython > 2.1.0
+                '1 h3pool alpn=h2,h3 ech="MTIzLi4uCg=="',
                 # made-up (not from RFC)
                 # made-up (not from RFC)
                 '0 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',  # no keys in alias mode
                 '0 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',  # no keys in alias mode
                 '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1 ipv4hint=192.168.123.2',  # dup
                 '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1 ipv4hint=192.168.123.2',  # dup
             ],
             ],
             # 'IPSECKEY': [],
             # 'IPSECKEY': [],
             'KX': ['-1 example.com', '10 example.com'],
             'KX': ['-1 example.com', '10 example.com'],
+            'L32': ['65536 10.1.2.0', '5 a.1.2.0', '10 10.1.02.0'],
+            'L64': ['65536 2001:0DB8:4140:4000', '5 01:0DB8:4140:4000'],
             'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
             'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
+            'LP': ['10 l64-subnet1.example.com', '-3 l64-subnet1.example.com.', '65536 l64-subnet1.example.com.'],
             'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.', '10 _foo.example.com.', '10 $url.'],
             'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.', '10 _foo.example.com.', '10 $url.'],
             'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu',
             'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu',
                       '100  50  "s"     ""  _z3950._tcp.gatech.edu.',
                       '100  50  "s"     ""  _z3950._tcp.gatech.edu.',
                       '100  50  3 2  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
                       '100  50  3 2  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
+            'NID': ['010 14:4fff:ff20:Ee64', 'd 0014:4fff:ff20:ee64', '20 ::14::ee64'],
             'NS': ['ns1.example.com', '127.0.0.1', '_foobar.example.dedyn.io.', '.'],
             'NS': ['ns1.example.com', '127.0.0.1', '_foobar.example.dedyn.io.', '.'],
             'OPENPGPKEY': ['1 2 3'],
             'OPENPGPKEY': ['1 2 3'],
             'PTR': ['"example.com."', '10 *.example.com.'],
             'PTR': ['"example.com."', '10 *.example.com.'],
@@ -659,7 +671,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             'SVCB': [
             'SVCB': [
                 '0 svc4-baz.example.net. keys=val',
                 '0 svc4-baz.example.net. keys=val',
                 '1 not.fully.qualified key65333=...',
                 '1 not.fully.qualified key65333=...',
-                # '2 duplicate.key. ech="MjIyLi4uCg==" ech="MjIyLi4uCg=="',  # TODO dnspython > 2.1.0
+                '2 duplicate.key. ech="MjIyLi4uCg==" ech="MjIyLi4uCg=="',
             ],
             ],
             'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
             'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
             'TXT': [
             'TXT': [

+ 1 - 1
api/requirements.txt

@@ -9,7 +9,7 @@ django-celery-email~=3.0.0
 django-netfields~=1.2.4
 django-netfields~=1.2.4
 django-pgtrigger~=2.4.0
 django-pgtrigger~=2.4.0
 django-prometheus~=2.2.0
 django-prometheus~=2.2.0
-dnspython~=2.1.0
+dnspython~=2.2.0
 httpretty~=1.0.5
 httpretty~=1.0.5
 psycopg2~=2.9.2
 psycopg2~=2.9.2
 prometheus-client~=0.12.0  # added to control django-prometheus' dependency version
 prometheus-client~=0.12.0  # added to control django-prometheus' dependency version

+ 4 - 3
docs/dns/rrsets.rst

@@ -534,9 +534,10 @@ automatically.
 At least the following record types are supported: ``A``, ``AAAA``, ``AFSDB``,
 At least the following record types are supported: ``A``, ``AAAA``, ``AFSDB``,
 ``APL``, ``CAA``, ``CDNSKEY``, ``CDS``, ``CERT``, ``CNAME``, ``DHCID``,
 ``APL``, ``CAA``, ``CDNSKEY``, ``CDS``, ``CERT``, ``CNAME``, ``DHCID``,
 ``DNAME``, ``DNSKEY``, ``DLV``, ``DS``, ``EUI48``, ``EUI64``, ``HINFO``,
 ``DNAME``, ``DNSKEY``, ``DLV``, ``DS``, ``EUI48``, ``EUI64``, ``HINFO``,
-``HTTPS``, ``KX``, ``LOC``, ``MX``, ``NAPTR``, ``NS``, ``OPENPGPKEY``,
-``PTR``, ``RP``, ``SMIMEA``, ``SPF``, ``SRV``, ``SSHFP``, ``SVCB``, ``TLSA``,
-``TXT``, ``URI``.  (The ``SOA`` record is managed automatically.)
+``HTTPS``, ``KX``, ``L32``, ``L64``, ``LOC``, ``LP``, ``MX``,
+``NAPTR``, ``NID``, ``NS``, ``OPENPGPKEY``, ``PTR``, ``RP``, ``SMIMEA``,
+``SPF``, ``SRV``, ``SSHFP``, ``SVCB``, ``TLSA``, ``TXT``, ``URI``.
+(The ``SOA`` record is managed automatically.)
 
 
 Special care needs to be taken with some types of records, as explained below.
 Special care needs to be taken with some types of records, as explained below.
 
 

+ 1 - 1
test/e2e2/requirements.txt

@@ -2,4 +2,4 @@ pytest
 pytest-schema
 pytest-schema
 pytest-xdist
 pytest-xdist
 requests
 requests
-dnspython~=2.1.0
+dnspython~=2.2.0

+ 27 - 23
test/e2e2/spec/test_api_rr.py

@@ -40,7 +40,7 @@ VALID_RECORDS_CANONICAL = {
         '6454 8 1 24396e17e36d031f71c354b06a979a67a01f503e',
         '6454 8 1 24396e17e36d031f71c354b06a979a67a01f503e',
         '0 0 0 00',
         '0 0 0 00',
     ],
     ],
-    'CERT': ['6 0 0 sadfdQ=='],
+    'CERT': [],  # handled in non-canonical, because apply presentation format conversion on the stack (but not here)
     'CNAME': ['example.com.'],
     'CNAME': ['example.com.'],
     'CSYNC': ['0 0', '66 1 A', '66 2 AAAA', '66 3 A NS AAAA', '66 15 NSEC'],
     'CSYNC': ['0 0', '66 1 A', '66 2 AAAA', '66 3 A NS AAAA', '66 15 NSEC'],
     'DHCID': ['aaaaaaaaaaaa', 'xxxx'],
     'DHCID': ['aaaaaaaaaaaa', 'xxxx'],
@@ -58,17 +58,17 @@ VALID_RECORDS_CANONICAL = {
     'HINFO': ['"ARMv8-A" "Linux"'],
     'HINFO': ['"ARMv8-A" "Linux"'],
     'HTTPS': [
     'HTTPS': [
         '1 h3POOL.exaMPLe. alpn=h2,h3',
         '1 h3POOL.exaMPLe. alpn=h2,h3',
-        # '1 h3POOL.exaMPLe. alpn=h2,h3 ech="MTIzLi4uCg=="',  # TODO dnspython > 2.1.0
+        '1 h3POOL.exaMPLe. alpn=h2,h3 ech="MTIzLi4uCg=="',
     ],
     ],
     # 'IPSECKEY': ['12 0 2 . asdfdQ==', '3 1 1 127.0.0.1 asdfdQ==', '12 3 1 example.com. asdfdQ==',],
     # 'IPSECKEY': ['12 0 2 . asdfdQ==', '3 1 1 127.0.0.1 asdfdQ==', '12 3 1 example.com. asdfdQ==',],
     'KX': ['4 example.com.', '28 io.', '0 .'],
     'KX': ['4 example.com.', '28 io.', '0 .'],
-    'LOC': [
-        '23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m',
-    ],
+    'L32': ['10 10.1.2.0'],
+    'L64': ['10 2001:0db8:2140:2000'],
+    'LOC': ['23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m'],
+    'LP': ['10 l64-subnet1.example.com.'],
     'MX': ['10 example.com.', '20 1.1.1.1.'],
     'MX': ['10 example.com.', '20 1.1.1.1.'],
-    'NAPTR': [
-        '100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.',
-    ],
+    'NAPTR': ['100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.'],
+    'NID': ['10 0014:4fff:ff20:ee64'],
     'NS': ['ns1.example.com.'],
     'NS': ['ns1.example.com.'],
     'OPENPGPKEY': [
     'OPENPGPKEY': [
         'mQINBF3yev8BEADR9GxB6OJ5AJlXBWc3nWyWZ+yNNVBiy73XjgOs0uowbxph'
         'mQINBF3yev8BEADR9GxB6OJ5AJlXBWc3nWyWZ+yNNVBiy73XjgOs0uowbxph'
@@ -135,7 +135,7 @@ VALID_RECORDS_CANONICAL = {
     'SSHFP': ['2 2 aabbcceeddff'],
     'SSHFP': ['2 2 aabbcceeddff'],
     'SVCB': [
     'SVCB': [
         '2 sVc2.example.NET. port=1234 ipv6hint=2001:db8::2',
         '2 sVc2.example.NET. port=1234 ipv6hint=2001:db8::2',
-        # '2 sVc2.example.NET. port=1234 ech="MjIyLi4uCg==" ipv6hint=2001:db8::2',  # TODO dnspython > 2.1.0
+        '2 sVc2.example.NET. port=1234 ech="MjIyLi4uCg==" ipv6hint=2001:db8::2',
     ],
     ],
     'TLSA': ['3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TLSA': ['3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TXT': [
     'TXT': [
@@ -168,7 +168,7 @@ VALID_RECORDS_NON_CANONICAL = {
         '06454  08   01    24396e17e36d031f71c354b06a979a67a01f503e',
         '06454  08   01    24396e17e36d031f71c354b06a979a67a01f503e',
         '6454 8 2 5C BA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
         '6454 8 2 5C BA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
     ],
     ],
-    'CERT': ['06 00 00 sadfee=='],
+    'CERT': ['6 0 0 sadfdQ==', '06 00 00 sadfee==', 'IPGP 00 00 sadfee=='],
     'CNAME': ['EXAMPLE.TEST.'],
     'CNAME': ['EXAMPLE.TEST.'],
     'CSYNC': ['066 03  NS  AAAA A'],
     'CSYNC': ['066 03  NS  AAAA A'],
     'DHCID': ['aa aaa  aaaa a a a', 'xxxx'],
     'DHCID': ['aa aaa  aaaa a a a', 'xxxx'],
@@ -193,21 +193,21 @@ VALID_RECORDS_NON_CANONICAL = {
         # from https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-06.html#name-examples, with ech base64'd
         # from https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-06.html#name-examples, with ech base64'd
         '1 . alpn=h3',
         '1 . alpn=h3',
         '0 pool.svc.example.',
         '0 pool.svc.example.',
-        # '1 h3pool.example. alpn=h2,h3 ech="MTIzLi4uCg=="',  # TODO dnspython > 2.1.0
-        # '2 .      alpn=h2 ech="YWJjLi4uCg=="',  # TODO dnspython > 2.1.0
+        '1 h3pool.example. alpn=h2,h3 ech="MTIzLi4uCg=="',
+        '2 .      alpn=h2 ech="YWJjLi4uCg=="',
         # made-up (not from RFC)
         # made-up (not from RFC)
-        '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',
-        # '2 . ech=... key65333=ex1 key65444=ex2 mandatory=key65444,ech',  # see #section-7  # TODO dnspython > 2.1.0
+        '1 pool.svc.example. no-default-alpn alpn=h2 port=1234 ipv4hint=192.168.123.1',
+        '2 . ech=... key65333=ex1 key65444=ex2 mandatory=key65444,ech',  # see #section-7
     ],
     ],
     # 'IPSECKEY': ['12 0 2 . asdfdf==', '03 1 1 127.0.00.1 asdfdf==', '12 3 1 example.com. asdfdf==',],
     # 'IPSECKEY': ['12 0 2 . asdfdf==', '03 1 1 127.0.00.1 asdfdf==', '12 3 1 example.com. asdfdf==',],
     'KX': ['012 example.TEST.'],
     'KX': ['012 example.TEST.'],
-    'LOC': [
-        '023 012 59 N 042 022 48.500 W 65.00m 20.00m 10.00m 10.00m',
-    ],
+    'L32': ['010  10.1.2.0', '65535 1.2.3.4'],
+    'L64': ['010   2001:0Db8:2140:2000', '10 2001:0DB8:1140:1000'],
+    'LOC': ['023 012 59 N 042 022 48.500 W 65.00m 20.00m 10.00m 10.00m'],
+    'LP': ['010   l64-subnet1.example.com.', '65535 .'],
     'MX': ['10 010.1.1.1.'],
     'MX': ['10 010.1.1.1.'],
-    'NAPTR': [
-        '100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.',
-    ],
+    'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
+    'NID': ['010 0014:4fff:ff20:Ee64', '65535   0014:4fff:ff20:ee64'],
     'NS': ['EXaMPLE.COM.'],
     'NS': ['EXaMPLE.COM.'],
     'OPENPGPKEY': [
     'OPENPGPKEY': [
         'mG8EXtVIsRMFK4EEAC==',
         'mG8EXtVIsRMFK4EEAC==',
@@ -271,7 +271,7 @@ VALID_RECORDS_NON_CANONICAL = {
     'SVCB': [
     'SVCB': [
         '0 svc4-baz.example.net.',
         '0 svc4-baz.example.net.',
         '1 . key65333=...',
         '1 . key65333=...',
-        # '2 svc2.example.net. ech="MjIyLi4uCg==" ipv6hint=2001:db8::2 port=1234',  # TODO dnspython > 2.1.0
+        '2 svc2.example.net. ech="MjIyLi4uCg==" ipv6hint=2001:db8::2 port=1234',
     ],
     ],
     'TLSA': ['003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TLSA': ['003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TXT': [
     'TXT': [
@@ -323,18 +323,22 @@ INVALID_RECORDS = {
     'HINFO': ['"ARMv8-A"', f'"a" "{"b" * 256}"'],
     'HINFO': ['"ARMv8-A"', f'"a" "{"b" * 256}"'],
     'HTTPS': [
     'HTTPS': [
         # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with ech base64'd
         # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with ech base64'd
-        # '1 h3pool alpn=h2,h3 ech="MTIzLi4uCg=="',  # TODO dnspython > 2.1.0
+        '1 h3pool alpn=h2,h3 ech="MTIzLi4uCg=="',
         # made-up (not from RFC)
         # made-up (not from RFC)
         '0 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',  # no keys in alias mode
         '0 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',  # no keys in alias mode
         '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1 ipv4hint=192.168.123.2',  # dup
         '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1 ipv4hint=192.168.123.2',  # dup
     ],
     ],
     # 'IPSECKEY': [],
     # 'IPSECKEY': [],
     'KX': ['-1 example.com', '10 example.com'],
     'KX': ['-1 example.com', '10 example.com'],
+    'L32': ['65536 10.1.2.0', '5 a.1.2.0', '10 10.1.02.0'],
+    'L64': ['65536 2001:0DB8:4140:4000', '5 01:0DB8:4140:4000'],
     'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
     'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
+    'LP': ['10 l64-subnet1.example.com', '-3 l64-subnet1.example.com.', '65536 l64-subnet1.example.com.'],
     'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.' '10 _foo.example.com.', '10 $url.'],
     'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.' '10 _foo.example.com.', '10 $url.'],
     'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu',
     'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu',
               '100  50  "s"     ""  _z3950._tcp.gatech.edu.',
               '100  50  "s"     ""  _z3950._tcp.gatech.edu.',
               '100  50  3 2  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
               '100  50  3 2  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
+    'NID': ['010 14:4fff:ff20:Ee64', 'd 0014:4fff:ff20:ee64', '20 ::14::ee64'],
     'NS': ['ns1.example.com', '127.0.0.1'],
     'NS': ['ns1.example.com', '127.0.0.1'],
     'OPENPGPKEY': ['1 2 3'],
     'OPENPGPKEY': ['1 2 3'],
     'PTR': ['"example.com."', '10 *.example.com.'],
     'PTR': ['"example.com."', '10 *.example.com.'],
@@ -346,7 +350,7 @@ INVALID_RECORDS = {
     'SVCB': [
     'SVCB': [
         '0 svc4-baz.example.net. keys=val',
         '0 svc4-baz.example.net. keys=val',
         '1 not.fully.qualified key65333=...',
         '1 not.fully.qualified key65333=...',
-        # '2 duplicate.key. ech="MjIyLi4uCg==" ech="MjIyLi4uCg=="',  # TODO dnspython > 2.1.0
+        '2 duplicate.key. ech="MjIyLi4uCg==" ech="MjIyLi4uCg=="',
     ],
     ],
     'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
     'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
     'TXT': [
     'TXT': [