]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Implement dynamic blocking on ratio of rcode/total responses
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 3 Sep 2019 15:53:10 +0000 (17:53 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 22 Nov 2019 10:06:16 +0000 (11:06 +0100)
pdns/dnsdist-dynblocks.hh
pdns/dnsdist-lua-inspection.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-dynblocks.cc [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc

index 118e70e8af6afa8bb7c8f313ea7d95f0085ad587..31bc9e79648b7ebbb571b2f7a88b9b902b2e0263 100644 (file)
@@ -149,6 +149,67 @@ private:
     bool d_enabled{false};
   };
 
+  struct DynBlockRatioRule: DynBlockRule
+  {
+    DynBlockRatioRule(): DynBlockRule()
+    {
+    }
+
+    DynBlockRatioRule(const std::string& blockReason, unsigned int blockDuration, double ratio, double warningRatio, unsigned int seconds, DNSAction::Action action, size_t minimumNumberOfResponses): DynBlockRule(blockReason, blockDuration, 0, 0, seconds, action), d_minimumNumberOfResponses(minimumNumberOfResponses), d_ratio(ratio), d_warningRatio(warningRatio)
+    {
+    }
+
+    bool ratioExceeded(unsigned int total, unsigned int count) const
+    {
+      if (!d_enabled) {
+        return false;
+      }
+
+      if (total < d_minimumNumberOfResponses) {
+        return false;
+      }
+
+      double allowed = d_ratio * static_cast<double>(total);
+      return (count > allowed);
+    }
+
+    bool warningRatioExceeded(unsigned int total, unsigned int count) const
+    {
+      if (d_warningRate == 0) {
+        return false;
+      }
+
+      if (total < d_minimumNumberOfResponses) {
+        return false;
+      }
+
+      double allowed = d_warningRatio * static_cast<double>(total);
+      return (count > allowed);
+    }
+
+    std::string toString() const
+    {
+      if (!isEnabled()) {
+        return "";
+      }
+
+      std::stringstream result;
+      if (d_action != DNSAction::Action::None) {
+        result << DNSAction::typeToString(d_action) << " ";
+      }
+      else {
+        result << "Apply the global DynBlock action ";
+      }
+      result << "for " << std::to_string(d_blockDuration) << " seconds when over " << std::to_string(d_ratio) << " ratio during the last " << d_seconds << " seconds, reason: '" << d_blockReason << "'";
+
+      return result.str();
+    }
+
+    size_t d_minimumNumberOfResponses{0};
+    double d_ratio{0.0};
+    double d_warningRatio{0.0};
+  };
+
   typedef std::unordered_map<ComboAddress, Counts, ComboAddress::addressOnlyHash, ComboAddress::addressOnlyEqual> counts_t;
 
 public:
@@ -173,6 +234,12 @@ public:
     entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
+  void setRCodeRatio(uint8_t rcode, double ratio, double warningRatio, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action, size_t minimumNumberOfResponses)
+  {
+    auto& entry = d_rcodeRatioRules[rcode];
+    entry = DynBlockRatioRule(reason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses);
+  }
+
   void setQTypeRate(uint16_t qtype, unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
   {
     auto& entry = d_qtypeRules[qtype];
@@ -201,122 +268,7 @@ public:
     apply(now);
   }
 
-  void apply(const struct timespec& now)
-  {
-    counts_t counts;
-    StatNode statNodeRoot;
-
-    size_t entriesCount = 0;
-    if (hasQueryRules()) {
-      entriesCount += g_rings.getNumberOfQueryEntries();
-    }
-    if (hasResponseRules()) {
-      entriesCount += g_rings.getNumberOfResponseEntries();
-    }
-    counts.reserve(entriesCount);
-
-    processQueryRules(counts, now);
-    processResponseRules(counts, statNodeRoot, now);
-
-    if (counts.empty() && statNodeRoot.empty()) {
-      return;
-    }
-
-    boost::optional<NetmaskTree<DynBlock> > blocks;
-    bool updated = false;
-
-    for (const auto& entry : counts) {
-      const auto& requestor = entry.first;
-      const auto& counters = entry.second;
-
-      if (d_queryRateRule.warningRateExceeded(counters.queries, now)) {
-        handleWarning(blocks, now, requestor, d_queryRateRule, updated);
-      }
-
-      if (d_queryRateRule.rateExceeded(counters.queries, now)) {
-        addBlock(blocks, now, requestor, d_queryRateRule, updated);
-        continue;
-      }
-
-      if (d_respRateRule.warningRateExceeded(counters.respBytes, now)) {
-        handleWarning(blocks, now, requestor, d_respRateRule, updated);
-      }
-
-      if (d_respRateRule.rateExceeded(counters.respBytes, now)) {
-        addBlock(blocks, now, requestor, d_respRateRule, updated);
-        continue;
-      }
-
-      for (const auto& pair : d_qtypeRules) {
-        const auto qtype = pair.first;
-
-        const auto& typeIt = counters.d_qtypeCounts.find(qtype);
-        if (typeIt != counters.d_qtypeCounts.cend()) {
-
-          if (pair.second.warningRateExceeded(typeIt->second, now)) {
-            handleWarning(blocks, now, requestor, pair.second, updated);
-          }
-
-          if (pair.second.rateExceeded(typeIt->second, now)) {
-            addBlock(blocks, now, requestor, pair.second, updated);
-            break;
-          }
-        }
-      }
-
-      for (const auto& pair : d_rcodeRules) {
-        const auto rcode = pair.first;
-
-        const auto& rcodeIt = counters.d_rcodeCounts.find(rcode);
-        if (rcodeIt != counters.d_rcodeCounts.cend()) {
-          if (pair.second.warningRateExceeded(rcodeIt->second, now)) {
-            handleWarning(blocks, now, requestor, pair.second, updated);
-          }
-
-          if (pair.second.rateExceeded(rcodeIt->second, now)) {
-            addBlock(blocks, now, requestor, pair.second, updated);
-            break;
-          }
-        }
-      }
-    }
-
-    if (updated && blocks) {
-      g_dynblockNMG.setState(std::move(*blocks));
-    }
-
-    if (!statNodeRoot.empty()) {
-      StatNode::Stat node;
-      std::unordered_set<DNSName> namesToBlock;
-      statNodeRoot.visit([this,&namesToBlock](const StatNode* node_, const StatNode::Stat& self, const StatNode::Stat& children) {
-                           bool block = false;
-
-                           if (d_smtVisitorFFI) {
-                             dnsdist_ffi_stat_node_t tmp(*node_, self, children);
-                             block = d_smtVisitorFFI(&tmp);
-                           }
-                           else {
-                             block = d_smtVisitor(*node_, self, children);
-                           }
-
-                           if (block) {
-                             namesToBlock.insert(DNSName(node_->fullname));
-                           }
-                         },
-        node);
-
-      if (!namesToBlock.empty()) {
-        updated = false;
-        SuffixMatchTree<DynBlock> smtBlocks = g_dynblockSMT.getCopy();
-        for (const auto& name : namesToBlock) {
-          addOrRefreshBlockSMT(smtBlocks, now, name, d_suffixMatchRule, updated);
-        }
-        if (updated) {
-          g_dynblockSMT.setState(std::move(smtBlocks));
-        }
-      }
-    }
-  }
+  void apply(const struct timespec& now);
 
   void excludeRange(const Netmask& range)
   {
@@ -344,6 +296,9 @@ public:
     for (const auto& rule : d_rcodeRules) {
       result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl;
     }
+    for (const auto& rule : d_rcodeRatioRules) {
+      result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl;
+    }
     result << "QType rules: " << std::endl;
     for (const auto& rule : d_qtypeRules) {
       result << "- " << QType(rule.first).getName() << ": " << rule.second.toString() << std::endl;
@@ -360,116 +315,11 @@ public:
   }
 
 private:
-  bool checkIfQueryTypeMatches(const Rings::Query& query)
-  {
-    auto rule = d_qtypeRules.find(query.qtype);
-    if (rule == d_qtypeRules.end()) {
-      return false;
-    }
-
-    return rule->second.matches(query.when);
-  }
-
-  bool checkIfResponseCodeMatches(const Rings::Response& response)
-  {
-    auto rule = d_rcodeRules.find(response.dh.rcode);
-    if (rule == d_rcodeRules.end()) {
-      return false;
-    }
-
-    return rule->second.matches(response.when);
-  }
-
-  void addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated, bool warning)
-  {
-    if (d_excludedSubnets.match(requestor)) {
-      /* do not add a block for excluded subnets */
-      return;
-    }
-
-    if (!blocks) {
-      blocks = g_dynblockNMG.getCopy();
-    }
-    struct timespec until = now;
-    until.tv_sec += rule.d_blockDuration;
-    unsigned int count = 0;
-    const auto& got = blocks->lookup(Netmask(requestor));
-    bool expired = false;
-    bool wasWarning = false;
-
-    if (got) {
-      if (warning && !got->second.warning) {
-        /* we have an existing entry which is not a warning,
-           don't override it */
-        return;
-      }
-      else if (!warning && got->second.warning) {
-        wasWarning = true;
-      }
-      else {
-        if (until < got->second.until) {
-          // had a longer policy
-          return;
-        }
-      }
 
-      if (now < got->second.until) {
-        // only inherit count on fresh query we are extending
-        count = got->second.blocks;
-      }
-      else {
-        expired = true;
-      }
-    }
-
-    DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action};
-    db.blocks = count;
-    db.warning = warning;
-    if (!d_beQuiet && (!got || expired || wasWarning)) {
-      warnlog("Inserting %sdynamic block for %s for %d seconds: %s", warning ? "(warning) " :"", requestor.toString(), rule.d_blockDuration, rule.d_blockReason);
-    }
-    blocks->insert(Netmask(requestor)).second = db;
-    updated = true;
-  }
-
-  void addOrRefreshBlockSMT(SuffixMatchTree<DynBlock>& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated)
-  {
-    if (d_excludedDomains.check(name)) {
-      /* do not add a block for excluded domains */
-      return;
-    }
-
-    struct timespec until = now;
-    until.tv_sec += rule.d_blockDuration;
-    unsigned int count = 0;
-    const auto& got = blocks.lookup(name);
-    bool expired = false;
-    DNSName domain(name.makeLowerCase());
-
-    if (got) {
-      if (until < got->until) {
-        // had a longer policy
-        return;
-      }
-
-      if (now < got->until) {
-        // only inherit count on fresh query we are extending
-        count = got->blocks;
-      }
-      else {
-        expired = true;
-      }
-    }
-
-    DynBlock db{rule.d_blockReason, until, domain, rule.d_action};
-    db.blocks = count;
-
-    if (!d_beQuiet && (!got || expired)) {
-      warnlog("Inserting dynamic block for %s for %d seconds: %s", domain, rule.d_blockDuration, rule.d_blockReason);
-    }
-    blocks.add(domain, db);
-    updated = true;
-  }
+  bool checkIfQueryTypeMatches(const Rings::Query& query);
+  bool checkIfResponseCodeMatches(const Rings::Response& response);
+  void addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated, bool warning);
+  void addOrRefreshBlockSMT(SuffixMatchTree<DynBlock>& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated);
 
   void addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
   {
@@ -488,7 +338,7 @@ private:
 
   bool hasResponseRules() const
   {
-    return d_respRateRule.isEnabled() || !d_rcodeRules.empty();
+    return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty();
   }
 
   bool hasSuffixMatchRules() const
@@ -501,89 +351,11 @@ private:
     return hasQueryRules() || hasResponseRules();
   }
 
