From: Remi Gacogne Date: Tue, 13 Jan 2026 13:59:21 +0000 (+0100) Subject: dnsdist: Test dynamic blocks for DoQ/DoH3 clients, including cache hits X-Git-Tag: rec-5.4.0-beta1~43^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1ed092fdcb854e5c8eca4cbd6722efeb9bce6936;p=thirdparty%2Fpdns.git dnsdist: Test dynamic blocks for DoQ/DoH3 clients, including cache hits Signed-off-by: Remi Gacogne --- diff --git a/regression-tests.dnsdist/dnsdistDynBlockTests.py b/regression-tests.dnsdist/dnsdistDynBlockTests.py index c644e77cda..a850099da9 100644 --- a/regression-tests.dnsdist/dnsdistDynBlockTests.py +++ b/regression-tests.dnsdist/dnsdistDynBlockTests.py @@ -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, diff --git a/regression-tests.dnsdist/test_DynBlocksRatio.py b/regression-tests.dnsdist/test_DynBlocksRatio.py index 560ac857f9..67c6e70506 100644 --- a/regression-tests.dnsdist/test_DynBlocksRatio.py +++ b/regression-tests.dnsdist/test_DynBlocksRatio.py @@ -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