Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dnsdist: Add netmask-based {ex,in}clusions to DynBlockRulesGroup #6760

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions pdns/dnsdist-dynblocks.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand All @@ -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();
}
Expand Down Expand Up @@ -328,4 +380,5 @@ private:
std::map<uint16_t, DynBlockRule> d_qtypeRules;
DynBlockRule d_queryRateRule;
DynBlockRule d_respRateRule;
NetmaskGroup d_excludedSubnets;
};
21 changes: 21 additions & 0 deletions pdns/dnsdist-lua-inspection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
30 changes: 30 additions & 0 deletions pdns/dnsdist.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
10 changes: 10 additions & 0 deletions pdns/dnsdistdist/docs/guides/dynblocks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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")

22 changes: 22 additions & 0 deletions pdns/dnsdistdist/docs/reference/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------

Expand Down
63 changes: 63 additions & 0 deletions regression-tests.dnsdist/test_DynBlocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,3 +757,66 @@ def testDynBlocksResponseByteRate(self):
"""
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)