-  void processQueryRules(counts_t& counts, const struct timespec& now)
-  {
-    if (!hasQueryRules()) {
-      return;
-    }
-
-    d_queryRateRule.d_cutOff = d_queryRateRule.d_minTime = now;
-    d_queryRateRule.d_cutOff.tv_sec -= d_queryRateRule.d_seconds;
-
-    for (auto& rule : d_qtypeRules) {
-      rule.second.d_cutOff = rule.second.d_minTime = now;
-      rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
-    }
-
-    for (const auto& shard : g_rings.d_shards) {
-      std::lock_guard<std::mutex> rl(shard->queryLock);
-      for(const auto& c : shard->queryRing) {
-        if (now < c.when) {
-          continue;
-        }
-
-        bool qRateMatches = d_queryRateRule.matches(c.when);
-        bool typeRuleMatches = checkIfQueryTypeMatches(c);
-
-        if (qRateMatches || typeRuleMatches) {
-          auto& entry = counts[c.requestor];
-          if (qRateMatches) {
-            entry.queries++;
-          }
-          if (typeRuleMatches) {
-            entry.d_qtypeCounts[c.qtype]++;
-          }
-        }
-      }
-    }
-  }
-
-  void processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now)
-  {
-    if (!hasResponseRules() && !hasSuffixMatchRules()) {
-      return;
-    }
-
-    d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now;
-    d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds;
-
-    d_suffixMatchRule.d_cutOff = d_suffixMatchRule.d_minTime = now;
-    d_suffixMatchRule.d_cutOff.tv_sec -= d_suffixMatchRule.d_seconds;
-
-    for (auto& rule : d_rcodeRules) {
-      rule.second.d_cutOff = rule.second.d_minTime = now;
-      rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
-    }
-
-    for (const auto& shard : g_rings.d_shards) {
-      std::lock_guard<std::mutex> rl(shard->respLock);
-      for(const auto& c : shard->respRing) {
-        if (now < c.when) {
-          continue;
-        }
-
-        bool respRateMatches = d_respRateRule.matches(c.when);
-        bool suffixMatchRuleMatches = d_suffixMatchRule.matches(c.when);
-        bool rcodeRuleMatches = checkIfResponseCodeMatches(c);
-
-        if (respRateMatches || rcodeRuleMatches) {
-          auto& entry = counts[c.requestor];
-          if (respRateMatches) {
-            entry.respBytes += c.size;
-          }
-          if (rcodeRuleMatches) {
-            entry.d_rcodeCounts[c.dh.rcode]++;
-          }
-        }
-
-        if (suffixMatchRuleMatches) {
-          root.submit(c.name, ((c.dh.rcode == 0 && c.usec == std::numeric_limits<unsigned int>::max()) ? -1 : c.dh.rcode), c.size, boost::none);
-        }
-      }
-    }
-  }
+  void processQueryRules(counts_t& counts, const struct timespec& now);
+  void processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now);
 
   std::map<uint8_t, DynBlockRule> d_rcodeRules;
