From: Remi Gacogne Date: Thu, 24 Jul 2025 08:57:28 +0000 (+0200) Subject: dnsdist: Fix QType rate dynamic block with YAML X-Git-Tag: auth-5.1.0-alpha0~20^2~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=eb01c11a5418da08d5e11acdd519e2816e937835;p=thirdparty%2Fpdns.git dnsdist: Fix QType rate dynamic block with YAML The YAML configuration for the the "QType rate" dynamic block was totally broken, trying to configure a rcode rate rule instead of a qtype rate one. Thanks to HellSpawn for reporting this the issue! Signed-off-by: Remi Gacogne --- diff --git a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc index 1d0f534b4..70731bc42 100644 --- a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc +++ b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc @@ -143,6 +143,18 @@ static uint8_t strToRCode(const std::string& context, const std::string& paramet return *rcode; } +static uint16_t strToQType(const std::string& context, const std::string& parameterName, const ::rust::String& qtype_rust_string) +{ + auto qtype_str = std::string(qtype_rust_string); + boost::to_lower(qtype_str); + QType qtype; + qtype = std::string(qtype_str); + if (qtype.getCode() == 0) { + return checkedConversionFromStr(context, parameterName, qtype_rust_string); + } + return qtype; +} + static std::optional loadContentFromConfigurationFile(const std::string& fileName) { /* no check on the file size, don't do this with just any file! */ @@ -647,7 +659,7 @@ static void loadDynamicBlockConfiguration(const dnsdist::rust::settings::Dynamic ruleParams.d_tagSettings->d_name = std::string(rule.tag_name); ruleParams.d_tagSettings->d_value = std::string(rule.tag_value); } - dbrgObj->setRCodeRate(checkedConversionFromStr("dynamic-rules.rules.qtype_rate", "qtype", rule.qtype), std::move(ruleParams)); + dbrgObj->setQTypeRate(strToQType("dynamic-rules.rules.qtype_rate", "qtype", rule.qtype), std::move(ruleParams)); } else if (rule.rule_type == "cache-miss-ratio") { DynBlockRulesGroup::DynBlockCacheMissRatioRule ruleParams(std::string(rule.comment), rule.action_duration, rule.ratio, rule.warning_ratio, rule.seconds, rule.action.empty() ? DNSAction::Action::None : DNSAction::typeFromString(std::string(rule.action)), rule.minimum_number_of_responses, rule.minimum_global_cache_hit_ratio); diff --git a/regression-tests.dnsdist/dnsdistDynBlockTests.py b/regression-tests.dnsdist/dnsdistDynBlockTests.py index 6e67d58fc..c644e77cd 100644 --- a/regression-tests.dnsdist/dnsdistDynBlockTests.py +++ b/regression-tests.dnsdist/dnsdistDynBlockTests.py @@ -18,6 +18,7 @@ class DynBlocksTest(DNSDistTest): _webServerAPIKey = 'apisecret' _webServerAPIKeyHashed = '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso=' _dynBlockQPS = 10 + _dynBlockANYQPS = 10 _dynBlockPeriod = 2 # this needs to be greater than maintenanceWaitTime _dynBlockDuration = _maintenanceWaitTime + 2 @@ -129,6 +130,47 @@ class DynBlocksTest(DNSDistTest): self.assertEqual(query, receivedQuery) self.assertEqual(response, receivedResponse) + def doTestQTypeRate(self, name): + query = dns.message.make_query(name, 'ANY', 'IN') + response = dns.message.make_response(query) + blockedResponse = dns.message.make_response(query) + blockedResponse.set_rcode(dns.rcode.REFUSED) + + allowed = 0 + sent = 0 + for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1): + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + sent = sent + 1 + if receivedQuery: + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + allowed = allowed + 1 + else: + # the query has not reached the responder, + # let's clear the response queue + self.clearToResponderQueue() + + # we might be already blocked, but we should have been able to send + # at least self._dynBlockQPS queries + self.assertGreaterEqual(allowed, self._dynBlockQPS) + + if allowed == sent: + waitForMaintenanceToRun() + + # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False, timeout=0.5) + self.assertEqual(receivedResponse, blockedResponse) + + # 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) + def doTestQRateRCode(self, name, rcode): query = dns.message.make_query(name, 'A', 'IN') response = dns.message.make_response(query) diff --git a/regression-tests.dnsdist/test_DynBlocksGroup.py b/regression-tests.dnsdist/test_DynBlocksGroup.py index fc6299805..a2d92e4a8 100644 --- a/regression-tests.dnsdist/test_DynBlocksGroup.py +++ b/regression-tests.dnsdist/test_DynBlocksGroup.py @@ -28,6 +28,55 @@ class TestDynBlockGroupQPS(DynBlocksTest): name = 'qrate.group.dynblocks.tests.powerdns.com.' self.doTestQRate(name) +class TestDynBlockGroupQTypeRate(DynBlocksTest): + + _config_template = """ + local dbr = dynBlockRulesGroup() + dbr:setQTypeRate(DNSQType.ANY, %d, %d, "Exceeded qtype rate", %d) + + function maintenance() + dbr:apply() + end + setDynBlocksAction(DNSAction.Refused) + newServer{address="127.0.0.1:%d"} + """ + _config_params = ['_dynBlockANYQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort'] + + def testDynBlocksQTypeRate(self): + """ + Dyn Blocks (Group): QType Rate + """ + name = 'qtype-rate.group.dynblocks.tests.powerdns.com.' + self.doTestQTypeRate(name) + +class TestDynBlockGroupQTypeRateYAML(DynBlocksTest): + + _yaml_config_template = """--- +dynamic_rules: + - name: "Block client generating too many ANY queries" + rules: + - type: "qtype-rate" + rate: %d + seconds: %d + action_duration: %d + comment: "Exceeded ANY rate" + action: "Refused" + qtype: "ANY" + +backends: + - address: "127.0.0.1:%d" + protocol: Do53 +""" + _config_params = [] + _yaml_config_params = ['_dynBlockANYQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort'] + + def testDynBlocksQTypeRate(self): + """ + Dyn Blocks (Group / YAML): QType Rate + """ + name = 'qtype-rate-yaml.group.dynblocks.tests.powerdns.com.' + self.doTestQTypeRate(name) + class TestDynBlockGroupQPSRefused(DynBlocksTest): _config_template = """