uint64_t queries{0};
uint64_t responses{0};
uint64_t respBytes{0};
+ uint64_t cacheMisses{0};
};
struct DynBlockRule
double d_warningRatio{0.0};
};
- typedef std::unordered_map<AddressAndPortRange, Counts, AddressAndPortRange::hash> counts_t;
+ using counts_t = std::unordered_map<AddressAndPortRange, Counts, AddressAndPortRange::hash>;
public:
DynBlockRulesGroup()
entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
}
+ void setCacheMissRatio(double ratio, double warningRatio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, size_t minimumNumberOfResponses)
+ {
+ d_respCacheMissRatioRule = DynBlockRatioRule(reason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses);
+ }
+
using smtVisitor_t = std::function<std::tuple<bool, boost::optional<std::string>, boost::optional<int>>(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)>;
void setSuffixMatchRule(unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, smtVisitor_t visitor)
bool hasResponseRules() const
{
- return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty();
+ return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty() || d_respCacheMissRatioRule.isEnabled();
}
bool hasSuffixMatchRules() const
DynBlockRule d_queryRateRule;
DynBlockRule d_respRateRule;
DynBlockRule d_suffixMatchRule;
+ DynBlockRatioRule d_respCacheMissRatioRule;
NetmaskGroup d_excludedSubnets;
SuffixMatchNode d_excludedDomains;
smtVisitor_t d_smtVisitor;
continue;
}
- bool hit = c.ds.sin4.sin_family == 0;
- if (!hit && c.ds.isIPv4() && c.ds.sin4.sin_addr.s_addr == 0 && c.ds.sin4.sin_port == 0) {
- hit = true;
- }
-
+ const bool hit = c.isACacheHit();
root.submit(c.name, ((c.dh.rcode == 0 && c.usec == std::numeric_limits<unsigned int>::max()) ? -1 : c.dh.rcode), c.size, hit, boost::none);
}
}
group->setQTypeRate(qtype, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None);
}
});
+ luaCtx.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(double, unsigned int, const std::string&, unsigned int, size_t, boost::optional<DNSAction::Action>, boost::optional<double>)>("setCacheMissRatio", [](std::shared_ptr<DynBlockRulesGroup>& group, 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->setCacheMissRatio(ratio, warningRatio ? *warningRatio : 0.0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None, minimumNumberOfResponses);
+ }
+ });
luaCtx.registerFunction<void(std::shared_ptr<DynBlockRulesGroup>::*)(uint8_t, uint8_t, uint8_t)>("setMasks", [](std::shared_ptr<DynBlockRulesGroup>& group, uint8_t v4, uint8_t v6, uint8_t port) {
if (group) {
if (v4 > 32) {
return inserted;
}
+
+bool Rings::Response::isACacheHit() const
+{
+ bool hit = ds.sin4.sin_family == 0;
+ if (!hit && ds.isIPv4() && ds.sin4.sin_addr.s_addr == 0 && ds.sin4.sin_port == 0) {
+ hit = true;
+ }
+ return hit;
+}
uint16_t qtype;
// outgoing protocol
dnsdist::Protocol protocol;
+
+ bool isACacheHit() const;
};
struct Shard
continue;
}
+ if (d_respCacheMissRatioRule.warningRatioExceeded(counters.responses, counters.cacheMisses)) {
+ handleWarning(blocks, now, requestor, d_respCacheMissRatioRule, updated);
+ continue;
+ }
+
+ if (d_respCacheMissRatioRule.ratioExceeded(counters.responses, counters.cacheMisses)) {
+ addBlock(blocks, now, requestor, d_respCacheMissRatioRule, updated);
+ continue;
+ }
+
for (const auto& pair : d_qtypeRules) {
const auto qtype = pair.first;
responseCutOff = d_suffixMatchRule.d_cutOff;
}
+ d_respCacheMissRatioRule.d_cutOff = d_respCacheMissRatioRule.d_minTime = now;
+ d_respCacheMissRatioRule.d_cutOff.tv_sec -= d_respCacheMissRatioRule.d_seconds;
+ if (d_respCacheMissRatioRule.d_cutOff < responseCutOff) {
+ responseCutOff = d_respCacheMissRatioRule.d_cutOff;
+ }
+
for (auto& rule : d_rcodeRules) {
rule.second.d_cutOff = rule.second.d_minTime = now;
rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
bool respRateMatches = d_respRateRule.matches(ringEntry.when);
bool suffixMatchRuleMatches = d_suffixMatchRule.matches(ringEntry.when);
bool rcodeRuleMatches = checkIfResponseCodeMatches(ringEntry);
+ bool respCacheMissRatioRuleMatches = d_respCacheMissRatioRule.matches(ringEntry.when);
- if (respRateMatches || rcodeRuleMatches) {
- if (respRateMatches) {
- entry.respBytes += ringEntry.size;
- }
- if (rcodeRuleMatches) {
- ++entry.d_rcodeCounts[ringEntry.dh.rcode];
- }
+ if (respRateMatches) {
+ entry.respBytes += ringEntry.size;
+ }
+ if (rcodeRuleMatches) {
+ ++entry.d_rcodeCounts[ringEntry.dh.rcode];
+ }
+ if (respCacheMissRatioRuleMatches && !ringEntry.isACacheHit()) {
+ ++entry.cacheMisses;
}
if (suffixMatchRuleMatches) {
- bool hit = ringEntry.ds.sin4.sin_family == 0;
- if (!hit && ringEntry.ds.isIPv4() && ringEntry.ds.sin4.sin_addr.s_addr == 0 && ringEntry.ds.sin4.sin_port == 0) {
- hit = true;
- }
-
+ const bool hit = ringEntry.isACacheHit();
root.submit(ringEntry.name, ((ringEntry.dh.rcode == 0 && ringEntry.usec == std::numeric_limits<unsigned int>::max()) ? -1 : ringEntry.dh.rcode), ringEntry.size, hit, boost::none);
}
}
Represents a group of dynamic block rules.
+ .. method:: DynBlockRulesGroup:setCacheMissRatio(ratio, seconds, reason, blockingTime, minimumNumberOfResponses [, action [, warningRate]])
+
+ .. versionadded:: 1.9.0
+
+ Adds a rate-limiting rule for the ratio of cache-misses responses over the total number of responses for a given client.
+
+ :param int ratio: Ratio of cache-miss responses per second 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:`DNSAction <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:setMasks(v4, v6, port)
.. versionadded:: 1.7.0
BOOST_CHECK_EQUAL(block.blocks, 0U);
BOOST_CHECK_EQUAL(block.warning, false);
}
+}
+
+BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_CacheMissRatio, TestFixture) {
+ dnsheader dh;
+ memset(&dh, 0, sizeof(dh));
+ DNSName qname("rings.powerdns.com.");
+ ComboAddress requestor1("192.0.2.1");
+ ComboAddress requestor2("192.0.2.2");
+ ComboAddress backend("192.0.2.42");
+ ComboAddress cacheHit;
+ uint16_t qtype = QType::AAAA;
+ uint16_t size = 42;
+ dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP;
+ unsigned int responseTime = 100 * 1000; /* 100ms */
+ struct timespec now;
+ gettime(&now);
+ NetmaskTree<DynBlock, AddressAndPortRange> emptyNMG;
+
+ time_t numberOfSeconds = 10;
+ unsigned int blockDuration = 60;
+ const auto action = DNSAction::Action::Drop;
+ const std::string reason = "Exceeded cache-miss ratio";
+
+ DynBlockRulesGroup dbrg;
+ dbrg.setQuiet(true);
+
+ /* block above 0.5 Cache-Miss/Total ratio over numberOfSeconds seconds, no warning, minimum number of queries should be at least 51 */
+ dbrg.setCacheMissRatio(0.5, 0, numberOfSeconds, reason, blockDuration, action, 51);
+
+ {
+ /* insert 50 cache misses and 50 cache hits from a given client in the last 10s
+ this should not trigger the rule */
+ g_rings.clear();
+ BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0U);
+ g_dynblockNMG.setState(emptyNMG);
+
+ for (size_t idx = 0; idx < 20; idx++) {
+ g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol);
+ }
+ for (size_t idx = 0; idx < 80; idx++) {
+ g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, cacheHit, outgoingProtocol);
+ }
+ BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100U);
+
+ dbrg.apply(now);
+ BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U);
+ BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+ }
+
+ {
+ /* insert 51 cache misses and 49 hits from a given client in the last 10s
+ this should trigger the rule this time */
+ g_rings.clear();
+ BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0U);
+ g_dynblockNMG.setState(emptyNMG);
+
+ for (size_t idx = 0; idx < 51; idx++) {
+ g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol);
+ }
+ for (size_t idx = 0; idx < 49; idx++) {
+ g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, cacheHit, outgoingProtocol);
+ }
+ BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 100U);
+
+ dbrg.apply(now);
+ BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U);
+ BOOST_REQUIRE(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, 0U);
+ BOOST_CHECK_EQUAL(block.warning, false);
+ }
+
+ {
+ /* insert 40 misses and 10 hits 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(), 0U);
+ g_dynblockNMG.setState(emptyNMG);
+
+ for (size_t idx = 0; idx < 40; idx++) {
+ g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol);
+ }
+ for (size_t idx = 0; idx < 10; idx++) {
+ g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, cacheHit, outgoingProtocol);
+ }
+ BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 50U);
+ dbrg.apply(now);
+ BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U);
+ BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr);
+ }
}
BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_Warning, TestFixture) {