Browse Source

feat(api): enable HTTPS/SVCB record types, closes #348

Peter Thomassen 4 years ago
parent
commit
22fc1d07a4

+ 16 - 1
api/desecapi/dns.py

@@ -1,7 +1,22 @@
 import struct
 
 import dns
-import dns.rdtypes.txtbase
+import dns.rdtypes.txtbase, dns.rdtypes.svcbbase
+
+
+def _strip_quotes_decorator(func):
+    return lambda *args, **kwargs: func(*args, **kwargs)[1:-1]
+
+
+# Ensure that dnspython agrees with pdns' expectations for SVCB / HTTPS parameters.
+# WARNING: This is a global side-effect. It can't be done by extending a class, because dnspython hardcodes the use of
+# their dns.rdtypes.svcbbase.*Param classes in the global dns.rdtypes.svcbbase._class_for_key dictionary. We either have
+# to globally mess with that dict and insert our custom class, or we just mess with their classes directly.
+dns.rdtypes.svcbbase.ALPNParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.ALPNParam.to_text)
+dns.rdtypes.svcbbase.IPv4HintParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.IPv4HintParam.to_text)
+dns.rdtypes.svcbbase.IPv6HintParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.IPv6HintParam.to_text)
+dns.rdtypes.svcbbase.MandatoryParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.MandatoryParam.to_text)
+dns.rdtypes.svcbbase.PortParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.PortParam.to_text)
 
 
 @dns.immutable.immutable

+ 2 - 3
api/desecapi/models.py

@@ -465,9 +465,7 @@ class Donation(ExportModelOperationsMixin('Donation'), models.Model):
 RR_SET_TYPES_UNSUPPORTED = {
     'ALIAS',  # Requires signing at the frontend, hence unsupported in desec-stack
     'DNAME',  # "do not combine with DNSSEC", https://doc.powerdns.com/authoritative/settings.html#dname-processing
-    'HTTPS',  # TODO enable
     'IPSECKEY',  # broken in pdns, https://github.com/PowerDNS/pdns/issues/9055 TODO enable with pdns auth 4.5.0
-    'SVCB',  # TODO enable
     '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)
 }
@@ -481,7 +479,8 @@ RR_SET_TYPES_AUTOMATIC = {
 # backend types are types that are the types supported by the backend(s)
 RR_SET_TYPES_BACKEND = pdns.SUPPORTED_RRSET_TYPES
 # 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
 # manageable types are directly managed by the API client
 RR_SET_TYPES_MANAGEABLE = \
         (RR_SET_TYPES_BACKEND & RR_SET_TYPES_VALIDATION) - RR_SET_TYPES_UNSUPPORTED - RR_SET_TYPES_AUTOMATIC

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

@@ -634,8 +634,8 @@ class DesecTestCase(MockPDNSTestCase):
     PUBLIC_SUFFIXES = {'de', 'com', 'io', 'gov.cd', 'edu.ec', 'xxx', 'pinb.gov.pl', 'valer.ostfold.no',
                        'kota.aichi.jp', 's3.amazonaws.com', 'wildcard.ck'}
     SUPPORTED_RR_SET_TYPES = {'A', 'AAAA', 'AFSDB', 'APL', 'CAA', 'CERT', 'CNAME', 'DHCID', 'DLV', 'DS', 'EUI48',
-                              'EUI64', 'HINFO', 'KX', 'LOC', 'MX', 'NAPTR', 'NS', 'OPENPGPKEY', 'PTR', 'RP',
-                              'SMIMEA', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'TXT', 'URI'}
+                              'EUI64', 'HINFO', 'HTTPS', 'KX', 'LOC', 'MX', 'NAPTR', 'NS', 'OPENPGPKEY', 'PTR', 'RP',
+                              'SMIMEA', 'SPF', 'SRV', 'SSHFP', 'SVCB', 'TLSA', 'TXT', 'URI'}
 
     admin = None
     auto_delegation_domains = None