+  std::map<uint8_t, DynBlockRatioRule> d_rcodeRatioRules;
   std::map<uint16_t, DynBlockRule> d_qtypeRules;
   DynBlockRule d_queryRateRule;
   DynBlockRule d_respRateRule;
index c1f0a1cb801dff34a61d5dc582f5f7d4d6a1a835..0d37ff320c583d27ce214151de2bc9a43abfe125 100644 (file)
@@ -741,6 +741,11 @@ void setupLuaInspection()
         group->setRCodeRate(rcode, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
       }
     });
+  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint8_t, double, unsigned int, const std::string&, unsigned int, size_t, boost::optional<DNSAction::Action>, boost::optional<double>)>("setRCodeRatio", [](std::shared_ptr<DynBlockRulesGroup>& group, uint8_t rcode, 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->setRCodeRatio(rcode, ratio, warningRatio ? *warningRatio : 0.0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None, minimumNumberOfResponses);
+      }
+    });
   g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint16_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>, boost::optional<unsigned int>)>("setQTypeRate", [](std::shared_ptr<DynBlockRulesGroup>& group, uint16_t qtype, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional<DNSAction::Action> action, boost::optional<unsigned int> warningRate) {
       if (group) {
         group->setQTypeRate(qtype, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
index 163d4413802daa3d48c167834e3d5fccc3aa4a47..145d6b0713aadce6010d41d020f6a4f24309bb69 100644 (file)
@@ -122,7 +122,7 @@ dnsdist_SOURCES = \
        dnsdist-carbon.cc \
        dnsdist-console.cc dnsdist-console.hh \
        dnsdist-dnscrypt.cc \
-       dnsdist-dynblocks.hh \
+       dnsdist-dynblocks.cc dnsdist-dynblocks.hh \
        dnsdist-ecs.cc dnsdist-ecs.hh \
        dnsdist-idstate.cc \
        dnsdist-kvs.hh dnsdist-kvs.cc \
@@ -204,6 +204,7 @@ testrunner_SOURCES = \
        circular_buffer.hh \
        dnsdist.hh \
        dnsdist-cache.cc dnsdist-cache.hh \
+       dnsdist-dynblocks.cc dnsdist-dynblocks.hh \
        dnsdist-ecs.cc dnsdist-ecs.hh \
        dnsdist-kvs.cc dnsdist-kvs.hh \
        dnsdist-rings.hh \
diff --git a/pdns/dnsdistdist/dnsdist-dynblocks.cc b/pdns/dnsdistdist/dnsdist-dynblocks.cc
new file mode 100644 (file)
index 0000000..808d56b
--- /dev/null
@@ -0,0 +1,340 @@
+
+#include "dnsdist.hh"
+#include "dnsdist-dynblocks.hh"
+
+void DynBlockRulesGroup::apply(const struct timespec& now)
+{
+  counts_t counts;
+  StatNode statNodeRoot;
+
+  size_t entriesCount = 0;
+  if (hasQueryRules()) {
+    entriesCount += g_rings.getNumberOfQueryEntries();
+  }
+  if (hasResponseRules()) {
+    entriesCount += g_rings.getNumberOfResponseEntries();
+  }
+  counts.reserve(entriesCount);
+
+  processQueryRules(counts, now);
+  processResponseRules(counts, statNodeRoot, now);
+
+  if (counts.empty() && statNodeRoot.empty()) {
+    return;
+  }
+
+  boost::optional<NetmaskTree<DynBlock> > blocks;
+  bool updated = false;
+
+  for (const auto& entry : counts) {
+    const auto& requestor = entry.first;
+    const auto& counters = entry.second;
+
+    if (d_queryRateRule.warningRateExceeded(counters.queries, now)) {
+      handleWarning(blocks, now, requestor, d_queryRateRule, updated);
+    }
+
+    if (d_queryRateRule.rateExceeded(counters.queries, now)) {
+      addBlock(blocks, now, requestor, d_queryRateRule, updated);
+      continue;
+    }
+
+    if (d_respRateRule.warningRateExceeded(counters.respBytes, now)) {
+      handleWarning(blocks, now, requestor, d_respRateRule, updated);
+    }
+
+    if (d_respRateRule.rateExceeded(counters.respBytes, now)) {
+      addBlock(blocks, now, requestor, d_respRateRule, updated);
+      continue;
+    }
+
+    for (const auto& pair : d_qtypeRules) {
+      const auto qtype = pair.first;
+
+      const auto& typeIt = counters.d_qtypeCounts.find(qtype);
+      if (typeIt != counters.d_qtypeCounts.cend()) {
+
+        if (pair.second.warningRateExceeded(typeIt->second, now)) {
+          handleWarning(blocks, now, requestor, pair.second, updated);
+        }
+
+        if (pair.second.rateExceeded(typeIt->second, now)) {
+          addBlock(blocks, now, requestor, pair.second, updated);
+          break;
+        }
+      }
+    }
+
+    for (const auto& pair : d_rcodeRules) {
+      const auto rcode = pair.first;
+
+      const auto& rcodeIt = counters.d_rcodeCounts.find(rcode);
+      if (rcodeIt != counters.d_rcodeCounts.cend()) {
+        if (pair.second.warningRateExceeded(rcodeIt->second, now)) {
+          handleWarning(blocks, now, requestor, pair.second, updated);
+        }
+
+        if (pair.second.rateExceeded(rcodeIt->second, now)) {
+          addBlock(blocks, now, requestor, pair.second, updated);
+          break;
+        }
+      }
+    }
+
+    for (const auto& pair : d_rcodeRatioRules) {
+      const auto rcode = pair.first;
+
+      const auto& rcodeIt = counters.d_rcodeCounts.find(rcode);
+      if (rcodeIt != counters.d_rcodeCounts.cend()) {
+        if (pair.second.warningRatioExceeded(counters.queries, rcodeIt->second)) {
+          handleWarning(blocks, now, requestor, pair.second, updated);
+        }
+
+        if (pair.second.ratioExceeded(counters.queries, rcodeIt->second)) {
+          addBlock(blocks, now, requestor, pair.second, updated);
+          break;
+        }
+      }
+    }
+  }
+
+  if (updated && blocks) {
+    g_dynblockNMG.setState(std::move(*blocks));
+  }
+
+  if (!statNodeRoot.empty()) {
+    StatNode::Stat node;
+    std::unordered_set<DNSName> namesToBlock;
+    statNodeRoot.visit([this,&namesToBlock](const StatNode* node_, const StatNode::Stat& self, const StatNode::Stat& children) {
+                         bool block = false;
+
+                         if (d_smtVisitorFFI) {
+                           dnsdist_ffi_stat_node_t tmp(*node_, self, children);
+                           block = d_smtVisitorFFI(&tmp);
+                         }
+                         else {
+                           block = d_smtVisitor(*node_, self, children);
+                         }
+
+                         if (block) {
+                           namesToBlock.insert(DNSName(node_->fullname));
+                         }
+                       },
+      node);
+
+    if (!namesToBlock.empty()) {
+      updated = false;
+      SuffixMatchTree<DynBlock> smtBlocks = g_dynblockSMT.getCopy();
+      for (const auto& name : namesToBlock) {
+        addOrRefreshBlockSMT(smtBlocks, now, name, d_suffixMatchRule, updated);
+      }
+      if (updated) {
+        g_dynblockSMT.setState(std::move(smtBlocks));
+      }
+    }
+  }
+}
+
+bool DynBlockRulesGroup::checkIfQueryTypeMatches(const Rings::Query& query)
+{
+  auto rule = d_qtypeRules.find(query.qtype);
+  if (rule == d_qtypeRules.end()) {
+    return false;
+  }
+
+  return rule->second.matches(query.when);
+}
+
+bool DynBlockRulesGroup::checkIfResponseCodeMatches(const Rings::Response& response)
+{
+  auto rule = d_rcodeRules.find(response.dh.rcode);
+  if (rule != d_rcodeRules.end() && rule->second.matches(response.when)) {
+    return true;
+  }
+
+  auto ratio = d_rcodeRatioRules.find(response.dh.rcode);
+  if (ratio != d_rcodeRatioRules.end() && ratio->second.matches(response.when)) {
+    return true;
+  }
+
+  return false;
+}
+
+void DynBlockRulesGroup::addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated, bool warning)
+{
+  if (d_excludedSubnets.match(requestor)) {
+    /* do not add a block for excluded subnets */
+    return;
+  }
+
+  if (!blocks) {
+    blocks = g_dynblockNMG.getCopy();
+  }
+  struct timespec until = now;
+  until.tv_sec += rule.d_blockDuration;
+  unsigned int count = 0;
+  const auto& got = blocks->lookup(Netmask(requestor));
+  bool expired = false;
+  bool wasWarning = false;
+
+  if (got) {
+    if (warning && !got->second.warning) {
+      /* we have an existing entry which is not a warning,
+         don't override it */
+      return;
+    }
+    else if (!warning && got->second.warning) {
+      wasWarning = true;
+    }
+    else {
+      if (until < got->second.until) {
+        // had a longer policy
+        return;
+      }
+    }
+
+    if (now < got->second.until) {
+      // only inherit count on fresh query we are extending
+      count = got->second.blocks;
+    }
+    else {
+      expired = true;
+    }
+  }
+
+  DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action};
+  db.blocks = count;
+  db.warning = warning;
+  if (!d_beQuiet && (!got || expired || wasWarning)) {
+    warnlog("Inserting %sdynamic block for %s for %d seconds: %s", warning ? "(warning) " :"", requestor.toString(), rule.d_blockDuration, rule.d_blockReason);
+  }
+  blocks->insert(Netmask(requestor)).second = db;
+  updated = true;
+}
+
+void DynBlockRulesGroup::addOrRefreshBlockSMT(SuffixMatchTree<DynBlock>& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated)
+{
+  if (d_excludedDomains.check(name)) {
+    /* do not add a block for excluded domains */
+    return;
+  }
+
+  struct timespec until = now;
+  until.tv_sec += rule.d_blockDuration;
+  unsigned int count = 0;
+  const auto& got = blocks.lookup(name);
+  bool expired = false;
+  DNSName domain(name.makeLowerCase());
+
+  if (got) {
+    if (until < got->until) {
+      // had a longer policy
+      return;
+    }
+
+    if (now < got->until) {
+      // only inherit count on fresh query we are extending
+      count = got->blocks;
+    }
+    else {
+      expired = true;
+    }
+  }
+
+  DynBlock db{rule.d_blockReason, until, domain, rule.d_action};
+  db.blocks = count;
+
+  if (!d_beQuiet && (!got || expired)) {
+    warnlog("Inserting dynamic block for %s for %d seconds: %s", domain, rule.d_blockDuration, rule.d_blockReason);
+  }
+  blocks.add(domain, db);
+  updated = true;
+}
+
+void DynBlockRulesGroup::processQueryRules(counts_t& counts, const struct timespec& now)
+{
+  if (!hasQueryRules()) {
+    return;
+  }
+
+  d_queryRateRule.d_cutOff = d_queryRateRule.d_minTime = now;
+  d_queryRateRule.d_cutOff.tv_sec -= d_queryRateRule.d_seconds;
+
+  for (auto& rule : d_qtypeRules) {
+    rule.second.d_cutOff = rule.second.d_minTime = now;
+    rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+  }
+
+  for (const auto& shard : g_rings.d_shards) {
+    std::lock_guard<std::mutex> rl(shard->queryLock);
+    for(const auto& c : shard->queryRing) {
+      if (now < c.when) {
+        continue;
+      }
+
+      bool qRateMatches = d_queryRateRule.matches(c.when);
+      bool typeRuleMatches = checkIfQueryTypeMatches(c);
+
+      if (qRateMatches || typeRuleMatches) {
+        auto& entry = counts[c.requestor];
+        if (qRateMatches) {
+          ++entry.queries;
+        }
+        if (typeRuleMatches) {
+          ++entry.d_qtypeCounts[c.qtype];
+        }
+      }
+    }
+  }
+}
+
+void DynBlockRulesGroup::processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now)
+{
+  if (!hasResponseRules() && !hasSuffixMatchRules()) {
+    return;
+  }
+
+  d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now;
+  d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds;
+
+  d_suffixMatchRule.d_cutOff = d_suffixMatchRule.d_minTime = now;
+  d_suffixMatchRule.d_cutOff.tv_sec -= d_suffixMatchRule.d_seconds;
+
+  for (auto& rule : d_rcodeRules) {
+    rule.second.d_cutOff = rule.second.d_minTime = now;
+    rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+  }
+
+  for (auto& rule : d_rcodeRatioRules) {
+    rule.second.d_cutOff = rule.second.d_minTime = now;
+    rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+  }
+
+  for (const auto& shard : g_rings.d_shards) {
+    std::lock_guard<std::mutex> rl(shard->respLock);
+    for(const auto& c : shard->respRing) {
+      if (now < c.when) {
+        continue;
+      }
+
+      auto& entry = counts[c.requestor];
+      ++entry.queries;
+      bool respRateMatches = d_respRateRule.matches(c.when);
+      bool suffixMatchRuleMatches = d_suffixMatchRule.matches(c.when);
+      bool rcodeRuleMatches = checkIfResponseCodeMatches(c);
+
+      if (respRateMatches || rcodeRuleMatches) {
+        if (respRateMatches) {
+          entry.respBytes += c.size;
+        }
+        if (rcodeRuleMatches) {
+          ++entry.d_rcodeCounts[c.dh.rcode];
+        }
+      }
+
+      if (suffixMatchRuleMatches) {
+        root.submit(c.name, ((c.dh.rcode == 0 && c.usec == std::numeric_limits<unsigned int>::max()) ? -1 : c.dh.rcode), c.size, boost::none);
+      }
+    }
+  }
+}
index f842a3c20aac175a22d415e3bcccead14d49ea06..2f58b7ff8deb1a4f4cc60f22b1fba3b7fa107554 100644 (file)
@@ -1020,6 +1020,21 @@ faster than the existing rules.
     :param int action: The action to take when the dynamic block matches, see :ref:`here <DNSAction>`. (default to the one set with :func:`setDynBlocksAction`)
     :param int warningRate: If set to a non-zero value, the rate above which a warning message will be issued and a no-op block inserted
 
+  .. method:: DynBlockRulesGroup:setRCodeRatio(rcode, ratio, seconds, reason, blockingTime, minimumNumberOfResponses [, action [, warningRate]])
+
+    .. versionadded:: 1.5.0
+
+    Adds a rate-limiting rule for the ratio of responses of code ``rcode`` over the total number of responses for a given client.
+
+    :param int rcode: The response code
+    :param int ratio: Ratio of responses per second of the given rcode 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:`here <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:setQTypeRate(qtype, rate, seconds, reason, blockingTime [, action [, warningRate]])
 
     .. versionchanged:: 1.3.3
index f62e8a223b5a8e29236af20fef7a66f8608f853f..3216d4692cb55c2650bd15d11c92565c94044416 100644 (file)
@@ -259,6 +259,123 @@ BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRate) {
 
 }
 
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRatio) {
+  dnsheader dh;
+  DNSName qname("rings.powerdns.com.");
+  ComboAddress requestor1("192.0.2.1");
+  ComboAddress requestor2("192.0.2.2");
+  ComboAddress backend("192.0.2.42");
+  uint16_t qtype = QType::AAAA;
+  uint16_t size = 42;
+  unsigned int responseTime = 100 * 1000; /* 100ms */
+  struct timespec now;
+  gettime(&now);
+  NetmaskTree<DynBlock> emptyNMG;
+
+  size_t numberOfSeconds = 10;
+  size_t blockDuration = 60;
+  const auto action = DNSAction::Action::Drop;
+  const std::string reason = "Exceeded query ratio";
+  const uint16_t rcode = RCode::ServFail;
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* block above 0.2 ServFail/Total ratio over numberOfSeconds seconds, no warning, minimum number of queries should be at least 51 */
+  dbrg.setRCodeRatio(rcode, 0.2, 0, numberOfSeconds, reason, blockDuration, action, 51);
+
+  {
+    /* insert 20 ServFail and 80 NoErrors from a given client in the last 10s
+       this should not trigger the rule */
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < 20; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    dh.rcode = RCode::NoError;
+    for (size_t idx = 0; idx < 80; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just 50 FormErrs and nothing else, from a given client in the last 10s */
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = RCode::FormErr;
+    for (size_t idx = 0; idx < 50; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 50);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert 21 ServFails and 79 NoErrors from a given client in the last 10s
+       this should trigger the rule this time */
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < 21; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    dh.rcode = RCode::NoError;
+    for (size_t idx = 0; idx < 79; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1);
+    BOOST_CHECK(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, 0);
+    BOOST_CHECK_EQUAL(block.warning, false);
+  }
+
+  {
+    /* insert 11 ServFails and 39 NoErrors 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(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < 11; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    dh.rcode = RCode::NoError;
+    for (size_t idx = 0; idx < 39; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 50);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+}
+
 BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_ResponseByteRate) {
   dnsheader dh;
   DNSName qname("rings.powerdns.com.");