From: Remi Gacogne Date: Tue, 25 Sep 2018 09:49:39 +0000 (+0200) Subject: dnsdist: Add warning rates to dynBlockRulesGroup rules X-Git-Tag: dnsdist-1.3.3~39^2~1 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=1d3ba13328d6f0d5baffe8a3a62d5849e4820315;p=pdns dnsdist: Add warning rates to dynBlockRulesGroup rules --- diff --git a/pdns/dnsdist-dynblocks.hh b/pdns/dnsdist-dynblocks.hh index ca1aa0998..091ffa553 100644 --- a/pdns/dnsdist-dynblocks.hh +++ b/pdns/dnsdist-dynblocks.hh @@ -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 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 > 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 >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated) + void addOrRefreshBlock(boost::optional >& 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 >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated) + { + addOrRefreshBlock(blocks, now, requestor, rule, updated, false); + } + + void handleWarning(boost::optional >& 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}; }; diff --git a/pdns/dnsdist-lua-inspection.cc b/pdns/dnsdist-lua-inspection.cc index 985488bca..c64bf563f 100644 --- a/pdns/dnsdist-lua-inspection.cc +++ b/pdns/dnsdist-lua-inspection.cc @@ -650,24 +650,24 @@ void setupLuaInspection() /* DynBlockRulesGroup */ g_lua.writeFunction("dynBlockRulesGroup", []() { return std::make_shared(); }); - g_lua.registerFunction::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional)>("setQueryRate", [](std::shared_ptr& group, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action) { + g_lua.registerFunction::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional, boost::optional)>("setQueryRate", [](std::shared_ptr& group, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action, boost::optional 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::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional)>("setResponseByteRate", [](std::shared_ptr& group, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action) { + g_lua.registerFunction::*)(unsigned int, unsigned int, const std::string&, unsigned int, boost::optional, boost::optional)>("setResponseByteRate", [](std::shared_ptr& group, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action, boost::optional 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::*)(uint8_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional)>("setRCodeRate", [](std::shared_ptr& group, uint8_t rcode, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action) { + g_lua.registerFunction::*)(uint8_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional, boost::optional)>("setRCodeRate", [](std::shared_ptr& group, uint8_t rcode, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action, boost::optional 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::*)(uint16_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional)>("setQTypeRate", [](std::shared_ptr& group, uint16_t qtype, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action) { + g_lua.registerFunction::*)(uint16_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional, boost::optional)>("setQTypeRate", [](std::shared_ptr& group, uint16_t qtype, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional action, boost::optional 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::*)(boost::variant>>)>("excludeRange", [](std::shared_ptr& group, boost::variant>> ranges) { @@ -690,6 +690,8 @@ void setupLuaInspection() group->includeRange(Netmask(*boost::get(&ranges))); } }); - g_lua.registerFunction("apply", &DynBlockRulesGroup::apply); + g_lua.registerFunction::*)()>("apply", [](std::shared_ptr& group) { + group->apply(); + }); g_lua.registerFunction("toString", &DynBlockRulesGroup::toString); } diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index ce7e6dafe..49c5cac61 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -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& 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(); } }); diff --git a/pdns/dnsdist-rings.hh b/pdns/dnsdist-rings.hh index 0ff9a1ec9..a773ed6e9 100644 --- a/pdns/dnsdist-rings.hh +++ b/pdns/dnsdist-rings.hh @@ -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 wl(shard->queryLock); + shard->queryRing.clear(); + } + { + std::lock_guard 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 > d_shards; std::atomic d_blockingQueryInserts; std::atomic d_blockingResponseInserts; diff --git a/pdns/dnsdist-web.cc b/pdns/dnsdist-web.cc index e2e756c8e..d2fa817c0 100644 --- a/pdns/dnsdist-web.cc +++ b/pdns/dnsdist-web.cc @@ -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}); } diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index c66e1f217..1c9e401e2 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -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 blocks; + bool warning; }; extern GlobalStateHolder> g_dynblockNMG; diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index b6e1d39ee..900ddaea6 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 \ diff --git a/pdns/dnsdistdist/docs/guides/dynblocks.rst b/pdns/dnsdistdist/docs/guides/dynblocks.rst index 084b6a062..fd718430e 100644 --- a/pdns/dnsdistdist/docs/guides/dynblocks.rst +++ b/pdns/dnsdistdist/docs/guides/dynblocks.rst @@ -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) + diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 0889ebe58..f4247f8cd 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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 `. (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 `. (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 `. (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 `. (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 index 000000000..68a89e3cc --- /dev/null +++ b/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc @@ -0,0 +1,546 @@ + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#include + +#include "dnsdist.hh" +#include "dnsdist-dynblocks.hh" +#include "dnsdist-rings.hh" + +Rings g_rings; +GlobalStateHolder> 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 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 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 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 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 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 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() diff --git a/regression-tests.dnsdist/test_DynBlocks.py b/regression-tests.dnsdist/test_DynBlocks.py index 5cd01e46a..0012ba80a 100644 --- a/regression-tests.dnsdist/test_DynBlocks.py +++ b/regression-tests.dnsdist/test_DynBlocks.py @@ -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)