]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add DynBlockRulesGroup
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 21 Mar 2018 16:23:50 +0000 (17:23 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 21 Mar 2018 16:23:50 +0000 (17:23 +0100)
The new `DynBlockRulesGroup` object is 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.

For example, instead of having something like:
```
function maintenance()
  addDynBlocks(exceedQRate(30, 10), "Exceeded query rate", 60)
  addDynBlocks(exceedNXDOMAINs(20, 10), "Exceeded NXD rate", 60)
  addDynBlocks(exceedServFails(20, 10), "Exceeded ServFail rate", 60)
  addDynBlocks(exceedQTypeRate(dnsdist.ANY, 5, 10), "Exceeded ANY rate", 60)
  addDynBlocks(exceedRespByterate(1000000, 10), "Exceeded resp BW rate", 60)
end
```

The new syntax would be:
```
local dbr = dynBlockRulesGroup()
dbr:setQueryRate(30, 10, "Exceeded query rate", 60)
dbr:setRCodeRate(dnsdist.NXDOMAIN, 20, 10, "Exceeded NXD rate", 60)
dbr:setRCodeRate(dnsdist.SERVFAIL, 20, 10, "Exceeded ServFail rate", 60)
dbr:setQTypeRate(dnsdist.ANY, 5, 10, "Exceeded ANY rate", 60)
dbr:setResponseByteRate(10000, 10, "Exceeded resp BW rate", 60)

function maintenance()
  dbr:apply()
end
```

The old syntax would walk the query buffer 2 times and the response
one 3 times, while the new syntax does it only once for each.
It also reuse the same internal table to keep track of the source
IPs, reducing the CPU usage.

pdns/dnsdist-console.cc
pdns/dnsdist-dynblocks.hh [new file with mode: 0644]
pdns/dnsdist-lua-inspection.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-dynblocks.hh [new symlink]
pdns/dnsdistdist/docs/guides/dynblocks.rst
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_DynBlocks.py

index 7d9d0209eedbb9c934338d71942db6f3a9d2d92a..45ed2048e3de425b7ebabb7aa7e72c7d61c2fde1 100644 (file)
@@ -314,6 +314,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "DropResponseAction", true, "", "drop these packets" },
   { "dumpStats", true, "", "print all statistics we gather" },
   { "exceedNXDOMAINs", true, "rate, seconds", "get set of addresses that exceed `rate` NXDOMAIN/s over `seconds` seconds" },
+  { "dynBlockRulesGroup", true, "", "return a new DynBlockRulesGroup object" },
   { "exceedQRate", true, "rate, seconds", "get set of address that exceed `rate` queries/s over `seconds` seconds" },
   { "exceedQTypeRate", true, "type, rate, seconds", "get set of address that exceed `rate` queries/s for queries of type `type` over `seconds` seconds" },
   { "exceedRespByterate", true, "rate, seconds", "get set of addresses that exceeded `rate` bytes/s answers over `seconds` seconds" },
