]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add warning rates to dynBlockRulesGroup rules
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 25 Sep 2018 09:49:39 +0000 (11:49 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 3 Oct 2018 09:05:59 +0000 (11:05 +0200)
pdns/dnsdist-dynblocks.hh
pdns/dnsdist-lua-inspection.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-rings.hh
pdns/dnsdist-web.cc
pdns/dnsdist.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/docs/guides/dynblocks.rst
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc [new file with mode: 0644]
regression-tests.dnsdist/test_DynBlocks.py

index ca1aa0998751e014244492bb6e1bde5f98feb43b..091ffa553ab0f80c2dc8bfe02c96741565a19eb8 100644 (file)
@@ -42,8 +42,11 @@ private:
     {
     }
 
-    DynBlockRule(const std::string& blockReason, unsigned int blockDuration, unsigned int rate, unsigned int seconds, DNSAction::Action action): d_blockReason(blockReason), d_blockDuration(blockDuration), d_rate(rate), d_seconds(seconds), d_action(action), d_enabled(true)
+    DynBlockRule(const std::string& blockReason, unsigned int blockDuration, unsigned int rate, unsigned int warningRate, unsigned int seconds, DNSAction::Action action): d_blockReason(blockReason), d_blockDuration(blockDuration), d_rate(rate), d_warningRate(warningRate), d_seconds(seconds), d_action(action), d_enabled(true)
     {
+      if (d_warningRate > 0) {
+        d_warningEnabled = true;
+      }
     }
 
     bool matches(const struct timespec& when)
@@ -74,9 +77,20 @@ private:
       return (count > limit);
     }
 
+    bool warningRateExceeded(unsigned int count, const struct timespec& now) const
+    {
+      if (!d_warningEnabled) {
+        return false;
+      }
+
+      double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime);
+      double limit = delta * d_warningRate;
+      return (count > limit);
+    }
+
     bool isEnabled() const
     {
-      return d_enabled;
+      return d_enabled || d_warningEnabled;
     }
 
     std::string toString() const
@@ -102,9 +116,11 @@ private:
     struct timespec d_minTime;
     unsigned int d_blockDuration{0};
     unsigned int d_rate{0};
+    unsigned int d_warningRate{0};
     unsigned int d_seconds{0};
     DNSAction::Action d_action{DNSAction::Action::None};
     bool d_enabled{false};
+    bool d_warningEnabled{false};
   };
 
     typedef std::unordered_map<ComboAddress, Counts, ComboAddress::addressOnlyHash, ComboAddress::addressOnlyEqual> counts_t;
@@ -114,29 +130,38 @@ public:
   {
   }
 