+ 31 - 0
api/desecapi/tests/test_rrsets.py

@@ -363,6 +363,8 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             ('EUI64', ('AA-BB-CC-DD-EE-FF-aa-aa', 'aa-bb-cc-dd-ee-ff-aa-aa')),
             ('HINFO', ('cpu os', '"cpu" "os"')),
             ('HINFO', ('"cpu" "os"', '"cpu" "os"')),
+            ('HTTPS', ('01 h3POOL.exaMPLe. aLPn=h2,h3 ECHCONFIG=MTIzLi4uCg==',
+                       '1 h3POOL.exaMPLe. alpn=h2,h3 echconfig="MTIzLi4uCg=="')),
             # ('IPSECKEY', ('01 00 02 . ASDFAF==', '1 0 2 . ASDFAA==')),
             # ('IPSECKEY', ('01 00 02 . 000000==', '1 0 2 . 00000w==')),
             ('KX', ('010 example.com.', '10 example.com.')),
@@ -386,6 +388,8 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             ('SRV', ('100 1 5061 EXAMPLE.com.', '100 1 5061 example.com.')),
             ('SRV', ('100 1 5061 example.com.', '100 1 5061 example.com.')),
             ('SSHFP', ('2 2 aabbccEEddff', '2 2 aabbcceeddff')),
+            ('SVCB', ('2 sVc2.example.NET. ECHCONFIG=MjIyLi4uCg== IPV6hint=2001:db8:00:0::2 port=01234',
+                      '2 sVc2.example.NET. port=1234 echconfig="MjIyLi4uCg==" ipv6hint=2001:db8::2')),
             ('TLSA', ('3 0001 1 000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '3 1 1 000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')),
             ('TLSA', ('003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',
                       '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd')),
@@ -437,6 +441,16 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             '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'],
             'HINFO': ['"ARMv8-A" "Linux"'],
+            'HTTPS': [
+                # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with echconfig base64'd
+                '1 . alpn=h3',
+                '0 pool.svc.example.',
+                '1 h3pool.example. alpn=h2,h3 echconfig="MTIzLi4uCg=="',
+                '2 .      alpn=h2 echconfig="YWJjLi4uCg=="',
+                # made-up (not from RFC)
+                '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',
+                '2 . echconfig=... key65333=ex1 key65444=ex2 mandatory=key65444,echconfig',  # see #section-7
+            ],
             # 'IPSECKEY': [
             #     '12 0 2 . asdfdf==',
             #     '03 1 1 127.0.0.1 asdfdf==',
@@ -460,6 +474,11 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                     '"spf2.0/pra,mfrom ip6:2001:558:fe14:76:68:87:28:0/120 -all"'],
             'SRV': ['0 0 0 .', '100 1 5061 example.com.'],
             'SSHFP': ['2 2 aabbcceeddff'],
+            'SVCB': [
+                '0 svc4-baz.example.net.',
+                '1 . key65333=...',
+                '2 svc2.example.net. echconfig="MjIyLi4uCg==" ipv6hint=2001:db8::2 port=1234',
+            ],
             'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
                      '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',
                      '3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd'],
@@ -503,6 +522,13 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             'EUI48': ['aa-bb-ccdd-ee-ff', 'AA-BB-CC-DD-EE-GG'],
             'EUI64': ['aa-bb-cc-dd-ee-ff-gg-11', 'AA-BB-C C-DD-EE-FF-00-11'],
             'HINFO': ['"ARMv8-A"', f'"a" "{"b"*256}"'],
+            'HTTPS': [
+                # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with echconfig base64'd
+                '1 h3pool alpn=h2,h3 echconfig="MTIzLi4uCg=="',
+                # made-up (not from RFC)
+                '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
+            ],
             # 'IPSECKEY': [],
             'KX': ['-1 example.com', '10 example.com'],
             'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
