--- /dev/null
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include "dolog.hh"
+
+class DynBlockRulesGroup
+{
+private:
+
+ struct Counts
+ {
+ std::map<uint8_t, uint64_t> d_rcodeCounts;
+ std::map<uint16_t, uint64_t> d_qtypeCounts;
+ uint64_t queries{0};
+ uint64_t respBytes{0};
+ };
+
+ struct DynBlockRule
+ {
+ DynBlockRule(): d_enabled(false)
+ {
+ }
+
+ DynBlockRule(const std::string& blockReason, unsigned int blockDuration, unsigned int rate, unsigned int seconds, DNSAction::Action action): d_blockReason(blockReason), d_blockDuration(blockDuration), d_rate(rate), d_seconds(seconds), d_action(action), d_enabled(true)
+ {
+ }
+
+ bool matches(const struct timespec& when)
+ {
+ if (!d_enabled) {
+ return false;
+ }
+
+ if (d_seconds && when < d_cutOff) {
+ return false;
+ }
+
+ if (when < d_minTime) {
+ d_minTime = when;
+ }
+
+ return true;
+ }
+
+ bool rateExceeded(unsigned int count, const struct timespec& now) const
+ {
+ if (!d_enabled) {
+ return false;
+ }
+
+ double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime);
+ double limit = delta * d_rate;
+ return (count > limit);
+ }
+
+ bool isEnabled() const
+ {
+ return d_enabled;
+ }
+
+ std::string d_blockReason;
+ struct timespec d_cutOff;
+ struct timespec d_minTime;
+ unsigned int d_blockDuration{0};
+ unsigned int d_rate{0};
+ unsigned int d_seconds{0};
+ DNSAction::Action d_action{DNSAction::Action::None};
+ bool d_enabled{false};
+ };
+
+ typedef std::unordered_map<ComboAddress, Counts, ComboAddress::addressOnlyHash, ComboAddress::addressOnlyEqual> counts_t;
+
+public:
+ DynBlockRulesGroup()
+ {
+ }
+
+ void setQueryRate(unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+ {
+ d_queryRateRule = DynBlockRule(reason, blockDuration, rate, seconds, action);
+ }
+
+ void setResponseByteRate(unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+ {
+ d_respRateRule = DynBlockRule(reason, blockDuration, rate, seconds, action);
+ }
+
+ void setRCodeRate(uint8_t rcode, unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+ {
+ auto& entry = d_rcodeRules[rcode];
+ entry = DynBlockRule(reason, blockDuration, rate, seconds, action);
+ }
+
+ void setQTypeRate(uint16_t qtype, unsigned int rate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
+ {
+ auto& entry = d_qtypeRules[qtype];
+ entry = DynBlockRule(reason, blockDuration, rate, seconds, action);
+ }
+
+ void apply()
+ {
+ counts_t counts;
+
+ size_t entriesCount = 0;
+ if (hasQueryRules()) {
+ ReadLock rl(&g_rings.queryLock);
+ entriesCount += g_rings.queryRing.size();
+ }
+ if (hasResponseRules()) {
+ std::lock_guard<std::mutex> lock(g_rings.respMutex);
+ entriesCount += g_rings.respRing.size();
+ }
+ counts.reserve(entriesCount);
+
+ processQueryRules(counts);
+ processResponseRules(counts);
+
+ if (counts.empty()) {
+ return;
+ }
+
+ boost::optional<NetmaskTree<DynBlock> > blocks;
+ bool updated = false;
+ struct timespec now;
+ gettime(&now);
+
+ for (const auto& entry : counts) {
+ if (d_queryRateRule.rateExceeded(entry.second.queries, now)) {
+ addBlock(blocks, now, entry.first, d_queryRateRule, updated);
+ continue;
+ }
+
+ if (d_respRateRule.rateExceeded(entry.second.respBytes, now)) {
+ addBlock(blocks, now, entry.first, d_respRateRule, updated);
+ continue;
+ }
+
+ for (const auto& rule : d_qtypeRules) {
+ const auto& typeIt = entry.second.d_qtypeCounts.find(rule.first);
+ if (typeIt != entry.second.d_qtypeCounts.cend() && rule.second.rateExceeded(typeIt->second, now)) {
+ addBlock(blocks, now, entry.first, rule.second, updated);
+ break;
+ }
+ }
+
+ for (const auto& rule : d_rcodeRules) {
+ const auto& rcodeIt = entry.second.d_rcodeCounts.find(rule.first);
+ if (rcodeIt != entry.second.d_rcodeCounts.cend() && rule.second.rateExceeded(rcodeIt->second, now)) {
+ addBlock(blocks, now, entry.first, rule.second, updated);
+ break;
+ }
+ }
+ }
+
+ if (updated && blocks) {
+ g_dynblockNMG.setState(*blocks);
+ }
+ }
+
+private:
+ bool checkIfQueryTypeMatches(const Rings::Query& query)
+ {
+ auto rule = d_qtypeRules.find(query.qtype);
+ if (rule == d_qtypeRules.end()) {
+ return false;
+ }
+
+ return rule->second.matches(query.when);
+ }
+
+ bool checkIfResponseCodeMatches(const Rings::Response& response)
+ {
+ auto rule = d_rcodeRules.find(response.dh.rcode);
+ if (rule == d_rcodeRules.end()) {
+ return false;
+ }
+
+ return rule->second.matches(response.when);
+ }
+
+ void addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
+ {
+ if (!blocks) {
+ blocks = g_dynblockNMG.getCopy();
+ }
+ struct timespec until = now;
+ until.tv_sec += rule.d_seconds;
+ unsigned int count = 0;
+ const auto& got = blocks->lookup(Netmask(requestor));
+ bool expired = false;
+ if (got) {
+ if (until < got->second.until) {
+ // had a longer policy
+ return;
+ }
+
+ if (now < got->second.until) {
+ // only inherit count on fresh query we are extending
+ count = got->second.blocks;
+ }
+ else {
+ expired = true;
+ }
+ }
+
+ DynBlock db{rule.d_blockReason, until, DNSName(), rule.d_action};
+ db.blocks = count;
+ if (!got || expired) {
+ warnlog("Inserting dynamic block for %s for %d seconds: %s", requestor.toString(), rule.d_seconds, rule.d_blockReason);
+ }
+ blocks->insert(Netmask(requestor)).second = db;
+ updated = true;
+ }
+
+ bool hasQueryRules() const
+ {
+ return d_queryRateRule.isEnabled() || !d_qtypeRules.empty();
+ }
+
+ bool hasResponseRules() const
+ {
+ return d_respRateRule.isEnabled() || !d_rcodeRules.empty();
+ }
+
+ bool hasRules() const
+ {
+ return hasQueryRules() || hasResponseRules();
+ }
+
+ void processQueryRules(counts_t& counts)
+ {
+ if (!hasQueryRules()) {
+ return;
+ }
+
+ struct timespec now;
+ gettime(&now);
+ d_queryRateRule.d_cutOff = d_queryRateRule.d_minTime = now;
+ d_queryRateRule.d_cutOff.tv_sec -= d_queryRateRule.d_seconds;
+
+ for (auto& rule : d_qtypeRules) {
+ rule.second.d_cutOff = rule.second.d_minTime = now;
+ rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+ }
+
+ {
+ ReadLock rl(&g_rings.queryLock);
+ for(const auto& c : g_rings.queryRing) {
+ if (now < c.when) {
+ continue;
+ }
+
+ bool qRateMatches = d_queryRateRule.matches(c.when);
+ bool typeRuleMatches = checkIfQueryTypeMatches(c);
+
+ if (qRateMatches || typeRuleMatches) {
+ auto& entry = counts[c.requestor];
+ if (qRateMatches) {
+ entry.queries++;
+ }
+ if (typeRuleMatches) {
+ entry.d_qtypeCounts[c.qtype]++;
+ }
+ }
+ }
+ }
+ }
+
+ void processResponseRules(counts_t& counts)
+ {
+ if (!hasResponseRules()) {
+ return;
+ }
+
+ struct timespec now;
+ gettime(&now);
+ d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now;
+ d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds;
+
+ for (auto& rule : d_rcodeRules) {
+ rule.second.d_cutOff = rule.second.d_minTime = now;
+ rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
+ }
+
+ {
+ std::lock_guard<std::mutex> lock(g_rings.respMutex);
+ for(const auto& c : g_rings.respRing) {
+ if (now < c.when) {
+ continue;
+ }
+
+ bool respRateMatches = d_respRateRule.matches(c.when);
+ bool rcodeRuleMatches = checkIfResponseCodeMatches(c);
+
+ if (respRateMatches || rcodeRuleMatches) {
+ auto& entry = counts[c.requestor];
+ if (respRateMatches) {
+ entry.respBytes += c.size;
+ }
+ if (rcodeRuleMatches) {
+ entry.d_rcodeCounts[c.dh.rcode]++;
+ }
+ }
+ }
+ }
+ }
+
+ std::map<uint8_t, DynBlockRule> d_rcodeRules;
+ std::map<uint16_t, DynBlockRule> d_qtypeRules;
+ DynBlockRule d_queryRateRule;
+ DynBlockRule d_respRateRule;
+};
import dns
from dnsdisttests import DNSDistTest, range
-class TestDynBlockQPS(DNSDistTest):
+class DynBlocksTest(DNSDistTest):
- _dynBlockQPS = 10
- _dynBlockPeriod = 2
- _dynBlockDuration = 5
- _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
- _config_template = """
- function maintenance()
- addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
- end
- newServer{address="127.0.0.1:%s"}
- """
-
- def testDynBlocksQRate(self):
- """
- Dyn Blocks: QRate
- """
- name = 'qrate.dynblocks.tests.powerdns.com.'
+ def doTestQRate(self, name):
query = dns.message.make_query(name, 'A', 'IN')
response = dns.message.make_response(query)
rrset = dns.rrset.from_text(name,
self.assertEquals(query, receivedQuery)
self.assertEquals(response, receivedResponse)
-class TestDynBlockQPSRefused(DNSDistTest):
-
- _dynBlockQPS = 10
- _dynBlockPeriod = 2
- _dynBlockDuration = 5
- _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
- _config_template = """
- function maintenance()
- addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
- end
- setDynBlocksAction(DNSAction.Refused)
- newServer{address="127.0.0.1:%s"}
- """
-
- def testDynBlocksQRate(self):
- """
- Dyn Blocks: QRate refused
- """
- name = 'qraterefused.dynblocks.tests.powerdns.com.'
+ def doTestQRateRCode(self, name, rcode):
query = dns.message.make_query(name, 'A', 'IN')
response = dns.message.make_response(query)
rrset = dns.rrset.from_text(name,
dns.rdatatype.A,
'192.0.2.1')
response.answer.append(rrset)
- refusedResponse = dns.message.make_response(query)
- refusedResponse.set_rcode(dns.rcode.REFUSED)
+ expectedResponse = dns.message.make_response(query)
+ expectedResponse.set_rcode(rcode)
allowed = 0
sent = 0
self.assertEquals(receivedResponse, response)
allowed = allowed + 1
else:
- self.assertEquals(receivedResponse, refusedResponse)
+ self.assertEquals(receivedResponse, expectedResponse)
# the query has not reached the responder,
# let's clear the response queue
self.clearToResponderQueue()
# wait for the maintenance function to run
time.sleep(2)
- # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+ # we should now be 'rcode' for up to self._dynBlockDuration + self._dynBlockPeriod
(_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, refusedResponse)
+ self.assertEquals(receivedResponse, expectedResponse)
# wait until we are not blocked anymore
time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
self.assertEquals(receivedResponse, response)
allowed = allowed + 1
else:
- self.assertEquals(receivedResponse, refusedResponse)
+ self.assertEquals(receivedResponse, expectedResponse)
# the query has not reached the responder,
# let's clear the response queue
self.clearToResponderQueue()
# wait for the maintenance function to run
time.sleep(2)
- # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+ # we should now be 'rcode' for up to self._dynBlockDuration + self._dynBlockPeriod
(_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, refusedResponse)
+ self.assertEquals(receivedResponse, expectedResponse)
# wait until we are not blocked anymore
time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
self.assertEquals(query, receivedQuery)
self.assertEquals(response, receivedResponse)
-class TestDynBlockQPSActionRefused(DNSDistTest):
-
- _dynBlockQPS = 10
- _dynBlockPeriod = 2
- _dynBlockDuration = 5
- _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
- _config_template = """
- function maintenance()
- addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Refused)
- end
- setDynBlocksAction(DNSAction.Drop)
- newServer{address="127.0.0.1:%s"}
- """
-
- def testDynBlocksQRate(self):
- """
- Dyn Blocks: QRate refused (action)
- """
- name = 'qrateactionrefused.dynblocks.tests.powerdns.com.'
+ def doTestResponseByteRate(self, name):
query = dns.message.make_query(name, 'A', 'IN')
response = dns.message.make_response(query)
- rrset = dns.rrset.from_text(name,
- 60,
- dns.rdataclass.IN,
- dns.rdatatype.A,
- '192.0.2.1')
- response.answer.append(rrset)
- refusedResponse = dns.message.make_response(query)
- refusedResponse.set_rcode(dns.rcode.REFUSED)
+ response.answer.append(dns.rrset.from_text_list(name,
+ 60,
+ dns.rdataclass.IN,
+ dns.rdatatype.A,
+ ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4']))
+ response.answer.append(dns.rrset.from_text(name,
+ 60,
+ dns.rdataclass.IN,
+ dns.rdatatype.AAAA,
+ '2001:DB8::1'))
allowed = 0
sent = 0
- for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
+
+ print(time.time())
+
+ for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
(receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
- sent = sent + 1
+ sent = sent + len(response.to_wire())
if receivedQuery:
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
- self.assertEquals(receivedResponse, response)
- allowed = allowed + 1
+ self.assertEquals(response, receivedResponse)
+ allowed = allowed + len(response.to_wire())
else:
- self.assertEquals(receivedResponse, refusedResponse)
# the query has not reached the responder,
# let's clear the response queue
self.clearToResponderQueue()
+ # and stop right there, otherwise we might
+ # wait for so long that the dynblock is gone
+ # by the time we finished
+ break
# we might be already blocked, but we should have been able to send
- # at least self._dynBlockQPS queries
- self.assertGreaterEqual(allowed, self._dynBlockQPS)
+ # at least self._dynBlockBytesPerSecond bytes
+ print(allowed)
+ print(sent)
+ print(time.time())
+ self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
+
+ print(self.sendConsoleCommand("showDynBlocks()"))
+ print(self.sendConsoleCommand("grepq(\"\")"))
+ print(time.time())
if allowed == sent:
# wait for the maintenance function to run
+ print("Waiting for the maintenance function to run")
time.sleep(2)
- # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+ print(self.sendConsoleCommand("showDynBlocks()"))
+ print(self.sendConsoleCommand("grepq(\"\")"))
+ print(time.time())
+
+ # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
(_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, refusedResponse)
+ self.assertEquals(receivedResponse, None)
+
+ print(self.sendConsoleCommand("showDynBlocks()"))
+ print(self.sendConsoleCommand("grepq(\"\")"))
+ print(time.time())
# wait until we are not blocked anymore
time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
+ print(self.sendConsoleCommand("showDynBlocks()"))
+ print(self.sendConsoleCommand("grepq(\"\")"))
+ print(time.time())
+
# this one should succeed
(receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
self.assertEquals(response, receivedResponse)
+ # again, over TCP this time
allowed = 0
sent = 0
- # again, over TCP this time
- for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
+ for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
(receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
- sent = sent + 1
+ sent = sent + len(response.to_wire())
if receivedQuery:
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
- self.assertEquals(receivedResponse, response)
- allowed = allowed + 1
+ self.assertEquals(response, receivedResponse)
+ allowed = allowed + len(response.to_wire())
else:
- self.assertEquals(receivedResponse, refusedResponse)
# the query has not reached the responder,
# let's clear the response queue
self.clearToResponderQueue()
+ # and stop right there, otherwise we might
+ # wait for so long that the dynblock is gone
+ # by the time we finished
+ break
# we might be already blocked, but we should have been able to send
- # at least self._dynBlockQPS queries
- self.assertGreaterEqual(allowed, self._dynBlockQPS)
+ # at least self._dynBlockBytesPerSecond bytes
+ self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
if allowed == sent:
# wait for the maintenance function to run
time.sleep(2)
- # we should now be 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod
+ # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
(_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, refusedResponse)
+ self.assertEquals(receivedResponse, None)
# wait until we are not blocked anymore
time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
self.assertEquals(query, receivedQuery)
self.assertEquals(response, receivedResponse)
-class TestDynBlockQPSActionTruncated(DNSDistTest):
-
- _dynBlockQPS = 10
- _dynBlockPeriod = 2
- _dynBlockDuration = 5
- _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
- _config_template = """
- function maintenance()
- addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Truncate)
- end
- setDynBlocksAction(DNSAction.Drop)
- newServer{address="127.0.0.1:%s"}
- """
-
- def testDynBlocksQRate(self):
- """
- Dyn Blocks: QRate truncated (action)
- """
- name = 'qrateactiontruncated.dynblocks.tests.powerdns.com.'
- query = dns.message.make_query(name, 'A', 'IN')
- response = dns.message.make_response(query)
- rrset = dns.rrset.from_text(name,
- 60,
- dns.rdataclass.IN,
- dns.rdatatype.A,
- '192.0.2.1')
- response.answer.append(rrset)
- truncatedResponse = dns.message.make_response(query)
- truncatedResponse.flags |= dns.flags.TC
-
- allowed = 0
- sent = 0
- for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
- (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
- sent = sent + 1
- if receivedQuery:
- receivedQuery.id = query.id
- self.assertEquals(query, receivedQuery)
- self.assertEquals(receivedResponse, response)
- allowed = allowed + 1
- else:
- self.assertEquals(receivedResponse, truncatedResponse)
- # the query has not reached the responder,
- # let's clear the response queue
- self.clearToResponderQueue()
-
- # we might be already truncated, but we should have been able to send
- # at least self._dynBlockQPS queries
- self.assertGreaterEqual(allowed, self._dynBlockQPS)
-
- if allowed == sent:
- # wait for the maintenance function to run
- time.sleep(2)
-
- # we should now be 'truncated' for up to self._dynBlockDuration + self._dynBlockPeriod
- (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, truncatedResponse)
-
- # check over TCP, which should not be truncated
- (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
-
- self.assertEquals(query, receivedQuery)
- self.assertEquals(receivedResponse, response)
-
- # wait until we are not blocked anymore
- time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
-
- # this one should succeed
- (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
- receivedQuery.id = query.id
- self.assertEquals(query, receivedQuery)
- self.assertEquals(response, receivedResponse)
-
- allowed = 0
- sent = 0
- # again, over TCP this time, we should never get truncated!
- for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
- (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
- sent = sent + 1
- self.assertEquals(query, receivedQuery)
- self.assertEquals(receivedResponse, response)
- receivedQuery.id = query.id
- allowed = allowed + 1
-
- self.assertEquals(allowed, sent)
-
-class TestDynBlockServFails(DNSDistTest):
-
- _dynBlockQPS = 10
- _dynBlockPeriod = 2
- _dynBlockDuration = 5
- _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
- _config_template = """
- function maintenance()
- addDynBlocks(exceedServFails(%d, %d), "Exceeded servfail rate", %d)
- end
- newServer{address="127.0.0.1:%s"}
- """
-
- def testDynBlocksServFailRate(self):
- """
- Dyn Blocks: Server Failure Rate
- """
- name = 'servfailrate.dynblocks.tests.powerdns.com.'
+ def doTestRCodeRate(self, name, rcode):
query = dns.message.make_query(name, 'A', 'IN')
response = dns.message.make_response(query)
rrset = dns.rrset.from_text(name,
dns.rdatatype.A,
'192.0.2.1')
response.answer.append(rrset)
- servfailResponse = dns.message.make_response(query)
- servfailResponse.set_rcode(dns.rcode.SERVFAIL)
+ expectedResponse = dns.message.make_response(query)
+ expectedResponse.set_rcode(rcode)
# start with normal responses
for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
(_, receivedResponse) = self.sendUDPQuery(query, response)
self.assertEquals(receivedResponse, response)
- # now with ServFail!
+ # now with rcode!
sent = 0
allowed = 0
for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
- (receivedQuery, receivedResponse) = self.sendUDPQuery(query, servfailResponse)
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, expectedResponse)
sent = sent + 1
if receivedQuery:
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
- self.assertEquals(servfailResponse, receivedResponse)
+ self.assertEquals(expectedResponse, receivedResponse)
allowed = allowed + 1
else:
# the query has not reached the responder,
(_, receivedResponse) = self.sendUDPQuery(query, response)
self.assertEquals(receivedResponse, response)
- # now with ServFail!
+ # now with rcode!
sent = 0
allowed = 0
for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
- (receivedQuery, receivedResponse) = self.sendTCPQuery(query, servfailResponse)
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, expectedResponse)
sent = sent + 1
if receivedQuery:
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
- self.assertEquals(servfailResponse, receivedResponse)
+ self.assertEquals(expectedResponse, receivedResponse)
allowed = allowed + 1
else:
# the query has not reached the responder,
self.assertEquals(query, receivedQuery)
self.assertEquals(response, receivedResponse)
-class TestDynBlockResponseBytes(DNSDistTest):
+class TestDynBlockQPS(DynBlocksTest):
- _dynBlockBytesPerSecond = 200
+ _dynBlockQPS = 10
_dynBlockPeriod = 2
_dynBlockDuration = 5
- _consoleKey = DNSDistTest.generateConsoleKey()
- _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
- _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockBytesPerSecond', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
_config_template = """
- setKey("%s")
- controlSocket("127.0.0.1:%s")
function maintenance()
- addDynBlocks(exceedRespByterate(%d, %d), "Exceeded response byterate", %d)
+ addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
end
newServer{address="127.0.0.1:%s"}
"""
- def testDynBlocksResponseByteRate(self):
+ def testDynBlocksQRate(self):
"""
- Dyn Blocks: Response Byte Rate
+ Dyn Blocks: QRate
"""
- name = 'responsebyterate.dynblocks.tests.powerdns.com.'
- query = dns.message.make_query(name, 'A', 'IN')
- response = dns.message.make_response(query)
- response.answer.append(dns.rrset.from_text_list(name,
- 60,
- dns.rdataclass.IN,
- dns.rdatatype.A,
- ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4']))
- response.answer.append(dns.rrset.from_text(name,
- 60,
- dns.rdataclass.IN,
- dns.rdatatype.AAAA,
- '2001:DB8::1'))
+ name = 'qrate.dynblocks.tests.powerdns.com.'
+ self.doTestQRate(name)
- allowed = 0
- sent = 0
+class TestDynBlockGroupQPS(DynBlocksTest):
- print(time.time())
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ local dbr = dynBlockRulesGroup()
+ dbr:setQueryRate(%d, %d, "Exceeded query rate", %d)
- for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
+ function maintenance()
+ dbr:apply()
+ end
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksQRate(self):
+ """
+ Dyn Blocks (Group): QRate
+ """
+ name = 'qrate.group.dynblocks.tests.powerdns.com.'
+ self.doTestQRate(name)
+
+
+class TestDynBlockQPSRefused(DynBlocksTest):
+
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ function maintenance()
+ addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d)
+ end
+ setDynBlocksAction(DNSAction.Refused)
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksQRate(self):
+ """
+ Dyn Blocks: QRate refused
+ """
+ name = 'qraterefused.dynblocks.tests.powerdns.com.'
+ self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockGroupQPSRefused(DynBlocksTest):
+
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ local dbr = dynBlockRulesGroup()
+ dbr:setQueryRate(%d, %d, "Exceeded query rate", %d)
+
+ function maintenance()
+ dbr:apply()
+ end
+ setDynBlocksAction(DNSAction.Refused)
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksQRate(self):
+ """
+ Dyn Blocks (Group): QRate refused
+ """
+ name = 'qraterefused.group.dynblocks.tests.powerdns.com.'
+ self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockQPSActionRefused(DynBlocksTest):
+
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ function maintenance()
+ addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Refused)
+ end
+ setDynBlocksAction(DNSAction.Drop)
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksQRate(self):
+ """
+ Dyn Blocks: QRate refused (action)
+ """
+ name = 'qrateactionrefused.dynblocks.tests.powerdns.com.'
+ self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockQPSActionRefused(DynBlocksTest):
+
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ local dbr = dynBlockRulesGroup()
+ dbr:setQueryRate(%d, %d, "Exceeded query rate", %d, DNSAction.Refused)
+
+ function maintenance()
+ dbr:apply()
+ end
+ setDynBlocksAction(DNSAction.Drop)
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksQRate(self):
+ """
+ Dyn Blocks (group): QRate refused (action)
+ """
+ name = 'qrateactionrefused.group.dynblocks.tests.powerdns.com.'
+ self.doTestQRateRCode(name, dns.rcode.REFUSED)
+
+class TestDynBlockQPSActionTruncated(DNSDistTest):
+
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ function maintenance()
+ addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Truncate)
+ end
+ setDynBlocksAction(DNSAction.Drop)
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksQRate(self):
+ """
+ Dyn Blocks: QRate truncated (action)
+ """
+ name = 'qrateactiontruncated.dynblocks.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'A', 'IN')
+ response = dns.message.make_response(query)
+ rrset = dns.rrset.from_text(name,
+ 60,
+ dns.rdataclass.IN,
+ dns.rdatatype.A,
+ '192.0.2.1')
+ response.answer.append(rrset)
+ truncatedResponse = dns.message.make_response(query)
+ truncatedResponse.flags |= dns.flags.TC
+
+ allowed = 0
+ sent = 0
+ for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
(receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
- sent = sent + len(response.to_wire())
+ sent = sent + 1
if receivedQuery:
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
- self.assertEquals(response, receivedResponse)
- allowed = allowed + len(response.to_wire())
+ self.assertEquals(receivedResponse, response)
+ allowed = allowed + 1
else:
+ self.assertEquals(receivedResponse, truncatedResponse)
# the query has not reached the responder,
# let's clear the response queue
self.clearToResponderQueue()
- # and stop right there, otherwise we might
- # wait for so long that the dynblock is gone
- # by the time we finished
- break
- # we might be already blocked, but we should have been able to send
- # at least self._dynBlockBytesPerSecond bytes
- print(allowed)
- print(sent)
- print(time.time())
- self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
-
- print(self.sendConsoleCommand("showDynBlocks()"))
- print(self.sendConsoleCommand("grepq(\"\")"))
- print(time.time())
+ # we might be already truncated, but we should have been able to send
+ # at least self._dynBlockQPS queries
+ self.assertGreaterEqual(allowed, self._dynBlockQPS)
if allowed == sent:
# wait for the maintenance function to run
- print("Waiting for the maintenance function to run")
time.sleep(2)
- print(self.sendConsoleCommand("showDynBlocks()"))
- print(self.sendConsoleCommand("grepq(\"\")"))
- print(time.time())
-
- # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
+ # we should now be 'truncated' for up to self._dynBlockDuration + self._dynBlockPeriod
(_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, None)
+ self.assertEquals(receivedResponse, truncatedResponse)
- print(self.sendConsoleCommand("showDynBlocks()"))
- print(self.sendConsoleCommand("grepq(\"\")"))
- print(time.time())
+ # check over TCP, which should not be truncated
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(receivedResponse, response)
# wait until we are not blocked anymore
time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
- print(self.sendConsoleCommand("showDynBlocks()"))
- print(self.sendConsoleCommand("grepq(\"\")"))
- print(time.time())
-
# this one should succeed
(receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
receivedQuery.id = query.id
self.assertEquals(query, receivedQuery)
self.assertEquals(response, receivedResponse)
- # again, over TCP this time
allowed = 0
sent = 0
- for _ in range(int(self._dynBlockBytesPerSecond * 5 / len(response.to_wire()))):
+ # again, over TCP this time, we should never get truncated!
+ for _ in range((self._dynBlockQPS * self._dynBlockPeriod) + 1):
(receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
- sent = sent + len(response.to_wire())
- if receivedQuery:
- receivedQuery.id = query.id
- self.assertEquals(query, receivedQuery)
- self.assertEquals(response, receivedResponse)
- allowed = allowed + len(response.to_wire())
- else:
- # the query has not reached the responder,
- # let's clear the response queue
- self.clearToResponderQueue()
- # and stop right there, otherwise we might
- # wait for so long that the dynblock is gone
- # by the time we finished
- break
+ sent = sent + 1
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(receivedResponse, response)
+ receivedQuery.id = query.id
+ allowed = allowed + 1
- # we might be already blocked, but we should have been able to send
- # at least self._dynBlockBytesPerSecond bytes
- self.assertGreaterEqual(allowed, self._dynBlockBytesPerSecond)
+ self.assertEquals(allowed, sent)
- if allowed == sent:
- # wait for the maintenance function to run
- time.sleep(2)
+class TestDynBlockServFails(DynBlocksTest):
- # we should now be dropped for up to self._dynBlockDuration + self._dynBlockPeriod
- (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
- self.assertEquals(receivedResponse, None)
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ function maintenance()
+ addDynBlocks(exceedServFails(%d, %d), "Exceeded servfail rate", %d)
+ end
+ newServer{address="127.0.0.1:%s"}
+ """
- # wait until we are not blocked anymore
- time.sleep(self._dynBlockDuration + self._dynBlockPeriod)
+ def testDynBlocksServFailRate(self):
+ """
+ Dyn Blocks: Server Failure Rate
+ """
+ name = 'servfailrate.dynblocks.tests.powerdns.com.'
+ self.doTestRCodeRate(name, dns.rcode.SERVFAIL)
- # this one should succeed
- (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
- receivedQuery.id = query.id
- self.assertEquals(query, receivedQuery)
- self.assertEquals(response, receivedResponse)
+class TestDynBlockGroupServFails(DynBlocksTest):
+
+ _dynBlockQPS = 10
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ local dbr = dynBlockRulesGroup()
+ dbr:setRCodeRate(dnsdist.SERVFAIL, %d, %d, "Exceeded query rate", %d)
+
+ function maintenance()
+ dbr:apply()
+ end
+
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksServFailRate(self):
+ """
+ Dyn Blocks (group): Server Failure Rate
+ """
+ name = 'servfailrate.group.dynblocks.tests.powerdns.com.'
+ self.doTestRCodeRate(name, dns.rcode.SERVFAIL)
+
+class TestDynBlockResponseBytes(DynBlocksTest):
+
+ _dynBlockBytesPerSecond = 200
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _consoleKey = DNSDistTest.generateConsoleKey()
+ _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+ _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockBytesPerSecond', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ setKey("%s")
+ controlSocket("127.0.0.1:%s")
+ function maintenance()
+ addDynBlocks(exceedRespByterate(%d, %d), "Exceeded response byterate", %d)
+ end
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksResponseByteRate(self):
+ """
+ Dyn Blocks: Response Byte Rate
+ """
+ name = 'responsebyterate.dynblocks.tests.powerdns.com.'
+ self.doTestResponseByteRate(name)
+
+class TestDynBlockGroupResponseBytes(DynBlocksTest):
+
+ _dynBlockBytesPerSecond = 200
+ _dynBlockPeriod = 2
+ _dynBlockDuration = 5
+ _consoleKey = DNSDistTest.generateConsoleKey()
+ _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+ _config_params = ['_consoleKeyB64', '_consolePort', '_dynBlockBytesPerSecond', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort']
+ _config_template = """
+ setKey("%s")
+ controlSocket("127.0.0.1:%s")
+ local dbr = dynBlockRulesGroup()
+ dbr:setResponseByteRate(%d, %d, "Exceeded query rate", %d)
+
+ function maintenance()
+ dbr:apply()
+ end
+
+ newServer{address="127.0.0.1:%s"}
+ """
+
+ def testDynBlocksResponseByteRate(self):
+ """
+ Dyn Blocks (group) : Response Byte Rate
+ """
+ name = 'responsebyterate.group.dynblocks.tests.powerdns.com.'
+ self.doTestResponseByteRate(name)