]> granicus.if.org Git - pdns/commitdiff
dnsdist: Use LRU to clean the MaxQPSIPRule's store
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 11 Jun 2018 12:22:25 +0000 (14:22 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 22 Jun 2018 08:10:41 +0000 (10:10 +0200)
This makes it possible to remove expired entries from the store
without having to scan more than a fraction of it. Entries are
ordered by their last usage, with least recently used ones at
the front, so we can stop scanning as soon as we find an entry
still valid. Even so, we will only consider a fraction of the
store during each pass to keep the cleaning fast, even with a
large store.

pdns/dnsdist-lua-rules.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/cachecleaner.hh [new symlink]
pdns/dnsdistdist/dnsdist-rules.hh
pdns/dnsdistdist/docs/rules-actions.rst
pdns/dnsdistdist/test-dnsdistrules_cc.cc [new file with mode: 0644]

index 6f1509af7e14476b3aac5538f72259ae0c8fc760..edfbafcf713dbb183e6364516fa61bcd3b360f0d 100644 (file)
@@ -20,8 +20,8 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 #include "dnsdist.hh"
-#include "dnsdist-rules.hh"
 #include "dnsdist-lua.hh"
+#include "dnsdist-rules.hh"
 
 std::shared_ptr<DNSRule> makeRule(const luadnsrule_t& var)
 {
@@ -263,8 +263,8 @@ void setupLuaRules()
         });
     });
 
