]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Test dynamic blocks for DoQ/DoH3 clients, including cache hits
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 13 Jan 2026 13:59:21 +0000 (14:59 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 13 Jan 2026 14:53:28 +0000 (15:53 +0100)
Signed-off-by: Remi Gacogne <remi.gacogne@powerdns.com>
regression-tests.dnsdist/dnsdistDynBlockTests.py
regression-tests.dnsdist/test_DynBlocksRatio.py

index c644e77cda7d9af44b8e429c1cf667b5bee78469..a850099da900220433402add5818e7929e73e7df 100644 (file)
@@ -483,7 +483,8 @@ class DynBlocksTest(DNSDistTest):
         self.assertEqual(query, receivedQuery)
         self.assertEqual(response, receivedResponse)
 
-    def doTestRCodeRatio(self, name, rcode, noerrorcount, rcodecount):
+    def doTestRCodeRatioViaProtocol(self, name, rcode, noerrorcount, rcodecount, method, cached=False):
+        sender = sender = getattr(self, method)
         query = dns.message.make_query(name, 'A', 'IN')
         response = dns.message.make_response(query)
         rrset = dns.rrset.from_text(name,
@@ -492,85 +493,50 @@ class DynBlocksTest(DNSDistTest):
                                     dns.rdatatype.A,
                                     '192.0.2.1')
         response.answer.append(rrset)
-        expectedResponse = dns.message.make_response(query)
+        rcodeQuery = dns.message.make_query('rcode-' + name, 'A', 'IN')
+        expectedResponse = dns.message.make_response(rcodeQuery)
         expectedResponse.set_rcode(rcode)
 
+        firstQuery = True
         # start with normal responses
         for _ in range(noerrorcount-1):
-            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-            receivedQuery.id = query.id
-            self.assertEqual(query, receivedQuery)
-            self.assertEqual(response, receivedResponse)
-
-        waitForMaintenanceToRun()
-
-        # we should NOT be dropped!
-        (_, receivedResponse) = self.sendUDPQuery(query, response)
-        self.assertEqual(receivedResponse, response)
-
-        # now with rcode!
-        sent = 0
-        allowed = 0
-        for _ in range(rcodecount):
-            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, expectedResponse)
-            sent = sent + 1
-            if receivedQuery:
+            if cached and not firstQuery:
+                # should be a cache hit
+                (receivedQuery, receivedResponse) = sender(query, response=None, useQueue=False)
+                self.assertEqual(receivedQuery, None)
+            else:
+                (receivedQuery, receivedResponse) = sender(query, response)
                 receivedQuery.id = query.id
                 self.assertEqual(query, receivedQuery)
-                self.assertEqual(expectedResponse, receivedResponse)
-                allowed = allowed + 1
-            else:
-                # the query has not reached the responder,
-                # let's clear the response queue
-                self.clearToResponderQueue()
-
-        # we should have been able to send all our queries since the minimum number of queries is set to noerrorcount + rcodecount
-        self.assertGreaterEqual(allowed, rcodecount)
-
-        waitForMaintenanceToRun()
-
-        # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
-        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False, timeout=1)
-        self.assertEqual(receivedResponse, None)
-
-        # wait until we are not blocked anymore
-        time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
-
-        # this one should succeed
-        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-        receivedQuery.id = query.id
-        self.assertEqual(query, receivedQuery)
-        self.assertEqual(response, receivedResponse)
-
-        # again, over TCP this time
-        # start with normal responses
-        for _ in range(noerrorcount-1):
-            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-            receivedQuery.id = query.id
-            self.assertEqual(query, receivedQuery)
             self.assertEqual(response, receivedResponse)
+            firstQuery = False
 
         waitForMaintenanceToRun()
 
         # we should NOT be dropped!
-        (_, receivedResponse) = self.sendUDPQuery(query, response)
+        if cached:
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+        else:
+            (_, receivedResponse) = sender(query, response)
         self.assertEqual(receivedResponse, response)
 
         # now with rcode!
         sent = 0
         allowed = 0
         for _ in range(rcodecount):
-            (receivedQuery, receivedResponse) = self.sendTCPQuery(query, expectedResponse)
+            (receivedQuery, receivedResponse) = sender(rcodeQuery, expectedResponse)
             sent = sent + 1
             if receivedQuery:
