diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:34:30 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:34:30 +0000 |
commit | 4fc2f55f761d71aae1f145d5aa94ba929cc39676 (patch) | |
tree | 5c1e1db3b46dd4edbe11f612d93cb94b96891ce3 /test-dnsdistdynblocks_hh.cc | |
parent | Initial commit. (diff) | |
download | dnsdist-4fc2f55f761d71aae1f145d5aa94ba929cc39676.tar.xz dnsdist-4fc2f55f761d71aae1f145d5aa94ba929cc39676.zip |
Adding upstream version 1.7.3.upstream/1.7.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | test-dnsdistdynblocks_hh.cc | 1482 |
1 files changed, 1482 insertions, 0 deletions
diff --git a/test-dnsdistdynblocks_hh.cc b/test-dnsdistdynblocks_hh.cc new file mode 100644 index 0000000..2e6a34e --- /dev/null +++ b/test-dnsdistdynblocks_hh.cc @@ -0,0 +1,1482 @@ + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#include <boost/test/unit_test.hpp> + +#include "dnsdist.hh" +#include "dnsdist-dynblocks.hh" +#include "dnsdist-rings.hh" + +Rings g_rings; +shared_ptr<BPFFilter> g_defaultBPFFilter{nullptr}; + +BOOST_AUTO_TEST_SUITE(dnsdistdynblocks_hh) + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QueryRate) { + 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"); + uint16_t qtype = QType::AAAA; + uint16_t size = 42; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP; + unsigned int responseTime = 0; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + /* we do not care about the response during that test, but we want to make sure + these do not interfere with the computation */ + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfQueries); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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); + } + + { + /* clear the rings and dynamic blocks */ + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0U); + g_dynblockNMG.setState(emptyNMG); + + /* Insert 100 qps from a given client in the last 10s + this should trigger the rule */ + size_t numberOfQueries = 100; + + for (size_t timeIdx = 0; timeIdx < numberOfSeconds; timeIdx++) { + for (size_t idx = 0; idx < numberOfQueries; idx++) { + struct timespec when = now; + when.tv_sec -= (9 - timeIdx); + g_rings.insertQuery(when, requestor1, qname, qtype, size, dh, protocol); + g_rings.insertResponse(when, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries * numberOfSeconds); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + + /* now we clean up the dynamic blocks, simulating an admin removing the block */ + g_dynblockNMG.setState(emptyNMG); + /* we apply the rules again, but as if we were 20s in the future. + Since we have a time windows of 10s nothing should be added, + regardless of the number of queries + */ + struct timespec later = now; + later.tv_sec += 20; + dbrg.apply(later); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + + /* just in case */ + g_dynblockNMG.setState(emptyNMG); + + /* we apply the rules again, this tile as if we were 5s in the future. + Since we have a time windows of 10s, and 100 qps over 5s then 0 qps over 5s + is more than 50qps over 10s, the block should be added + */ + later = now; + later.tv_sec += 5; + dbrg.apply(later); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + + /* clean up */ + g_dynblockNMG.setState(emptyNMG); + + /* we apply the rules again, this tile as if we were 6s in the future. + Since we have a time windows of 10s, and 100 qps over 4s then 0 qps over 6s + is LESS than 50qps over 10s, the block should NOT be added + */ + later = now; + later.tv_sec += 6; + dbrg.apply(later); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + } +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QueryRate_RangeV6) { + /* Check that we correctly group IPv6 addresses from the same /64 subnet into the same + dynamic block entry, if instructed to do so */ + dnsheader dh; + memset(&dh, 0, sizeof(dh)); + DNSName qname("rings.powerdns.com."); + ComboAddress requestor1("2001:db8::1"); + ComboAddress backend("2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff"); + uint16_t qtype = QType::AAAA; + uint16_t size = 42; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP; + unsigned int responseTime = 0; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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); + dbrg.setMasks(32, 64, 0); + + /* 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + /* we do not care about the response during that test, but we want to make sure + these do not interfere with the computation */ + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfQueries); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(requestor1, 128, 16)) == nullptr); + } + + { + /* insert just above 50 qps from several clients in the same /64 IPv6 range 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + ComboAddress requestor("2001:db8::" + std::to_string(idx)); + g_rings.insertQuery(now, requestor, qname, qtype, size, dh, protocol); + g_rings.insertResponse(now, requestor, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + + { + /* beginning of the range should be blocked */ + const auto& block = g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(requestor1, 128, 16))->second; + BOOST_CHECK_EQUAL(block.reason, reason); + BOOST_CHECK_EQUAL(static_cast<size_t>(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); + } + + { + /* end of the range should be blocked as well */ + ComboAddress end("2001:0db8:0000:0000:ffff:ffff:ffff:ffff"); + const auto& block = g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(end, 128, 16))->second; + BOOST_CHECK_EQUAL(block.reason, reason); + BOOST_CHECK_EQUAL(static_cast<size_t>(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); + } + + { + /* outside of the range should NOT */ + ComboAddress out("2001:0db8:0000:0001::0"); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(out, 128, 16)) == nullptr); + } + } +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QueryRate_V4Ports) { + /* Check that we correctly split IPv4 addresses based on port ranges, when instructed to do so */ + dnsheader dh; + memset(&dh, 0, sizeof(dh)); + DNSName qname("rings.powerdns.com."); + ComboAddress requestor1("192.0.2.1:42"); + ComboAddress backend("192.0.2.254"); + uint16_t qtype = QType::AAAA; + uint16_t size = 42; + unsigned int responseTime = 0; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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); + /* split v4 by ports using a /2 (0 - 16383, 16384 - 32767, 32768 - 49151, 49152 - 65535) */ + dbrg.setMasks(32, 128, 2); + + /* 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + /* we do not care about the response during that test, but we want to make sure + these do not interfere with the computation */ + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfQueries); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(requestor1, 128, 16)) == nullptr); + } + + { + /* insert just above 50 qps from several clients in the same IPv4 port range 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + ComboAddress requestor("192.0.2.1:" + std::to_string(idx)); + g_rings.insertQuery(now, requestor, qname, qtype, size, dh, protocol); + g_rings.insertResponse(now, requestor, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + + { + /* beginning of the port range should be blocked */ + const auto& block = g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(ComboAddress("192.0.2.1:0"), 32, 16))->second; + BOOST_CHECK_EQUAL(block.reason, reason); + BOOST_CHECK_EQUAL(static_cast<size_t>(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); + } + + { + /* end of the range should be blocked as well */ + const auto& block = g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(ComboAddress("192.0.2.1:16383"), 32, 16))->second; + BOOST_CHECK_EQUAL(block.reason, reason); + BOOST_CHECK_EQUAL(static_cast<size_t>(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); + } + + { + /* outside of the range should not */ + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(ComboAddress("192.0.2.1:16384"), 32, 16)) == nullptr); + } + + /* we (again) insert just above 50 qps from several clients the same IPv4 port range, this should update the block which will + check by looking at the blocked counter */ + { + auto block = g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(ComboAddress("192.0.2.1:0"), 32, 16)); + BOOST_REQUIRE(block != nullptr); + BOOST_CHECK_EQUAL(block->second.blocks, 0U); + block->second.blocks = 42U; + } + + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0U); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + ComboAddress requestor("192.0.2.1:" + std::to_string(idx)); + g_rings.insertQuery(now, requestor, qname, qtype, size, dh, protocol); + g_rings.insertResponse(now, requestor, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + { + /* previous address/port should still be blocked */ + auto block = g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(ComboAddress("192.0.2.1:0"), 32, 16)); + BOOST_REQUIRE(block != nullptr); + BOOST_CHECK_EQUAL(block->second.blocks, 42U); + } + + /* but not a different one */ + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(AddressAndPortRange(ComboAddress("192.0.2.1:16384"), 32, 16)) == nullptr); + + } +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QueryRate_responses) { + /* check that the responses are not accounted as queries when a + rcode rate rule is defined (sounds very specific but actually happened) */ + 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"); + uint16_t qtype = QType::AAAA; + uint16_t size = 42; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP; + unsigned int responseTime = 0; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> emptyNMG; + + /* 100k entries, one shard */ + g_rings.setCapacity(1000000, 1); + + size_t numberOfSeconds = 10; + size_t blockDuration = 60; + const auto action = DNSAction::Action::Drop; + const std::string reason = "Exceeded query rate"; + + /* 100k entries, one shard */ + g_rings.setCapacity(1000000, 1); + + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + + /* block above 50 qps for numberOfSeconds seconds, no warning */ + dbrg.setQueryRate(50, 0, numberOfSeconds, reason, blockDuration, action); + dbrg.setRCodeRate(RCode::ServFail, 50, 40, 5, "Exceeded ServFail rate", 60, DNSAction::Action::Drop); + + { + /* insert 45 qps (including responses) from a given client for the last 100s + this should not trigger the rule */ + size_t numberOfQueries = 45; + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t timeIdx = 0; timeIdx < 100; timeIdx++) { + struct timespec when = now; + when.tv_sec -= (99 - timeIdx); + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(when, requestor1, qname, qtype, size, dh, protocol); + /* we do not care about the response during that test, but we want to make sure + these do not interfere with the computation */ + g_rings.insertResponse(when, requestor1, qname, qtype, responseTime, size, dh, backend, outgoingProtocol); + } + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfQueries * 100); + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries * 100); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + BOOST_CHECK(g_dynblockNMG.getLocal()->lookup(requestor1) == nullptr); + } +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_QTypeRate) { + dnsheader dh; + memset(&dh, 0, sizeof(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; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, QType::A, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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); + } + +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRate) { + 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"); + 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; + + 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(), 0U); + 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, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + 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, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + 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, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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); + } + +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_RCodeRatio) { + 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"); + 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 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(), 0U); + 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, outgoingProtocol); + } + dh.rcode = RCode::NoError; + for (size_t idx = 0; idx < 80; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, 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 just 50 FormErrs and nothing else, from a given client in the last 10s */ + g_rings.clear(); + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), 0U); + 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, 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); + } + + { + /* 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(), 0U); + 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, outgoingProtocol); + } + dh.rcode = RCode::NoError; + for (size_t idx = 0; idx < 79; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, 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 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(), 0U); + 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, outgoingProtocol); + } + dh.rcode = RCode::NoError; + for (size_t idx = 0; idx < 39; idx++) { + g_rings.insertResponse(now, requestor1, qname, qtype, responseTime, size, dh, backend, 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_AUTO_TEST_CASE(test_DynBlockRulesGroup_ResponseByteRate) { + 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"); + uint16_t qtype = QType::AAAA; + uint16_t size = 100; + dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP; + unsigned int responseTime = 100 * 1000; /* 100ms */ + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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(), 0U); + 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, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + 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, outgoingProtocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfResponseEntries(), numberOfResponses); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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); + } + +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_Warning) { + dnsheader dh; + memset(&dh, 0, sizeof(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; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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, 0U); + 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(), 0U); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(block.until.tv_sec), now.tv_sec + blockDuration); + BOOST_CHECK(block.domain.empty()); + BOOST_CHECK(block.action == action); + /* this should have been preserved */ + BOOST_CHECK_EQUAL(block.blocks, 1U); + 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(), 0U); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(block.until.tv_sec), now.tv_sec + blockDuration); + BOOST_CHECK(block.domain.empty()); + BOOST_CHECK(block.action == action); + /* this should have been preserved */ + BOOST_CHECK_EQUAL(block.blocks, 2U); + 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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); + } + } +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesGroup_Ranges) { + dnsheader dh; + memset(&dh, 0, sizeof(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; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> 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(), 0U); + g_dynblockNMG.setState(emptyNMG); + + for (size_t idx = 0; idx < numberOfQueries; idx++) { + g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol); + g_rings.insertQuery(now, requestor2, qname, qtype, size, dh, protocol); + } + BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), numberOfQueries * 2); + + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1U); + 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(static_cast<size_t>(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); + } + +} + +BOOST_AUTO_TEST_CASE(test_DynBlockRulesMetricsCache_GetTopN) { + dnsheader dh; + memset(&dh, 0, sizeof(dh)); + DNSName qname("rings.powerdns.com."); + uint16_t qtype = QType::AAAA; + uint16_t size = 42; + dnsdist::Protocol protocol = dnsdist::Protocol::DoUDP; + dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP; + struct timespec now; + gettime(&now); + NetmaskTree<DynBlock, AddressAndPortRange> emptyNMG; + SuffixMatchTree<DynBlock> emptySMT; + + size_t numberOfSeconds = 10; + size_t blockDuration = 60; + const auto action = DNSAction::Action::Drop; + const std::string reason = "Exceeded query rate"; + + /* 10M entries, only one shard */ + g_rings.setCapacity(10000000, 1); + + { + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + g_rings.clear(); + g_dynblockNMG.setState(emptyNMG); + + /* block above 0 qps for numberOfSeconds seconds, no warning */ + dbrg.setQueryRate(0, 0, numberOfSeconds, reason, blockDuration, action); + + /* insert one fake query from 255 clients: + */ + for (size_t idx = 0; idx < 256; idx++) { + const ComboAddress requestor("192.0.2." + std::to_string(idx)); + g_rings.insertQuery(now, requestor, qname, qtype, size, dh, protocol); + } + + /* we apply the rules, all clients should be blocked */ + dbrg.apply(now); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 256U); + + for (size_t idx = 0; idx < 256; idx++) { + const ComboAddress requestor("192.0.2." + std::to_string(idx)); + const auto& block = g_dynblockNMG.getLocal()->lookup(requestor)->second; + /* simulate that: + - .1 does 1 query + ... + - .255 does 255 queries + */ + block.blocks = idx; + } + + /* now we ask for the top 20 offenders for each reason */ + StopWatch sw; + sw.start(); + auto top = DynBlockMaintenance::getTopNetmasks(20); + BOOST_REQUIRE_EQUAL(top.size(), 1U); + auto offenders = top.at(reason); + BOOST_REQUIRE_EQUAL(offenders.size(), 20U); + auto it = offenders.begin(); + for (size_t idx = 236; idx < 256; idx++) { + BOOST_CHECK_EQUAL(it->first.toString(), Netmask(ComboAddress("192.0.2." + std::to_string(idx))).toString()); + BOOST_CHECK_EQUAL(it->second, idx); + ++it; + } + + struct timespec expired = now; + expired.tv_sec += blockDuration + 1; + DynBlockMaintenance::purgeExpired(expired); + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + } + + { + /* === reset everything for SMT === */ + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + g_rings.clear(); + g_dynblockNMG.setState(emptyNMG); + g_dynblockSMT.setState(emptySMT); + + dbrg.setSuffixMatchRule(numberOfSeconds, reason, blockDuration, action, [](const StatNode& node, const StatNode::Stat& self, const StatNode::Stat& children) { + if (self.queries > 0) { + return std::tuple<bool, boost::optional<std::string>>(true, boost::none); + } + return std::tuple<bool, boost::optional<std::string>>(false, boost::none); + }); + + /* insert one fake response for 255 DNS names */ + const ComboAddress requestor("192.0.2.1"); + for (size_t idx = 0; idx < 256; idx++) { + g_rings.insertResponse(now, requestor, DNSName(std::to_string(idx)) + qname, qtype, 1000 /*usec*/, size, dh, requestor /* backend, technically, but we don't care */, outgoingProtocol); + } + + /* we apply the rules, all suffixes should be blocked */ + dbrg.apply(now); + + for (size_t idx = 0; idx < 256; idx++) { + const DNSName name(DNSName(std::to_string(idx)) + qname); + const auto* block = g_dynblockSMT.getLocal()->lookup(name); + BOOST_REQUIRE(block != nullptr); + /* simulate that: + - 1.rings.powerdns.com. got 1 query + ... + - 255. does 255 queries + */ + block->blocks = idx; + } + + /* now we ask for the top 20 offenders for each reason */ + StopWatch sw; + sw.start(); + auto top = DynBlockMaintenance::getTopSuffixes(20); + BOOST_REQUIRE_EQUAL(top.size(), 1U); + auto suffixes = top.at(reason); + BOOST_REQUIRE_EQUAL(suffixes.size(), 20U); + auto it = suffixes.begin(); + for (size_t idx = 236; idx < 256; idx++) { + BOOST_CHECK_EQUAL(it->first, (DNSName(std::to_string(idx)) + qname)); + BOOST_CHECK_EQUAL(it->second, idx); + ++it; + } + + struct timespec expired = now; + expired.tv_sec += blockDuration + 1; + DynBlockMaintenance::purgeExpired(expired); + BOOST_CHECK(g_dynblockSMT.getLocal()->getNodes().empty()); + } + + { + /* === reset everything for SMT, this time we will check that we can override the 'reason' via the visitor function === */ + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + g_rings.clear(); + g_dynblockNMG.setState(emptyNMG); + g_dynblockSMT.setState(emptySMT); + + dbrg.setSuffixMatchRule(numberOfSeconds, reason, blockDuration, action, [](const StatNode& node, const StatNode::Stat& self, const StatNode::Stat& children) { + if (self.queries > 0) { + return std::tuple<bool, boost::optional<std::string>>(true, "blocked for a different reason"); + } + return std::tuple<bool, boost::optional<std::string>>(false, boost::none); + }); + + /* insert one fake response for 255 DNS names */ + const ComboAddress requestor("192.0.2.1"); + for (size_t idx = 0; idx < 256; idx++) { + g_rings.insertResponse(now, requestor, DNSName(std::to_string(idx)) + qname, qtype, 1000 /*usec*/, size, dh, requestor /* backend, technically, but we don't care */, dnsdist::Protocol::DoUDP); + } + + /* we apply the rules, all suffixes should be blocked */ + dbrg.apply(now); + + for (size_t idx = 0; idx < 256; idx++) { + const DNSName name(DNSName(std::to_string(idx)) + qname); + const auto* block = g_dynblockSMT.getLocal()->lookup(name); + BOOST_REQUIRE(block != nullptr); + /* simulate that: + - 1.rings.powerdns.com. got 1 query + ... + - 255. does 255 queries + */ + block->blocks = idx; + } + + /* now we ask for the top 20 offenders for each reason */ + StopWatch sw; + sw.start(); + auto top = DynBlockMaintenance::getTopSuffixes(20); + BOOST_REQUIRE_EQUAL(top.size(), 1U); + auto suffixes = top.at("blocked for a different reason"); + BOOST_REQUIRE_EQUAL(suffixes.size(), 20U); + auto it = suffixes.begin(); + for (size_t idx = 236; idx < 256; idx++) { + BOOST_CHECK_EQUAL(it->first, (DNSName(std::to_string(idx)) + qname)); + BOOST_CHECK_EQUAL(it->second, idx); + ++it; + } + + struct timespec expired = now; + expired.tv_sec += blockDuration + 1; + DynBlockMaintenance::purgeExpired(expired); + BOOST_CHECK(g_dynblockSMT.getLocal()->getNodes().empty()); + } + +#ifdef BENCH_DYNBLOCKS + { + /* now insert 1M names */ + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + g_rings.clear(); + g_dynblockNMG.setState(emptyNMG); + g_dynblockSMT.setState(emptySMT); + + dbrg.setSuffixMatchRule(numberOfSeconds, reason, blockDuration, action, [](const StatNode& node, const StatNode::Stat& self, const StatNode::Stat& children) { + if (self.queries > 0) { + return std::tuple<bool, boost::optional<std::string>>(true, boost::none); + } + return std::tuple<bool, boost::optional<std::string>>(false, boost::none); + }); + + bool done = false; + const ComboAddress requestor("192.0.2.1"); + for (size_t idxB = 0; !done && idxB < 256; idxB++) { + for (size_t idxC = 0; !done && idxC < 256; idxC++) { + for (size_t idxD = 0; !done && idxD < 256; idxD++) { + const DNSName victim(std::to_string(idxB) + "." + std::to_string(idxC) + "." + std::to_string(idxD) + qname.toString()); + g_rings.insertResponse(now, requestor, victim, qtype, 1000 /*usec*/, size, dh, requestor /* backend, technically, but we don't care */, outgoingProtocol); + if (g_rings.getNumberOfQueryEntries() == 1000000) { + done = true; + break; + } + } + } + } + + /* we apply the rules, all suffixes should be blocked */ + StopWatch sw; + sw.start(); + dbrg.apply(now); + cerr<<"added 1000000 entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl; + + sw.start(); + auto top = DynBlockMaintenance::getTopSuffixes(20); + cerr<<"scanned 1000000 entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl; + BOOST_CHECK_EQUAL(top.at(reason).size(), 20U); + BOOST_CHECK_EQUAL(top.size(), 1U); + + struct timespec expired = now; + expired.tv_sec += blockDuration + 1; + sw.start(); + DynBlockMaintenance::purgeExpired(expired); + cerr<<"removed 1000000 entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl; + BOOST_CHECK_EQUAL(g_dynblockSMT.getLocal()->getNodes().size(), 0U); + } +#endif + +#ifdef BENCH_DYNBLOCKS + { + /* now insert 1M clients */ + DynBlockRulesGroup dbrg; + dbrg.setQuiet(true); + g_rings.clear(); + g_dynblockNMG.setState(emptyNMG); + g_dynblockSMT.setState(emptySMT); + dbrg.setQueryRate(0, 0, numberOfSeconds, reason, blockDuration, action); + + bool done = false; + for (size_t idxB = 0; !done && idxB < 256; idxB++) { + for (size_t idxC = 0; !done && idxC < 256; idxC++) { + for (size_t idxD = 0; !done && idxD < 256; idxD++) { + const ComboAddress requestor("192." + std::to_string(idxB) + "." + std::to_string(idxC) + "." + std::to_string(idxD)); + g_rings.insertQuery(now, requestor, qname, qtype, size, dh, protocol); + if (g_rings.getNumberOfQueryEntries() == 1000000) { + done = true; + break; + } + } + } + } + + /* we apply the rules, all clients should be blocked */ + StopWatch sw; + sw.start(); + dbrg.apply(now); + cerr<<"added "<<g_dynblockNMG.getLocal()->size()<<" entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl; + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 1000000U); + + sw.start(); + auto top = DynBlockMaintenance::getTopNetmasks(20); + cerr<<"scanned "<<g_dynblockNMG.getLocal()->size()<<" entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl; + + struct timespec expired = now; + expired.tv_sec += blockDuration + 1; + sw.start(); + DynBlockMaintenance::purgeExpired(expired); + cerr<<"removed 1000000 entries in "<<std::to_string(sw.udiff()/1024)<<"ms"<<endl; + BOOST_CHECK_EQUAL(g_dynblockNMG.getLocal()->size(), 0U); + } +#endif +} + +BOOST_AUTO_TEST_CASE(test_NetmaskTree) { + NetmaskTree<int, AddressAndPortRange> nmt; + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + nmt.insert(AddressAndPortRange(ComboAddress("130.161.252.0"), 24, 0)).second = 0; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + nmt.insert(AddressAndPortRange(ComboAddress("130.161.0.0"), 16, 0)).second = 1; + BOOST_CHECK_EQUAL(nmt.size(), 2U); + nmt.insert(AddressAndPortRange(ComboAddress("130.0.0.0"), 8, 0)).second = 2; + BOOST_CHECK_EQUAL(nmt.size(), 3U); + + BOOST_CHECK(nmt.lookup(ComboAddress("213.244.168.210")) == nullptr); + auto found = nmt.lookup(ComboAddress("130.161.252.29")); + BOOST_REQUIRE(found); + BOOST_CHECK_EQUAL(found->second, 0); + found = nmt.lookup(ComboAddress("130.161.180.1")); + BOOST_CHECK(found); + BOOST_CHECK_EQUAL(found->second, 1); + + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("130.255.255.255"))->second, 2); + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("130.161.252.255"))->second, 0); + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("130.161.253.255"))->second, 1); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("130.255.255.255"), 32, 16))->second, 2); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.255"), 32, 16))->second, 0); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.253.255"), 32, 16))->second, 1); + + found = nmt.lookup(ComboAddress("130.145.180.1")); + BOOST_CHECK(found); + BOOST_CHECK_EQUAL(found->second, 2); + + nmt.insert(AddressAndPortRange(ComboAddress("0.0.0.0"), 0, 0)).second = 3; + BOOST_CHECK_EQUAL(nmt.size(), 4U); + nmt.insert(AddressAndPortRange(ComboAddress("0.0.0.0"), 7, 0)).second = 4; + BOOST_CHECK_EQUAL(nmt.size(), 5U); + nmt.insert(AddressAndPortRange(ComboAddress("0.0.0.0"), 15, 0)).second = 5; + BOOST_CHECK_EQUAL(nmt.size(), 6U); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("0.0.0.0"), 0, 0))->second, 3); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("0.0.0.0"), 7, 0))->second, 4); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("0.0.0.0"), 15, 0))->second, 5); + BOOST_CHECK_EQUAL(nmt.lookup(AddressAndPortRange(ComboAddress("0.0.0.0"), 32, 0))->second, 5); + + nmt.clear(); + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + BOOST_CHECK(!nmt.lookup(ComboAddress("130.161.180.1"))); + + nmt.insert(AddressAndPortRange(ComboAddress("::1"), 128, 0)).second = 1; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + nmt.insert(AddressAndPortRange(ComboAddress("::"), 0, 0)).second = 0; + BOOST_CHECK_EQUAL(nmt.size(), 2U); + nmt.insert(AddressAndPortRange(ComboAddress("fe80::"), 16, 0)).second = 2; + BOOST_CHECK_EQUAL(nmt.size(), 3U); + BOOST_CHECK(nmt.lookup(ComboAddress("130.161.253.255")) == nullptr); + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("::2"))->second, 0); + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("::ffff"))->second, 0); + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("::1"))->second, 1); + BOOST_CHECK_EQUAL(nmt.lookup(ComboAddress("fe80::1"))->second, 2); +} + +BOOST_AUTO_TEST_CASE(test_NetmaskTreePort) { + { + /* exact port matching */ + NetmaskTree<int, AddressAndPortRange> nmt; + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + nmt.insert(AddressAndPortRange(ComboAddress("130.161.252.42:65534"), 32, 16)).second = 0; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("213.244.168.210"), 32, 16)) == nullptr); + + auto found = nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:65534"), 32, 16)); + BOOST_CHECK(found != nullptr); + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:65533"), 32, 16)) == nullptr); + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:65535"), 32, 16)) == nullptr); + } + + { + /* /15 port matching */ + NetmaskTree<int, AddressAndPortRange> nmt; + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + nmt.insert(AddressAndPortRange(ComboAddress("130.161.252.42:0"), 32, 15)).second = 0; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("213.244.168.210"), 32, 16)) == nullptr); + + auto found = nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:0"), 32, 16)); + BOOST_CHECK(found != nullptr); + + found = nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:1"), 32, 16)); + BOOST_CHECK(found != nullptr); + + /* everything else should be a miss */ + for (size_t idx = 2; idx <= 65535; idx++) { + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:" + std::to_string(idx)), 32, 16)) == nullptr); + } + + nmt.clear(); + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + nmt.insert(AddressAndPortRange(ComboAddress("130.161.252.42:65535"), 32, 15)).second = 0; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("213.244.168.210"), 32, 16)) == nullptr); + + /* everything else should be a miss */ + for (size_t idx = 0; idx <= 65533; idx++) { + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:" + std::to_string(idx)), 32, 16)) == nullptr); + } + found = nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:65534"), 32, 16)); + BOOST_CHECK(found != nullptr); + found = nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:65535"), 32, 16)); + BOOST_CHECK(found != nullptr); + } + + { + /* /1 port matching */ + NetmaskTree<int, AddressAndPortRange> nmt; + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + nmt.insert(AddressAndPortRange(ComboAddress("130.161.252.42:0"), 32, 1)).second = 0; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("213.244.168.210"), 32, 16)) == nullptr); + + for (size_t idx = 0; idx <= 32767; idx++) { + auto found = nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:" + std::to_string(idx)), 32, 16)); + BOOST_CHECK(found != nullptr); + } + + /* everything else should be a miss */ + for (size_t idx = 32768; idx <= 65535; idx++) { + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("130.161.252.42:" + std::to_string(idx)), 32, 16)) == nullptr); + } + } + + { + /* Check that the port matching does not apply to IPv6, where it does not make sense */ + + /* /1 port matching */ + NetmaskTree<int, AddressAndPortRange> nmt; + BOOST_CHECK_EQUAL(nmt.empty(), true); + BOOST_CHECK_EQUAL(nmt.size(), 0U); + nmt.insert(AddressAndPortRange(ComboAddress("[2001:db8::1]:0"), 128, 1)).second = 0; + BOOST_CHECK_EQUAL(nmt.empty(), false); + BOOST_CHECK_EQUAL(nmt.size(), 1U); + + /* different IP, no match */ + BOOST_CHECK(nmt.lookup(AddressAndPortRange(ComboAddress("[2001:db8::2]:0"), 128, 16)) == nullptr); + + /* all ports should match */ + for (size_t idx = 1; idx <= 65535; idx++) { + auto found = nmt.lookup(AddressAndPortRange(ComboAddress("[2001:db8::1]:" + std::to_string(idx)), 128, 16)); + BOOST_CHECK(found != nullptr); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() |