]> git.ipfire.org Git - thirdparty/pdns.git/blobdiff - regression-tests.dnsdist/test_BackendDiscovery.py
dnsdist: Clarify the Lua FFI DNS header set/get regression tests
[thirdparty/pdns.git] / regression-tests.dnsdist / test_BackendDiscovery.py
index 5ce70a3fe756a2fb902091a51ea32e6c9f07de9f..645bc59be8e047794cbb6a98da40a1fff5dade34 100644 (file)
@@ -8,6 +8,7 @@ import ssl
 from dnsdisttests import DNSDistTest
 
 class TestBackendDiscovery(DNSDistTest):
+    # these ports are hardcoded for now, sorry about that!
     _noSVCBackendPort = 10600
     _svcNoUpgradeBackendPort = 10601
     _svcUpgradeDoTBackendPort = 10602
@@ -15,11 +16,22 @@ class TestBackendDiscovery(DNSDistTest):
     _svcUpgradeDoTBackendDifferentAddrPort1 = 10604
     _svcUpgradeDoTBackendDifferentAddrPort2 = 10605
     _svcUpgradeDoTUnreachableBackendPort = 10606
+    _svcBrokenDNSResponseBackendPort = 10607
+    _svcUpgradeDoHBackendWithoutPathPort = 10608
+    _connectionRefusedBackendPort = 10609
+    _eofBackendPort = 10610
+    _servfailBackendPort = 10611
+    _wrongNameBackendPort = 10612
+    _wrongIDBackendPort = 10613
+    _tooManyQuestionsBackendPort = 10614
+    _badQNameBackendPort = 10615
+    _svcUpgradeDoTNoPortBackendPort = 10616
+    _svcUpgradeDoHNoPortBackendPort = 10617
     _upgradedBackendsPool = 'upgraded'
 
     _consoleKey = DNSDistTest.generateConsoleKey()
     _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
-    _config_params = ['_consoleKeyB64', '_consolePort', '_noSVCBackendPort', '_svcNoUpgradeBackendPort', '_svcUpgradeDoTBackendPort', '_upgradedBackendsPool', '_svcUpgradeDoHBackendPort', '_svcUpgradeDoTBackendDifferentAddrPort1', '_svcUpgradeDoTBackendDifferentAddrPort2', '_svcUpgradeDoTUnreachableBackendPort']
+    _config_params = ['_consoleKeyB64', '_consolePort', '_noSVCBackendPort', '_svcNoUpgradeBackendPort', '_svcUpgradeDoTBackendPort', '_upgradedBackendsPool', '_svcUpgradeDoHBackendPort', '_svcUpgradeDoTBackendDifferentAddrPort1', '_svcUpgradeDoTBackendDifferentAddrPort2', '_svcUpgradeDoTUnreachableBackendPort', '_svcBrokenDNSResponseBackendPort', '_svcUpgradeDoHBackendWithoutPathPort', '_connectionRefusedBackendPort', '_eofBackendPort', '_servfailBackendPort', '_wrongNameBackendPort', '_wrongIDBackendPort', '_tooManyQuestionsBackendPort', '_badQNameBackendPort', '_svcUpgradeDoTNoPortBackendPort', '_svcUpgradeDoHNoPortBackendPort']
     _config_template = """
     setKey("%s")
     controlSocket("127.0.0.1:%d")
@@ -33,10 +45,10 @@ class TestBackendDiscovery(DNSDistTest):
     newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
 
     -- SVCB upgrade to DoT, same address, keep the backend, different pool
-    newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradePool='%s', autoUpgradeKeep=true}:setUp()
+    newServer{address="127.0.0.1:%s", caStore='ca.pem', pool={'', 'another-pool'}, autoUpgrade=true, autoUpgradePool='%s', autoUpgradeKeep=true, source='127.0.0.1@lo'}:setUp()
 
     -- SVCB upgrade to DoH, same address, do not keep the backend, same pool
-    newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
+    newServer{address="127.0.0.1:%s", caStore='ca.pem', pool={'another-pool'}, autoUpgrade=true, autoUpgradeKeep=false}:setUp()
 
     -- SVCB upgrade to DoT, different address, certificate is valid for the initial address
     newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
@@ -46,6 +58,39 @@ class TestBackendDiscovery(DNSDistTest):
 
     -- SVCB upgrade to DoT but upgraded port is not reachable
     newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
+
+    -- The SVCB response is not valid
+    newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
+
+    -- SVCB upgrade to DoH except the path is not specified
+    newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
+
+    -- Connection refused
+    newServer({address="127.0.0.1:%s", caStore='ca.pem', pool={"", "other-pool"}, autoUpgrade=true, source='127.0.0.1@lo'}):setUp()
+
+    -- EOF
+    newServer({address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true}):setUp()
+
+    -- ServFail
+    newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
+
+    -- Wrong name
+    newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
+
+    -- Wrong ID
+    newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
+
+    -- Too many questions
+    newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
+
+    -- Bad QName
+    newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
+
+    -- SVCB upgrade to DoT, same address, no port specified via SVCB
+    newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
+
+    -- SVCB upgrade to DoH, same address, no port specified via SVCB
+    newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
     """
     _verboseMode = True
 