@@ -518,6 +544,11 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             'SPF': ['"v=spf1', 'v=spf1 include:example.com ~all'],
             'SRV': ['0 0 0 0', '100 5061 example.com.'],
             'SSHFP': ['aabbcceeddff'],
+            'SVCB': [
+                '0 svc4-baz.example.net. keys=val',
+                '1 not.fully.qualified key65333=...',
+                '2 duplicate.key. echconfig="MjIyLi4uCg==" echconfig="MjIyLi4uCg=="',
+            ],
             'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
             'TXT': ['foob"ar', 'v=spf1 include:example.com ~all', '"foo\nbar"', '"\x00" "NUL byte yo"'],
             'URI': ['"1" "2" "3"'],

+ 11 - 8
docs/dns/rrsets.rst

@@ -586,16 +586,19 @@ Record types with priority field
 
 ``CNAME`` record
     - The record value (target) must be terminated by a dot ``.`` (as in
-      ``example.com.``).
-
-    - RRsets cannot have multiple values.  This is a limitation of the DNS
-      specification.
+      ``example.com.``).  Only one value is allowed.
 
     - A ``CNAME`` record is not allowed when other records exist at the same
-      subname.  In particular, this means that a CNAME is not allowed at the
-      zone apex (empty subname), as it will always collide with the NS record
-      (and the internally managed SOA record).  This is a limitation of
-      the DNS specification.
+      subname.  This is a limitation of the DNS specification.
+
+    - Due to the previous limitation, a CNAME is not allowed at the zone apex
+      (empty subname), as it would always collide with the NS record (and the
+      internally managed SOA record).
+
+      If you need redirect functionality at the zone apex, consider using the
+      HTTPS record type which serves exactly this purpose. Although new,
+      browser vendor support is under way (with Chrome planning to roll out
+      experimental support in February 2021).
 
 ``MX`` record
     The ``MX`` record value consists of the priority value and a mail server

+ 16 - 0
test/e2e2/conftest.py

@@ -9,11 +9,27 @@ from typing import Optional, Tuple, Iterable, Callable
 import dns
 import dns.name
 import dns.query
+import dns.rdtypes.svcbbase
 import pytest
 import requests
 from requests.exceptions import SSLError
 
 
+def _strip_quotes_decorator(func):
+    return lambda *args, **kwargs: func(*args, **kwargs)[1:-1]
+
+
+# Ensure that dnspython agrees with pdns' expectations for SVCB / HTTPS parameters.
+# WARNING: This is a global side-effect. It can't be done by extending a class, because dnspython hardcodes the use of
+# their dns.rdtypes.svcbbase.*Param classes in the global dns.rdtypes.svcbbase._class_for_key dictionary. We either have
+# to globally mess with that dict and insert our custom class, or we just mess with their classes directly.
+dns.rdtypes.svcbbase.ALPNParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.ALPNParam.to_text)
+dns.rdtypes.svcbbase.IPv4HintParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.IPv4HintParam.to_text)
+dns.rdtypes.svcbbase.IPv6HintParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.IPv6HintParam.to_text)
+dns.rdtypes.svcbbase.MandatoryParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.MandatoryParam.to_text)
+dns.rdtypes.svcbbase.PortParam.to_text = _strip_quotes_decorator(dns.rdtypes.svcbbase.PortParam.to_text)
+
+
 def random_mixed_case_string(n):
     k = random.randint(1, n-1)
     s = random.choices(string.ascii_lowercase, k=k) + random.choices(string.ascii_uppercase, k=n-k)

+ 29 - 0
test/e2e2/spec/test_api_rr_validation.py