-                receivedQuery.id = query.id
-                self.assertEqual(query, receivedQuery)
+                receivedQuery.id = rcodeQuery.id
+                self.assertEqual(rcodeQuery, receivedQuery)
                 self.assertEqual(expectedResponse, receivedResponse)
                 allowed = allowed + 1
             else:
                 # the query has not reached the responder,
                 # let's clear the response queue
                 self.clearToResponderQueue()
+                if cached and receivedResponse:
+                    allowed = allowed + 1
 
         # we should have been able to send all our queries since the minimum number of queries is set to noerrorcount + rcodecount
         self.assertGreaterEqual(allowed, rcodecount)
@@ -578,18 +544,25 @@ class DynBlocksTest(DNSDistTest):
         waitForMaintenanceToRun()
 
         # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
-        (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False, timeout=0.5)
+        (_, receivedResponse) = sender(query, response=None, useQueue=False, timeout=1)
         self.assertEqual(receivedResponse, None)
 
         # wait until we are not blocked anymore
         time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
 
         # this one should succeed
-        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-        receivedQuery.id = query.id
-        self.assertEqual(query, receivedQuery)
+        if cached:
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+        else:
+            (receivedQuery, receivedResponse) = sender(query, response)
+            receivedQuery.id = query.id
+            self.assertEqual(query, receivedQuery)
         self.assertEqual(response, receivedResponse)
 