@@ -70,6 +115,33 @@ class TestBackendDiscovery(DNSDistTest):
                                     dns.rdatatype.SVCB,
                                     '1 tls.tests.dnsdist.org. alpn="dot" port=10652 ipv4hint=127.0.0.1')
         response.answer.append(rrset)
+        # add a useless A record for good measure
+        rrset = dns.rrset.from_text(request.question[0].name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+        # plus more useless records in authority
+        rrset = dns.rrset.from_text(request.question[0].name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.authority.append(rrset)
+        # and finally valid, albeit useless, hints
+        rrset = dns.rrset.from_text('tls.tests.dnsdist.org.',
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '127.0.0.1')
+        response.additional.append(rrset)
+        rrset = dns.rrset.from_text('tls.tests.dnsdist.org.',
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.additional.append(rrset)
         return response.to_wire()
 
     def UpgradeDoHCallback(request):
@@ -112,57 +184,171 @@ class TestBackendDiscovery(DNSDistTest):
         response.answer.append(rrset)
         return response.to_wire()
 
+    def BrokenResponseCallback(request):
+        response = dns.message.make_response(request)
+        response.use_edns(edns=False)
+        response.question = []
+        return response.to_wire()
+
+    def UpgradeDoHMissingPathCallback(request):
+        response = dns.message.make_response(request)
+        rrset = dns.rrset.from_text(request.question[0].name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.SVCB,
+                                    '1 tls.tests.dnsdist.org. alpn="h2" port=10653 ipv4hint=127.0.0.1')
+        response.answer.append(rrset)
+        return response.to_wire()
+
+    def EOFCallback(request):
+        return None
+
+    def ServFailCallback(request):
+        response = dns.message.make_response(request)
+        response.set_rcode(dns.rcode.SERVFAIL)
+        return response.to_wire()
+
+    def WrongNameCallback(request):
+        query = dns.message.make_query('not-the-right-one.', dns.rdatatype.SVCB)
+        response = dns.message.make_response(query)
+        response.id = request.id
+        return response.to_wire()
+
+    def WrongIDCallback(request):
+        response = dns.message.make_response(request)
+        response.id = request.id ^ 42
+        return response.to_wire()
+
+    def WrongIDCallback(request):
+        response = dns.message.make_response(request)
+        response.id = request.id ^ 42
+        return response.to_wire()
+
+    def TooManyQuestionsCallback(request):
+        response = dns.message.make_response(request)
+        response.question.append(response.question[0])
+        return response.to_wire()
+
+    def BadQNameCallback(request):
+        response = dns.message.make_response(request)
+        wire = bytearray(response.to_wire())
+        # mess up the first label length
+        wire[12] = 0xFF
+        return wire
+
+    def UpgradeDoTNoPortCallback(request):
+        response = dns.message.make_response(request)
+        rrset = dns.rrset.from_text(request.question[0].name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.SVCB,
+                                    '1 tls.tests.dnsdist.org. alpn="dot" ipv4hint=127.0.0.1')
+        response.answer.append(rrset)
+        return response.to_wire()
+
+    def UpgradeDoHNoPortCallback(request):
+        response = dns.message.make_response(request)
+        rrset = dns.rrset.from_text(request.question[0].name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.SVCB,
+                                    '1 tls.tests.dnsdist.org. alpn="h2" ipv4hint=127.0.0.1 key7="/dns-query{?dns}"')
+        response.answer.append(rrset)
+        return response.to_wire()
+
     @classmethod
     def startResponders(cls):
         tlsContext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
         tlsContext.load_cert_chain('server.chain', 'server.key')
 
         TCPNoSVCResponder = threading.Thread(name='TCP no SVC Responder', target=cls.TCPResponder, args=[cls._noSVCBackendPort, cls._toResponderQueue, cls._fromResponderQueue, True, False, cls.NoSVCCallback])
-        TCPNoSVCResponder.setDaemon(True)
+        TCPNoSVCResponder.daemon = True
         TCPNoSVCResponder.start()
 
         TCPNoUpgradeResponder = threading.Thread(name='TCP no upgrade Responder', target=cls.TCPResponder, args=[cls._svcNoUpgradeBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.NoUpgradePathCallback])
-        TCPNoUpgradeResponder.setDaemon(True)
+        TCPNoUpgradeResponder.daemon = True
         TCPNoUpgradeResponder.start()
 
-        TCPUpgradeToDoTResponder = threading.Thread(name='TCP upgrade to DoT Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoTBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoTCallback])
-        TCPUpgradeToDoTResponder.setDaemon(True)
+        # this one is special, does partial writes!
+        TCPUpgradeToDoTResponder = threading.Thread(name='TCP upgrade to DoT Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoTBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoTCallback, None, False, '127.0.0.1', True])
+        TCPUpgradeToDoTResponder.daemon = True
         TCPUpgradeToDoTResponder.start()
         # and the corresponding DoT responder
         UpgradedDoTResponder = threading.Thread(name='DoT upgraded Responder', target=cls.TCPResponder, args=[10652, cls._toResponderQueue, cls._fromResponderQueue, False, False, None, tlsContext])
-        UpgradedDoTResponder.setDaemon(True)
+        UpgradedDoTResponder.daemon = True
         UpgradedDoTResponder.start()
 
         TCPUpgradeToDoHResponder = threading.Thread(name='TCP upgrade to DoH Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoHBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoHCallback])
-        TCPUpgradeToDoHResponder.setDaemon(True)
+        TCPUpgradeToDoHResponder.daemon = True
         TCPUpgradeToDoHResponder.start()
         # and the corresponding DoH responder
         UpgradedDOHResponder = threading.Thread(name='DOH Responder', target=cls.DOHResponder, args=[10653, cls._toResponderQueue, cls._fromResponderQueue, False, False, None, tlsContext])
-        UpgradedDOHResponder.setDaemon(True)
+        UpgradedDOHResponder.daemon = True
         UpgradedDOHResponder.start()
 
         TCPUpgradeToDoTDifferentAddrResponder = threading.Thread(name='TCP upgrade to DoT different addr 1 Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoTBackendDifferentAddrPort1, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoTDifferentAddr1Callback])
-        TCPUpgradeToDoTDifferentAddrResponder.setDaemon(True)
+        TCPUpgradeToDoTDifferentAddrResponder.daemon = True
         TCPUpgradeToDoTDifferentAddrResponder.start()
         # and the corresponding DoT responder
         UpgradedDoTResponder = threading.Thread(name='DoT upgraded different addr 1 Responder', target=cls.TCPResponder, args=[10654, cls._toResponderQueue, cls._fromResponderQueue, False, False, None, tlsContext, False, '127.0.0.2'])
-        UpgradedDoTResponder.setDaemon(True)
+        UpgradedDoTResponder.daemon = True
         UpgradedDoTResponder.start()
 
         TCPUpgradeToDoTDifferentAddrResponder = threading.Thread(name='TCP upgrade to DoT different addr 2 Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoTBackendDifferentAddrPort2, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoTDifferentAddr2Callback, None, False, '127.0.0.2'])
-        TCPUpgradeToDoTDifferentAddrResponder.setDaemon(True)
+        TCPUpgradeToDoTDifferentAddrResponder.daemon = True
         TCPUpgradeToDoTDifferentAddrResponder.start()
         # and the corresponding DoT responder
         UpgradedDoTResponder = threading.Thread(name='DoT upgraded different addr 2 Responder', target=cls.TCPResponder, args=[10655, cls._toResponderQueue, cls._fromResponderQueue, False, False, None, tlsContext, False])
-        UpgradedDoTResponder.setDaemon(True)
+        UpgradedDoTResponder.daemon = True
         UpgradedDoTResponder.start()
 
         TCPUpgradeToUnreachableDoTResponder = threading.Thread(name='TCP upgrade to unreachable DoT Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoTUnreachableBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoTUnreachableCallback])
-        TCPUpgradeToUnreachableDoTResponder.setDaemon(True)
+        TCPUpgradeToUnreachableDoTResponder.daemon = True
         TCPUpgradeToUnreachableDoTResponder.start()
         # and NO corresponding DoT responder
         # this is not a mistake!
 
+        BrokenResponseResponder = threading.Thread(name='Broken response Responder', target=cls.TCPResponder, args=[cls._svcBrokenDNSResponseBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.BrokenResponseCallback])
+        BrokenResponseResponder.daemon = True
+        BrokenResponseResponder.start()
+
+        DOHMissingPathResponder = threading.Thread(name='DoH missing path Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoHBackendWithoutPathPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoHMissingPathCallback])
+        DOHMissingPathResponder.daemon = True
+        DOHMissingPathResponder.start()
+
+        EOFResponder = threading.Thread(name='EOF Responder', target=cls.TCPResponder, args=[cls._eofBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.EOFCallback])
+        EOFResponder.daemon = True
+        EOFResponder.start()
+
+        ServFailResponder = threading.Thread(name='ServFail Responder', target=cls.TCPResponder, args=[cls._servfailBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.ServFailCallback])
+        ServFailResponder.daemon = True
+        ServFailResponder.start()
+
+        WrongNameResponder = threading.Thread(name='Wrong Name Responder', target=cls.TCPResponder, args=[cls._wrongNameBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.WrongNameCallback])
+        WrongNameResponder.daemon = True
+        WrongNameResponder.start()
+
+        WrongIDResponder = threading.Thread(name='Wrong ID Responder', target=cls.TCPResponder, args=[cls._wrongIDBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.WrongIDCallback])
+        WrongIDResponder.daemon = True
+        WrongIDResponder.start()
+
+        TooManyQuestionsResponder = threading.Thread(name='Too many questions Responder', target=cls.TCPResponder, args=[cls._tooManyQuestionsBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.TooManyQuestionsCallback])
+        TooManyQuestionsResponder.daemon = True
+        TooManyQuestionsResponder.start()
+
+        badQNameResponder = threading.Thread(name='Bad QName Responder', target=cls.TCPResponder, args=[cls._badQNameBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.BadQNameCallback])
+        badQNameResponder.daemon = True
+        badQNameResponder.start()
+
+        TCPUpgradeToDoTNoPortResponder = threading.Thread(name='TCP upgrade to DoT (no port) Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoTNoPortBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoTNoPortCallback])
+        TCPUpgradeToDoTNoPortResponder.daemon = True
+        TCPUpgradeToDoTNoPortResponder.start()
+
+        TCPUpgradeToDoHNoPortResponder = threading.Thread(name='TCP upgrade to DoH (no port) Responder', target=cls.TCPResponder, args=[cls._svcUpgradeDoHNoPortBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, cls.UpgradeDoHNoPortCallback])
+        TCPUpgradeToDoHNoPortResponder.daemon = True
+        TCPUpgradeToDoHNoPortResponder.start()
+
+
     def checkBackendsUpgraded(self):
         output = self.sendConsoleCommand('showServers()')
         print(output)
@@ -172,23 +358,41 @@ class TestBackendDiscovery(DNSDistTest):
             if line.startswith('#') or line.startswith('All'):
                 continue
             tokens = line.split()
-            self.assertTrue(len(tokens) == 12 or len(tokens) == 13)
-            self.assertEquals(tokens[2], 'UP')
+            self.assertTrue(len(tokens) == 13 or len(tokens) == 14)
+            if tokens[1] == '127.0.0.1:10652':
+                # in this particular case, the upgraded backend
+                # does not replace the existing one and thus
+                # the health-check is forced to auto (or lazy auto)
+                self.assertEqual(tokens[2], 'up')
+            else:
+                self.assertEqual(tokens[2], 'UP')
             pool = ''
-            if len(tokens) == 13:
-                pool = tokens[12]
+            if len(tokens) == 14:
+                pool = tokens[13]
             backends[tokens[1]] = pool
 
         expected = {
             '127.0.0.1:10600': '',
             '127.0.0.1:10601': '',
-            '127.0.0.1:10602': '',
+            '127.0.0.1:10602': 'another-pool',
             # 10603 has been upgraded to 10653 and removed
             # 10604 has been upgraded to 10654 and removed
             '127.0.0.2:10605': '',
             '127.0.0.1:10606': '',
+            '127.0.0.1:10607': '',
+            '127.0.0.1:10608': '',
+            '127.0.0.1:10609': 'other-pool',
+            '127.0.0.1:10610': '',
+            '127.0.0.1:10611': '',
+            '127.0.0.1:10612': '',
+            '127.0.0.1:10613': '',
+            '127.0.0.1:10614': '',
+            '127.0.0.1:10615': '',
+            # these two are not upgraded because there is no backend listening on the default ports (443 and 853)
+            '127.0.0.1:10616': '',
+            '127.0.0.1:10617': '',
             '127.0.0.1:10652': 'upgraded',
-            '127.0.0.1:10653': '',
+            '127.0.0.1:10653': 'another-pool',
             '127.0.0.2:10654': ''
         }
         print(backends)
@@ -198,7 +402,6 @@ class TestBackendDiscovery(DNSDistTest):
         """
         Backend Discovery: Upgrade
         """
-
         # enough time for discovery to happen
         # 5s is not enough with TSAN
         time.sleep(10)
@@ -206,3 +409,51 @@ class TestBackendDiscovery(DNSDistTest):
             # let's wait a bit longer
             time.sleep(5)
             self.assertTrue(self.checkBackendsUpgraded())
+
+class TestBackendDiscoveryByHostname(DNSDistTest):
+    _consoleKey = DNSDistTest.generateConsoleKey()
+    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+    _config_params = ['_consoleKeyB64', '_consolePort']
+    _config_template = """
+    setKey("%s")
+    controlSocket("127.0.0.1:%d")
+
+    function resolveCB(hostname, ips)
+      print('Got response for '..hostname)
+      for _, ip in ipairs(ips) do
+        print(ip)
+        newServer(ip:toString())
+      end
+    end
+
+    getAddressInfo('dns.quad9.net.', resolveCB)
+    """
+    def checkBackends(self):
+        output = self.sendConsoleCommand('showServers()')
+        print(output)
+        backends = {}
+        for line in output.splitlines(False):
+            if line.startswith('#') or line.startswith('All'):
+                continue
+            tokens = line.split()
+            self.assertTrue(len(tokens) == 13 or len(tokens) == 14)
+            backends[tokens[1]] = tokens[2]
+
+        if len(backends) != 4:
+            return False
+
+        for expected in ['9.9.9.9:53', '149.112.112.112:53', '[2620:fe::9]:53', '[2620:fe::fe]:53']:
+            self.assertIn(expected, backends)
+        for backend in backends:
+            self.assertTrue(backends[backend])
+        return True
+
+    def testBackendFromHostname(self):
+        """
+        Backend Discovery: From hostname
+        """
+        # enough time for resolution to happen
+        time.sleep(4)
+        if not self.checkBackends():
+            time.sleep(4)
+            self.assertTrue(self.checkBackends())