]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Allow setting the block reason from the SMT callback 10835/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 11 Oct 2021 12:19:31 +0000 (14:19 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 11 Oct 2021 12:19:31 +0000 (14:19 +0200)
pdns/dnsdist-dynblocks.hh
pdns/dnsdistdist/dnsdist-dynblocks.cc
pdns/dnsdistdist/dnsdist-lua-inspection-ffi.cc
pdns/dnsdistdist/dnsdist-lua-inspection-ffi.hh
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc

index d802d05e1e1ce4eb7de967657ee33e66624d046e..2833bfe42288ed1f0c5b4a548986cf456526c7bc 100644 (file)
@@ -45,13 +45,14 @@ typedef std::function<bool(dnsdist_ffi_stat_node_t*)> dnsdist_ffi_stat_node_visi
 
 struct dnsdist_ffi_stat_node_t
 {
-  dnsdist_ffi_stat_node_t(const StatNode& node_, const StatNode::Stat& self_, const StatNode::Stat& children_): node(node_), self(self_), children(children_)
+  dnsdist_ffi_stat_node_t(const StatNode& node_, const StatNode::Stat& self_, const StatNode::Stat& children_, std::optional<std::string>& reason_): node(node_), self(self_), children(children_), reason(reason_)
   {
   }
 
   const StatNode& node;
   const StatNode::Stat& self;
   const StatNode::Stat& children;
+  std::optional<std::string>& reason;
 };
 
 class DynBlockRulesGroup
@@ -247,7 +248,7 @@ public:
     entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
-  typedef std::function<bool(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)> smtVisitor_t;
+  typedef std::function<std::tuple<bool, boost::optional<std::string>>(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)> smtVisitor_t;
 
   void setSuffixMatchRule(unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action, smtVisitor_t visitor)
   {
index fac0624264bcb16cb7cf7edf66dae78502f3a50d..4d11edd24cce49cb0f43b1c35451156c11c8307d 100644 (file)
@@ -104,20 +104,27 @@ void DynBlockRulesGroup::apply(const struct timespec& now)
 
   if (!statNodeRoot.empty()) {
     StatNode::Stat node;
-    std::unordered_set<DNSName> namesToBlock;
+    std::unordered_map<DNSName, std::optional<std::string>> namesToBlock;
     statNodeRoot.visit([this,&namesToBlock](const StatNode* node_, const StatNode::Stat& self, const StatNode::Stat& children) {
                          bool block = false;
+                         std::optional<std::string> reason;
 
                          if (d_smtVisitorFFI) {
-                           dnsdist_ffi_stat_node_t tmp(*node_, self, children);
+                           dnsdist_ffi_stat_node_t tmp(*node_, self, children, reason);
                            block = d_smtVisitorFFI(&tmp);
                          }
                          else {
-                           block = d_smtVisitor(*node_, self, children);
+                           auto ret = d_smtVisitor(*node_, self, children);
+                           block = std::get<0>(ret);
+                           if (block) {
+                             if (boost::optional<std::string> tmp = std::get<1>(ret)) {
+                               reason = std::move(*tmp);
+                             }
+                           }
                          }
 
                          if (block) {
-                           namesToBlock.insert(DNSName(node_->fullname));
+                           namesToBlock.insert({DNSName(node_->fullname), std::move(reason)});
                          }
                        },
       node);
@@ -125,8 +132,15 @@ void DynBlockRulesGroup::apply(const struct timespec& now)
     if (!namesToBlock.empty()) {
       updated = false;
       SuffixMatchTree<DynBlock> smtBlocks = g_dynblockSMT.getCopy();
-      for (const auto& name : namesToBlock) {
-        addOrRefreshBlockSMT(smtBlocks, now, name, d_suffixMatchRule, updated);
+      for (auto& [name, reason] : namesToBlock) {
+        if (reason) {
+          DynBlockRule rule(d_suffixMatchRule);
+          rule.d_blockReason = std::move(*reason);
+          addOrRefreshBlockSMT(smtBlocks, now, std::move(name), std::move(rule), updated);
+        }
+        else {
+          addOrRefreshBlockSMT(smtBlocks, now, std::move(name), d_suffixMatchRule, updated);
+        }
       }
       if (updated) {
         g_dynblockSMT.setState(std::move(smtBlocks));
index e4ebea66c4c77aa9cbc16795d16ddb0d45e7ef19..13158faa035a8bba050f1cc15b7d05d33a267006 100644 (file)
@@ -99,3 +99,8 @@ uint64_t dnsdist_ffi_stat_node_get_children_bytes_count(const dnsdist_ffi_stat_n
 {
   return node->children.bytes;
 }
+
+void dnsdist_ffi_state_node_set_reason(dnsdist_ffi_stat_node_t* node, const char* reason, size_t reasonSize)
+{
+  node->reason = std::string(reason, reasonSize);
+}
index 6988d451b87ed8a7e3cf3d480239e8c13c3681f5..05f087f649d4f31126b0b48aefe965ebcc26a800 100644 (file)
@@ -40,4 +40,6 @@ extern "C" {
   uint64_t dnsdist_ffi_stat_node_get_children_servfails_count(const dnsdist_ffi_stat_node_t* node) __attribute__ ((visibility ("default")));
   uint64_t dnsdist_ffi_stat_node_get_children_drops_count(const dnsdist_ffi_stat_node_t* node) __attribute__ ((visibility ("default")));
   uint64_t dnsdist_ffi_stat_node_get_children_bytes_count(const dnsdist_ffi_stat_node_t* node) __attribute__ ((visibility ("default")));
+
+  void dnsdist_ffi_state_node_set_reason(dnsdist_ffi_stat_node_t* node, const char* reason, size_t reasonSize) __attribute__ ((visibility ("default")));
 }
index e001042d9e22ad339c7e4ee80a24188ef86491f7..10fda815fb43e1af4a2ed04597bd6d570586ccee 100644 (file)
@@ -1332,9 +1332,12 @@ faster than the existing rules.
 
     .. versionadded:: 1.4.0
 
+    .. versionchanged:: 1.7.0
+      This visitor function can now optionally return an additional string which will be set as the ``reason`` for the dynamic block.
+
     Set a Lua visitor function that will be called for each label of every domain seen in queries and responses. The function receives a `StatNode` object representing the stats of the parent, a second one with the stats of the current label and one with the stats of the current node plus all its children.
     Note that this function will not be called if a FFI version has been set using :meth:`DynBlockRulesGroup:setSuffixMatchRuleFFI`
-    If the function returns true, the current label will be blocked according to the `seconds`, `reason`, `blockingTime` and `action` parameters.
+    If the function returns true, the current label will be blocked according to the `seconds`, `reason`, `blockingTime` and `action` parameters. Since 1.7.0, the function can return an additional string, in addition to the boolean, which will be set as the ``reason`` for the dynamic block.
     Selected domains can be excluded from this processing using the :meth:`DynBlockRulesGroup:excludeDomains` method.
 
     This replaces the existing :func:`addDynBlockSMT` function.
index 374f8db2a750d91cccb64295aead2a2d663c91fb..035febae89cd8869d67c1ad14e1d96de6c5f6e22 100644 (file)
@@ -869,9 +869,9 @@ BOOST_AUTO_TEST_CASE(test_DynBlockRulesMetricsCache_GetTopN) {
 
     dbrg.setSuffixMatchRule(numberOfSeconds, reason, blockDuration, action, [](const StatNode& node, const StatNode::Stat& self, const StatNode::Stat& children) {
       if (self.queries > 0) {
-        return true;
+        return std::tuple<bool, boost::optional<std::string>>(true, boost::none);
       }
-      return false;
+      return std::tuple<bool, boost::optional<std::string>>(false, boost::none);
     });
 
     /* insert one fake response for 255 DNS names */
@@ -915,6 +915,62 @@ BOOST_AUTO_TEST_CASE(test_DynBlockRulesMetricsCache_GetTopN) {
     BOOST_CHECK(g_dynblockSMT.getLocal()->getNodes().empty());
   }
 
+  {
+    /* === reset everything for SMT, this time we will check that we can override the 'reason' via the visitor function === */
+    DynBlockRulesGroup dbrg;
+    dbrg.setQuiet(true);
+    g_rings.clear();
+    g_dynblockNMG.setState(emptyNMG);
+    g_dynblockSMT.setState(emptySMT);
+
+    dbrg.setSuffixMatchRule(numberOfSeconds, reason, blockDuration, action, [](const StatNode& node, const StatNode::Stat& self, const StatNode::Stat& children) {
+      if (self.queries > 0) {
+        return std::tuple<bool, boost::optional<std::string>>(true, "blocked for a different reason");
+      }
+      return std::tuple<bool, boost::optional<std::string>>(false, boost::none);
+    });
+
+    /* insert one fake response for 255 DNS names */
+    const ComboAddress requestor("192.0.2.1");
+    for (size_t idx = 0; idx < 256; idx++) {
+      g_rings.insertResponse(now, requestor, DNSName(std::to_string(idx)) + qname, qtype, 1000 /*usec*/, size, dh, requestor /* backend, technically, but we don't care */);
+    }
+
+    /* we apply the rules, all suffixes should be blocked */
+    dbrg.apply(now);
+
+    for (size_t idx = 0; idx < 256; idx++) {
+      const DNSName name(DNSName(std::to_string(idx)) + qname);
+      const auto* block = g_dynblockSMT.getLocal()->lookup(name);
+      BOOST_REQUIRE(block != nullptr);
+      /* simulate that:
+         - 1.rings.powerdns.com. got 1 query
+         ...
+         - 255. does 255 queries
+      */
+      block->blocks = idx;
+    }
+
+    /* now we ask for the top 20 offenders for each reason */
+    StopWatch sw;
+    sw.start();
+    auto top = DynBlockMaintenance::getTopSuffixes(20);
+    BOOST_REQUIRE_EQUAL(top.size(), 1U);
+    auto suffixes = top.at("blocked for a different reason");
+    BOOST_REQUIRE_EQUAL(suffixes.size(), 20U);
+    auto it = suffixes.begin();
+    for (size_t idx = 236; idx < 256; idx++) {
+      BOOST_CHECK_EQUAL(it->first, (DNSName(std::to_string(idx)) + qname));
+      BOOST_CHECK_EQUAL(it->second, idx);
+      ++it;
+    }
+
+    struct timespec expired = now;
+    expired.tv_sec += blockDuration + 1;
+    DynBlockMaintenance::purgeExpired(expired);
+    BOOST_CHECK(g_dynblockSMT.getLocal()->getNodes().empty());
+  }
+
 #ifdef BENCH_DYNBLOCKS
   {
     /* now insert 1M names */
@@ -926,9 +982,9 @@ BOOST_AUTO_TEST_CASE(test_DynBlockRulesMetricsCache_GetTopN) {
 
     dbrg.setSuffixMatchRule(numberOfSeconds, reason, blockDuration, action, [](const StatNode& node, const StatNode::Stat& self, const StatNode::Stat& children) {
       if (self.queries > 0) {
-        return true;
+        return std::tuple<bool, boost::optional<std::string>>(true, boost::none);
       }
-      return false;
+      return std::tuple<bool, boost::optional<std::string>>(false, boost::none);
     });
 
     bool done = false;
@@ -955,12 +1011,15 @@ BOOST_AUTO_TEST_CASE(test_DynBlockRulesMetricsCache_GetTopN) {
     sw.start();
     auto top = DynBlockMaintenance::getTopSuffixes(20);
     cerr<<"scanned 1000000 entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl;
+    BOOST_CHECK_EQUAL(top.at(reason).size(), 20U);
+    BOOST_CHECK_EQUAL(top.size(), 1U);
 
     struct timespec expired = now;
     expired.tv_sec += blockDuration + 1;
     sw.start();
     DynBlockMaintenance::purgeExpired(expired);
     cerr<<"removed 1000000 entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl;
+    BOOST_CHECK_EQUAL(g_dynblockSMT.getLocal()->getNodes().size(), 0U);
   }
 #endif