From: Remi Gacogne Date: Mon, 11 Jun 2018 12:22:25 +0000 (+0200) Subject: dnsdist: Use LRU to clean the MaxQPSIPRule's store X-Git-Tag: dnsdist-1.3.1~4^2 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=05f4003d5690026a077aa10e3037f0c941abc046;p=pdns dnsdist: Use LRU to clean the MaxQPSIPRule's store 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. --- diff --git a/pdns/dnsdist-lua-rules.cc b/pdns/dnsdist-lua-rules.cc index 6f1509af7..edfbafcf7 100644 --- a/pdns/dnsdist-lua-rules.cc +++ b/pdns/dnsdist-lua-rules.cc @@ -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 makeRule(const luadnsrule_t& var) { @@ -263,8 +263,8 @@ void setupLuaRules() }); }); - g_lua.writeFunction("MaxQPSIPRule", [](unsigned int qps, boost::optional ipv4trunc, boost::optional ipv6trunc, boost::optional burst, boost::optional expiration, boost::optional cleanupDelay) { - return std::shared_ptr(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 ipv4trunc, boost::optional ipv6trunc, boost::optional burst, boost::optional expiration, boost::optional cleanupDelay, boost::optional scanFraction) { + return std::shared_ptr(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 burst) { diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 982901414..c7c563c7d 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 index 000000000..8b8f773aa --- /dev/null +++ b/pdns/dnsdistdist/cachecleaner.hh @@ -0,0 +1 @@ +../cachecleaner.hh \ No newline at end of file diff --git a/pdns/dnsdistdist/dnsdist-rules.hh b/pdns/dnsdistdist/dnsdist-rules.hh index ee91edacc..de5eb9201 100644 --- a/pdns/dnsdistdist/dnsdist-rules.hh +++ b/pdns/dnsdistdist/dnsdist-rules.hh @@ -29,13 +29,46 @@ 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 lock(d_lock); + d_limits.clear(); + } + + size_t cleanup(const struct timespec& cutOff, size_t* scannedCount=nullptr) const + { + std::lock_guard 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(); + 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 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 lock(d_lock); + return d_limits.size(); + } private: - mutable pthread_rwlock_t d_lock; - mutable std::map 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, member, ComboAddress::addressOnlyLessThan >, + sequenced > + > + > 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 diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index 58f71ecea..0f8339f99 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -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 index 000000000..32fd67343 --- /dev/null +++ b/pdns/dnsdistdist/test-dnsdistrules_cc.cc @@ -0,0 +1,103 @@ + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#include +#include + +#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()