From: Remi Gacogne Date: Tue, 14 Nov 2023 15:42:14 +0000 (+0100) Subject: dnsdist: Add a cache-miss ratio dynamic block rule X-Git-Tag: dnsdist-1.9.0-alpha4~10^2~6 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=75290f43029aa2b82ec1cfbf77c4c42aa38ee691;p=thirdparty%2Fpdns.git dnsdist: Add a cache-miss ratio dynamic block rule This PR adds the `DynBlockRulesGroup:setCacheMissRatio()` method which can be used to throttle clients exceeding a ratio of cache misses for a minimum number of queries over a period of time. --- diff --git a/pdns/dnsdist-dynblocks.hh b/pdns/dnsdist-dynblocks.hh index 1e52c29a93..62c58c0528 100644 --- a/pdns/dnsdist-dynblocks.hh +++ b/pdns/dnsdist-dynblocks.hh @@ -80,6 +80,7 @@ private: uint64_t queries{0}; uint64_t responses{0}; uint64_t respBytes{0}; + uint64_t cacheMisses{0}; }; struct DynBlockRule @@ -238,7 +239,7 @@ private: double d_warningRatio{0.0}; }; - typedef std::unordered_map counts_t; + using counts_t = std::unordered_map; public: DynBlockRulesGroup() @@ -274,6 +275,11 @@ public: 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, boost::optional>(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) @@ -398,7 +404,7 @@ private: 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 @@ -420,6 +426,7 @@ private: DynBlockRule d_queryRateRule; DynBlockRule d_respRateRule; DynBlockRule d_suffixMatchRule; + DynBlockRatioRule d_respCacheMissRatioRule; NetmaskGroup d_excludedSubnets; SuffixMatchNode d_excludedDomains; smtVisitor_t d_smtVisitor; diff --git a/pdns/dnsdist-lua-inspection.cc b/pdns/dnsdist-lua-inspection.cc index 8ff1159ec5..dcd5496e99 100644 --- a/pdns/dnsdist-lua-inspection.cc +++ b/pdns/dnsdist-lua-inspection.cc @@ -138,11 +138,7 @@ static void statNodeRespRing(statvisitor_t visitor, uint64_t seconds) 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::max()) ? -1 : c.dh.rcode), c.size, hit, boost::none); } } @@ -880,6 +876,11 @@ void setupLuaInspection(LuaContext& luaCtx) group->setQTypeRate(qtype, rate, warningRate ? *warningRate : 0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None); } }); + luaCtx.registerFunction::*)(double, unsigned int, const std::string&, unsigned int, size_t, boost::optional, boost::optional)>("setCacheMissRatio", [](std::shared_ptr& group, double ratio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, size_t minimumNumberOfResponses, boost::optional action, boost::optional warningRatio) { + if (group) { + group->setCacheMissRatio(ratio, warningRatio ? *warningRatio : 0.0, seconds, reason, blockDuration, action ? *action : DNSAction::Action::None, minimumNumberOfResponses); + } + }); luaCtx.registerFunction::*)(uint8_t, uint8_t, uint8_t)>("setMasks", [](std::shared_ptr& group, uint8_t v4, uint8_t v6, uint8_t port) { if (group) { if (v4 > 32) { diff --git a/pdns/dnsdist-rings.cc b/pdns/dnsdist-rings.cc index 5485b33869..b97b44e645 100644 --- a/pdns/dnsdist-rings.cc +++ b/pdns/dnsdist-rings.cc @@ -214,3 +214,12 @@ size_t Rings::loadFromFile(const std::string& filepath, const struct timespec& n 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; +} diff --git a/pdns/dnsdist-rings.hh b/pdns/dnsdist-rings.hh index 755d769797..084044f058 100644 --- a/pdns/dnsdist-rings.hh +++ b/pdns/dnsdist-rings.hh @@ -62,6 +62,8 @@ struct Rings { uint16_t qtype; // outgoing protocol dnsdist::Protocol protocol; + + bool isACacheHit() const; }; struct Shard diff --git a/pdns/dnsdistdist/dnsdist-dynblocks.cc b/pdns/dnsdistdist/dnsdist-dynblocks.cc index 45eb5879bb..1225f3932e 100644 --- a/pdns/dnsdistdist/dnsdist-dynblocks.cc +++ b/pdns/dnsdistdist/dnsdist-dynblocks.cc @@ -54,6 +54,16 @@ void DynBlockRulesGroup::apply(const struct timespec& now) 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; @@ -412,6 +422,12 @@ void DynBlockRulesGroup::processResponseRules(counts_t& counts, StatNode& root, 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; @@ -445,22 +461,20 @@ void DynBlockRulesGroup::processResponseRules(counts_t& counts, StatNode& root, 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::max()) ? -1 : ringEntry.dh.rcode), ringEntry.size, hit, boost::none); } } diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 826c348f00..7f33304d66 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -1629,6 +1629,20 @@ faster than the existing rules. 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 `. (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 diff --git a/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc b/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc index 6694a6ac12..e01306528b 100644 --- a/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc +++ b/pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc @@ -831,7 +831,102 @@ BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_ResponseByteRate, TestFixture) { 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 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) {