{ "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" },
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);
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);
}
}
private:
mutable pthread_rwlock_t d_lock;
- mutable std::map<ComboAddress, QPSLimiter> d_limits;
- unsigned int d_qps, d_burst, d_ipv4trunc, d_ipv6trunc;
-
+ mutable std::map<ComboAddress, BasicQPSLimiter> 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
});
});
- g_lua.writeFunction("MaxQPSIPRule", [](unsigned int qps, boost::optional<int> ipv4trunc, boost::optional<int> ipv6trunc, boost::optional<int> burst) {
- return std::shared_ptr<DNSRule>(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<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("MaxQPSRule", [](unsigned int qps, boost::optional<int> burst) {
};
-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;
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;
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)