From 7b9254324102aedec30b964d497a2d4025c80471 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Tue, 23 May 2017 13:57:02 +0100 Subject: [PATCH] dnsdist: Add an optional action to `addDynBlocks()` This allows using different actions for different blocks, refusing some and dropping others. --- pdns/README-dnsdist.md | 2 +- pdns/dnsdist-console.cc | 3 +- pdns/dnsdist-lua2.cc | 8 +- pdns/dnsdist.cc | 12 +- pdns/dnsdist.hh | 126 +++++++++++---------- regression-tests.dnsdist/dnsdisttests.py | 4 +- regression-tests.dnsdist/test_DynBlocks.py | 105 +++++++++++++++++ 7 files changed, 189 insertions(+), 71 deletions(-) diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index 6585a76548..2f61a352b8 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -1530,7 +1530,7 @@ instantiate a server with additional parameters * `maintenance()`: called every second by dnsdist if defined, call functions below from it * `clearDynBlocks()`: clear all dynamic blocks * `showDynBlocks()`: show dynamic blocks in force - * `addDynBlocks(addresses, message[, seconds])`: block the set of addresses with message `msg`, for `seconds` seconds (10 by default) + * `addDynBlocks(addresses, message[, seconds[, action]])`: block the set of addresses with message `msg`, for `seconds` seconds (10 by default), applying `action` (default to the one set with `setDynBlocksAction()`) * `setDynBlocksAction(DNSAction)`: set which action is performed when a query is blocked. Only DNSAction.Drop (the default) and DNSAction.Refused are supported * `addBPFFilterDynBlocks(addresses, DynBPFFilter[, seconds])`: block the set of addresses using the supplied BPF Filter, for `seconds` seconds (10 by default) * `exceedServFails(rate, seconds)`: get set of addresses that exceed `rate` servfails/s over `seconds` seconds diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index 167d031095..4bd01e23df 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -278,7 +278,7 @@ const std::vector g_consoleKeywords{ { "addDNSCryptBind", true, "\"127.0.0.1:8443\", \"provider name\", \"/path/to/resolver.cert\", \"/path/to/resolver.key\", [false], [TCP Fast Open queue size]", "listen to incoming DNSCrypt queries on 127.0.0.1 port 8443, with a provider name of `provider name`, using a resolver certificate and associated key stored respectively in the `resolver.cert` and `resolver.key` files. The fifth optional parameter sets SO_REUSEPORT when available. The last parameter sets the TCP Fast Open queue size, enabling TCP Fast Open when available and the value is larger than 0" }, { "addDomainBlock", true, "domain", "block queries within this domain" }, { "addDomainSpoof", true, "domain, ip[, ip6]", "generate answers for A/AAAA/ANY queries using the ip parameters" }, - { "addDynBlocks", true, "addresses, message[, seconds]", "block the set of addresses with message `msg`, for `seconds` seconds (10 by default)" }, + { "addDynBlocks", true, "addresses, message[, seconds[, action]]", "block the set of addresses with message `msg`, for `seconds` seconds (10 by default), applying `action` (default to the one set with `setDynBlocksAction()`)" }, { "addLocal", true, "netmask, [true], [false], [TCP Fast Open queue size]", "add to addresses we listen on. Second optional parameter sets TCP or not. Third optional parameter sets SO_REUSEPORT when available. Last parameter sets the TCP Fast Open queue size, enabling TCP Fast Open when available and the value is larger than 0" }, { "addLuaAction", true, "x, func", "where 'x' is all the combinations from `addPoolRule`, and func is a function with the parameter `dq`, which returns an action to be taken on this packet. Good for rare packets but where you want to do a lot of processing" }, { "addLuaResponseAction", true, "x, func", "where 'x' is all the combinations from `addPoolRule`, and func is a function with the parameter `dr`, which returns an action to be taken on this response packet. Good for rare packets but where you want to do a lot of processing" }, @@ -357,6 +357,7 @@ const std::vector g_consoleKeywords{ { "setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us" }, { "setAPIWritable", true, "bool, dir", "allow modifications via the API. if `dir` is set, it must be a valid directory where the configuration files will be written by the API" }, { "setDNSSECPool", true, "pool name", "move queries requesting DNSSEC processing to this pool" }, + { "setDynBlocksAction", true, "action", "set which action is performed when a query is blocked. Only DNSAction.Drop (the default) and DNSAction.Refused are supported" }, { "setECSOverride", true, "bool", "whether to override an existing EDNS Client Subnet value in the query" }, { "setECSSourcePrefixV4", true, "prefix-length", "the EDNS Client Subnet prefix-length used for IPv4 queries" }, { "setECSSourcePrefixV6", true, "prefix-length", "the EDNS Client Subnet prefix-length used for IPv6 queries" }, diff --git a/pdns/dnsdist-lua2.cc b/pdns/dnsdist-lua2.cc index 89276ec9b3..79728815e0 100644 --- a/pdns/dnsdist-lua2.cc +++ b/pdns/dnsdist-lua2.cc @@ -253,7 +253,7 @@ void moreLua(bool client) }); g_lua.writeFunction("addDynBlocks", - [](const map& m, const std::string& msg, boost::optional seconds) { + [](const map& m, const std::string& msg, boost::optional seconds, boost::optional action) { setLuaSideEffect(); auto slow = g_dynblockNMG.getCopy(); struct timespec until, now; @@ -273,7 +273,7 @@ void moreLua(bool client) else expired=true; } - DynBlock db{msg,until}; + DynBlock db{msg,until,DNSName(),(action ? *action : DNSAction::Action::None)}; db.blocks=count; if(!got || expired) warnlog("Inserting dynamic block for %s for %d seconds: %s", capair.first.toString(), actualSeconds, msg); @@ -283,7 +283,7 @@ void moreLua(bool client) }); g_lua.writeFunction("addDynBlockSMT", - [](const vector >&names, const std::string& msg, boost::optional seconds) { + [](const vector >&names, const std::string& msg, boost::optional seconds, boost::optional action) { setLuaSideEffect(); auto slow = g_dynblockSMT.getCopy(); struct timespec until, now; @@ -306,7 +306,7 @@ void moreLua(bool client) expired=true; } - DynBlock db{msg,until,domain}; + DynBlock db{msg,until,domain,(action ? *action : DNSAction::Action::None)}; db.blocks=count; if(!got || expired) warnlog("Inserting dynamic block for %s for %d seconds: %s", domain, actualSeconds, msg); diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index c89214e95f..cb6f93cc3a 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -853,7 +853,11 @@ bool processQuery(LocalStateHolder >& localDynNMGBlock, if(now < got->second.until) { g_stats.dynBlocked++; got->second.blocks++; - if (g_dynBlockAction == DNSAction::Action::Refused) { + DNSAction::Action action = got->second.action; + if (action == DNSAction::Action::None) { + action = g_dynBlockAction; + } + if (action == DNSAction::Action::Refused) { vinfolog("Query from %s refused because of dynamic block", dq.remote->toStringWithPort()); dq.dh->rcode = RCode::Refused; dq.dh->qr=true; @@ -870,7 +874,11 @@ bool processQuery(LocalStateHolder >& localDynNMGBlock, if(now < got->until) { g_stats.dynBlocked++; got->blocks++; - if (g_dynBlockAction == DNSAction::Action::Refused) { + DNSAction::Action action = got->action; + if (action == DNSAction::Action::None) { + action = g_dynBlockAction; + } + if (action == DNSAction::Action::Refused) { vinfolog("Query from %s for %s refused because of dynamic block", dq.remote->toStringWithPort(), dq.qname->toString()); dq.dh->rcode = RCode::Refused; dq.dh->qr=true; diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 36823820dd..9658e015e9 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -47,6 +47,68 @@ void* carbonDumpThread(); uint64_t uptimeOfProcess(const std::string& str); +extern uint16_t g_ECSSourcePrefixV4; +extern uint16_t g_ECSSourcePrefixV6; +extern bool g_ECSOverride; + +struct DNSQuestion +{ + DNSQuestion(const DNSName* name, uint16_t type, uint16_t class_, const ComboAddress* lc, const ComboAddress* rem, struct dnsheader* header, size_t bufferSize, uint16_t queryLen, bool isTcp): qname(name), qtype(type), qclass(class_), local(lc), remote(rem), dh(header), size(bufferSize), len(queryLen), ecsPrefixLength(rem->sin4.sin_family == AF_INET ? g_ECSSourcePrefixV4 : g_ECSSourcePrefixV6), tcp(isTcp), ecsOverride(g_ECSOverride) { } + +#ifdef HAVE_PROTOBUF + boost::uuids::uuid uniqueId; +#endif + const DNSName* qname; + const uint16_t qtype; + const uint16_t qclass; + const ComboAddress* local; + const ComboAddress* remote; + struct dnsheader* dh; + size_t size; + uint16_t len; + uint16_t ecsPrefixLength; + const bool tcp; + bool skipCache{false}; + bool ecsOverride; + bool useECS{true}; +}; + +struct DNSResponse : DNSQuestion +{ + DNSResponse(const DNSName* name, uint16_t type, uint16_t class_, const ComboAddress* lc, const ComboAddress* rem, struct dnsheader* header, size_t bufferSize, uint16_t queryLen, bool isTcp, const struct timespec* queryTime_): DNSQuestion(name, type, class_, lc, rem, header, bufferSize, queryLen, isTcp), queryTime(queryTime_) { } + + const struct timespec* queryTime; +}; + +/* so what could you do: + drop, + fake up nxdomain, + provide actual answer, + allow & and stop processing, + continue processing, + modify header: (servfail|refused|notimp), set TC=1, + send to pool */ + +class DNSAction +{ +public: + enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, None}; + virtual Action operator()(DNSQuestion*, string* ruleresult) const =0; + virtual string toString() const = 0; + virtual std::unordered_map getStats() const + { + return {{}}; + } +}; + +class DNSResponseAction +{ +public: + enum class Action { Allow, Delay, Drop, HeaderModify, None }; + virtual Action operator()(DNSResponse*, string* ruleresult) const =0; + virtual string toString() const = 0; +}; + struct DynBlock { DynBlock& operator=(const DynBlock& rhs) @@ -54,6 +116,7 @@ struct DynBlock reason=rhs.reason; until=rhs.until; domain=rhs.domain; + action=rhs.action; blocks.store(rhs.blocks); return *this; } @@ -61,6 +124,7 @@ struct DynBlock string reason; struct timespec until; DNSName domain; + DNSAction::Action action; mutable std::atomic blocks; }; @@ -523,39 +587,6 @@ struct DownstreamState }; using servers_t =vector>; -extern uint16_t g_ECSSourcePrefixV4; -extern uint16_t g_ECSSourcePrefixV6; -extern bool g_ECSOverride; - -struct DNSQuestion -{ - DNSQuestion(const DNSName* name, uint16_t type, uint16_t class_, const ComboAddress* lc, const ComboAddress* rem, struct dnsheader* header, size_t bufferSize, uint16_t queryLen, bool isTcp): qname(name), qtype(type), qclass(class_), local(lc), remote(rem), dh(header), size(bufferSize), len(queryLen), ecsPrefixLength(rem->sin4.sin_family == AF_INET ? g_ECSSourcePrefixV4 : g_ECSSourcePrefixV6), tcp(isTcp), ecsOverride(g_ECSOverride) { } - -#ifdef HAVE_PROTOBUF - boost::uuids::uuid uniqueId; -#endif - const DNSName* qname; - const uint16_t qtype; - const uint16_t qclass; - const ComboAddress* local; - const ComboAddress* remote; - struct dnsheader* dh; - size_t size; - uint16_t len; - uint16_t ecsPrefixLength; - const bool tcp; - bool skipCache{false}; - bool ecsOverride; - bool useECS{true}; -}; - -struct DNSResponse : DNSQuestion -{ - DNSResponse(const DNSName* name, uint16_t type, uint16_t class_, const ComboAddress* lc, const ComboAddress* rem, struct dnsheader* header, size_t bufferSize, uint16_t queryLen, bool isTcp, const struct timespec* queryTime_): DNSQuestion(name, type, class_, lc, rem, header, bufferSize, queryLen, isTcp), queryTime(queryTime_) { } - - const struct timespec* queryTime; -}; - typedef std::function blockfilter_t; template using NumberedVector = std::vector >; @@ -572,35 +603,6 @@ public: mutable std::atomic d_matches{0}; }; -/* so what could you do: - drop, - fake up nxdomain, - provide actual answer, - allow & and stop processing, - continue processing, - modify header: (servfail|refused|notimp), set TC=1, - send to pool */ - -class DNSAction -{ -public: - enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, None}; - virtual Action operator()(DNSQuestion*, string* ruleresult) const =0; - virtual string toString() const = 0; - virtual std::unordered_map getStats() const - { - return {{}}; - } -}; - -class DNSResponseAction -{ -public: - enum class Action { Allow, Delay, Drop, HeaderModify, None }; - virtual Action operator()(DNSResponse*, string* ruleresult) const =0; - virtual string toString() const = 0; -}; - using NumberedServerVector = NumberedVector>; typedef std::function(const NumberedServerVector& servers, const DNSQuestion*)> policyfunc_t; diff --git a/regression-tests.dnsdist/dnsdisttests.py b/regression-tests.dnsdist/dnsdisttests.py index ecaf2de399..0c7d894cb8 100644 --- a/regression-tests.dnsdist/dnsdisttests.py +++ b/regression-tests.dnsdist/dnsdisttests.py @@ -391,11 +391,13 @@ class DNSDistTest(unittest.TestCase): sock.connect(("127.0.0.1", cls._consolePort)) sock.send(ourNonce) theirNonce = sock.recv(len(ourNonce)) + if len(theirNonce) != len(ourNonce): + print("Received a nonce of size %, expecting %, console command will not be sent!" % (len(theirNonce), len(ourNonce))) + return None halfNonceSize = len(ourNonce) / 2 readingNonce = ourNonce[0:halfNonceSize] + theirNonce[halfNonceSize:] writingNonce = theirNonce[0:halfNonceSize] + ourNonce[halfNonceSize:] - msg = cls._encryptConsole(command, writingNonce) sock.send(struct.pack("!I", len(msg))) sock.send(msg) diff --git a/regression-tests.dnsdist/test_DynBlocks.py b/regression-tests.dnsdist/test_DynBlocks.py index fb7c0ce54e..548a9889c5 100644 --- a/regression-tests.dnsdist/test_DynBlocks.py +++ b/regression-tests.dnsdist/test_DynBlocks.py @@ -209,6 +209,111 @@ class TestDynBlockQPSRefused(DNSDistTest): 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.' + 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) + + allowed = 0 + sent = 0 + for _ in xrange((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, refusedResponse) + # the query has not reached the responder, + # let's clear the response queue + self.clearToResponderQueue() + + # we might be already blocked, 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 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, refusedResponse) + + # 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 + for _ in xrange((self._dynBlockQPS * self._dynBlockPeriod) + 1): + (receivedQuery, receivedResponse) = self.sendTCPQuery(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, refusedResponse) + # the query has not reached the responder, + # let's clear the response queue + self.clearToResponderQueue() + + # we might be already blocked, 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 'refused' for up to self._dynBlockDuration + self._dynBlockPeriod + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, refusedResponse) + + # wait until we are not blocked anymore + time.sleep(self._dynBlockDuration + self._dynBlockPeriod) + + # this one should succeed + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + class TestDynBlockServFails(DNSDistTest): _dynBlockQPS = 10 -- 2.47.2