-  void setQueryRate(unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  void setQueryRate(unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
   {
-    d_queryRateRule = DynBlockRule(reason, blockDuration, rate, seconds, action);
+    d_queryRateRule = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
-  void setResponseByteRate(unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  /* rate is in bytes per second */
+  void setResponseByteRate(unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
   {
-    d_respRateRule = DynBlockRule(reason, blockDuration, rate, seconds, action);
+    d_respRateRule = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
-  void setRCodeRate(uint8_t rcode, unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  void setRCodeRate(uint8_t rcode, unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
   {
     auto& entry = d_rcodeRules[rcode];
-    entry = DynBlockRule(reason, blockDuration, rate, seconds, action);
+    entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
-  void setQTypeRate(uint16_t qtype, unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  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];
-    entry = DynBlockRule(reason, blockDuration, rate, seconds, action);
+    entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
   }
 
   void apply()
+  {
+    struct timespec now;
+    gettime(&now);
+
+    apply(now);
+  }
+
+  void apply(const struct timespec& now)
   {
     counts_t counts;
 
@@ -149,8 +174,8 @@ public:
     }
     counts.reserve(entriesCount);
 
-    processQueryRules(counts);
-    processResponseRules(counts);
+    processQueryRules(counts, now);
+    processResponseRules(counts, now);
 
     if (counts.empty()) {
       return;
@@ -158,33 +183,59 @@ public:
 
     boost::optional<NetmaskTree<DynBlock> > blocks;
     bool updated = false;
-    struct timespec now;
-    gettime(&now);
 
     for (const auto& entry : counts) {
-      if (d_queryRateRule.rateExceeded(entry.second.queries, now)) {
-        addBlock(blocks, now, entry.first, d_queryRateRule, updated);
+      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.rateExceeded(entry.second.respBytes, now)) {
-        addBlock(blocks, now, entry.first, d_respRateRule, updated);
+      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& rule : d_qtypeRules) {
-        const auto& typeIt = entry.second.d_qtypeCounts.find(rule.first);
-        if (typeIt != entry.second.d_qtypeCounts.cend() && rule.second.rateExceeded(typeIt->second, now)) {
-          addBlock(blocks, now, entry.first, rule.second, updated);
-          break;
+      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& rule : d_rcodeRules) {
-        const auto& rcodeIt = entry.second.d_rcodeCounts.find(rule.first);
-        if (rcodeIt != entry.second.d_rcodeCounts.cend() && rule.second.rateExceeded(rcodeIt->second, now)) {
-          addBlock(blocks, now, entry.first, rule.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;
+          }
         }
       }
     }
@@ -223,6 +274,11 @@ public:
     return result.str();
   }
 
+  void setQuiet(bool quiet)
+  {
+    d_beQuiet = quiet;
+  }
+
 private:
   bool checkIfQueryTypeMatches(const Rings::Query& query)
   {
@@ -244,7 +300,7 @@ private:
     return rule->second.matches(response.when);
   }
 
-  void addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
+  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 */
@@ -259,11 +315,23 @@ private:
     unsigned int count = 0;
     const auto& got = blocks->lookup(Netmask(requestor));
     bool expired = false;
+    bool wasWarning = false;
+
     if (got) {
-      if (until < got->second.until) {
-        // had a longer policy
+      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
@@ -274,15 +342,26 @@ private:
       }
     }
 
-    DynBlock db{rule.d_blockReason, until, DNSName(), rule.d_action};
+    DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action};
     db.blocks = count;
-    if (!got || expired) {
-      warnlog("Inserting dynamic block for %s for %d seconds: %s", requestor.toString(), rule.d_blockDuration, rule.d_blockReason);
+    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 addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
+  {
+    addOrRefreshBlock(blocks, now, requestor, rule, updated, false);
+  }
+
+  void handleWarning(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
+  {
+    addOrRefreshBlock(blocks, now, requestor, rule, updated, true);
+  }
+
   bool hasQueryRules() const
   {
     return d_queryRateRule.isEnabled() || !d_qtypeRules.empty();
@@ -298,14 +377,12 @@ private:
     return hasQueryRules() || hasResponseRules();
   }
 
-  void processQueryRules(counts_t& counts)
+  void processQueryRules(counts_t& counts, const struct timespec& now)
   {
     if (!hasQueryRules()) {
       return;
     }
 
-    struct timespec now;
-    gettime(&now);
     d_queryRateRule.d_cutOff = d_queryRateRule.d_minTime = now;
     d_queryRateRule.d_cutOff.tv_sec -= d_queryRateRule.d_seconds;
 
@@ -337,14 +414,12 @@ private:
     }
   }
 
-  void processResponseRules(counts_t& counts)
+  void processResponseRules(counts_t& counts, const struct timespec& now)
   {
     if (!hasResponseRules()) {
       return;
     }
 
-    struct timespec now;
-    gettime(&now);
     d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now;
     d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds;
 
@@ -381,4 +456,5 @@ private:
   DynBlockRule d_queryRateRule;
   DynBlockRule d_respRateRule;
   NetmaskGroup d_excludedSubnets;
+  bool d_beQuiet{false};
 };
index 985488bcaa168137fe07e755a14c3515a78b6454..c64bf563f1bf4c5dab3ccace2b30b09d7acc1e2f 100644 (file)
@@ -650,24 +650,24 @@ void setupLuaInspection()
 
   /* DynBlockRulesGroup */
   g_lua.writeFunction("dynBlockRulesGroup", []() { return std::make_shared<DynBlockRulesGroup>(); });
-  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>)>("setQueryRate", [](std::shared_ptr<DynBlockRulesGroup>& group, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional<DNSAction::Action> action) {
+  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>, boost::optional<unsigned int>)>("setQueryRate", [](std::shared_ptr<DynBlockRulesGroup>& group, 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->setQueryRate(rate, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
+        group->setQueryRate(rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
       }
     });
-  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>)>("setResponseByteRate", [](std::shared_ptr<DynBlockRulesGroup>& group, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional<DNSAction::Action> action) {
+  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>, boost::optional<unsigned int>)>("setResponseByteRate", [](std::shared_ptr<DynBlockRulesGroup>& group, 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->setResponseByteRate(rate, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
+        group->setResponseByteRate(rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
       }
     });
-  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint8_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>)>("setRCodeRate", [](std::shared_ptr<DynBlockRulesGroup>& group, uint8_t rcode, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional<DNSAction::Action> action) {
+  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint8_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>, boost::optional<unsigned int>)>("setRCodeRate", [](std::shared_ptr<DynBlockRulesGroup>& group, uint8_t rcode, 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->setRCodeRate(rcode, rate, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
+        group->setRCodeRate(rcode, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
       }
     });
-  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint16_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>)>("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) {
+  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, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
+        group->setQTypeRate(qtype, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
       }
     });
   g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(boost::variant<std::string, std::vector<std::pair<int, std::string>>>)>("excludeRange", [](std::shared_ptr<DynBlockRulesGroup>& group, boost::variant<std::string, std::vector<std::pair<int, std::string>>> ranges) {
@@ -690,6 +690,8 @@ void setupLuaInspection()
         group->includeRange(Netmask(*boost::get<std::string>(&ranges)));
       }
     });
-  g_lua.registerFunction("apply", &DynBlockRulesGroup::apply);
+  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)()>("apply", [](std::shared_ptr<DynBlockRulesGroup>& group) {
+    group->apply();
+  });
   g_lua.registerFunction("toString", &DynBlockRulesGroup::toString);
 }
index ce7e6dafe434f5d4c0a9e50f30784d9f4c26bcce..49c5cac61d25f6eb8f363837c84e706920adcdda 100644 (file)
@@ -887,11 +887,11 @@ void setupLuaConfig(bool client)
       auto slow = g_dynblockNMG.getCopy();
       struct timespec now;
       gettime(&now);
-      boost::format fmt("%-24s %8d %8d %-20s %s\n");
-      g_outputBuffer = (fmt % "What" % "Seconds" % "Blocks" % "Action" % "Reason").str();
+      boost::format fmt("%-24s %8d %8d %-10s %-20s %s\n");
+      g_outputBuffer = (fmt % "What" % "Seconds" % "Blocks" % "Warning" % "Action" % "Reason").str();
       for(const auto& e: slow) {
        if(now < e->second.until)
-         g_outputBuffer+= (fmt % e->first.toString() % (e->second.until.tv_sec - now.tv_sec) % e->second.blocks % DNSAction::typeToString(e->second.action != DNSAction::Action::None ? e->second.action : g_dynBlockAction) % e->second.reason).str();
+         g_outputBuffer+= (fmt % e->first.toString() % (e->second.until.tv_sec - now.tv_sec) % e->second.blocks % (e->second.warning ? "true" : "false") % DNSAction::typeToString(e->second.action != DNSAction::Action::None ? e->second.action : g_dynBlockAction) % e->second.reason).str();
       }
       auto slow2 = g_dynblockSMT.getCopy();
       slow2.visit([&now, &fmt](const SuffixMatchTree<DynBlock>& node) {
@@ -899,7 +899,7 @@ void setupLuaConfig(bool client)
             string dom("empty");
             if(!node.d_value.domain.empty())
               dom = node.d_value.domain.toString();
-            g_outputBuffer+= (fmt % dom % (node.d_value.until.tv_sec - now.tv_sec) % node.d_value.blocks % DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction) % node.d_value.reason).str();
+            g_outputBuffer+= (fmt % dom % (node.d_value.until.tv_sec - now.tv_sec) % node.d_value.blocks % (node.d_value.warning ? "true" : "false") % DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction) % node.d_value.reason).str();
           }
         });
 
index 0ff9a1ec9438ba56f99023ac7daabc3112e0a188..a773ed6e96ba2e1172405be87f0def959e6fb070 100644 (file)
@@ -169,6 +169,28 @@ struct Rings {
     insertResponseLocked(shard, when, requestor, name, qtype, usec, size, dh, backend);
   }
 
+  void clear()
+  {
+    for (auto& shard : d_shards) {
+      {
+        std::lock_guard<std::mutex> wl(shard->queryLock);
+        shard->queryRing.clear();
+      }
+      {
+        std::lock_guard<std::mutex> wl(shard->respLock);
+        shard->respRing.clear();
+      }
+    }
+
+    d_nbQueryEntries.store(0);
+    d_nbResponseEntries.store(0);
+    d_currentShardId.store(0);
+    d_blockingQueryInserts.store(0);
+    d_blockingResponseInserts.store(0);
+    d_deferredQueryInserts.store(0);
+    d_deferredResponseInserts.store(0);
+  }
+
   std::vector<std::unique_ptr<Shard> > d_shards;
   std::atomic<uint64_t> d_blockingQueryInserts;
   std::atomic<uint64_t> d_blockingResponseInserts;
index e2e756c8ea629e95807d0f8845cf8552f64f6641..d2fa817c0e545937fe2e95d900094f9757b154f9 100644 (file)
@@ -334,7 +334,8 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
               {"reason", e->second.reason},
               {"seconds", (double)(e->second.until.tv_sec - now.tv_sec)},
               {"blocks", (double)e->second.blocks},
-              {"action", DNSAction::typeToString(e->second.action != DNSAction::Action::None ? e->second.action : g_dynBlockAction) }
+              {"action", DNSAction::typeToString(e->second.action != DNSAction::Action::None ? e->second.action : g_dynBlockAction) },
+              {"warning", e->second.warning }
             };
             obj.insert({e->first.toString(), thing});
           }
index c66e1f217b5a005958be8871e3f83e2679e69841..1c9e401e24f89d88414204b066c649e740f19443 100644 (file)
@@ -161,15 +161,15 @@ public:
 
 struct DynBlock
 {
-  DynBlock(): action(DNSAction::Action::None)
+  DynBlock(): action(DNSAction::Action::None), warning(false)
   {
   }
 
-  DynBlock(const std::string& reason_, const struct timespec& until_, const DNSName& domain_, DNSAction::Action action_): reason(reason_), until(until_), domain(domain_), action(action_)
+  DynBlock(const std::string& reason_, const struct timespec& until_, const DNSName& domain_, DNSAction::Action action_): reason(reason_), until(until_), domain(domain_), action(action_), warning(false)
   {
   }
 
-  DynBlock(const DynBlock& rhs): reason(rhs.reason), until(rhs.until), domain(rhs.domain), action(rhs.action)
+  DynBlock(const DynBlock& rhs): reason(rhs.reason), until(rhs.until), domain(rhs.domain), action(rhs.action), warning(rhs.warning)
   {
     blocks.store(rhs.blocks);
   }
@@ -181,6 +181,7 @@ struct DynBlock
     domain=rhs.domain;
     action=rhs.action;
     blocks.store(rhs.blocks);
+    warning=rhs.warning;
     return *this;
   }
 
@@ -189,6 +190,7 @@ struct DynBlock
   DNSName domain;
   DNSAction::Action action;
   mutable std::atomic<unsigned int> blocks;
+  bool warning;
 };
 
 extern GlobalStateHolder<NetmaskTree<DynBlock>> g_dynblockNMG;
index b6e1d39eea9b5e4a31ae7de0345410c9b602b0e9..900ddaea6e5dfb97dcaff159b92cd563e4627e3a 100644 (file)
@@ -217,6 +217,7 @@ testrunner_SOURCES = \
        test-base64_cc.cc \
        test-dnscrypt_cc.cc \
        test-dnsdist_cc.cc \
+       test-dnsdistdynblocks_hh.cc \
        test-dnsdistpacketcache_cc.cc \
        test-dnsdistrings_cc.cc \
        test-dnsdistrules_cc.cc \
index 084b6a062824e0df2cde29c9b3f3841d5488ddc9..fd718430e0a200a784c2b032b292458528e2a063 100644 (file)
@@ -30,7 +30,7 @@ Please see the documentation for :func:`setDynBlocksAction` to confirm which act
 DynBlockRulesGroup
 ------------------
 
-Starting with dnsdist 1.3.0, a new `:ref:dynBlockRulesGroup` function can be used to return a `DynBlockRulesGroup` instance,
+Starting with dnsdist 1.3.0, a new :ref:`dynBlockRulesGroup` function can be used to return a `DynBlockRulesGroup` instance,
 designed to make the processing of multiple rate-limiting rules faster by walking the query and response buffers only once
 for each invocation, instead of once per existing `exceed*()` invocation.
 
@@ -73,3 +73,14 @@ DynBlockRulesGroup also offers the ability to specify that some network ranges s
   -- except for 192.0.2.1
   dbr:includeRange("192.0.2.1/32")
 
+
+Since 1.3.3, it's also possible to define a warning rate. When the query or response rate raises above the warning level but below
+the trigger level, a warning message will be issued along with a no-op block. If the rate reaches the trigger level, the regular
+action is applied.
+
+.. code-block:: lua
+
+  local dbr = dynBlockRulesGroup()
+  -- generate a warning above 100 qps for 10s, and start dropping incoming queries above 300 qps for 10s
+  dbr:setQueryRate(300, 10, "Exceeded query rate", 60, 100)
+
index 0889ebe584070fec6a66268f7b608076c51805ad..f4247f8cd898a243c3f761d84fe03226acf76bcb 100644 (file)
@@ -824,7 +824,10 @@ faster than the existing rules.
 
   Represents a group of dynamic block rules.
 
-  .. method:: DynBlockRulesGroup:setQueryRate(rate, seconds, reason, blockingTime [, action])
+  .. method:: DynBlockRulesGroup:setQueryRate(rate, seconds, reason, blockingTime [, action [, warningRate]])
+
+    .. versionchanged:: 1.3.3
+        ``warningRate`` parameter added.
 
     Adds a query rate-limiting rule, equivalent to:
     ```
@@ -836,8 +839,12 @@ faster than the existing rules.
     :param string reason: The message to show next to the blocks
     :param int blockingTime: The number of seconds this block to expire
     :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:setRCodeRate(rcode, rate, seconds, reason, blockingTime [, action [, warningRate]])
 
-  .. method:: DynBlockRulesGroup:setRCodeRate(rcode, rate, seconds, reason, blockingTime [, action])
+    .. versionchanged:: 1.3.3
+        ``warningRate`` parameter added.
 
     Adds a rate-limiting rule for responses of code ``rcode``, equivalent to:
     ```
@@ -850,8 +857,12 @@ faster than the existing rules.
     :param string reason: The message to show next to the blocks
     :param int blockingTime: The number of seconds this block to expire
     :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:setQTypeRate(qtype, rate, seconds, reason, blockingTime [, action])
+  .. method:: DynBlockRulesGroup:setQTypeRate(qtype, rate, seconds, reason, blockingTime [, action [, warningRate]])
+
+    .. versionchanged:: 1.3.3
+        ``warningRate`` parameter added.
 
     Adds a rate-limiting rule for queries of type ``qtype``, equivalent to:
     ```
@@ -864,8 +875,12 @@ faster than the existing rules.
     :param string reason: The message to show next to the blocks
     :param int blockingTime: The number of seconds this block to expire
     :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:setResponseByteRate(rate, seconds, reason, blockingTime [, action [, warningRate]])
 
-  .. method:: DynBlockRulesGroup:setRespByteRate(rate, seconds, reason, blockingTime [, action])
+    .. versionchanged:: 1.3.3
+        ``warningRate`` parameter added.
 
     Adds a bandwidth rate-limiting rule for responses, equivalent to:
     ```
@@ -877,6 +892,7 @@ faster than the existing rules.
     :param string reason: The message to show next to the blocks
     :param int blockingTime: The number of seconds this block to expire
     :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:apply()
 
diff --git a/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc b/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc
new file mode 100644 (file)
index 0000000..68a89e3
--- /dev/null
@@ -0,0 +1,546 @@
+
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+
+#include <boost/test/unit_test.hpp>
+
+#include "dnsdist.hh"
+#include "dnsdist-dynblocks.hh"
+#include "dnsdist-rings.hh"
+
+Rings g_rings;
+GlobalStateHolder<NetmaskTree<DynBlock>> g_dynblockNMG;
+
+BOOST_AUTO_TEST_SUITE(dnsdistdynblocks_hh)
+
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QueryRate) {
+  dnsheader dh;
+  DNSName qname("rings.powerdns.com.");
+  ComboAddress requestor1("192.0.2.1");
+  ComboAddress requestor2("192.0.2.2");
+  uint16_t qtype = QType::AAAA;
+  uint16_t size = 42;
+  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 rate";
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* block above 50 qps for numberOfSeconds seconds, no warning */
+  dbrg.setQueryRate(50, 0, numberOfSeconds, reason, blockDuration, action);
+
+  {
+    /* insert 45 qps from a given client in the last 10s
+       this should not trigger the rule */
+    size_t numberOfQueries = 45 * numberOfSeconds;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    dbrg.apply();
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just above 50 qps from a given client in the last 10s
+       this should trigger the rule this time */
+    size_t numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    dbrg.apply();
+    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);
+  }
+
+}
+
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QTypeRate) {
+  dnsheader dh;
+  DNSName qname("rings.powerdns.com.");
+  ComboAddress requestor1("192.0.2.1");
+  ComboAddress requestor2("192.0.2.2");
+  uint16_t qtype = QType::AAAA;
+  uint16_t size = 42;
+  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 rate";
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* block above 50 qps for numberOfSeconds seconds, no warning */
+  dbrg.setQTypeRate(QType::AAAA, 50, 0, numberOfSeconds, reason, blockDuration, action);
+
+  {
+    /* insert 45 qps from a given client in the last 10s
+       this should not trigger the rule */
+    size_t numberOfQueries = 45 * numberOfSeconds;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    dbrg.apply();
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just above 50 qps from a given client in the last 10s
+       but for the wrong QType */
+    size_t numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, QType::A, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    dbrg.apply();
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    // insert just above 50 qps from a given client in the last 10s
+    // this should trigger the rule this time
+    size_t numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    dbrg.apply();
+    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);
+  }
+
+}
+
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRate) {
+  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 rate";
+  const uint16_t rcode = RCode::ServFail;
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* block above 50 ServFail/s for numberOfSeconds seconds, no warning */
+  dbrg.setRCodeRate(rcode, 50, 0, numberOfSeconds, reason, blockDuration, action);
+
+  {
+    /* insert 45 ServFail/s from a given client in the last 10s
+       this should not trigger the rule */
+    size_t numberOfResponses = 45 * numberOfSeconds;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < numberOfResponses; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses);
+
+    dbrg.apply();
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just above 50 FormErr/s from a given client in the last 10s */
+    size_t numberOfResponses = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = RCode::FormErr;
+    for (size_t idx = 0; idx < numberOfResponses; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses);
+
+    dbrg.apply();
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just above 50 ServFail/s from a given client in the last 10s
+       this should trigger the rule this time */
+    size_t numberOfResponses = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < numberOfResponses; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses);
+
+    dbrg.apply();
+    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);
+  }
+
+}
+
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_ResponseByteRate) {
+  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 = 100;
+  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 rate";
+  const uint16_t rcode = RCode::NoError;
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* block above 10kB/s for numberOfSeconds seconds, no warning */
+  dbrg.setResponseByteRate(10000, 0, numberOfSeconds, reason, blockDuration, action);
+
+  {
+    /* insert 99 answers of 100 bytes per second from a given client in the last 10s
+       this should not trigger the rule */
+    size_t numberOfResponses = 99 * numberOfSeconds;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < numberOfResponses; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses);
+
+    dbrg.apply();
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just above 100 answers of 100 bytes per second from a given client in the last 10s */
+    size_t numberOfResponses = 100 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    dh.rcode = rcode;
+    for (size_t idx = 0; idx < numberOfResponses; idx++) {
+      g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses);
+
+    dbrg.apply();
+    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);
+  }
+
+}
+
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_Warning) {
+  dnsheader dh;
+  DNSName qname("rings.powerdns.com.");
+  ComboAddress requestor1("192.0.2.1");
+  ComboAddress requestor2("192.0.2.2");
+  uint16_t qtype = QType::AAAA;
+  uint16_t size = 42;
+  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 rate";
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+
+  /* warn above 20 qps for numberOfSeconds seconds, block above 50 qps */
+  dbrg.setQueryRate(50, 20, numberOfSeconds, reason, blockDuration, action);
+
+  {
+    /* insert 20 qps from a given client in the last 10s
+       this should not trigger the rule */
+    size_t numberOfQueries = 20 * numberOfSeconds;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    dbrg.apply(now);
+    BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0);
+    BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+  }
+
+  {
+    /* insert just above 20 qps from a given client in the last 10s
+       this should trigger the warning rule this time */
+    size_t numberOfQueries = 20 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    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 == DNSAction::Action::NoOp);
+      BOOST_CHECK_EQUAL(block.blocks, 0);
+      BOOST_CHECK_EQUAL(block.warning, true);
+      /* let's increment the number of blocks so we can check that the counter
+         is preserved when the block is upgraded to a non-warning one */
+      block.blocks++;
+    }
+
+    /* now inserts 50 qps for the same duration, we should reach the blocking threshold */
+    numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    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);
+      /* this hsould have been preserved */
+      BOOST_CHECK_EQUAL(block.blocks, 1);
+      BOOST_CHECK_EQUAL(block.warning, false);
+      block.blocks++;
+    }
+
+    /* 30s later, with the same amount of qps the duration of the block
+       should be increased. */
+    now.tv_sec += 30;
+    numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    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);
+      /* should have been updated */
+      BOOST_CHECK_EQUAL(block.until.tv_sec, now.tv_sec + blockDuration);
+      BOOST_CHECK(block.domain.empty());
+      BOOST_CHECK(block.action == action);
+      /* this hsould have been preserved */
+      BOOST_CHECK_EQUAL(block.blocks, 2);
+      BOOST_CHECK_EQUAL(block.warning, false);
+    }
+  }
+
+  {
+    /* insert directly just above 50 qps from a given client in the last 10s
+       this should trigger the blocking rule right away this time */
+    size_t numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries);
+
+    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);
+    }
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_Ranges) {
+  dnsheader dh;
+  DNSName qname("rings.powerdns.com.");
+  ComboAddress requestor1("192.0.2.1");
+  ComboAddress requestor2("192.0.2.42");
+  uint16_t qtype = QType::AAAA;
+  uint16_t size = 42;
+  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 rate";
+
+  DynBlockRulesGroup dbrg;
+  dbrg.setQuiet(true);
+  /* include 192.0.2.0 -> 192.0.2.63 */
+  dbrg.includeRange(Netmask("192.0.2.0/26"));
+  /* but exclude 192.0.2.42 only */
+  dbrg.excludeRange(Netmask("192.0.2.42/32"));
+
+  /* block above 50 qps for numberOfSeconds seconds, no warning */
+  dbrg.setQueryRate(50, 0, numberOfSeconds, reason, blockDuration, action);
+
+  {
+    /* insert just above 50 qps from the two clients in the last 10s
+       this should trigger the rule for the first one but not the second one */
+    size_t numberOfQueries = 50 * numberOfSeconds + 1;
+    g_rings.clear();
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0);
+    g_dynblockNMG.setState(emptyNMG);
+
+    for (size_t idx = 0; idx < numberOfQueries; idx++) {
+      g_rings.insertQuery(now, requestor1, qname, qtype, size, dh);
+      g_rings.insertQuery(now, requestor2, qname, qtype, size, dh);
+    }
+    BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries * 2);
+
+    dbrg.apply();
+    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);
+  }
+
+}
+
+BOOST_AUTO_TEST_SUITE_END()
index 5cd01e46ae5fc6fd2d08424356e975858d0013bf..0012ba80a5a28833bad7c0219d26cac7c506f58e 100644 (file)
@@ -966,3 +966,69 @@ class TestDynBlockGroupNoOp(DynBlocksTest):
 
         # check that the rule has been inserted
         self.doTestDynBlockViaAPI('127.0.0.1/32', 'Exceeded query rate', self._dynBlockDuration - 4, self._dynBlockDuration, 0, sent)
