From 67ce0bdd65016e8c59442277fb570f584cca88f9 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Wed, 16 May 2018 15:47:35 +0200 Subject: [PATCH] dnsdist: Remove 'expired' states from MaxQPSIPRule --- pdns/dnsdist-console.cc | 2 +- pdns/dnsdist-lua-rules.cc | 49 +++++++++++++--- pdns/dnsdist.hh | 77 ++++++++++++++++++------- pdns/dnsdistdist/docs/rules-actions.rst | 10 +++- 4 files changed, 104 insertions(+), 34 deletions(-) diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index be71a5f86..f8134fff4 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -345,7 +345,7 @@ const std::vector g_consoleKeywords{ { "leastOutstanding", false, "", "Send traffic to downstream server with least outstanding queries, with the lowest 'order', and within that the lowest recent latency"}, { "LogAction", true, "[filename], [binary], [append], [buffered]", "Log a line for each query, to the specified file if any, to the console (require verbose) otherwise. When logging to a file, the `binary` optional parameter specifies whether we log in binary form (default) or in textual form, the `append` optional parameter specifies whether we open the file for appending or truncate each time (default), and the `buffered` optional parameter specifies whether writes to the file are buffered (default) or not." }, { "makeKey", true, "", "generate a new server access key, emit configuration line ready for pasting" }, - { "MaxQPSIPRule", true, "qps, v4Mask=32, v6Mask=64, burst=qps", "matches traffic exceeding the qps limit per subnet" }, + { "MaxQPSIPRule", true, "qps, [v4Mask=32 [, v6Mask=64 [, burst=qps [, expiration=300 [, cleanupDelay=60]]]]]", "matches traffic exceeding the qps limit per subnet" }, { "MaxQPSRule", true, "qps", "matches traffic **not** exceeding this qps limit" }, { "mvCacheHitResponseRule", true, "from, to", "move cache hit response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" }, { "mvResponseRule", true, "from, to", "move response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" }, diff --git a/pdns/dnsdist-lua-rules.cc b/pdns/dnsdist-lua-rules.cc index e79a05f7c..437b47b4e 100644 --- a/pdns/dnsdist-lua-rules.cc +++ b/pdns/dnsdist-lua-rules.cc @@ -28,14 +28,44 @@ class MaxQPSIPRule : public DNSRule { public: - MaxQPSIPRule(unsigned int qps, unsigned int burst, unsigned int ipv4trunc=32, unsigned int ipv6trunc=64) : - d_qps(qps), d_burst(burst), d_ipv4trunc(ipv4trunc), d_ipv6trunc(ipv6trunc) + 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) { pthread_rwlock_init(&d_lock, 0); + gettime(&d_lastCleanup, true); + } + + void cleanupIfNeeded(const struct timespec& now) const + { + if (d_cleanupDelay > 0) { + struct timespec cutOff = d_lastCleanup; + 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; + } + } + + d_lastCleanup = now; + } + } } bool matches(const DNSQuestion* dq) const override { + cleanupIfNeeded(*dq->queryTime); + ComboAddress zeroport(*dq->remote); zeroport.sin4.sin_port=0; zeroport.truncate(zeroport.sin4.sin_family == AF_INET ? d_ipv4trunc : d_ipv6trunc); @@ -43,16 +73,17 @@ public: ReadLock r(&d_lock); const auto iter = d_limits.find(zeroport); if (iter != d_limits.end()) { - return !iter->second.check(); + return !iter->second.check(d_qps, d_burst); } } { WriteLock w(&d_lock); + auto iter = d_limits.find(zeroport); if(iter == d_limits.end()) { iter=d_limits.insert({zeroport,QPSLimiter(d_qps, d_burst)}).first; } - return !iter->second.check(); + return !iter->second.check(d_qps, d_burst); } } @@ -64,9 +95,9 @@ public: private: mutable pthread_rwlock_t d_lock; - mutable std::map d_limits; - unsigned int d_qps, d_burst, d_ipv4trunc, d_ipv6trunc; - + mutable std::map d_limits; + mutable struct timespec d_lastCleanup; + unsigned int d_qps, d_burst, d_ipv4trunc, d_ipv6trunc, d_cleanupDelay, d_expiration; }; class MaxQPSRule : public DNSRule @@ -1097,8 +1128,8 @@ void setupLuaRules() }); }); - g_lua.writeFunction("MaxQPSIPRule", [](unsigned int qps, boost::optional ipv4trunc, boost::optional ipv6trunc, boost::optional burst) { - return std::shared_ptr(new MaxQPSIPRule(qps, burst.get_value_or(qps), ipv4trunc.get_value_or(32), ipv6trunc.get_value_or(64))); + 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("MaxQPSRule", [](unsigned int qps, boost::optional burst) { diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 96a050b8d..d020d0b44 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -272,28 +272,69 @@ struct StopWatch }; -class QPSLimiter +class BasicQPSLimiter { public: - QPSLimiter() + BasicQPSLimiter() { } - QPSLimiter(unsigned int rate, unsigned int burst) : d_rate(rate), d_burst(burst), d_tokens(burst) + BasicQPSLimiter(unsigned int rate, unsigned int burst): d_tokens(burst) + { + d_prev.start(); + } + + bool check(unsigned int rate, unsigned int burst) const // this is not quite fair + { + auto delta = d_prev.udiffAndSet(); + + d_tokens += 1.0 * rate * (delta/1000000.0); + + if(d_tokens > burst) { + d_tokens = burst; + } + + bool ret=false; + if(d_tokens >= 1.0) { // we need this because burst=1 is weird otherwise + ret=true; + --d_tokens; + } + + return ret; + } + + bool seenSince(const struct timespec& cutOff) const + { + return cutOff < d_prev.d_start; + } + +protected: + mutable StopWatch d_prev; + mutable double d_tokens; +}; + +class QPSLimiter : public BasicQPSLimiter +{ +public: + QPSLimiter(): BasicQPSLimiter() + { + } + + QPSLimiter(unsigned int rate, unsigned int burst): BasicQPSLimiter(rate, burst), d_rate(rate), d_burst(burst), d_passthrough(false) { - d_passthrough=false; d_prev.start(); } unsigned int getRate() const { - return d_passthrough? 0 : d_rate; + return d_passthrough ? 0 : d_rate; } int getPassed() const { return d_passed; } + int getBlocked() const { return d_blocked; @@ -301,34 +342,26 @@ public: bool check() const // this is not quite fair { - if(d_passthrough) + if (d_passthrough) { return true; - auto delta = d_prev.udiffAndSet(); - - d_tokens += 1.0*d_rate * (delta/1000000.0); - - if(d_tokens > d_burst) - d_tokens = d_burst; + } - bool ret=false; - if(d_tokens >= 1.0) { // we need this because burst=1 is weird otherwise - ret=true; - --d_tokens; + bool ret = BasicQPSLimiter::check(d_rate, d_burst); + if (ret) { d_passed++; } - else + else { d_blocked++; + } return ret; } private: - bool d_passthrough{true}; - unsigned int d_rate; - unsigned int d_burst; - mutable double d_tokens; - mutable StopWatch d_prev; mutable unsigned int d_passed{0}; mutable unsigned int d_blocked{0}; + unsigned int d_rate; + unsigned int d_burst; + bool d_passthrough{true}; }; struct ClientState; diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index 288508796..58f71ecea 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -537,14 +537,20 @@ These ``DNSRule``\ s be one of the following items: Matches queries with the DO flag set -.. function:: MaxQPSIPRule(qps[, v4Mask[, v6Mask[, burst]]]) +.. function:: MaxQPSIPRule(qps[, v4Mask[, v6Mask[, burst[, expiration[, cleanupDelay]]]]]) + .. versionchanged:: 1.3.1 + Added the optional parameters ``expiration`` and ``cleanupDelay``. - Matches traffic for a subnet specified by ``v4Mask`` or ``v6Mask`` exceeding ``qps`` queries per second up to ``burst`` allowed + 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, + removing existing netmasks or IP addresses that have not been seen in the last ``expiration`` seconds. :param int qps: The number of queries per second allowed, above this number traffic is matched :param int v4Mask: The IPv4 netmask to match on. Default is 32 (the whole address) :param int v6Mask: The IPv6 netmask to match on. Default is 64 :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 .. function:: MaxQPSRule(qps) -- 2.40.0