]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Fix QType rate dynamic block with YAML
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 24 Jul 2025 08:57:28 +0000 (10:57 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 24 Jul 2025 08:57:28 +0000 (10:57 +0200)
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 <remi.gacogne@powerdns.com>
pdns/dnsdistdist/dnsdist-configuration-yaml.cc
regression-tests.dnsdist/dnsdistDynBlockTests.py
regression-tests.dnsdist/test_DynBlocksGroup.py

index 1d0f534b432695217a98b6d896468c0ee664f9fd..70731bc42537c3ee61b296ac070fca40e1837c2c 100644 (file)
@@ -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<uint8_t>(context, parameterName, qtype_rust_string);
+  }
+  return qtype;
+}
+
 static std::optional<std::string> 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<int>("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);
index 6e67d58fc6e4e59514bc73198089fc59f3da3280..c644e77cda7d9af44b8e429c1cf667b5bee78469 100644 (file)
@@ -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)
index fc6299805eada5ab2a973cf1233a0df3b3f89b2e..a2d92e4a87c64c18ba9df4ca6982289e3d28aa03 100644 (file)
@@ -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 = """