From: Remi Gacogne Date: Tue, 3 Sep 2019 15:53:10 +0000 (+0200) Subject: dnsdist: Implement dynamic blocking on ratio of rcode/total responses X-Git-Tag: auth-4.3.0-beta1~34^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=838c2f00cc9ae56944bd1c845387badf01a6b7da;p=thirdparty%2Fpdns.git dnsdist: Implement dynamic blocking on ratio of rcode/total responses --- diff --git a/pdns/dnsdist-dynblocks.hh b/pdns/dnsdist-dynblocks.hh index 118e70e8af..31bc9e7964 100644 --- a/pdns/dnsdist-dynblocks.hh +++ b/pdns/dnsdist-dynblocks.hh @@ -149,6 +149,67 @@ private: bool d_enabled{false}; }; + struct DynBlockRatioRule: DynBlockRule + { + DynBlockRatioRule(): DynBlockRule() + { + } + + DynBlockRatioRule(const std::string& blockReason, unsigned int blockDuration, double ratio, double warningRatio, unsigned int seconds, DNSAction::Action action, size_t minimumNumberOfResponses): DynBlockRule(blockReason, blockDuration, 0, 0, seconds, action), d_minimumNumberOfResponses(minimumNumberOfResponses), d_ratio(ratio), d_warningRatio(warningRatio) + { + } + + bool ratioExceeded(unsigned int total, unsigned int count) const + { + if (!d_enabled) { + return false; + } + + if (total < d_minimumNumberOfResponses) { + return false; + } + + double allowed = d_ratio * static_cast(total); + return (count > allowed); + } + + bool warningRatioExceeded(unsigned int total, unsigned int count) const + { + if (d_warningRate == 0) { + return false; + } + + if (total < d_minimumNumberOfResponses) { + return false; + } + + double allowed = d_warningRatio * static_cast(total); + return (count > allowed); + } + + 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_ratio) << " ratio during the last " << d_seconds << " seconds, reason: '" << d_blockReason << "'"; + + return result.str(); + } + + size_t d_minimumNumberOfResponses{0}; + double d_ratio{0.0}; + double d_warningRatio{0.0}; + }; + typedef std::unordered_map counts_t; public: @@ -173,6 +234,12 @@ public: entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action); } + void setRCodeRatio(uint8_t rcode, double ratio, double warningRatio, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action, size_t minimumNumberOfResponses) + { + auto& entry = d_rcodeRatioRules[rcode]; + entry = DynBlockRatioRule(reason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses); + } + 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]; @@ -201,122 +268,7 @@ public: apply(now); } - void apply(const struct timespec& now) - { - counts_t counts; - StatNode statNodeRoot; - - size_t entriesCount = 0; - if (hasQueryRules()) { - entriesCount += g_rings.getNumberOfQueryEntries(); - } - if (hasResponseRules()) { - entriesCount += g_rings.getNumberOfResponseEntries(); - } - counts.reserve(entriesCount); - - processQueryRules(counts, now); - processResponseRules(counts, statNodeRoot, now); - - if (counts.empty() && statNodeRoot.empty()) { - return; - } - - boost::optional > blocks; - bool updated = false; - - for (const auto& entry : counts) { - 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.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& 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& 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; - } - } - } - } - - if (updated && blocks) { - g_dynblockNMG.setState(std::move(*blocks)); - } - - if (!statNodeRoot.empty()) { - StatNode::Stat node; - std::unordered_set namesToBlock; - statNodeRoot.visit([this,&namesToBlock](const StatNode* node_, const StatNode::Stat& self, const StatNode::Stat& children) { - bool block = false; - - if (d_smtVisitorFFI) { - dnsdist_ffi_stat_node_t tmp(*node_, self, children); - block = d_smtVisitorFFI(&tmp); - } - else { - block = d_smtVisitor(*node_, self, children); - } - - if (block) { - namesToBlock.insert(DNSName(node_->fullname)); - } - }, - node); - - if (!namesToBlock.empty()) { - updated = false; - SuffixMatchTree smtBlocks = g_dynblockSMT.getCopy(); - for (const auto& name : namesToBlock) { - addOrRefreshBlockSMT(smtBlocks, now, name, d_suffixMatchRule, updated); - } - if (updated) { - g_dynblockSMT.setState(std::move(smtBlocks)); - } - } - } - } + void apply(const struct timespec& now); void excludeRange(const Netmask& range) { @@ -344,6 +296,9 @@ public: for (const auto& rule : d_rcodeRules) { result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl; } + for (const auto& rule : d_rcodeRatioRules) { + 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; @@ -360,116 +315,11 @@ public: } 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 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 */ - return; - } - - if (!blocks) { - blocks = g_dynblockNMG.getCopy(); - } - struct timespec until = now; - until.tv_sec += rule.d_blockDuration; - unsigned int count = 0; - const auto& got = blocks->lookup(Netmask(requestor)); - bool expired = false; - bool wasWarning = false; - - if (got) { - 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 - count = got->second.blocks; - } - else { - expired = true; - } - } - - DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action}; - db.blocks = count; - 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 addOrRefreshBlockSMT(SuffixMatchTree& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated) - { - if (d_excludedDomains.check(name)) { - /* do not add a block for excluded domains */ - return; - } - - struct timespec until = now; - until.tv_sec += rule.d_blockDuration; - unsigned int count = 0; - const auto& got = blocks.lookup(name); - bool expired = false; - DNSName domain(name.makeLowerCase()); - - if (got) { - if (until < got->until) { - // had a longer policy - return; - } - - if (now < got->until) { - // only inherit count on fresh query we are extending - count = got->blocks; - } - else { - expired = true; - } - } - - DynBlock db{rule.d_blockReason, until, domain, rule.d_action}; - db.blocks = count; - - if (!d_beQuiet && (!got || expired)) { - warnlog("Inserting dynamic block for %s for %d seconds: %s", domain, rule.d_blockDuration, rule.d_blockReason); - } - blocks.add(domain, db); - updated = true; - } + bool checkIfQueryTypeMatches(const Rings::Query& query); + bool checkIfResponseCodeMatches(const Rings::Response& response); + void addOrRefreshBlock(boost::optional >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated, bool warning); + void addOrRefreshBlockSMT(SuffixMatchTree& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated); void addBlock(boost::optional >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated) { @@ -488,7 +338,7 @@ private: bool hasResponseRules() const { - return d_respRateRule.isEnabled() || !d_rcodeRules.empty(); + return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty(); } bool hasSuffixMatchRules() const @@ -501,89 +351,11 @@ private: return hasQueryRules() || hasResponseRules(); } - void processQueryRules(counts_t& counts, const struct timespec& now) - { - if (!hasQueryRules()) { - return; - } - - 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; - } - - for (const auto& shard : g_rings.d_shards) { - std::lock_guard rl(shard->queryLock); - for(const auto& c : shard->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, StatNode& root, const struct timespec& now) - { - if (!hasResponseRules() && !hasSuffixMatchRules()) { - return; - } - - d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now; - d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds; - - d_suffixMatchRule.d_cutOff = d_suffixMatchRule.d_minTime = now; - d_suffixMatchRule.d_cutOff.tv_sec -= d_suffixMatchRule.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; - } - - for (const auto& shard : g_rings.d_shards) { - std::lock_guard rl(shard->respLock); - for(const auto& c : shard->respRing) { - if (now < c.when) { - continue; - } - - bool respRateMatches = d_respRateRule.matches(c.when); - bool suffixMatchRuleMatches = d_suffixMatchRule.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]++; - } - } - - if (suffixMatchRuleMatches) { - root.submit(c.name, ((c.dh.rcode == 0 && c.usec == std::numeric_limits::max()) ? -1 : c.dh.rcode), c.size, boost::none); - } - } - } - } + void processQueryRules(counts_t& counts, const struct timespec& now); + void processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now); std::map d_rcodeRules; + std::map d_rcodeRatioRules; std::map d_qtypeRules; DynBlockRule d_queryRateRule; DynBlockRule d_respRateRule; diff --git a/pdns/dnsdist-lua-inspection.cc b/pdns/dnsdist-lua-inspection.cc index c1f0a1cb80..0d37ff320c 100644 --- a/pdns/dnsdist-lua-inspection.cc +++ b/pdns/dnsdist-lua-inspection.cc @@ -741,6 +741,11 @@ void setupLuaInspection() group->setRCodeRate(rcode, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None); } }); + g_lua.registerFunction::*)(uint8_t, double, unsigned int, const std::string&, unsigned int, size_t, boost::optional, boost::optional)>("setRCodeRatio", [](std::shared_ptr& group, uint8_t rcode, double ratio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, size_t minimumNumberOfResponses, boost::optional action, boost::optional warningRatio) { + if (group) { + group->setRCodeRatio(rcode, ratio, warningRatio ? *warningRatio : 0.0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None, minimumNumberOfResponses); + } + }); 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, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None); diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 163d441380..145d6b0713 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -122,7 +122,7 @@ dnsdist_SOURCES = \ dnsdist-carbon.cc \ dnsdist-console.cc dnsdist-console.hh \ dnsdist-dnscrypt.cc \ - dnsdist-dynblocks.hh \ + dnsdist-dynblocks.cc dnsdist-dynblocks.hh \ dnsdist-ecs.cc dnsdist-ecs.hh \ dnsdist-idstate.cc \ dnsdist-kvs.hh dnsdist-kvs.cc \ @@ -204,6 +204,7 @@ testrunner_SOURCES = \ circular_buffer.hh \ dnsdist.hh \ dnsdist-cache.cc dnsdist-cache.hh \ + dnsdist-dynblocks.cc dnsdist-dynblocks.hh \ dnsdist-ecs.cc dnsdist-ecs.hh \ dnsdist-kvs.cc dnsdist-kvs.hh \ dnsdist-rings.hh \ diff --git a/pdns/dnsdistdist/dnsdist-dynblocks.cc b/pdns/dnsdistdist/dnsdist-dynblocks.cc new file mode 100644 index 0000000000..808d56b259 --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-dynblocks.cc @@ -0,0 +1,340 @@ + +#include "dnsdist.hh" +#include "dnsdist-dynblocks.hh" + +void DynBlockRulesGroup::apply(const struct timespec& now) +{ + counts_t counts; + StatNode statNodeRoot; + + size_t entriesCount = 0; + if (hasQueryRules()) { + entriesCount += g_rings.getNumberOfQueryEntries(); + } + if (hasResponseRules()) { + entriesCount += g_rings.getNumberOfResponseEntries(); + } + counts.reserve(entriesCount); + + processQueryRules(counts, now); + processResponseRules(counts, statNodeRoot, now); + + if (counts.empty() && statNodeRoot.empty()) { + return; + } + + boost::optional > blocks; + bool updated = false; + + for (const auto& entry : counts) { + 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.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& 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& 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; + } + } + } + + for (const auto& pair : d_rcodeRatioRules) { + const auto rcode = pair.first; + + const auto& rcodeIt = counters.d_rcodeCounts.find(rcode); + if (rcodeIt != counters.d_rcodeCounts.cend()) { + if (pair.second.warningRatioExceeded(counters.queries, rcodeIt->second)) { + handleWarning(blocks, now, requestor, pair.second, updated); + } + + if (pair.second.ratioExceeded(counters.queries, rcodeIt->second)) { + addBlock(blocks, now, requestor, pair.second, updated); + break; + } + } + } + } + + if (updated && blocks) { + g_dynblockNMG.setState(std::move(*blocks)); + } + + if (!statNodeRoot.empty()) { + StatNode::Stat node; + std::unordered_set namesToBlock; + statNodeRoot.visit([this,&namesToBlock](const StatNode* node_, const StatNode::Stat& self, const StatNode::Stat& children) { + bool block = false; + + if (d_smtVisitorFFI) { + dnsdist_ffi_stat_node_t tmp(*node_, self, children); + block = d_smtVisitorFFI(&tmp); + } + else { + block = d_smtVisitor(*node_, self, children); + } + + if (block) { + namesToBlock.insert(DNSName(node_->fullname)); + } + }, + node); + + if (!namesToBlock.empty()) { + updated = false; + SuffixMatchTree smtBlocks = g_dynblockSMT.getCopy(); + for (const auto& name : namesToBlock) { + addOrRefreshBlockSMT(smtBlocks, now, name, d_suffixMatchRule, updated); + } + if (updated) { + g_dynblockSMT.setState(std::move(smtBlocks)); + } + } + } +} + +bool DynBlockRulesGroup::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 DynBlockRulesGroup::checkIfResponseCodeMatches(const Rings::Response& response) +{ + auto rule = d_rcodeRules.find(response.dh.rcode); + if (rule != d_rcodeRules.end() && rule->second.matches(response.when)) { + return true; + } + + auto ratio = d_rcodeRatioRules.find(response.dh.rcode); + if (ratio != d_rcodeRatioRules.end() && ratio->second.matches(response.when)) { + return true; + } + + return false; +} + +void DynBlockRulesGroup::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 */ + return; + } + + if (!blocks) { + blocks = g_dynblockNMG.getCopy(); + } + struct timespec until = now; + until.tv_sec += rule.d_blockDuration; + unsigned int count = 0; + const auto& got = blocks->lookup(Netmask(requestor)); + bool expired = false; + bool wasWarning = false; + + if (got) { + 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 + count = got->second.blocks; + } + else { + expired = true; + } + } + + DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action}; + db.blocks = count; + 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 DynBlockRulesGroup::addOrRefreshBlockSMT(SuffixMatchTree& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated) +{ + if (d_excludedDomains.check(name)) { + /* do not add a block for excluded domains */ + return; + } + + struct timespec until = now; + until.tv_sec += rule.d_blockDuration; + unsigned int count = 0; + const auto& got = blocks.lookup(name); + bool expired = false; + DNSName domain(name.makeLowerCase()); + + if (got) { + if (until < got->until) { + // had a longer policy + return; + } + + if (now < got->until) { + // only inherit count on fresh query we are extending + count = got->blocks; + } + else { + expired = true; + } + } + + DynBlock db{rule.d_blockReason, until, domain, rule.d_action}; + db.blocks = count; + + if (!d_beQuiet && (!got || expired)) { + warnlog("Inserting dynamic block for %s for %d seconds: %s", domain, rule.d_blockDuration, rule.d_blockReason); + } + blocks.add(domain, db); + updated = true; +} + +void DynBlockRulesGroup::processQueryRules(counts_t& counts, const struct timespec& now) +{ + if (!hasQueryRules()) { + return; + } + + 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; + } + + for (const auto& shard : g_rings.d_shards) { + std::lock_guard rl(shard->queryLock); + for(const auto& c : shard->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 DynBlockRulesGroup::processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now) +{ + if (!hasResponseRules() && !hasSuffixMatchRules()) { + return; + } + + d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now; + d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds; + + d_suffixMatchRule.d_cutOff = d_suffixMatchRule.d_minTime = now; + d_suffixMatchRule.d_cutOff.tv_sec -= d_suffixMatchRule.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; + } + + for (auto& rule : d_rcodeRatioRules) { + rule.second.d_cutOff = rule.second.d_minTime = now; + rule.second.d_cutOff.tv_sec -= rule.second.d_seconds; + } + + for (const auto& shard : g_rings.d_shards) { + std::lock_guard rl(shard->respLock); + for(const auto& c : shard->respRing) { + if (now < c.when) { + continue; + } + + auto& entry = counts[c.requestor]; + ++entry.queries; + bool respRateMatches = d_respRateRule.matches(c.when); + bool suffixMatchRuleMatches = d_suffixMatchRule.matches(c.when); + bool rcodeRuleMatches = checkIfResponseCodeMatches(c); + + if (respRateMatches || rcodeRuleMatches) { + if (respRateMatches) { + entry.respBytes += c.size; + } + if (rcodeRuleMatches) { + ++entry.d_rcodeCounts[c.dh.rcode]; + } + } + + if (suffixMatchRuleMatches) { + root.submit(c.name, ((c.dh.rcode == 0 && c.usec == std::numeric_limits::max()) ? -1 : c.dh.rcode), c.size, boost::none); + } + } + } +} diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index f842a3c20a..2f58b7ff8d 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -1020,6 +1020,21 @@ faster than the existing rules. :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:setRCodeRatio(rcode, ratio, seconds, reason, blockingTime, minimumNumberOfResponses [, action [, warningRate]]) + + .. versionadded:: 1.5.0 + + Adds a rate-limiting rule for the ratio of responses of code ``rcode`` over the total number of responses for a given client. + + :param int rcode: The response code + :param int ratio: Ratio of responses per second of the given rcode over the total number of responses for this client to exceed + :param int seconds: Number of seconds the ratio 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 minimumNumberOfResponses: How many total responses is required for this rule to apply + :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 warningRatio: If set to a non-zero value, the ratio above which a warning message will be issued and a no-op block inserted + .. method:: DynBlockRulesGroup:setQTypeRate(qtype, rate, seconds, reason, blockingTime [, action [, warningRate]]) .. versionchanged:: 1.3.3 diff --git a/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc b/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc index f62e8a223b..3216d4692c 100644 --- a/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc +++ b/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc @@ -259,6 +259,123 @@ BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRate) { } +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRatio) { + 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 ratio"; + const uint16_t rcode = RCode::ServFail; + + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + + /* block above 0.2 ServFail/Total ratio over numberOfSeconds seconds, no warning, minimum number of queries should be at least 51 */ + dbrg.setRCodeRatio(rcode, 0.2, 0, numberOfSeconds, reason, blockDuration, action, 51); + + { + /* insert 20 ServFail and 80 NoErrors from a given client in the last 10s + this should not trigger the rule */ + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0); + g_dynblockNMG.setState(emptyNMG); + + dh.rcode = rcode; + for (size_t idx = 0; idx < 20; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + dh.rcode = RCode::NoError; + for (size_t idx = 0; idx < 80; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr); + } + + { + /* insert just 50 FormErrs and nothing else, from a given client in the last 10s */ + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0); + g_dynblockNMG.setState(emptyNMG); + + dh.rcode = RCode::FormErr; + for (size_t idx = 0; idx < 50; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 50); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr); + } + + { + /* insert 21 ServFails and 79 NoErrors from a given client in the last 10s + this should trigger the rule this time */ + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0); + g_dynblockNMG.setState(emptyNMG); + + dh.rcode = rcode; + for (size_t idx = 0; idx < 21; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + dh.rcode = RCode::NoError; + for (size_t idx = 0; idx < 79; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100); + + 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); + } + + { + /* insert 11 ServFails and 39 NoErrors from a given client in the last 10s + this should NOT trigger the rule since we don't have more than 50 queries */ + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0); + g_dynblockNMG.setState(emptyNMG); + + dh.rcode = rcode; + for (size_t idx = 0; idx < 11; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + dh.rcode = RCode::NoError; + for (size_t idx = 0; idx < 39; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 50); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr); + } +} + BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_ResponseByteRate) { dnsheader dh; DNSName qname("rings.powerdns.com.");