+
+class TestDynBlockGroupWarning(DynBlocksTest):
+
+    _dynBlockWarningQPS = 5
+    _dynBlockQPS = 20
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_dynBlockWarningQPS', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setQueryRate(%d, %d, "Exceeded query rate", %d, DNSAction.Drop, %d)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    newServer{address="127.0.0.1:%s"}
+    webserver("127.0.0.1:%s", "%s", "%s")
+    """
+
+    def testWarning(self):
+        """
+        Dyn Blocks (group) : Warning
+        """
+        name = 'warning.group.dynblocks.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+
+        allowed = 0
+        sent = 0
+        for _ in range((self._dynBlockWarningQPS * self._dynBlockPeriod) + 1):
+            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+            sent = sent + 1
+            if receivedQuery:
+                receivedQuery.id = query.id
+                self.assertEquals(query, receivedQuery)
+                self.assertEquals(response, receivedResponse)
+                allowed = allowed + 1
+            else:
+                # the query has not reached the responder,
+                # let's clear the response queue
+                self.clearToResponderQueue()
+
+        # a dynamic rule should have been inserted, but the queries should
+        # still go on because we are still at warning level
+        self.assertEqual(allowed, sent)
+
+        # wait for the maintenance function to run
+        time.sleep(2)
+
+        # the rule should still be present, but the queries pass through anyway
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, receivedResponse)
+
+        # check that the rule has been inserted
+        self.doTestDynBlockViaAPI('127.0.0.1/32', 'Exceeded query rate', self._dynBlockDuration - 4, self._dynBlockDuration, 0, sent)
+
+        self.doTestQRate(name)