diff --git a/pdns/dnsdist-dynblocks.hh b/pdns/dnsdist-dynblocks.hh
new file mode 100644 (file)
index 0000000..3613b2e
--- /dev/null
@@ -0,0 +1,332 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include "dolog.hh"
+
+class DynBlockRulesGroup
+{
+private:
+
+  struct Counts
+  {
+    std::map<uint8_t, uint64_t> d_rcodeCounts;
+    std::map<uint16_t, uint64_t> d_qtypeCounts;
+    uint64_t queries{0};
+    uint64_t respBytes{0};
+  };
+
+  struct DynBlockRule
+  {
+    DynBlockRule(): d_enabled(false)
+    {
+    }
+
+    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)
+    {
+    }
+
+    bool matches(const struct timespec& when)
+    {
+      if (!d_enabled) {
+        return false;
+      }
+
+      if (d_seconds && when < d_cutOff) {
+        return false;
+      }
+
+      if (when < d_minTime) {
+        d_minTime = when;
+      }
+
+      return true;
+    }
+
+    bool rateExceeded(unsigned int count, const struct timespec& now) const
+    {
+      if (!d_enabled) {
+        return false;
+      }
+
+      double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime);
+      double limit = delta * d_rate;
+      return (count > limit);
+    }
+
+    bool isEnabled() const
+    {
+      return d_enabled;
+    }
+
+    std::string d_blockReason;
+    struct timespec d_cutOff;
+    struct timespec d_minTime;
+    unsigned int d_blockDuration{0};
+    unsigned int d_rate{0};
+    unsigned int d_seconds{0};
+    DNSAction::Action d_action{DNSAction::Action::None};
+    bool d_enabled{false};
+  };
+
+    typedef std::unordered_map<ComboAddress, Counts, ComboAddress::addressOnlyHash, ComboAddress::addressOnlyEqual> counts_t;
+
+public:
+  DynBlockRulesGroup()
+  {
+  }
+
+  void setQueryRate(unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  {
+    d_queryRateRule = DynBlockRule(reason, blockDuration, rate, seconds, action);
+  }
+
+  void setResponseByteRate(unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  {
+    d_respRateRule = DynBlockRule(reason, blockDuration, rate, seconds, action);
+  }
+
+  void setRCodeRate(uint8_t rcode, unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  {
+    auto& entry = d_rcodeRules[rcode];
+    entry = DynBlockRule(reason, blockDuration, rate, seconds, action);
+  }
+
+  void setQTypeRate(uint16_t qtype, unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+  {
+    auto& entry = d_qtypeRules[qtype];
+    entry = DynBlockRule(reason, blockDuration, rate, seconds, action);
+  }
+
+  void apply()
+  {
+    counts_t counts;
+
+    size_t entriesCount = 0;
+    if (hasQueryRules()) {
+      ReadLock rl(&g_rings.queryLock);
+      entriesCount += g_rings.queryRing.size();
+    }
+    if (hasResponseRules()) {
+      std::lock_guard<std::mutex> lock(g_rings.respMutex);
+      entriesCount += g_rings.respRing.size();
+    }
+    counts.reserve(entriesCount);
+
+    processQueryRules(counts);
+    processResponseRules(counts);
+
+    if (counts.empty()) {
+      return;
+    }
+
+    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);
+        continue;
+      }
+
+      if (d_respRateRule.rateExceeded(entry.second.respBytes, now)) {
+        addBlock(blocks, now, entry.first, 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& 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;
+        }
+      }
+    }
+
+    if (updated && blocks) {
+      g_dynblockNMG.setState(*blocks);
+    }
+  }
+
+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 addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
+  {
+    if (!blocks) {
+      blocks = g_dynblockNMG.getCopy();
+    }
+    struct timespec until = now;
+    until.tv_sec += rule.d_seconds;
+    unsigned int count = 0;
+    const auto& got = blocks->lookup(Netmask(requestor));
+    bool expired = false;
+    if (got) {
+      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(), rule.d_action};
+    db.blocks = count;
+    if (!got || expired) {
+      warnlog("Inserting dynamic block for %s for %d seconds: %s", requestor.toString(), rule.d_seconds, rule.d_blockReason);
+    }
+    blocks->insert(Netmask(requestor)).second = db;
+    updated = true;
+  }
+
+  bool hasQueryRules() const
+  {
+    return d_queryRateRule.isEnabled() || !d_qtypeRules.empty();
+  }
+
+  bool hasResponseRules() const
+  {
+    return d_respRateRule.isEnabled() || !d_rcodeRules.empty();
+  }
+
+  bool hasRules() const
+  {
+    return hasQueryRules() || hasResponseRules();
+  }
+
+  void processQueryRules(counts_t& counts)
+  {
+    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;
+
+    for (auto& rule : d_qtypeRules) {
+      rule.second.d_cutOff = rule.second.d_minTime = now;
+      rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+    }
+
+    {
+      ReadLock rl(&g_rings.queryLock);
+      for(const auto& c : g_rings.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)
+  {
+    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;
+
+    for (auto& rule : d_rcodeRules) {
+      rule.second.d_cutOff = rule.second.d_minTime = now;
+      rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+    }
+
+    {
+      std::lock_guard<std::mutex> lock(g_rings.respMutex);
+      for(const auto& c : g_rings.respRing) {
+        if (now < c.when) {
+          continue;
+        }
+
+        bool respRateMatches = d_respRateRule.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]++;
+          }
+        }
+      }
+    }
+  }
+
+  std::map<uint8_t, DynBlockRule> d_rcodeRules;
+  std::map<uint16_t, DynBlockRule> d_qtypeRules;
+  DynBlockRule d_queryRateRule;
+  DynBlockRule d_respRateRule;
+};
index d3b14e2c6d1af01f98f5310e800554c09f213a24..452d7a236717ab384d91fbe85aa7c42e61d55d8b 100644 (file)
@@ -21,6 +21,7 @@
  */
 #include "dnsdist.hh"
 #include "dnsdist-lua.hh"
+#include "dnsdist-dynblocks.hh"
 
 #include "statnode.hh"
 
@@ -613,4 +614,28 @@ void setupLuaInspection()
   g_lua.writeFunction("statNodeRespRing", [](statvisitor_t visitor, boost::optional<unsigned int> seconds) {
       statNodeRespRing(visitor, seconds ? *seconds : 0);
     });
