
#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};

#ifndef DISABLE_DYNBLOCKS

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";

  g_rings.reset();
  g_rings.init();

  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);

  g_rings.reset();
  g_rings.init();

  /* 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);

  g_rings.reset();
  g_rings.init();

  /* 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.reset();
  g_rings.setCapacity(1000000, 1);
  g_rings.init();

  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);
  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);
  g_rings.reset();
  g_rings.init();

  /* 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);

  g_rings.reset();
  g_rings.init();

  /* 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);

  g_rings.reset();
  g_rings.init();

  /* 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);

  g_rings.reset();
  g_rings.init();

  /* 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);

  g_rings.reset();
  g_rings.init();

  {
    /* 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";

  g_rings.reset();
  /* 10M entries, only one shard */
  g_rings.setCapacity(10000000, 1);
  g_rings.init();

  {
    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()
#endif /* DISABLE_DYNBLOCKS */