+    def doTestRCodeRatio(self, name, rcode, noerrorcount, rcodecount):
+        self.doTestRCodeRatioViaProtocol(name, rcode, noerrorcount, rcodecount, "sendUDPQuery")
+        self.doTestRCodeRatioViaProtocol(name, rcode, noerrorcount, rcodecount, "sendTCPQuery")
+
     def doTestCacheMissRatio(self, name, cacheHits, cacheMisses):
         rrset = dns.rrset.from_text(name,
                                     60,
index 560ac857f9f10a0fae9d3f862227422f15d1af7a..67c6e70506a0e33e84cab7dd3eed3bf97ebafbf4 100644 (file)
@@ -2,6 +2,7 @@
 import time
 import dns
 from dnsdistDynBlockTests import DynBlocksTest, waitForMaintenanceToRun
+from dnsdisttests import pickAvailablePort
 
 class TestDynBlockGroupServFailsRatio(DynBlocksTest):
 
@@ -27,6 +28,138 @@ class TestDynBlockGroupServFailsRatio(DynBlocksTest):
         name = 'servfailratio.group.dynblocks.tests.powerdns.com.'
         self.doTestRCodeRatio(name, dns.rcode.SERVFAIL, 10, 10)
 
+class TestDynBlockGroupServFailsRatioDoQ(DynBlocksTest):
+
+    # we need this period to be quite long because we request the valid
+    # queries to be still looked at to reach the 20 queries count!
+    _dynBlockPeriod = 6
+    _dnsDistListeningAddr = "127.0.0.2"
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _caCert = 'ca.pem'
+    _doqServerPort = pickAvailablePort()
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setRCodeRatio(DNSRCode.SERVFAIL, 0.2, %d, "Exceeded query rate", %d, 20)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    addDOQLocal("%s:%d", "%s", "%s")
+    newServer{address="127.0.0.1:%d"}
+    """
+    _config_params = ['_dynBlockPeriod', '_dynBlockDuration', '_dnsDistListeningAddr', '_doqServerPort', '_serverCert', '_serverKey', '_testServerPort']
+
+    def testDynBlocksServFailRatio(self):
+        """
+        Dyn Blocks (group): Server Failure Ratio via DoQ
+        """
+        name = 'servfailratio-doq.group.dynblocks.tests.powerdns.com.'
+        self.doTestRCodeRatioViaProtocol(name, dns.rcode.SERVFAIL, 10, 10, "sendDOQQueryWrapper")
+
+class TestDynBlockGroupServFailsRatioDoQCacheHit(DynBlocksTest):
+
+    # we need this period to be quite long because we request the valid
+    # queries to be still looked at to reach the 20 queries count!
+    _dynBlockPeriod = 6
+    _dnsDistListeningAddr = "127.0.0.2"
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _caCert = 'ca.pem'
+    _doqServerPort = pickAvailablePort()
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setRCodeRatio(DNSRCode.SERVFAIL, 0.2, %d, "Exceeded query rate", %d, 20)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    local pc = newPacketCache(1000, {maxTTL=86400, minTTL=1})
+    getPool(""):setCache(pc)
+
+    addDOQLocal("%s:%d", "%s", "%s")
+    newServer{address="127.0.0.1:%d"}
+    """
+    _config_params = ['_dynBlockPeriod', '_dynBlockDuration', '_dnsDistListeningAddr', '_doqServerPort', '_serverCert', '_serverKey', '_testServerPort']
+
+    def testDynBlocksServFailRatio(self):
+        """
+        Dyn Blocks (group): Server Failure Ratio via DoQ (cache hits)
+        """
+        name = 'servfailratio-doq-hits.group.dynblocks.tests.powerdns.com.'
+        self.doTestRCodeRatioViaProtocol(name, dns.rcode.SERVFAIL, 10, 10, "sendDOQQueryWrapper", cached=True)
+
+class TestDynBlockGroupServFailsRatioDoH3(DynBlocksTest):
+
+    # we need this period to be quite long because we request the valid
+    # queries to be still looked at to reach the 20 queries count!
+    _dynBlockPeriod = 6
+    _dnsDistListeningAddr = "127.0.0.2"
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _caCert = 'ca.pem'
+    _doh3ServerPort = pickAvailablePort()
+    _dohBaseURL = ("https://%s:%d/" % (_serverName, _doh3ServerPort))
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setRCodeRatio(DNSRCode.SERVFAIL, 0.2, %d, "Exceeded query rate", %d, 20)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    addDOH3Local("%s:%d", "%s", "%s")
+    newServer{address="127.0.0.1:%d"}
+    """
+    _config_params = ['_dynBlockPeriod', '_dynBlockDuration', '_dnsDistListeningAddr', '_doh3ServerPort', '_serverCert', '_serverKey', '_testServerPort']
+
+    def testDynBlocksServFailRatio(self):
+        """
+        Dyn Blocks (group): Server Failure Ratio via DoH3
+        """
+        name = 'servfailratio-doh3.group.dynblocks.tests.powerdns.com.'
+        self.doTestRCodeRatioViaProtocol(name, dns.rcode.SERVFAIL, 10, 10, "sendDOH3QueryWrapper")
+
+class TestDynBlockGroupServFailsRatioDoH3CacheHit(DynBlocksTest):
+
+    # we need this period to be quite long because we request the valid
+    # queries to be still looked at to reach the 20 queries count!
+    _dynBlockPeriod = 6
+    _dnsDistListeningAddr = "127.0.0.2"
+    _serverKey = 'server.key'
+    _serverCert = 'server.chain'
+    _serverName = 'tls.tests.dnsdist.org'
+    _caCert = 'ca.pem'
+    _doh3ServerPort = pickAvailablePort()
+    _dohBaseURL = ("https://%s:%d/" % (_serverName, _doh3ServerPort))
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setRCodeRatio(DNSRCode.SERVFAIL, 0.2, %d, "Exceeded query rate", %d, 20)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    local pc = newPacketCache(1000, {maxTTL=86400, minTTL=1})
+    getPool(""):setCache(pc)
+
+    addDOH3Local("%s:%d", "%s", "%s")
+    newServer{address="127.0.0.1:%d"}
+    """
+    _config_params = ['_dynBlockPeriod', '_dynBlockDuration', '_dnsDistListeningAddr', '_doh3ServerPort', '_serverCert', '_serverKey', '_testServerPort']
+
+    def testDynBlocksServFailRatio(self):
+        """
+        Dyn Blocks (group): Server Failure Ratio via DoH3 (cache hits)
+        """
+        name = 'servfailratio-doh3-hits.group.dynblocks.tests.powerdns.com.'
+        self.doTestRCodeRatioViaProtocol(name, dns.rcode.SERVFAIL, 10, 10, "sendDOH3QueryWrapper", cached=True)
+
 class TestDynBlockGroupCacheMissRatio(DynBlocksTest):
 
     # we need this period to be quite long because we request the valid