* 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)
{
});
});
- 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) {
ascii.hh \
base64.hh \
bpf-filter.cc bpf-filter.hh \
+ cachecleaner.hh \
dns.cc dns.hh \
dnscrypt.cc dnscrypt.hh \
dnsdist.cc dnsdist.hh \
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 \
--- /dev/null
+../cachecleaner.hh
\ No newline at end of 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) {
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;
}
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);
}
}
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
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,
: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)
--- /dev/null
+
+#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()