]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add a cache-miss ratio dynamic block rule
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 14 Nov 2023 15:42:14 +0000 (16:42 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 11 Dec 2023 08:45:39 +0000 (09:45 +0100)
This PR adds the `DynBlockRulesGroup:setCacheMissRatio()` method
which can be used to throttle clients exceeding a ratio of cache misses
for a minimum number of queries over a period of time.

pdns/dnsdist-dynblocks.hh
pdns/dnsdist-lua-inspection.cc
pdns/dnsdist-rings.cc
pdns/dnsdist-rings.hh
pdns/dnsdistdist/dnsdist-dynblocks.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc

index 1e52c29a93125c28f44aa8df0b99d1b541a1b51b..62c58c052830111d60ebaefa932d47c600767c3f 100644 (file)
@@ -80,6 +80,7 @@ private:
     uint64_t queries{0};
     uint64_t responses{0};
     uint64_t respBytes{0};
+    uint64_t cacheMisses{0};
   };
 
   struct DynBlockRule
@@ -238,7 +239,7 @@ private:
     double d_warningRatio{0.0};
   };
 
-  typedef std::unordered_map<AddressAndPortRange, Counts, AddressAndPortRange::hash> counts_t;
+  using counts_t = std::unordered_map<AddressAndPortRange, Counts, AddressAndPortRange::hash>;
 
 public:
   DynBlockRulesGroup()
@@ -274,6 +275,11 @@ public:
     entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
+  void setCacheMissRatio(double ratio, double warningRatio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, size_t minimumNumberOfResponses)
+  {
+    d_respCacheMissRatioRule = DynBlockRatioRule(reason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses);
+  }
+
   using smtVisitor_t = std::function<std::tuple<bool, boost::optional<std::string>, boost::optional<int>>(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)>;
 
   void setSuffixMatchRule(unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, smtVisitor_t visitor)
@@ -398,7 +404,7 @@ private:
 
   bool hasResponseRules() const
   {
-    return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty();
+    return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty() || d_respCacheMissRatioRule.isEnabled();
   }
 
   bool hasSuffixMatchRules() const
@@ -420,6 +426,7 @@ private:
   DynBlockRule d_queryRateRule;
   DynBlockRule d_respRateRule;
   DynBlockRule d_suffixMatchRule;
+  DynBlockRatioRule d_respCacheMissRatioRule;
   NetmaskGroup d_excludedSubnets;
   SuffixMatchNode d_excludedDomains;
   smtVisitor_t d_smtVisitor;
index 8ff1159ec57369fe1f6c2eb94a45ac3d7e5c972c..dcd5496e994584cb0e28341d59a2131e4d0f5ec3 100644 (file)
@@ -138,11 +138,7 @@ static void statNodeRespRing(statvisitor_t visitor, uint64_t seconds)
         continue;
       }
 
-      bool hit = c.ds.sin4.sin_family == 0;
-      if (!hit && c.ds.isIPv4() && c.ds.sin4.sin_addr.s_addr == 0 && c.ds.sin4.sin_port == 0) {
-        hit = true;
-      }
-
+      const bool hit = c.isACacheHit();
       root.submit(c.name, ((c.dh.rcode == 0 && c.usec == std::numeric_limits<unsigned int>::max()) ? -1 : c.dh.rcode), c.size, hit, boost::none);
     }
   }
@@ -880,6 +876,11 @@ void setupLuaInspection(LuaContext& luaCtx)
         group->setQTypeRate(qtype, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
       }
     });
