]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add an optional action to `addDynBlocks()`
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 23 May 2017 12:57:02 +0000 (13:57 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 23 May 2017 12:57:02 +0000 (13:57 +0100)
This allows using different actions for different blocks, refusing
some and dropping others.

pdns/README-dnsdist.md
pdns/dnsdist-console.cc
pdns/dnsdist-lua2.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
regression-tests.dnsdist/dnsdisttests.py
regression-tests.dnsdist/test_DynBlocks.py

index 6585a7654868031e62a09cd4cd8e8ef48dabfe18..2f61a352b8f3f5cbe41a94fb45d5213a267f8ad9 100644 (file)
@@ -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
index 167d0310952b9659778234d1842161d7caa627ef..4bd01e23df94d7485cd109aabf2c798f8d1ae76c 100644 (file)
@@ -278,7 +278,7 @@ const std::vector<ConsoleKeyword> 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<ConsoleKeyword> 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" },
index 89276ec9b369020ec8fd04e0eaaa1b746a2a0f46..79728815e0884228c5f588e7ecd42ba24c292e24 100644 (file)
@@ -253,7 +253,7 @@ void moreLua(bool client)
     });
 
   g_lua.writeFunction("addDynBlocks", 
-                         [](const map<ComboAddress,int>& m, const std::string& msg, boost::optional<int> seconds) { 
+                      [](const map<ComboAddress,int>& m, const std::string& msg, boost::optional<int> seconds, boost::optional<DNSAction::Action> 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<pair<unsigned int, string> >&names, const std::string& msg, boost::optional<int> seconds) { 
+                      [](const vector<pair<unsigned int, string> >&names, const std::string& msg, boost::optional<int> seconds, boost::optional<DNSAction::Action> 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);
index c89214e95fd621d3b85391201c762f13da0bafce..cb6f93cc3a7c810a9e815c72eb8fd6efd99e4d93 100644 (file)
@@ -853,7 +853,11 @@ bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& 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<NetmaskTree<DynBlock> >& 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;
index 36823820dd4de0cb19c464bc0dc18b83be5168c8..9658e015e99b6a88dd283954c3cf81223cd460cc 100644 (file)
 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<string, double> 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<unsigned int> blocks;
 };
 
@@ -523,39 +587,6 @@ struct DownstreamState
 };
 using servers_t =vector<std::shared_ptr<DownstreamState>>;
 
-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<bool(const DNSQuestion*)> blockfilter_t;
 template <class T> using NumberedVector = std::vector<std::pair<unsigned int, T> >;
 
@@ -572,35 +603,6 @@ public:
   mutable std::atomic<uint64_t> 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<string, double> 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<shared_ptr<DownstreamState>>;
 typedef std::function<shared_ptr<DownstreamState>(const NumberedServerVector& servers, const DNSQuestion*)> policyfunc_t;
 
index ecaf2de39902cb90916e993136f540e44d69be9b..0c7d894cb8104548417dba6b9b4ce72d2277bb46 100644 (file)
@@ -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)
index fb7c0ce54e52116b7acac56c0bc77154aa6eebe0..548a9889c554ebcc8f3c909df17522eb915dc02d 100644 (file)
@@ -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