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<double>(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<double>(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<ComboAddress, Counts, ComboAddress::addressOnlyHash, ComboAddress::addressOnlyEqual> counts_t;
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];
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<NetmaskTree<DynBlock> > 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<DNSName> 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<DynBlock> 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)
{
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;
}
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<NetmaskTree<DynBlock> >& 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<DynBlock>& 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<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated, bool warning);
+ void addOrRefreshBlockSMT(SuffixMatchTree<DynBlock>& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated);
void addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
{
bool hasResponseRules() const
{
- return d_respRateRule.isEnabled() || !d_rcodeRules.empty();
+ return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty();
}
bool hasSuffixMatchRules() const
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<std::mutex> 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<std::mutex> 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<unsigned int>::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<uint8_t, DynBlockRule> d_rcodeRules;
+ std::map<uint8_t, DynBlockRatioRule> d_rcodeRatioRules;
std::map<uint16_t, DynBlockRule> d_qtypeRules;
DynBlockRule d_queryRateRule;
DynBlockRule d_respRateRule;
group->setRCodeRate(rcode, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
}
});
+ g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint8_t, double, unsigned int, const std::string&, unsigned int, size_t, boost::optional<DNSAction::Action>, boost::optional<double>)>("setRCodeRatio", [](std::shared_ptr<DynBlockRulesGroup>& group, uint8_t rcode, double ratio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, size_t minimumNumberOfResponses, boost::optional<DNSAction::Action> action, boost::optional<double> warningRatio) {
+ if (group) {
+ group->setRCodeRatio(rcode, ratio, warningRatio ? *warningRatio : 0.0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None, minimumNumberOfResponses);
+ }
+ });
g_lua.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint16_t, unsigned int, unsigned int, const std::string&, unsigned int, boost::optional<DNSAction::Action>, boost::optional<unsigned int>)>("setQTypeRate", [](std::shared_ptr<DynBlockRulesGroup>& group, uint16_t qtype, unsigned int rate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, boost::optional<DNSAction::Action> action, boost::optional<unsigned int> warningRate) {
if (group) {
group->setQTypeRate(qtype, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
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 \
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 \
--- /dev/null
+
+#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<NetmaskTree<DynBlock> > 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<DNSName> 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<DynBlock> 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<NetmaskTree<DynBlock> >& 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<DynBlock>& 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<std::mutex> 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<std::mutex> 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<unsigned int>::max()) ? -1 : c.dh.rcode), c.size, boost::none);
+ }
+ }
+ }
+}
:param int action: The action to take when the dynamic block matches, see :ref:`here <DNSAction>`. (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 <DNSAction>`. (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
}
+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<DynBlock> 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.");