+  luaCtx.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(double, unsigned int, const std::string&, unsigned int, size_t, boost::optional<DNSAction::Action>, boost::optional<double>)>("setCacheMissRatio", [](std::shared_ptr<DynBlockRulesGroup>& group, double ratio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, size_t minimumNumberOfResponses, boost::optional<DNSAction::Action> action, boost::optional<double> warningRatio) {
+      if (group) {
+        group->setCacheMissRatio(ratio, warningRatio ? *warningRatio : 0.0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None, minimumNumberOfResponses);
+      }
+    });
   luaCtx.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint8_t, uint8_t, uint8_t)>("setMasks", [](std::shared_ptr<DynBlockRulesGroup>& group, uint8_t v4, uint8_t v6, uint8_t port) {
       if (group) {
         if (v4 > 32) {
index 5485b33869f22b7f86e52d3fc349e0290bd30c50..b97b44e6459d305a30154c2aaf08b73239e3ff26 100644 (file)
@@ -214,3 +214,12 @@ size_t Rings::loadFromFile(const std::string& filepath, const struct timespec& n
 
   return inserted;
 }
+
+bool Rings::Response::isACacheHit() const
+{
+  bool hit = ds.sin4.sin_family == 0;
+  if (!hit && ds.isIPv4() && ds.sin4.sin_addr.s_addr == 0 && ds.sin4.sin_port == 0) {
+    hit = true;
+  }
+  return hit;
+}
index 755d76979771ea235d2a053595f17f7bcdd0115e..084044f0584fdd3c68b6974f028c64e278483f84 100644 (file)
@@ -62,6 +62,8 @@ struct Rings {
     uint16_t qtype;
     // outgoing protocol
     dnsdist::Protocol protocol;
+
+    bool isACacheHit() const;
   };
 
   struct Shard
index 45eb5879bbc9bca60cbe717fe0efd94cf1857744..1225f3932e23dfa6d35984f487a241e94cf95f9c 100644 (file)
@@ -54,6 +54,16 @@ void DynBlockRulesGroup::apply(const struct timespec& now)
       continue;
     }
 
+    if (d_respCacheMissRatioRule.warningRatioExceeded(counters.responses, counters.cacheMisses)) {
+      handleWarning(blocks, now, requestor, d_respCacheMissRatioRule, updated);
+      continue;
+    }
+
+    if (d_respCacheMissRatioRule.ratioExceeded(counters.responses, counters.cacheMisses)) {
+      addBlock(blocks, now, requestor, d_respCacheMissRatioRule, updated);
+      continue;
+    }
+
     for (const auto& pair : d_qtypeRules) {
       const auto qtype = pair.first;
 
@@ -412,6 +422,12 @@ void DynBlockRulesGroup::processResponseRules(counts_t& counts, StatNode& root,
     responseCutOff = d_suffixMatchRule.d_cutOff;
   }
 
+  d_respCacheMissRatioRule.d_cutOff = d_respCacheMissRatioRule.d_minTime = now;
+  d_respCacheMissRatioRule.d_cutOff.tv_sec -= d_respCacheMissRatioRule.d_seconds;
+  if (d_respCacheMissRatioRule.d_cutOff < responseCutOff) {
+    responseCutOff = d_respCacheMissRatioRule.d_cutOff;
+  }
+
   for (auto& rule : d_rcodeRules) {
     rule.second.d_cutOff = rule.second.d_minTime = now;
     rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
@@ -445,22 +461,20 @@ void DynBlockRulesGroup::processResponseRules(counts_t& counts, StatNode& root,
       bool respRateMatches = d_respRateRule.matches(ringEntry.when);
       bool suffixMatchRuleMatches = d_suffixMatchRule.matches(ringEntry.when);
       bool rcodeRuleMatches = checkIfResponseCodeMatches(ringEntry);
+      bool respCacheMissRatioRuleMatches = d_respCacheMissRatioRule.matches(ringEntry.when);
 
-      if (respRateMatches || rcodeRuleMatches) {
-        if (respRateMatches) {
-          entry.respBytes += ringEntry.size;
-        }
-        if (rcodeRuleMatches) {
-          ++entry.d_rcodeCounts[ringEntry.dh.rcode];
-        }
+      if (respRateMatches) {
+        entry.respBytes += ringEntry.size;
+      }
+      if (rcodeRuleMatches) {
+        ++entry.d_rcodeCounts[ringEntry.dh.rcode];
+      }
+      if (respCacheMissRatioRuleMatches && !ringEntry.isACacheHit()) {
+        ++entry.cacheMisses;
       }
 
       if (suffixMatchRuleMatches) {
-        bool hit = ringEntry.ds.sin4.sin_family == 0;
-        if (!hit && ringEntry.ds.isIPv4() && ringEntry.ds.sin4.sin_addr.s_addr == 0 && ringEntry.ds.sin4.sin_port == 0) {
-          hit = true;
-        }
-
+        const bool hit = ringEntry.isACacheHit();
         root.submit(ringEntry.name, ((ringEntry.dh.rcode == 0 && ringEntry.usec == std::numeric_limits<unsigned int>::max()) ? -1 : ringEntry.dh.rcode), ringEntry.size, hit, boost::none);
       }
     }
index 826c348f005a3fe3ebc6a709c49671f11196298a..7f33304d6665eada19c8781b24539c77cf5776d6 100644 (file)
@@ -1629,6 +1629,20 @@ faster than the existing rules.
 
   Represents a group of dynamic block rules.
 
+  .. method:: DynBlockRulesGroup:setCacheMissRatio(ratio, seconds, reason, blockingTime, minimumNumberOfResponses [, action [, warningRate]])
+
+    .. versionadded:: 1.9.0
+
+    Adds a rate-limiting rule for the ratio of cache-misses responses over the total number of responses for a given client.
+
+    :param int ratio: Ratio of cache-miss responses per second over the total number of responses for this client to exceed
+    :param int seconds: Number of seconds the ratio has been exceeded
+    :param string reason: The message to show next to the blocks
+    :param int blockingTime: The number of seconds this block to expire
+    :param int minimumNumberOfResponses: How many total responses is required for this rule to apply
+    :param int action: The action to take when the dynamic block matches, see :ref:`DNSAction <DNSAction>`. (default to the one set with :func:`setDynBlocksAction`)
+    :param int warningRatio: If set to a non-zero value, the ratio above which a warning message will be issued and a no-op block inserted
+
   .. method:: DynBlockRulesGroup:setMasks(v4, v6, port)
 
     .. versionadded:: 1.7.0
index 6694a6ac12471d79d0d4aca74523186f0bc6a46c..e01306528b09cee6c88fe6b0ee3fa8bfd34728f8 100644 (file)
@@ -831,7 +831,102 @@ BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_ResponseByteRate, TestFixture) {
     BOOST_CHECK_EQUAL(block.blocks, 0U);
     BOOST_CHECK_EQUAL(block.warning, false);
   }
+}
+
+BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_CacheMissRatio, TestFixture) {
+  dnsheader dh;
+  memset(&dh, 0, sizeof(dh));
+  DNSName qname("rings.powerdns.com.");
+  ComboAddress requestor1("192.0.2.1");
+  ComboAddress requestor2("192.0.2.2");
+  ComboAddress backend("192.0.2.42");
+  ComboAddress cacheHit;
+  uint16_t qtype = QType::AAAA;
+  uint16_t size = 42;
+  dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP;
+  unsigned int responseTime = 100 * 1000; /* 100ms */
+  struct timespec now;
+  gettime(&now);
+  NetmaskTree<DynBlock, AddressAndPortRange> emptyNMG;
+
+  time_t numberOfSeconds = 10;
+  unsigned int blockDuration = 60;
+  const auto action = DNSAction::Action::Drop;
+  const std::string reason = "Exceeded cache-miss ratio";
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* block above 0.5 Cache-Miss/Total ratio over numberOfSeconds seconds, no warning, minimum number of queries should be at least 51 */
+  dbrg.setCacheMissRatio(0.5, 0, numberOfSeconds, reason, blockDuration, action, 51);
+
+  {
+    /* insert 50 cache misses and 50 cache hits from a given client in the last 10s
+       this should not trigger the rule */
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0U);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < 20; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol);
+    }
+    for (size_t idx = 0; idx < 80; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, cacheHit, outgoingProtocol);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100U);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert 51 cache misses and 49 hits from a given client in the last 10s
+       this should trigger the rule this time */
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0U);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < 51; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol);
+    }
+    for (size_t idx = 0; idx < 49; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, cacheHit, outgoingProtocol);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100U);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U);
+    BOOST_REQUIRE(g_dynblockNMG.getLocal()->lookup(requestor1) != nullptr);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor2) == nullptr);
+    const auto& block = g_dynblockNMG.getLocal()->lookup(requestor1)->second;
+    BOOST_CHECK_EQUAL(block.reason, reason);
+    BOOST_CHECK_EQUAL(block.until.tv_sec, now.tv_sec + blockDuration);
+    BOOST_CHECK(block.domain.empty());
+    BOOST_CHECK(block.action == action);
+    BOOST_CHECK_EQUAL(block.blocks, 0U);
+    BOOST_CHECK_EQUAL(block.warning, false);
+  }
+
+  {
+    /* insert 40 misses and 10 hits from a given client in the last 10s
+       this should NOT trigger the rule since we don't have more than 50 queries */
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0U);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < 40; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol);
+    }
+    for (size_t idx = 0; idx < 10; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, cacheHit, outgoingProtocol);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 50U);
 
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
 }
 
 BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_Warning, TestFixture) {