@@ -40,6 +40,7 @@ VALID_RECORDS_CANONICAL = {
     'EUI48': ['aa-bb-cc-dd-ee-ff'],
     'EUI64': ['aa-bb-cc-dd-ee-ff-00-11'],
     'HINFO': ['"ARMv8-A" "Linux"'],
+    'HTTPS': ['1 h3POOL.exaMPLe. alpn=h2,h3 echconfig="MTIzLi4uCg=="'],
     # 'IPSECKEY': ['12 0 2 . asdfdf==', '03 1 1 127.0.00.1 asdfdf==', '12 3 1 example.com. asdfdf==',],
     'KX': ['4 example.com.', '28 io.'],
     'LOC': [
@@ -113,6 +114,7 @@ VALID_RECORDS_CANONICAL = {
     ],
     'SRV': ['0 0 0 .', '100 1 5061 example.com.'],
     'SSHFP': ['2 2 aabbcceeddff'],
+    'SVCB': ['2 sVc2.example.NET. port=1234 echconfig="MjIyLi4uCg==" ipv6hint=2001:db8::2'],
     'TLSA': ['3 0 2 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd 696b8f6b92a913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TXT': [
         '"foobar"',
@@ -147,6 +149,16 @@ VALID_RECORDS_NON_CANONICAL = {
     'EUI48': ['AA-BB-CC-DD-EE-F1'],
     'EUI64': ['AA-BB-CC-DD-EE-FF-00-12'],
     'HINFO': ['cpu os'],
+    'HTTPS': [
+        # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with echconfig base64'd
+        '1 . alpn=h3',
+        '0 pool.svc.example.',
+        '1 h3pool.example. alpn=h2,h3 echconfig="MTIzLi4uCg=="',
+        '2 .      alpn=h2 echconfig="YWJjLi4uCg=="',
+        # made-up (not from RFC)
+        '1 pool.svc.example. no-default-alpn port=1234 ipv4hint=192.168.123.1',
+        '2 . echconfig=... key65333=ex1 key65444=ex2 mandatory=key65444,echconfig',  # see #section-7
+    ],
     # 'IPSECKEY': ['12 0 2 . asdfdf==', '03 1 1 127.0.00.1 asdfdf==', '12 3 1 example.com. asdfdf==',],
     'KX': ['012 example.TEST.'],
     'LOC': [
@@ -216,6 +228,11 @@ VALID_RECORDS_NON_CANONICAL = {
     'SPF': [],
     'SRV': ['100 01 5061 example.com.'],
     'SSHFP': ['02 2 aabbcceeddff'],
+    'SVCB': [
+        '0 svc4-baz.example.net.',
+        '1 . key65333=...',
+        '2 svc2.example.net. echconfig="MjIyLi4uCg==" ipv6hint=2001:db8::2 port=1234',
+    ],
     'TLSA': ['003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TXT': [
         f'"{"a" * 498}"',
@@ -248,6 +265,13 @@ INVALID_RECORDS = {
     'EUI48': ['aa-bb-ccdd-ee-ff', 'AA-BB-CC-DD-EE-GG'],
     'EUI64': ['aa-bb-cc-dd-ee-ff-gg-11', 'AA-BB-C C-DD-EE-FF-00-11'],
     'HINFO': ['"ARMv8-A"', f'"a" "{"b" * 256}"'],
+    'HTTPS': [
+        # from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-02#section-10.3, with echconfig base64'd
+        '1 h3pool alpn=h2,h3 echconfig="MTIzLi4uCg=="',
+        # made-up (not from RFC)
+        '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
+    ],
     # 'IPSECKEY': [],
     'KX': ['-1 example.com', '10 example.com'],
     'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
@@ -263,6 +287,11 @@ INVALID_RECORDS = {
     'SPF': ['"v=spf1', 'v=spf1 include:example.com ~all'],
     'SRV': ['0 0 0 0', '100 5061 example.com.'],
     'SSHFP': ['aabbcceeddff'],
+    'SVCB': [
+        '0 svc4-baz.example.net. keys=val',
+        '1 not.fully.qualified key65333=...',
+        '2 duplicate.key. echconfig="MjIyLi4uCg==" echconfig="MjIyLi4uCg=="',
+    ],
     'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
     'TXT': [
         'foob"ar',