+
+  /* 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) {
+      if (group) {
+        group->setQueryRate(rate, 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) {
+      if (group) {
+        group->setResponseByteRate(rate, 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) {
+      if (group) {
+        group->setRCodeRate(rcode, rate, 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) {
+      if (group) {
+        group->setQTypeRate(qtype, rate, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
+      }
+    });
+  g_lua.registerFunction("apply", &DynBlockRulesGroup::apply);
 }
index 1bcab9a84f9cfaa9c05d8bebc2776b130be0e730..9bb3d147e79a4a49e1f8c94fac02d10a0201faf5 100644 (file)
@@ -88,6 +88,7 @@ dnsdist_SOURCES = \
        dnsdist-carbon.cc \
        dnsdist-console.cc \
        dnsdist-dnscrypt.cc \
+       dnsdist-dynblocks.hh \
        dnsdist-ecs.cc dnsdist-ecs.hh \
        dnsdist-lua.hh dnsdist-lua.cc \
        dnsdist-lua-actions.cc \
diff --git a/pdns/dnsdistdist/dnsdist-dynblocks.hh b/pdns/dnsdistdist/dnsdist-dynblocks.hh
new file mode 120000 (symlink)
index 0000000..73fa1f7
--- /dev/null
@@ -0,0 +1 @@
+../dnsdist-dynblocks.hh
\ No newline at end of file
index 666f236c7d9cc7b56c8ebe2119e332b64fcb0a8c..a30b653a22bfdd84fab743b5b61d74a506471795 100644 (file)
@@ -22,3 +22,38 @@ Dynamic blocks drop matched queries by default, but this behavior can be changed
 For example, to send a REFUSED code instead of droppping the query::
 
   setDynBlocksAction(DNSAction.Refused)
+
+DynBlockRulesGroup
+------------------
+
+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.
+
+For example, instead of having something like:
+```
+function maintenance()
+  addDynBlocks(exceedQRate(30, 10), "Exceeded query rate", 60)
+  addDynBlocks(exceedNXDOMAINs(20, 10), "Exceeded NXD rate", 60)
+  addDynBlocks(exceedServFails(20, 10), "Exceeded ServFail rate", 60)
+  addDynBlocks(exceedQTypeRate(dnsdist.ANY, 5, 10), "Exceeded ANY rate", 60)
+  addDynBlocks(exceedRespByterate(1000000, 10), "Exceeded resp BW rate", 60)
+end
+```
+
+The new syntax would be:
+```
+local dbr = dynBlockRulesGroup()
+dbr:setQueryRate(30, 10, "Exceeded query rate", 60)
+dbr:setRCodeRate(dnsdist.NXDOMAIN, 20, 10, "Exceeded NXD rate", 60)
+dbr:setRCodeRate(dnsdist.SERVFAIL, 20, 10, "Exceeded ServFail rate", 60)
+dbr:setQTypeRate(dnsdist.ANY, 5, 10, "Exceeded ANY rate", 60)
+dbr:setResponseByteRate(10000, 10, "Exceeded resp BW rate", 60)
+
+function maintenance()
+  dbr:apply()
+end
+```
+
+The old syntax would walk the query buffer 2 times and the response one 3 times, while the new syntax does it only once for each.
+It also reuse the same internal table to keep track of the source IPs, reducing the CPU usage.
index a89d466346c8013b83865114dfb52828d23b6f81..566cf56124589a0607f2db048a5495ae7d963e13 100644 (file)
@@ -711,6 +711,83 @@ Getting addresses that exceeded parameters
   :param int rate: Number of QType queries per second to exceed
   :param int seconds: Number of seconds the rate has been exceeded
 
+DynBlockRulesGroup
+~~~~~~~~~~~~~~~~~~
+
+Instead of using several `exceed*()` lines, dnsdist 1.3.0 introduced a new `DynBlockRulesGroup` object
+which can be used to group dynamic block rules.
+
+See :doc:`../guides/dynblocks` for more information about the case where using a `DynBlockRulesGroup` might be
+faster than the existing rules.
+
+.. function:: dynBlockRulesGroup() -> DynBlockRulesGroup
+
+  .. versionaddeded:: 1.3.0
+
+  Creates a new :class:`DynBlockRulesGroup` object.
+
+.. class:: DynBlockRulesGroup
+
+  Represents a group of dynamic block rules.
+
+  .. method:: DynBlockRulesGroup:setQueryRate(rate, seconds, reason, blockingTime [, action])
+
+    Adds a query rate-limiting rule, equivalent to:
+    ```
+    addDynBlocks(exceedQRate(rate, seconds), reason, blockingTime, action)
+    ```
+
+    :param int rate: Number of queries per second to exceed
+    :param int seconds: Number of seconds the rate 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 action: The action to take when the dynamic block matches, see :ref:`here <DNSAction>`. (default to the one set with :func:`setDynBlocksAction`)
+
+  .. method:: DynBlockRulesGroup:setRCodeRate(rcode, rate, seconds, reason, blockingTime [, action])
+
+    Adds a rate-limiting rule for responses of code ``rcode``, equivalent to:
+    ```
+    addDynBlocks(exceedServfails(rcode, rate, seconds), reason, blockingTime, action)
+    ```
+
+    :param int rcode: The response code
+    :param int rate: Number of responses per second to exceed
+    :param int seconds: Number of seconds the rate 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 action: The action to take when the dynamic block matches, see :ref:`here <DNSAction>`. (default to the one set with :func:`setDynBlocksAction`)
+
+  .. method:: DynBlockRulesGroup:setQTypeRate(qtype, rate, seconds, reason, blockingTime [, action])
+
+    Adds a rate-limiting rule for queries of type ``qtype``, equivalent to:
+    ```
+    addDynBlocks(exceedQTypeRate(type, rate, seconds), reason, blockingTime, action)
+    ```
+
+    :param int qtype: The qtype
+    :param int rate: Number of queries per second to exceed
+    :param int seconds: Number of seconds the rate 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 action: The action to take when the dynamic block matches, see :ref:`here <DNSAction>`. (default to the one set with :func:`setDynBlocksAction`)
+
+  .. method:: DynBlockRulesGroup:setRespByteRate(rate, seconds, reason, blockingTime [, action])
+
+    Adds a bandwidth rate-limiting rule for responses, equivalent to:
+    ```
+    addDynBlocks(exceedRespByterate(rate, seconds), reason, blockingTime, action)
+    ```
+
+    :param int rate: Number of bytes per second to exceed
+    :param int seconds: Number of seconds the rate 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 action: The action to take when the dynamic block matches, see :ref:`here <DNSAction>`. (default to the one set with :func:`setDynBlocksAction`)
+
+  .. method:: DynBlockRulesGroup:apply()
+
+    Walk the in-memory query and response ring buffers and apply the configured rate-limiting rules, adding dynamic blocks when the limits have been exceeded.
+
 Other functions
 ---------------
 
index 83ef65db49b4cbb94c321104f97694efd186a953..fbb91496588c713b74d1cef7404f5ba57dc9a3a8 100644 (file)
@@ -4,24 +4,9 @@ import time
 import dns
 from dnsdisttests import DNSDistTest, range
 
-class TestDynBlockQPS(DNSDistTest):
+class DynBlocksTest(DNSDistTest):
 
-    _dynBlockQPS = 10
-    _dynBlockPeriod = 2
-    _dynBlockDuration = 5
-    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
-    _config_template = """
-    function maintenance()
-           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
-    end
-    newServer{address="127.0.0.1:%s"}
-    """
-
-    def testDynBlocksQRate(self):
-        """
-        Dyn Blocks: QRate
-        """
-        name = 'qrate.dynblocks.tests.powerdns.com.'
+    def doTestQRate(self, name):
         query = dns.message.make_query(name, 'A', 'IN')
         response = dns.message.make_response(query)
         rrset = dns.rrset.from_text(name,
@@ -104,25 +89,7 @@ class TestDynBlockQPS(DNSDistTest):
         self.assertEquals(query, receivedQuery)
         self.assertEquals(response, receivedResponse)
 
-class TestDynBlockQPSRefused(DNSDistTest):
-
-    _dynBlockQPS = 10
-    _dynBlockPeriod = 2
-    _dynBlockDuration = 5
-    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
-    _config_template = """
-    function maintenance()
-           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
-    end
-    setDynBlocksAction(DNSAction.Refused)
-    newServer{address="127.0.0.1:%s"}
-    """
-
-    def testDynBlocksQRate(self):
-        """
-        Dyn Blocks: QRate refused
-        """
-        name = 'qraterefused.dynblocks.tests.powerdns.com.'
+    def doTestQRateRCode(self, name, rcode):
         query = dns.message.make_query(name, 'A', 'IN')
         response = dns.message.make_response(query)
         rrset = dns.rrset.from_text(name,
@@ -131,8 +98,8 @@ class TestDynBlockQPSRefused(DNSDistTest):
                                     dns.rdatatype.A,
                                     '192.0.2.1')
         response.answer.append(rrset)
-        refusedResponse = dns.message.make_response(query)
-        refusedResponse.set_rcode(dns.rcode.REFUSED)
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.set_rcode(rcode)
 
         allowed = 0
         sent = 0
@@ -145,7 +112,7 @@ class TestDynBlockQPSRefused(DNSDistTest):
                 self.assertEquals(receivedResponse, response)
                 allowed = allowed + 1
             else:
-                self.assertEquals(receivedResponse, refusedResponse)
+                self.assertEquals(receivedResponse, expectedResponse)
                 # the query has not reached the responder,
                 # let's clear the response queue
                 self.clearToResponderQueue()
@@ -158,9 +125,9 @@ class TestDynBlockQPSRefused(DNSDistTest):
             # wait for the maintenance function to run
             time.sleep(2)
 
-        # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+        # we should now be 'rcode' for up to self._dynBlockDuration + self._dynBlockPeriod
         (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, refusedResponse)
+        self.assertEquals(receivedResponse, expectedResponse)
 
         # wait until we are not blocked anymore
         time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
@@ -183,7 +150,7 @@ class TestDynBlockQPSRefused(DNSDistTest):
                 self.assertEquals(receivedResponse, response)
                 allowed = allowed + 1
             else:
-                self.assertEquals(receivedResponse, refusedResponse)
+                self.assertEquals(receivedResponse, expectedResponse)
                 # the query has not reached the responder,
                 # let's clear the response queue
                 self.clearToResponderQueue()
@@ -196,9 +163,9 @@ class TestDynBlockQPSRefused(DNSDistTest):
             # wait for the maintenance function to run
             time.sleep(2)
 
-        # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+        # we should now be 'rcode' for up to self._dynBlockDuration + self._dynBlockPeriod
         (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, refusedResponse)
+        self.assertEquals(receivedResponse, expectedResponse)
 
         # wait until we are not blocked anymore
         time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
@@ -209,101 +176,114 @@ class TestDynBlockQPSRefused(DNSDistTest):
         self.assertEquals(query, receivedQuery)
         self.assertEquals(response, receivedResponse)
 
-class TestDynBlockQPSActionRefused(DNSDistTest):
-
-    _dynBlockQPS = 10
-    _dynBlockPeriod = 2
-    _dynBlockDuration = 5
-    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
-    _config_template = """
-    function maintenance()
-           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Refused)
-    end
-    setDynBlocksAction(DNSAction.Drop)
-    newServer{address="127.0.0.1:%s"}
-    """
-
-    def testDynBlocksQRate(self):
-        """
-        Dyn Blocks: QRate refused (action)
-        """
-        name = 'qrateactionrefused.dynblocks.tests.powerdns.com.'
+    def doTestResponseByteRate(self, name):
         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)
-        refusedResponse = dns.message.make_response(query)
-        refusedResponse.set_rcode(dns.rcode.REFUSED)
+        response.answer.append(dns.rrset.from_text_list(name,
+                                                       60,
+                                                       dns.rdataclass.IN,
+                                                       dns.rdatatype.A,
+                                                       ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4']))
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.AAAA,
+                                                   '2001:DB8::1'))
 
         allowed = 0
         sent = 0
-        for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
+
+        print(time.time())
+
+        for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
             (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-            sent = sent + 1
+            sent = sent + len(response.to_wire())
             if receivedQuery:
                 receivedQuery.id = query.id
                 self.assertEquals(query, receivedQuery)
-                self.assertEquals(receivedResponse, response)
-                allowed = allowed + 1
+                self.assertEquals(response, receivedResponse)
+                allowed = allowed + len(response.to_wire())
             else:
-                self.assertEquals(receivedResponse, refusedResponse)
                 # the query has not reached the responder,
                 # let's clear the response queue
                 self.clearToResponderQueue()
+                # and stop right there, otherwise we might
+                # wait for so long that the dynblock is gone
+                # by the time we finished
+                break
 
         # we might be already blocked, but we should have been able to send
-        # at least self._dynBlockQPS queries
-        self.assertGreaterEqual(allowed, self._dynBlockQPS)
+        # at least self._dynBlockBytesPerSecond bytes
+        print(allowed)
+        print(sent)
+        print(time.time())
+        self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
+
+        print(self.sendConsoleCommand("showDynBlocks()"))
+        print(self.sendConsoleCommand("grepq(\"\")"))
+        print(time.time())
 
         if allowed == sent:
             # wait for the maintenance function to run
+            print("Waiting for the maintenance function to run")
             time.sleep(2)
 
-        # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+        print(self.sendConsoleCommand("showDynBlocks()"))
+        print(self.sendConsoleCommand("grepq(\"\")"))
+        print(time.time())
+
+        # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
         (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, refusedResponse)
+        self.assertEquals(receivedResponse, None)
+
+        print(self.sendConsoleCommand("showDynBlocks()"))
+        print(self.sendConsoleCommand("grepq(\"\")"))
+        print(time.time())
 
         # wait until we are not blocked anymore
         time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
 
+        print(self.sendConsoleCommand("showDynBlocks()"))
+        print(self.sendConsoleCommand("grepq(\"\")"))
+        print(time.time())
+
         # this one should succeed
         (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
         receivedQuery.id = query.id
         self.assertEquals(query, receivedQuery)
         self.assertEquals(response, receivedResponse)
 
+        # again, over TCP this time
         allowed = 0
         sent = 0
-        # again, over TCP this time
-        for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
+        for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
             (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-            sent = sent + 1
+            sent = sent + len(response.to_wire())
             if receivedQuery:
                 receivedQuery.id = query.id
                 self.assertEquals(query, receivedQuery)
-                self.assertEquals(receivedResponse, response)
-                allowed = allowed + 1
+                self.assertEquals(response, receivedResponse)
+                allowed = allowed + len(response.to_wire())
             else:
-                self.assertEquals(receivedResponse, refusedResponse)
                 # the query has not reached the responder,
                 # let's clear the response queue
                 self.clearToResponderQueue()
+                # and stop right there, otherwise we might
+                # wait for so long that the dynblock is gone
+                # by the time we finished
+                break
 
         # we might be already blocked, but we should have been able to send
-        # at least self._dynBlockQPS queries
-        self.assertGreaterEqual(allowed, self._dynBlockQPS)
+        # at least self._dynBlockBytesPerSecond bytes
+        self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
 
         if allowed == sent:
             # wait for the maintenance function to run
             time.sleep(2)
 
-        # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+        # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
         (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, refusedResponse)
+        self.assertEquals(receivedResponse, None)
 
         # wait until we are not blocked anymore
         time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
@@ -314,110 +294,7 @@ class TestDynBlockQPSActionRefused(DNSDistTest):
         self.assertEquals(query, receivedQuery)
         self.assertEquals(response, receivedResponse)
 
-class TestDynBlockQPSActionTruncated(DNSDistTest):
-
-    _dynBlockQPS = 10
-    _dynBlockPeriod = 2
-    _dynBlockDuration = 5
-    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
-    _config_template = """
-    function maintenance()
-           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Truncate)
-    end
-    setDynBlocksAction(DNSAction.Drop)
-    newServer{address="127.0.0.1:%s"}
-    """
-
-    def testDynBlocksQRate(self):
-        """
-        Dyn Blocks: QRate truncated (action)
-        """
-        name = 'qrateactiontruncated.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)
-        truncatedResponse = dns.message.make_response(query)
-        truncatedResponse.flags |= dns.flags.TC
-
-        allowed = 0
-        sent = 0
-        for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
-            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-            sent = sent + 1
-            if receivedQuery:
-                receivedQuery.id = query.id
-                self.assertEquals(query, receivedQuery)
-                self.assertEquals(receivedResponse, response)
-                allowed = allowed + 1
-            else:
-                self.assertEquals(receivedResponse, truncatedResponse)
-                # the query has not reached the responder,
-                # let's clear the response queue
-                self.clearToResponderQueue()
-
-        # we might be already truncated, but we should have been able to send
-        # at least self._dynBlockQPS queries
-        self.assertGreaterEqual(allowed, self._dynBlockQPS)
-
-        if allowed == sent:
-            # wait for the maintenance function to run
-            time.sleep(2)
-
-        # we should now be 'truncated' for up to self._dynBlockDuration + self._dynBlockPeriod
-        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, truncatedResponse)
-
-        # check over TCP, which should not be truncated
-        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-
-        self.assertEquals(query, receivedQuery)
-        self.assertEquals(receivedResponse, response)
-
-        # wait until we are not blocked anymore
-        time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
-
-        # this one should succeed
-        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-        receivedQuery.id = query.id
-        self.assertEquals(query, receivedQuery)
-        self.assertEquals(response, receivedResponse)
-
-        allowed = 0
-        sent = 0
-        # again, over TCP this time, we should never get truncated!
-        for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
-            (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-            sent = sent + 1
-            self.assertEquals(query, receivedQuery)
-            self.assertEquals(receivedResponse, response)
-            receivedQuery.id = query.id
-            allowed = allowed + 1
-
-        self.assertEquals(allowed, sent)
-
-class TestDynBlockServFails(DNSDistTest):
-
-    _dynBlockQPS = 10
-    _dynBlockPeriod = 2
-    _dynBlockDuration = 5
-    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
-    _config_template = """
-    function maintenance()
-           addDynBlocks(exceedServFails(%d, %d), "Exceeded servfail rate", %d)
-    end
-    newServer{address="127.0.0.1:%s"}
-    """
-
-    def testDynBlocksServFailRate(self):
-        """
-        Dyn Blocks: Server Failure Rate
-        """
-        name = 'servfailrate.dynblocks.tests.powerdns.com.'
+    def doTestRCodeRate(self, name, rcode):
         query = dns.message.make_query(name, 'A', 'IN')
         response = dns.message.make_response(query)
         rrset = dns.rrset.from_text(name,
@@ -426,8 +303,8 @@ class TestDynBlockServFails(DNSDistTest):
                                     dns.rdatatype.A,
                                     '192.0.2.1')
         response.answer.append(rrset)
-        servfailResponse = dns.message.make_response(query)
-        servfailResponse.set_rcode(dns.rcode.SERVFAIL)
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.set_rcode(rcode)
 
         # start with normal responses
         for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
@@ -443,16 +320,16 @@ class TestDynBlockServFails(DNSDistTest):
         (_, receivedResponse) = self.sendUDPQuery(query, response)
         self.assertEquals(receivedResponse, response)
 
-        # now with ServFail!
+        # now with rcode!
         sent = 0
         allowed = 0
         for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
-            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, servfailResponse)
+            (receivedQuery, receivedResponse) = self.sendUDPQuery(query, expectedResponse)
             sent = sent + 1
             if receivedQuery:
                 receivedQuery.id = query.id
                 self.assertEquals(query, receivedQuery)
-                self.assertEquals(servfailResponse, receivedResponse)
+                self.assertEquals(expectedResponse, receivedResponse)
                 allowed = allowed + 1
             else:
                 # the query has not reached the responder,
@@ -495,16 +372,16 @@ class TestDynBlockServFails(DNSDistTest):
         (_, receivedResponse) = self.sendUDPQuery(query, response)
         self.assertEquals(receivedResponse, response)
 
-        # now with ServFail!
+        # now with rcode!
         sent = 0
         allowed = 0
         for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
-            (receivedQuery, receivedResponse) = self.sendTCPQuery(query, servfailResponse)
+            (receivedQuery, receivedResponse) = self.sendTCPQuery(query, expectedResponse)
             sent = sent + 1
             if receivedQuery:
                 receivedQuery.id = query.id
                 self.assertEquals(query, receivedQuery)
-                self.assertEquals(servfailResponse, receivedResponse)
+                self.assertEquals(expectedResponse, receivedResponse)
                 allowed = allowed + 1
             else:
                 # the query has not reached the responder,
@@ -532,141 +409,318 @@ class TestDynBlockServFails(DNSDistTest):
         self.assertEquals(query, receivedQuery)
         self.assertEquals(response, receivedResponse)
 
-class TestDynBlockResponseBytes(DNSDistTest):
+class TestDynBlockQPS(DynBlocksTest):
 
-    _dynBlockBytesPerSecond = 200
+    _dynBlockQPS = 10
     _dynBlockPeriod = 2
     _dynBlockDuration = 5
-    _consoleKey = DNSDistTest.generateConsoleKey()
-    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
-    _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockBytesPerSecond', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
     _config_template = """
-    setKey("%s")
-    controlSocket("127.0.0.1:%s")
     function maintenance()
-           addDynBlocks(exceedRespByterate(%d, %d), "Exceeded response byterate", %d)
+           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
     end
     newServer{address="127.0.0.1:%s"}
     """
 
-    def testDynBlocksResponseByteRate(self):
+    def testDynBlocksQRate(self):
         """
-        Dyn Blocks: Response Byte Rate
+        Dyn Blocks: QRate
         """
-        name = 'responsebyterate.dynblocks.tests.powerdns.com.'
-        query = dns.message.make_query(name, 'A', 'IN')
-        response = dns.message.make_response(query)
-        response.answer.append(dns.rrset.from_text_list(name,
-                                                       60,
-                                                       dns.rdataclass.IN,
-                                                       dns.rdatatype.A,
-                                                       ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4']))
-        response.answer.append(dns.rrset.from_text(name,
-                                                   60,
-                                                   dns.rdataclass.IN,
-                                                   dns.rdatatype.AAAA,
-                                                   '2001:DB8::1'))
+        name = 'qrate.dynblocks.tests.powerdns.com.'
+        self.doTestQRate(name)
 
-        allowed = 0
-        sent = 0
+class TestDynBlockGroupQPS(DynBlocksTest):
 
-        print(time.time())
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setQueryRate(%d, %d, "Exceeded query rate", %d)
 
-        for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
+    function maintenance()
+           dbr:apply()
+    end
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksQRate(self):
+        """
+        Dyn Blocks (Group): QRate
+        """
+        name = 'qrate.group.dynblocks.tests.powerdns.com.'
+        self.doTestQRate(name)
+
+
+class TestDynBlockQPSRefused(DynBlocksTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    function maintenance()
+           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
+    end
+    setDynBlocksAction(DNSAction.Refused)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksQRate(self):
+        """
+        Dyn Blocks: QRate refused
+        """
+        name = 'qraterefused.dynblocks.tests.powerdns.com.'
+        self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockGroupQPSRefused(DynBlocksTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setQueryRate(%d, %d, "Exceeded query rate", %d)
+
+    function maintenance()
+           dbr:apply()
+    end
+    setDynBlocksAction(DNSAction.Refused)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksQRate(self):
+        """
+        Dyn Blocks (Group): QRate refused
+        """
+        name = 'qraterefused.group.dynblocks.tests.powerdns.com.'
+        self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockQPSActionRefused(DynBlocksTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    function maintenance()
+           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Refused)
+    end
+    setDynBlocksAction(DNSAction.Drop)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksQRate(self):
+        """
+        Dyn Blocks: QRate refused (action)
+        """
+        name = 'qrateactionrefused.dynblocks.tests.powerdns.com.'
+        self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockQPSActionRefused(DynBlocksTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setQueryRate(%d, %d, "Exceeded query rate", %d, DNSAction.Refused)
+
+    function maintenance()
+           dbr:apply()
+    end
+    setDynBlocksAction(DNSAction.Drop)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksQRate(self):
+        """
+        Dyn Blocks (group): QRate refused (action)
+        """
+        name = 'qrateactionrefused.group.dynblocks.tests.powerdns.com.'
+        self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockQPSActionTruncated(DNSDistTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    function maintenance()
+           addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Truncate)
+    end
+    setDynBlocksAction(DNSAction.Drop)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksQRate(self):
+        """
+        Dyn Blocks: QRate truncated (action)
+        """
+        name = 'qrateactiontruncated.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)
+        truncatedResponse = dns.message.make_response(query)
+        truncatedResponse.flags |= dns.flags.TC
+
+        allowed = 0
+        sent = 0
+        for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
             (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
-            sent = sent + len(response.to_wire())
+            sent = sent + 1
             if receivedQuery:
                 receivedQuery.id = query.id
                 self.assertEquals(query, receivedQuery)
-                self.assertEquals(response, receivedResponse)
-                allowed = allowed + len(response.to_wire())
+                self.assertEquals(receivedResponse, response)
+                allowed = allowed + 1
             else:
+                self.assertEquals(receivedResponse, truncatedResponse)
                 # the query has not reached the responder,
                 # let's clear the response queue
                 self.clearToResponderQueue()
-                # and stop right there, otherwise we might
-                # wait for so long that the dynblock is gone
-                # by the time we finished
-                break
 
-        # we might be already blocked, but we should have been able to send
-        # at least self._dynBlockBytesPerSecond bytes
-        print(allowed)
-        print(sent)
-        print(time.time())
-        self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
-
-        print(self.sendConsoleCommand("showDynBlocks()"))
-        print(self.sendConsoleCommand("grepq(\"\")"))
-        print(time.time())
+        # we might be already truncated, but we should have been able to send
+        # at least self._dynBlockQPS queries
+        self.assertGreaterEqual(allowed, self._dynBlockQPS)
 
         if allowed == sent:
             # wait for the maintenance function to run
-            print("Waiting for the maintenance function to run")
             time.sleep(2)
 
-        print(self.sendConsoleCommand("showDynBlocks()"))
-        print(self.sendConsoleCommand("grepq(\"\")"))
-        print(time.time())
-
-        # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
+        # we should now be 'truncated' for up to self._dynBlockDuration + self._dynBlockPeriod
         (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, None)
+        self.assertEquals(receivedResponse, truncatedResponse)
 
-        print(self.sendConsoleCommand("showDynBlocks()"))
-        print(self.sendConsoleCommand("grepq(\"\")"))
-        print(time.time())
+        # check over TCP, which should not be truncated
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
 
         # wait until we are not blocked anymore
         time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
 
-        print(self.sendConsoleCommand("showDynBlocks()"))
-        print(self.sendConsoleCommand("grepq(\"\")"))
-        print(time.time())
-
         # this one should succeed
         (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
         receivedQuery.id = query.id
         self.assertEquals(query, receivedQuery)
         self.assertEquals(response, receivedResponse)
 
-        # again, over TCP this time
         allowed = 0
         sent = 0
-        for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
+        # again, over TCP this time, we should never get truncated!
+        for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
             (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-            sent = sent + len(response.to_wire())
-            if receivedQuery:
-                receivedQuery.id = query.id
-                self.assertEquals(query, receivedQuery)
-                self.assertEquals(response, receivedResponse)
-                allowed = allowed + len(response.to_wire())
-            else:
-                # the query has not reached the responder,
-                # let's clear the response queue
-                self.clearToResponderQueue()
-                # and stop right there, otherwise we might
-                # wait for so long that the dynblock is gone
-                # by the time we finished
-                break
+            sent = sent + 1
+            self.assertEquals(query, receivedQuery)
+            self.assertEquals(receivedResponse, response)
+            receivedQuery.id = query.id
+            allowed = allowed + 1
 
-        # we might be already blocked, but we should have been able to send
-        # at least self._dynBlockBytesPerSecond bytes
-        self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
+        self.assertEquals(allowed, sent)
 
-        if allowed == sent:
-            # wait for the maintenance function to run
-            time.sleep(2)
+class TestDynBlockServFails(DynBlocksTest):
 
-        # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
-        (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
-        self.assertEquals(receivedResponse, None)
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    function maintenance()
+           addDynBlocks(exceedServFails(%d, %d), "Exceeded servfail rate", %d)
+    end
+    newServer{address="127.0.0.1:%s"}
+    """
 
-        # wait until we are not blocked anymore
-        time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
+    def testDynBlocksServFailRate(self):
+        """
+        Dyn Blocks: Server Failure Rate
+        """
+        name = 'servfailrate.dynblocks.tests.powerdns.com.'
+        self.doTestRCodeRate(name, dns.rcode.SERVFAIL)
 
-        # this one should succeed
-        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-        receivedQuery.id = query.id
-        self.assertEquals(query, receivedQuery)
-        self.assertEquals(response, receivedResponse)
+class TestDynBlockGroupServFails(DynBlocksTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    local dbr = dynBlockRulesGroup()
+    dbr:setRCodeRate(dnsdist.SERVFAIL, %d, %d, "Exceeded query rate", %d)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksServFailRate(self):
+        """
+        Dyn Blocks (group): Server Failure Rate
+        """
+        name = 'servfailrate.group.dynblocks.tests.powerdns.com.'
+        self.doTestRCodeRate(name, dns.rcode.SERVFAIL)
+
+class TestDynBlockResponseBytes(DynBlocksTest):
+
+    _dynBlockBytesPerSecond = 200
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _consoleKey = DNSDistTest.generateConsoleKey()
+    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+    _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockBytesPerSecond', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    setKey("%s")
+    controlSocket("127.0.0.1:%s")
+    function maintenance()
+           addDynBlocks(exceedRespByterate(%d, %d), "Exceeded response byterate", %d)
+    end
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksResponseByteRate(self):
+        """
+        Dyn Blocks: Response Byte Rate
+        """
+        name = 'responsebyterate.dynblocks.tests.powerdns.com.'
+        self.doTestResponseByteRate(name)
+
+class TestDynBlockGroupResponseBytes(DynBlocksTest):
+
+    _dynBlockBytesPerSecond = 200
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _consoleKey = DNSDistTest.generateConsoleKey()
+    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+    _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockBytesPerSecond', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    setKey("%s")
+    controlSocket("127.0.0.1:%s")
+    local dbr = dynBlockRulesGroup()
+    dbr:setResponseByteRate(%d, %d, "Exceeded query rate", %d)
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDynBlocksResponseByteRate(self):
+        """
+        Dyn Blocks (group) : Response Byte Rate
+        """
+        name = 'responsebyterate.group.dynblocks.tests.powerdns.com.'
+        self.doTestResponseByteRate(name)