]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Make a `truncate` action available to DynBlock and Lua 5386/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 8 Jun 2017 11:01:17 +0000 (13:01 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 8 Jun 2017 11:01:17 +0000 (13:01 +0200)
pdns/README-dnsdist.md
pdns/dnsdist-lua.cc
pdns/dnsdist-lua2.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsrulactions.hh
regression-tests.dnsdist/test_Advanced.py
regression-tests.dnsdist/test_DynBlocks.py

index c5a79312d688dc173298dfb6cdb84aad77198923..1a8c0a6adff90cf60964f48f87ea277456b42405 100644 (file)
@@ -590,6 +590,7 @@ Valid return values for `LuaAction` functions are:
  * DNSAction.Pool: use the specified pool to forward this query
  * DNSAction.Refused: return a response with a Refused rcode
  * DNSAction.Spoof: spoof the response using the supplied IPv4 (A), IPv6 (AAAA) or string (CNAME) value
+ * DNSAction.Truncate: return a response with TC=1
 
 The same feature exists to hand off some responses for Lua inspection, using `addLuaResponseAction(x, func)`.
 
@@ -1531,7 +1532,7 @@ instantiate a server with additional parameters
     * `clearDynBlocks()`: clear all dynamic blocks
     * `showDynBlocks()`: show dynamic blocks in force
     * `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
+    * `setDynBlocksAction(DNSAction)`: set which action is performed when a query is blocked. Only DNSAction.Drop (the default), DNSAction.Refused and DNSAction.Truncate 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
     * `exceedNXDOMAINs(rate, seconds)`: get set of addresses that exceed `rate` NXDOMAIN/s over `seconds` seconds
index a56181b878071801334a89b524155de88614d783..14279d4860ea19001db5e92d2de22240fe0555c3 100644 (file)
@@ -209,8 +209,9 @@ vector<std::function<void(void)>> setupLua(bool client, const std::string& confi
       {"HeaderModify", (int)DNSAction::Action::HeaderModify},
       {"Pool", (int)DNSAction::Action::Pool},
       {"None",(int)DNSAction::Action::None},
-      {"Delay", (int)DNSAction::Action::Delay}}
-    );
+      {"Delay", (int)DNSAction::Action::Delay},
+      {"Truncate", (int)DNSAction::Action::Truncate}
+    });
 
   g_lua.writeVariable("DNSResponseAction", std::unordered_map<string,int>{
       {"Allow",        (int)DNSResponseAction::Action::Allow        },
index 3f31e2bee50041e0da1945db837c00a34fbbafc6..c25a8780a5848e3f2881d1817025310cdc71bf06 100644 (file)
@@ -317,12 +317,12 @@ void moreLua(bool client)
 
   g_lua.writeFunction("setDynBlocksAction", [](DNSAction::Action action) {
       if (!g_configurationDone) {
-        if (action == DNSAction::Action::Drop || action == DNSAction::Action::Refused) {
+        if (action == DNSAction::Action::Drop || action == DNSAction::Action::Refused || action == DNSAction::Action::Truncate) {
           g_dynBlockAction = action;
         }
         else {
-          errlog("Dynamic blocks action can only be Drop or Refused!");
-          g_outputBuffer="Dynamic blocks action can only be Drop or Refused!\n";
+          errlog("Dynamic blocks action can only be Drop, Refused or Truncate!");
+          g_outputBuffer="Dynamic blocks action can only be Drop, Refused or Truncate!\n";
         }
       } else {
         g_outputBuffer="Dynamic blocks action cannot be altered at runtime!\n";
index 592eae28e72921fba4a98570d99918575b235f57..9a365662c22e15a11834737846841980446ab718 100644 (file)
@@ -863,6 +863,12 @@ bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& localDynNMGBlock,
         dq.dh->qr=true;
         return true;
       }
+      else if (action == DNSAction::Action::Truncate && !dq.tcp) {
+        vinfolog("Query from %s truncated because of dynamic block", dq.remote->toStringWithPort());
+        dq.dh->tc = true;
+        dq.dh->qr = true;
+        return true;
+      }
       else {
         vinfolog("Query from %s dropped because of dynamic block", dq.remote->toStringWithPort());
         return false;
@@ -884,6 +890,12 @@ bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& localDynNMGBlock,
         dq.dh->qr=true;
         return true;
       }
+      else if (action == DNSAction::Action::Truncate && !dq.tcp) {
+        vinfolog("Query from %s for %s truncated because of dynamic block", dq.remote->toStringWithPort(), dq.qname->toString());
+        dq.dh->tc = true;
+        dq.dh->qr = true;
+        return true;
+      }
       else {
         vinfolog("Query from %s for %s dropped because of dynamic block", dq.remote->toStringWithPort(), dq.qname->toString());
         return false;
@@ -931,6 +943,11 @@ bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& localDynNMGBlock,
         spoofResponseFromString(dq, ruleresult);
         return true;
         break;
+      case DNSAction::Action::Truncate:
+        dq.dh->tc = true;
+        dq.dh->qr = true;
+        return true;
+        break;
       case DNSAction::Action::HeaderModify:
         return true;
         break;
index a65060f1f5ab497aa849a8c6afca2e0982841b4d..1b05cb47b6669a098eafa0962ea541a8eab4973d 100644 (file)
@@ -92,7 +92,7 @@ struct DNSResponse : DNSQuestion
 class DNSAction
 {
 public:
-  enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, None};
+  enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, Truncate, None};
   virtual Action operator()(DNSQuestion*, string* ruleresult) const =0;
   virtual string toString() const = 0;
   virtual std::unordered_map<string, double> getStats() const 
index a9cf5e50defce8bf03d2f0387bf36c64d9085d75..a7b2738fdcd9998651b4157edf5cbaccd8438ff5 100644 (file)
@@ -779,9 +779,7 @@ class TCAction : public DNSAction
 public:
   DNSAction::Action operator()(DNSQuestion* dq, string* ruleresult) const override
   {
-    dq->dh->tc = true;
-    dq->dh->qr = true; // for good measure
-    return Action::HeaderModify;
+    return Action::Truncate;
   }
   string toString() const override
   {
index 00ec12b36e039ae804882385b7e4142823dad81e..7864d8d3c01768ee8f0769b941b57afcecde7c5a 100644 (file)
@@ -1236,8 +1236,6 @@ class TestAdvancedLuaDO(DNSDistTest):
         (_, receivedResponse) = self.sendUDPQuery(queryWithDO, response=None, useQueue=False)
         self.assertTrue(receivedResponse)
         doResponse.id = receivedResponse.id
-        print(doResponse)
-        print(receivedResponse)
         self.assertEquals(receivedResponse, doResponse)
 
         (_, receivedResponse) = self.sendTCPQuery(queryWithDO, response=None, useQueue=False)
@@ -1245,6 +1243,85 @@ class TestAdvancedLuaDO(DNSDistTest):
         doResponse.id = receivedResponse.id
         self.assertEquals(receivedResponse, doResponse)
 
+class TestAdvancedLuaRefused(DNSDistTest):
+
+    _config_template = """
+    function refuse(dq)
+        return DNSAction.Refused, ""
+    end
+    addLuaAction(AllRule(), refuse)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testRefusedViaLua(self):
+        """
+        Advanced: Refused via Lua
+        """
+        name = 'refused.advanced.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+        refusedResponse = dns.message.make_response(query)
+        refusedResponse.set_rcode(dns.rcode.REFUSED)
+
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        refusedResponse.id = receivedResponse.id
+        self.assertEquals(receivedResponse, refusedResponse)
+
+        (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        refusedResponse.id = receivedResponse.id
+        self.assertEquals(receivedResponse, refusedResponse)
+
+class TestAdvancedLuaTruncated(DNSDistTest):
+
+    _config_template = """
+    function trunc(dq)
+        if not dq.tcp then
+          return DNSAction.Truncate, ""
+        end
+        return DNSAction.None, ""
+    end
+    addLuaAction(AllRule(), trunc)
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testTCViaLua(self):
+        """
+        Advanced: TC via Lua
+        """
+        name = 'tc.advanced.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response.answer.append(rrset)
+
+        truncatedResponse = dns.message.make_response(query)
+        truncatedResponse.flags |= dns.flags.TC
+
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        truncatedResponse.id = receivedResponse.id
+        self.assertEquals(receivedResponse, truncatedResponse)
+
+        # no truncation over TCP
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, response)
+
 class TestStatNodeRespRingSince(DNSDistTest):
 
     _consoleKey = DNSDistTest.generateConsoleKey()
index 548a9889c554ebcc8f3c909df17522eb915dc02d..b0aea87c6ece0ce6802e1b71849ca206aa354b79 100644 (file)
@@ -314,6 +314,86 @@ class TestDynBlockQPSActionRefused(DNSDistTest):
         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 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, truncatedResponse)
+                # 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 'truncated' for up to self._dynBlockDuration + self._dynBlockPeriod
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertEquals(receivedResponse, truncatedResponse)
+
+        # 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 xrange((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