]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add netmask-based {ex,in}clusions to DynBlockRulesGroup
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 27 Jun 2018 12:52:37 +0000 (14:52 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 27 Jun 2018 12:52:37 +0000 (14:52 +0200)
pdns/dnsdist-dynblocks.hh
pdns/dnsdist-lua-inspection.cc
pdns/dnsdist.hh
pdns/dnsdistdist/docs/guides/dynblocks.rst
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_DynBlocks.py

index 5b896e81ce08fa284faab70a6be187d3d04e6e24..ca1aa0998751e014244492bb6e1bde5f98feb43b 100644 (file)
@@ -79,6 +79,24 @@ private:
       return d_enabled;
     }
 
+    std::string toString() const
+    {
+      if (!isEnabled()) {
+        return "";
+      }
+
+      std::stringstream result;
+      if (d_action != DNSAction::Action::None) {
+        result << DNSAction::typeToString(d_action) << " ";
+      }
+      else {
+        result << "Apply the global DynBlock action ";
+      }
+      result << "for " << std::to_string(d_blockDuration) << " seconds when over " << std::to_string(d_rate) << " during the last " << d_seconds << " seconds, reason: '" << d_blockReason << "'";
+
+      return result.str();
+    }
+
     std::string d_blockReason;
     struct timespec d_cutOff;
     struct timespec d_minTime;
@@ -176,6 +194,35 @@ public:
     }
   }
 
+  void excludeRange(const Netmask& range)
+  {
+    d_excludedSubnets.addMask(range);
+  }
+
+  void includeRange(const Netmask& range)
+  {
+    d_excludedSubnets.addMask(range, false);
+  }
+
+  std::string toString() const
+  {
+    std::stringstream result;
+
+    result << "Query rate rule: " << d_queryRateRule.toString() << std::endl;
+    result << "Response rate rule: " << d_respRateRule.toString() << std::endl;
+    result << "RCode rules: " << std::endl;
+    for (const auto& rule : d_rcodeRules) {
+      result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl;
+    }
+    result << "QType rules: " << std::endl;
+    for (const auto& rule : d_qtypeRules) {
+      result << "- " << QType(rule.first).getName() << ": " << rule.second.toString() << std::endl;
+    }
+    result << "Excluded Subnets: " << d_excludedSubnets.toString() << std::endl;
+
+    return result.str();
+  }
+
 private:
   bool checkIfQueryTypeMatches(const Rings::Query& query)
   {
@@ -199,6 +246,11 @@ private:
 
   void addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
   {
+    if (d_excludedSubnets.match(requestor)) {
+      /* do not add a block for excluded subnets */
+      return;
+    }
+
     if (!blocks) {
       blocks = g_dynblockNMG.getCopy();
     }
@@ -328,4 +380,5 @@ private:
   std::map<uint16_t, DynBlockRule> d_qtypeRules;
   DynBlockRule d_queryRateRule;
   DynBlockRule d_respRateRule;
+  NetmaskGroup d_excludedSubnets;
 };
index 0c28d1e5a5a03f516c8224fe584d8a528c7f9c34..985488bcaa168137fe07e755a14c3515a78b6454 100644 (file)
@@ -670,5 +670,26 @@ void setupLuaInspection()
         group->setQTypeRate(qtype, rate, 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) {
+      if (ranges.type() == typeid(std::vector<std::pair<int, std::string>>)) {
+        for (const auto& range : *boost::get<std::vector<std::pair<int, std::string>>>(&ranges)) {
+          group->excludeRange(Netmask(range.second));
+        }
+      }
+      else {
+        group->excludeRange(Netmask(*boost::get<std::string>(&ranges)));
+      }
+    });
+  g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(boost::variant<std::string, std::vector<std::pair<int, std::string>>>)>("includeRange", [](std::shared_ptr<DynBlockRulesGroup>& group, boost::variant<std::string, std::vector<std::pair<int, std::string>>> ranges) {
+      if (ranges.type() == typeid(std::vector<std::pair<int, std::string>>)) {
+        for (const auto& range : *boost::get<std::vector<std::pair<int, std::string>>>(&ranges)) {
+          group->includeRange(Netmask(range.second));
+        }
+      }
+      else {
+        group->includeRange(Netmask(*boost::get<std::string>(&ranges)));
+      }
+    });
   g_lua.registerFunction("apply", &DynBlockRulesGroup::apply);
+  g_lua.registerFunction("toString", &DynBlockRulesGroup::toString);
 }
index cc30c09fd0173b8b5473f9e03debdca3b7f73f58..29023b9e2168fb986ad3b729c4b9e174d05f7666 100644 (file)
@@ -103,6 +103,36 @@ class DNSAction
 {
 public:
   enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, Truncate, ServFail, None};