-  g_lua.writeFunction("MaxQPSIPRule", [](unsigned int qps, boost::optional<int> ipv4trunc, boost::optional<int> ipv6trunc, boost::optional<int> burst, boost::optional<unsigned int> expiration, boost::optional<unsigned int> cleanupDelay) {
-      return std::shared_ptr<DNSRule>(new MaxQPSIPRule(qps, burst.get_value_or(qps), ipv4trunc.get_value_or(32), ipv6trunc.get_value_or(64), expiration.get_value_or(300), cleanupDelay.get_value_or(60)));
+  g_lua.writeFunction("MaxQPSIPRule", [](unsigned int qps, boost::optional<int> ipv4trunc, boost::optional<int> ipv6trunc, boost::optional<int> burst, boost::optional<unsigned int> expiration, boost::optional<unsigned int> cleanupDelay, boost::optional<unsigned int> scanFraction) {
+      return std::shared_ptr<DNSRule>(new MaxQPSIPRule(qps, burst.get_value_or(qps), ipv4trunc.get_value_or(32), ipv6trunc.get_value_or(64), expiration.get_value_or(300), cleanupDelay.get_value_or(60), scanFraction.get_value_or(10)));
     });
 
   g_lua.writeFunction("MaxQPSRule", [](unsigned int qps, boost::optional<int> burst) {
index 982901414e0ffad2025cf8d04bb5d38f90f4bf1a..c7c563c7daef2b7ed41911f8bb30aca7246c30c8 100644 (file)
@@ -86,6 +86,7 @@ dnsdist_SOURCES = \
        ascii.hh \
        base64.hh \
        bpf-filter.cc bpf-filter.hh \
+       cachecleaner.hh \
        dns.cc dns.hh \
        dnscrypt.cc dnscrypt.hh \
        dnsdist.cc dnsdist.hh \
@@ -219,8 +220,10 @@ testrunner_SOURCES = \
        test-dnsdist_cc.cc \
        test-dnsdistpacketcache_cc.cc \
        test-dnsdistrings_cc.cc \
+       test-dnsdistrules_cc.cc \
        test-dnsparser_cc.cc \
        test-iputils_hh.cc \
+       cachecleaner.hh \
        dnsdist.hh \
        dnsdist-cache.cc dnsdist-cache.hh \
        dnsdist-ecs.cc dnsdist-ecs.hh \
diff --git a/pdns/dnsdistdist/cachecleaner.hh b/pdns/dnsdistdist/cachecleaner.hh
new file mode 120000 (symlink)
index 0000000..8b8f773
--- /dev/null
@@ -0,0 +1 @@
+../cachecleaner.hh
\ No newline at end of file
index ee91edacc84e58da32526e06e4b63ea45891c36a..de5eb920163fc610715966bf458ef020655b5b2e 100644 (file)
 class MaxQPSIPRule : public DNSRule
 {
 public:
-  MaxQPSIPRule(unsigned int qps, unsigned int burst, unsigned int ipv4trunc=32, unsigned int ipv6trunc=64, unsigned int expiration=300, unsigned int cleanupDelay=60):
-    d_qps(qps), d_burst(burst), d_ipv4trunc(ipv4trunc), d_ipv6trunc(ipv6trunc), d_cleanupDelay(cleanupDelay), d_expiration(expiration)
+  MaxQPSIPRule(unsigned int qps, unsigned int burst, unsigned int ipv4trunc=32, unsigned int ipv6trunc=64, unsigned int expiration=300, unsigned int cleanupDelay=60, unsigned int scanFraction=10):
+    d_qps(qps), d_burst(burst), d_ipv4trunc(ipv4trunc), d_ipv6trunc(ipv6trunc), d_cleanupDelay(cleanupDelay), d_expiration(expiration), d_scanFraction(scanFraction)
   {
-    pthread_rwlock_init(&d_lock, 0);
     gettime(&d_lastCleanup, true);
   }
 
+  void clear()
+  {
+    std::lock_guard<std::mutex> lock(d_lock);
+    d_limits.clear();
+  }
+
+  size_t cleanup(const struct timespec& cutOff, size_t* scannedCount=nullptr) const
+  {
+    std::lock_guard<std::mutex> lock(d_lock);
+    size_t toLook = d_limits.size() / d_scanFraction + 1;
+    size_t lookedAt = 0;
+
+    size_t removed = 0;
+    auto& sequence = d_limits.get<SequencedTag>();
+    for (auto entry = sequence.begin(); entry != sequence.end() && lookedAt < toLook; lookedAt++) {
+      if (entry->d_limiter.seenSince(cutOff)) {
+        /* entries are ordered from least recently seen to more recently
+           seen, as soon as we see one that has not expired yet, we are
+           done */
+        lookedAt++;
+        break;
+      }
+
+      entry = sequence.erase(entry);
+      removed++;
+    }
+
+    if (scannedCount != nullptr) {
+      *scannedCount = lookedAt;
+    }
+
+    return removed;
+  }
+
   void cleanupIfNeeded(const struct timespec& now) const
   {
     if (d_cleanupDelay > 0) {
@@ -43,20 +76,11 @@ public:
       cutOff.tv_sec += d_cleanupDelay;
 
       if (cutOff < now) {
-        WriteLock w(&d_lock);
-
         /* the QPS Limiter doesn't use realtime, be careful! */
         gettime(&cutOff, false);
         cutOff.tv_sec -= d_expiration;
 
-        for (auto entry = d_limits.begin(); entry != d_limits.end(); ) {
-          if (!entry->second.seenSince(cutOff)) {
-            entry = d_limits.erase(entry);
-          }
-          else {
-            ++entry;
-          }
-        }
+        cleanup(cutOff);
 
         d_lastCleanup = now;
       }
@@ -71,20 +95,15 @@ public:
     zeroport.sin4.sin_port=0;
     zeroport.truncate(zeroport.sin4.sin_family == AF_INET ? d_ipv4trunc : d_ipv6trunc);
     {
-      ReadLock r(&d_lock);
-      const auto iter = d_limits.find(zeroport);
-      if (iter != d_limits.end()) {
-        return !iter->second.check(d_qps, d_burst);
-      }
-    }
-    {
-      WriteLock w(&d_lock);
-
+      std::lock_guard<std::mutex> lock(d_lock);
       auto iter = d_limits.find(zeroport);
-      if(iter == d_limits.end()) {
-        iter=d_limits.insert({zeroport,QPSLimiter(d_qps, d_burst)}).first;
+      if (iter == d_limits.end()) {
+        Entry e(zeroport, QPSLimiter(d_qps, d_burst));
+        iter = d_limits.insert(e).first;
       }
-      return !iter->second.check(d_qps, d_burst);
+
+      moveCacheItemToBack(d_limits, iter);
+      return !iter->d_limiter.check(d_qps, d_burst);
     }
   }
 
@@ -93,12 +112,37 @@ public:
     return "IP (/"+std::to_string(d_ipv4trunc)+", /"+std::to_string(d_ipv6trunc)+") match for QPS over " + std::to_string(d_qps) + " burst "+ std::to_string(d_burst);
   }
 
+  size_t getEntriesCount() const
+  {
+    std::lock_guard<std::mutex> lock(d_lock);
+    return d_limits.size();
+  }
 
 private:
-  mutable pthread_rwlock_t d_lock;
-  mutable std::map<ComboAddress, BasicQPSLimiter> d_limits;
+  struct OrderedTag {};
+  struct SequencedTag {};
+  struct Entry
+  {
+    Entry(const ComboAddress& addr, BasicQPSLimiter&& limiter): d_limiter(limiter), d_addr(addr)
+    {
+    }
+    mutable BasicQPSLimiter d_limiter;
+    ComboAddress d_addr;
+  };
+
+  typedef multi_index_container<
+    Entry,
+    indexed_by <
+      ordered_unique<tag<OrderedTag>, member<Entry,ComboAddress,&Entry::d_addr>, ComboAddress::addressOnlyLessThan >,
+      sequenced<tag<SequencedTag> >
+      >
+  > qpsContainer_t;
+
+  mutable std::mutex d_lock;
+  mutable qpsContainer_t d_limits;
   mutable struct timespec d_lastCleanup;
   unsigned int d_qps, d_burst, d_ipv4trunc, d_ipv6trunc, d_cleanupDelay, d_expiration;
+  unsigned int d_scanFraction{10};
 };
 
 class MaxQPSRule : public DNSRule
index 58f71ecea4b3f046e593362c2e897052a556a05a..0f8339f99bdf954c13a785550ffdea66fa599f76 100644 (file)
@@ -537,9 +537,9 @@ These ``DNSRule``\ s be one of the following items:
 
   Matches queries with the DO flag set
 
-.. function:: MaxQPSIPRule(qps[, v4Mask[, v6Mask[, burst[, expiration[, cleanupDelay]]]]])
+.. function:: MaxQPSIPRule(qps[, v4Mask[, v6Mask[, burst[, expiration[, cleanupDelay[, scanFraction]]]]]])
   .. versionchanged:: 1.3.1
-    Added the optional parameters ``expiration`` and ``cleanupDelay``.
+    Added the optional parameters ``expiration``, ``cleanupDelay`` and ``scanFraction``.
 
   Matches traffic for a subnet specified by ``v4Mask`` or ``v6Mask`` exceeding ``qps`` queries per second up to ``burst`` allowed.
   This rule keeps track of QPS by netmask or source IP. This state is cleaned up regularly if  ``cleanupDelay`` is greater than zero,
@@ -551,6 +551,7 @@ These ``DNSRule``\ s be one of the following items:
   :param int burst: The number of burstable queries per second allowed. Default is same as qps
   :param int expiration: How long to keep netmask or IP addresses after they have last been seen, in seconds. Default is 300
   :param int cleanupDelay: The number of seconds between two cleanups. Default is 60
+  :param int scanFraction: The maximum fraction of the store to scan for expired entries, for example 5 would scan at most 20% of it. Default is 10 so 10%
 
 .. function:: MaxQPSRule(qps)
 
diff --git a/pdns/dnsdistdist/test-dnsdistrules_cc.cc b/pdns/dnsdistdist/test-dnsdistrules_cc.cc
new file mode 100644 (file)
index 0000000..32fd673
--- /dev/null
@@ -0,0 +1,103 @@
+
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+
+#include <thread>
+#include <boost/test/unit_test.hpp>
+
+#include "dnsdist-rules.hh"
+
+BOOST_AUTO_TEST_SUITE(dnsdistluarules_cc)
+
+BOOST_AUTO_TEST_CASE(test_MaxQPSIPRule) {
+  size_t maxQPS = 10;
+  size_t maxBurst = maxQPS;
+  unsigned int expiration = 300;
+  unsigned int cleanupDelay = 60;
+  unsigned int scanFraction = 10;
+  MaxQPSIPRule rule(maxQPS, maxBurst, 32, 64, expiration, cleanupDelay, scanFraction);
+
+  DNSName qname("powerdns.com.");
+  uint16_t qtype = QType::A;
+  uint16_t qclass = QClass::IN;
+  ComboAddress lc("127.0.0.1:53");
+  ComboAddress rem("192.0.2.1:42");
+  struct dnsheader* dh = nullptr;
+  size_t bufferSize = 0;
+  size_t queryLen = 0;
+  bool isTcp = false;
+  struct timespec queryRealTime;
+  gettime(&queryRealTime, true);
+  struct timespec expiredTime;
+  /* the internal QPS limiter does not use the real time */
+  gettime(&expiredTime);
+
+  DNSQuestion dq(&qname, qtype, qclass, &lc, &rem, dh, bufferSize, queryLen, isTcp, &queryRealTime);
+
+  for (size_t idx = 0; idx < maxQPS; idx++) {
+    /* let's use different source ports, it shouldn't matter */
+    rem = ComboAddress("192.0.2.1:" + std::to_string(idx));
+    BOOST_CHECK_EQUAL(rule.matches(&dq), false);
+    BOOST_CHECK_EQUAL(rule.getEntriesCount(), 1);
+  }
+
+  /* maxQPS + 1, we should be blocked */
+  BOOST_CHECK_EQUAL(rule.matches(&dq), true);
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), 1);
+
+  expiredTime.tv_sec += 1;
+  rule.cleanup(expiredTime);
+
+  /* we should have been cleaned up */
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), 0);
+
+  /* we should not be blocked anymore */
+  BOOST_CHECK_EQUAL(rule.matches(&dq), false);
+  /* and we be back */
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), 1);
+
+
+  /* Let's insert a lot of different sources now */
+  struct timespec insertionTime;
+  gettime(&insertionTime);
+  for (size_t idxByte3 = 0; idxByte3 < 256; idxByte3++) {
+    for (size_t idxByte4 = 0; idxByte4 < 256; idxByte4++) {
+      rem = ComboAddress("10.0." + std::to_string(idxByte3) + "." + std::to_string(idxByte4));
+      BOOST_CHECK_EQUAL(rule.matches(&dq), false);
+    }
+  }
+
+  /* don't forget the existing entry */
+  size_t total = 1 + 256 * 256;
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), total);
+
+  /* make sure all entries are still valid */
+  struct timespec notExpiredTime = insertionTime;
+  notExpiredTime.tv_sec -= 1;
+
+  size_t scanned = 0;
+  auto removed = rule.cleanup(notExpiredTime, &scanned);
+  BOOST_CHECK_EQUAL(removed, 0);
+  /* the first entry should still have been valid, we should not have scanned more */
+  BOOST_CHECK_EQUAL(scanned, 1);
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), total);
+
+  /* make sure all entries are _not_ valid anymore */
+  expiredTime = insertionTime;
+  expiredTime.tv_sec += 1;
+
+  removed = rule.cleanup(expiredTime, &scanned);
+  BOOST_CHECK_EQUAL(removed, (total / scanFraction) + 1);
+  /* we should not have scanned more than scanFraction */
+  BOOST_CHECK_EQUAL(scanned, removed);
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), total - removed);
+
+  rule.clear();
+  BOOST_CHECK_EQUAL(rule.getEntriesCount(), 0);
+  removed = rule.cleanup(expiredTime, &scanned);
+  BOOST_CHECK_EQUAL(removed, 0);
+  BOOST_CHECK_EQUAL(scanned, 0);
+}
+
+
+BOOST_AUTO_TEST_SUITE_END()