]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Add test for Dot with client cert
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Wed, 4 Feb 2026 15:29:32 +0000 (16:29 +0100)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Mon, 16 Feb 2026 07:51:41 +0000 (08:51 +0100)
When run individually, the new test works. But there seems to be a race
condition: in some cases old responders look to be still running, making
subsequent test fail on larger test runs.

Signed-off-by: Otto Moerbeek <otto.moerbeek@open-xchange.com>
regression-tests.recursor-dnssec/Makefile
regression-tests.recursor-dnssec/configClient.conf [new file with mode: 0644]
regression-tests.recursor-dnssec/recursortests.py
regression-tests.recursor-dnssec/test_DoT.py

index f5ab48aea055a1f9f7916f68a7fddb18be26737f..93d41affbdd9635fed8bc1072fe9e07ed7c68674 100644 (file)
@@ -1,5 +1,5 @@
 clean-certs:
-       rm -f ca.key ca.pem ca.srl server.csr server.key server.pem server.chain server.ocsp client csr clien.pem client.key client.p12
+       rm -f ca.key ca.pem ca.srl server.csr server.key server.pem server.chain server.ocsp client.csr clien.pem client.key client.p12
 clean-configs:
        rm -rf configs/*
 certs:
diff --git a/regression-tests.recursor-dnssec/configClient.conf b/regression-tests.recursor-dnssec/configClient.conf
new file mode 100644 (file)
index 0000000..f3f8f1a
--- /dev/null
@@ -0,0 +1,21 @@
+[req]
+default_bits = 2048
+encrypt_key = no
+prompt = no
+distinguished_name = client_distinguished_name
+req_extensions = v3_req
+
+[client_distinguished_name]
+CN = client.tests.powerdns.com
+OU = PowerDNS.com BV
+countryName = NL
+
+[v3_req]
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = client.tests.powerdns.com
+DNS.2 = powerdns.com
+IP.3 = 127.0.0.1
index 996f0a499d7e2e50d89f3326fcc8f59b67efcd88..fbfe330b8afbf2be35af0133ecbf3c58d840cf72 100644 (file)
@@ -1339,7 +1339,7 @@ distributor-threads={threads}
         return response
 
     @classmethod
-    def handleTCPConnection(cls, conn, fromQueue, toQueue, trailingDataResponse=False, multipleResponses=False, callback=None, partialWrite=False):
+    def handleTCPConnection(cls, conn, fromQueue, toQueue, trailingDataResponse=False, multipleResponses=False, callback=None, partialWrite=False,clientCert=False):
       ignoreTrailing = trailingDataResponse is True
       try:
         data = conn.recv(2)
@@ -1350,6 +1350,12 @@ distributor-threads={threads}
         conn.close()
         return
 
+      if clientCert:
+          print("Checking client cert")
+          cert = conn.getpeercert()
+          if cert is None:
+              raise AssertionError("Client certificate expected, got none")
+
       (datalen,) = struct.unpack("!H", data)
       data = conn.recv(datalen)
       forceRcode = None
@@ -1410,7 +1416,7 @@ distributor-threads={threads}
       conn.close()
 
     @classmethod
-    def TCPResponder(cls, port, fromQueue, toQueue, trailingDataResponse=False, multipleResponses=False, callback=None, tlsContext=None, multipleConnections=False, listeningAddr='127.0.0.1', partialWrite=False):
+    def TCPResponder(cls, port, fromQueue, toQueue, trailingDataResponse=False, multipleResponses=False, callback=None, tlsContext=None, multipleConnections=False, listeningAddr='127.0.0.1', partialWrite=False,clientCert=False):
         cls._backgroundThreads[threading.get_native_id()] = True
         # trailingDataResponse=True means "ignore trailing data".
         # Other values are either False (meaning "raise an exception")
@@ -1450,11 +1456,11 @@ distributor-threads={threads}
             if multipleConnections:
               thread = threading.Thread(name='TCP Connection Handler',
                                         target=cls.handleTCPConnection,
-                                        args=[conn, fromQueue, toQueue, trailingDataResponse, multipleResponses, callback, partialWrite])
+                                        args=[conn, fromQueue, toQueue, trailingDataResponse, multipleResponses, callback, partialWrite,clientCert])
               thread.daemon = True
               thread.start()
             else:
-              cls.handleTCPConnection(conn, fromQueue, toQueue, trailingDataResponse, multipleResponses, callback, partialWrite)
+              cls.handleTCPConnection(conn, fromQueue, toQueue, trailingDataResponse, multipleResponses, callback, partialWrite,clientCert)
 
         sock.close()
 
index b43910d6014926cd4c31f9999f448efbae6abcba..2c2e55ce20088fbb559d98774afa2e4bbafe6184 100644 (file)
@@ -258,6 +258,7 @@ class DoTWithLocalResponderTests(RecursorTest):
     _responsesCounter = {}
     _answerUnexpected = True
     _roothints = None
+    _clientCert = False
 
     @staticmethod
     def sniCallback(sslSocket, sni, sslContext):
@@ -299,8 +300,12 @@ class DoTWithLocalResponderTests(RecursorTest):
         if hasattr(tlsContext, 'sni_callback'):
             tlsContext.sni_callback = cls.sniCallback
 
+        if cls._clientCert:
+            tlsContext.verify_mode = ssl.CERT_REQUIRED
+            tlsContext.load_verify_locations(cafile="ca.pem")
+
         print("Launching TLS responder..")
-        cls._TLSResponder = threading.Thread(name='TLS Responder', target=cls.TCPResponder, args=[cls._tlsBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, None, tlsContext])
+        cls._TLSResponder = threading.Thread(name='TLS Responder', target=cls.TCPResponder, args=[cls._tlsBackendPort, cls._toResponderQueue, cls._fromResponderQueue, False, False, None, tlsContext, False, '127.0.0.1', False, cls._clientCert])
         cls._TLSResponder.daemon = True
         cls._TLSResponder.start()
 
@@ -376,6 +381,75 @@ webservice:
             'dot-outqueries': 1
         })
 
+class DoTOKWithClientCertOpenSSLTest(DoTWithLocalResponderTests):
+    """
+    This tests DoT to responder with openssl validation using a proper CA store for the locally generated cert
+    """
+
+    _confdir = 'DoTOKWithClientCertOpenSSL'
+    _wsPort = 8042
+    _wsTimeout = 2
+    _wsPassword = 'secretpassword'
+    _apiKey = 'secretapikey'
+    _clientCert = True
+    _config_template = """
+dnssec:
+    validation: off
+outgoing:
+    dot_to_auth_names: [powerdns.com]
+    tls_configurations:
+    - name: dotwithverifyopensslandclientcert
+      ca_store: 'ca.pem'
+      subject_name: tls.tests.powerdns.com
+      subnets: ['127.0.0.1']
+      validate_certificate: true
+      verbose_logging: true
+      client_certificate: client.p12
+      client_certificate_password: passw0rd
+recursor:
+    forward_zones_recurse:
+      - zone: powerdns.com
+        forwarders: ['127.0.0.1:853']
+    devonly_regression_test_mode: true
+webservice:
+    webserver: true
+    port: %d
+    address: 127.0.0.1
+    password: %s
+    api_key: %s
+    """ % (_wsPort, _wsPassword, _apiKey)
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        super(DoTOKWithClientCertOpenSSLTest, cls).generateRecursorYamlConfig(confdir, False)
+
+    def testUDP(self):
+        """
+        Outgoing TLS: UDP query is sent via TLS
+        """
+        name = 'udp.outgoing-tls.test.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        expectedResponse = dns.message.make_response(query, True)
+        rrset = dns.rrset.from_text(name,
+                                    15,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '127.0.0.1')
+        expectedResponse.answer.append(rrset)
+
+        currentCount = 0
+        if 'TLS Responder' in self._responsesCounter:
+            currentCount = self._responsesCounter['TLS Responder']
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, expectedResponse)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(receivedResponse, expectedResponse)
+
+        # there was one TCP query
+        self.checkOnlyTLSResponderHit(currentCount + 1)
+        self.checkMetrics({
+            'dot-outqueries': 1
+        })
 
 class DoTOKGnuTLSTest(DoTWithLocalResponderTests):
     """