+  static std::string typeToString(const Action& action)
+  {
+    switch(action) {
+    case Action::Drop:
+      return "Drop";
+    case Action::Nxdomain:
+      return "Send NXDomain";
+    case Action::Refused:
+      return "Send Refused";
+    case Action::Spoof:
+      return "Spoof an answer";
+    case Action::Allow:
+      return "Allow";
+    case Action::HeaderModify:
+      return "Modify the header";
+    case Action::Pool:
+      return "Route to a pool";
+    case Action::Delay:
+      return "Delay";
+    case Action::Truncate:
+      return "Truncate over UDP";
+    case Action::ServFail:
+      return "Send ServFail";
+    case Action::None:
+      return "Do nothing";
+    }
+
+    return "Unknown";
+  }
+
   virtual Action operator()(DNSQuestion*, string* ruleresult) const =0;
   virtual ~DNSAction()
   {
index 6c9e58cd9ef0df4970da6a529aec30fbe52e7cfe..0fc9e4e788edcd69e48cf50a1414929b0f225be3 100644 (file)
@@ -61,3 +61,13 @@ The new syntax would be:
 
 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.
+
+DynBlockRulesGroup also offers the ability to specify that some network ranges should be excluded from dynamic blocking:
+
+.. code-block:: lua
+
+  -- do not add dynamic blocks for hosts in the 192.0.2.0/24 and 2001:db8::/32 ranges
+  dbr:excludeRange({"192.0.2.0/24", "2001:db8::/32" })
+  -- except for 192.0.2.1
+  dbr:includeRange("192.0.2.1/32")
+
index ab836f8c618a4b7daf3f4fc410367d252d63b607..17d7647099f3bcc2eb2d76f57fc6a7b35890940f 100644 (file)
@@ -864,6 +864,28 @@ faster than the existing rules.
 
     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.
 
+  .. method:: DynBlockRulesGroup:excludeRange(netmasks)
+
+    .. versionadded:: 1.3.1
+
+    Exclude this range, or list of ranges, meaning that no dynamic block will ever be inserted for clients in that range. Default to empty, meaning rules are applied to all ranges. When used in combination with :meth:`DynBlockRulesGroup:includeRange`, the more specific entry wins.
+
+    :param int netmasks: A netmask, or list of netmasks, as strings, like for example "192.0.2.1/24"
+
+  .. method:: DynBlockRulesGroup:includeRange(netmasks)
+
+    .. versionadded:: 1.3.1
+
+    Include this range, or list of ranges, meaning that rules will be applied to this range. When used in combination with :meth:`DynBlockRulesGroup:excludeRange`, the more specific entry wins.
+
+    :param int netmasks: A netmask, or list of netmasks, as strings, like for example "192.0.2.1/24"
+
+  .. method:: DynBlockRulesGroup:toString()
+
+    .. versionadded:: 1.3.1
+
+    Return a string describing the rules and range exclusions of this DynBlockRulesGroup.
+
 Other functions
 ---------------
 
index a7f883cd0edabc9e63bc9cbe3c9ba7688b268dd5..77e23e53c4811967f6d9243858771dd18ab36a8b 100644 (file)
@@ -757,3 +757,66 @@ class TestDynBlockGroupResponseBytes(DynBlocksTest):
         """
         name = 'responsebyterate.group.dynblocks.tests.powerdns.com.'
         self.doTestResponseByteRate(name)
+
+class TestDynBlockGroupResponseBytes(DynBlocksTest):
+
+    _dynBlockQPS = 10
+    _dynBlockPeriod = 2
+    _dynBlockDuration = 5
+    _consoleKey = DNSDistTest.generateConsoleKey()
+    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+    _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+    _config_template = """
+    setKey("%s")
+    controlSocket("127.0.0.1:%s")
+    local dbr = dynBlockRulesGroup()
+    dbr:setQueryRate(%d, %d, "Exceeded query rate", %d)
+    dbr:excludeRange("127.0.0.1/32")
+
+    function maintenance()
+           dbr:apply()
+    end
+
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testExcluded(self):
+        """
+        Dyn Blocks (group) : Excluded from the dynamic block rules
+        """
+        name = 'excluded.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._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(response, receivedResponse)
+                allowed = allowed + 1
+            else:
+                # the query has not reached the responder,
+                # let's clear the response queue
+                self.clearToResponderQueue()
+
+        # we should have been blocked
+        self.assertEqual(allowed, sent)
+
+        # wait for the maintenance function to run
+        time.sleep(2)
+
+        # we should still not be blocked
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, receivedResponse)