From b718792fde493b6df50d454a09bee0b172bc5ea8 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Wed, 27 Jun 2018 14:52:37 +0200 Subject: [PATCH] dnsdist: Add netmask-based {ex,in}clusions to DynBlockRulesGroup --- pdns/dnsdist-dynblocks.hh | 53 ++++++++++++++++++ pdns/dnsdist-lua-inspection.cc | 21 ++++++++ pdns/dnsdist.hh | 30 +++++++++++ pdns/dnsdistdist/docs/guides/dynblocks.rst | 10 ++++ pdns/dnsdistdist/docs/reference/config.rst | 22 ++++++++ regression-tests.dnsdist/test_DynBlocks.py | 63 ++++++++++++++++++++++ 6 files changed, 199 insertions(+) diff --git a/pdns/dnsdist-dynblocks.hh b/pdns/dnsdist-dynblocks.hh index 5b896e81c..ca1aa0998 100644 --- a/pdns/dnsdist-dynblocks.hh +++ b/pdns/dnsdist-dynblocks.hh @@ -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 >& 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 d_qtypeRules; DynBlockRule d_queryRateRule; DynBlockRule d_respRateRule; + NetmaskGroup d_excludedSubnets; }; diff --git a/pdns/dnsdist-lua-inspection.cc b/pdns/dnsdist-lua-inspection.cc index 0c28d1e5a..985488bca 100644 --- a/pdns/dnsdist-lua-inspection.cc +++ b/pdns/dnsdist-lua-inspection.cc @@ -670,5 +670,26 @@ void setupLuaInspection() group->setQTypeRate(qtype, rate, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None); } }); + g_lua.registerFunction::*)(boost::variant>>)>("excludeRange", [](std::shared_ptr& group, boost::variant>> ranges) { + if (ranges.type() == typeid(std::vector>)) { + for (const auto& range : *boost::get>>(&ranges)) { + group->excludeRange(Netmask(range.second)); + } + } + else { + group->excludeRange(Netmask(*boost::get(&ranges))); + } + }); + g_lua.registerFunction::*)(boost::variant>>)>("includeRange", [](std::shared_ptr& group, boost::variant>> ranges) { + if (ranges.type() == typeid(std::vector>)) { + for (const auto& range : *boost::get>>(&ranges)) { + group->includeRange(Netmask(range.second)); + } + } + else { + group->includeRange(Netmask(*boost::get(&ranges))); + } + }); g_lua.registerFunction("apply", &DynBlockRulesGroup::apply); + g_lua.registerFunction("toString", &DynBlockRulesGroup::toString); } diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index cc30c09fd..29023b9e2 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -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() { diff --git a/pdns/dnsdistdist/docs/guides/dynblocks.rst b/pdns/dnsdistdist/docs/guides/dynblocks.rst index 6c9e58cd9..0fc9e4e78 100644 --- a/pdns/dnsdistdist/docs/guides/dynblocks.rst +++ b/pdns/dnsdistdist/docs/guides/dynblocks.rst @@ -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") + diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index ab836f8c6..17d764709 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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 --------------- diff --git a/regression-tests.dnsdist/test_DynBlocks.py b/regression-tests.dnsdist/test_DynBlocks.py index a7f883cd0..77e23e53c 100644 --- a/regression-tests.dnsdist/test_DynBlocks.py +++ b/regression-tests.dnsdist/test_DynBlocks.py @@ -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) -- 2.40.0