]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add RCodeRule(), Allow, Delay and Drop response actions
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 4 Aug 2016 10:37:47 +0000 (12:37 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 19 Aug 2016 07:39:40 +0000 (09:39 +0200)
pdns/README-dnsdist.md
pdns/dnsdist-console.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-lua2.cc
pdns/dnsdist-tcp.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsrulactions.hh
regression-tests.dnsdist/test_Responses.py [new file with mode: 0644]

index 9afdd60b74f961b274a4da1b4a228f8eaf921ab6..4a740b7598bb5d113c42c106fabd22da3b8830fa 100644 (file)
@@ -333,6 +333,7 @@ Rules have selectors and actions. Current selectors are:
  * QType (QTypeRule)
  * RegexRule on query name
  * RE2Rule on query name (optional)
+ * Response code
  * Packet requests DNSSEC processing
  * Query received over UDP or TCP
  * Opcode (OpcodeRule)
@@ -363,6 +364,9 @@ Current actions are:
 
 Current response actions are:
 
+ * Allow (AllowResponseAction)
+ * Delay a response by n milliseconds (DelayResponseAction), over UDP only
+ * Drop (DropResponseAction)
  * Log response content to a remote server (RemoteLogResponseAction)
 
 Rules can be added via:
@@ -398,6 +402,7 @@ A DNS rule can be:
  * a QNameLabelsCountRule
  * a QNameWireLengthRule
  * a QTypeRule
+ * a RCodeRule
  * a RegexRule
  * a RE2Rule
  * a RecordsCountRule
@@ -1230,6 +1235,7 @@ instantiate a server with additional parameters
     * `QNameLabelsCountRule(min, max)`: matches if the qname has less than `min` or more than `max` labels
     * `QNameWireLengthRule(min, max)`: matches if the qname's length on the wire is less than `min` or more than `max` bytes
     * `QTypeRule(qtype)`: matches queries with the specified qtype
+    * `RCodeRule(rcode)`: matches queries or responses the specified rcode
     * `RegexRule(regex)`: matches the query name against the supplied regex
     * `RecordsCountRule(section, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records in the `section` section
     * `RecordsTypeCountRule(section, type, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records of type `type` in the `section` section
@@ -1254,9 +1260,12 @@ instantiate a server with additional parameters
     * `topRule()`: move the last rule to the first position
  * Built-in Actions for Rules:
     * `AllowAction()`: let these packets go through
+    * `AllowResponseAction()`: let these packets go through
     * `DelayAction(milliseconds)`: delay the response by the specified amount of milliseconds (UDP-only)
+    * `DelayResponseAction(milliseconds)`: delay the response by the specified amount of milliseconds (UDP-only)
     * `DisableValidationAction()`: set the CD bit in the question, let it go through
     * `DropAction()`: drop these packets
+    * `DropResponseAction()`: drop these packets
     * `LogAction([filename], [binary], [append], [buffered])`: Log a line for each query, to the specified file if any, to the console (require verbose) otherwise. When logging to a file, the `binary` optional parameter specifies whether we log in binary form (default) or in textual form, the `append` optional parameter specifies whether we open the file for appending or truncate each time (default), and the `buffered` optional parameter specifies whether writes to the file are buffered (default) or not.
     * `NoRecurseAction()`: strip RD bit from the question, let it go through
     * `PoolAction(poolname)`: set the packet into the specified pool
index 619f3790c9c1bde6209cf28217d382a89debac2b..731ea810e0db3b54089a606e7f0bcc20a85e4fe3 100644 (file)
@@ -238,6 +238,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "addQPSPoolRule", true, "x, limit, pool", "like `addPoolRule`, but only select at most 'limit' queries/s for this pool, letting the subsequent rules apply otherwise" },
   { "addResponseAction", true, "DNS rule, DNS response action", "add a response rule" },
   { "AllowAction", true, "", "let these packets go through" },
+  { "AllowResponseAction", true, "", "let these packets go through" },
   { "AllRule", true, "", "matches all traffic" },
   { "AndRule", true, "list of DNS rules", "matches if all sub-rules matches" },
   { "benchRule", true, "DNS Rule [, iterations [, suffix]]", "bench the specified DNS rule" },
@@ -246,9 +247,11 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "clearDynBlocks", true, "", "clear all dynamic blocks" },
   { "clearRules", true, "", "remove all current rules" },
   { "DelayAction", true, "milliseconds", "delay the response by the specified amount of milliseconds (UDP-only)" },
+  { "DelayResponseAction", true, "milliseconds", "delay the response by the specified amount of milliseconds (UDP-only)" },
   { "delta", true, "", "shows all commands entered that changed the configuration" },
   { "DisableValidationAction", true, "", "set the CD bit in the question, let it go through" },
   { "DropAction", true, "", "drop these packets" },
+  { "DropResponseAction", true, "", "drop these packets" },
   { "dumpStats", true, "", "print all statistics we gather" },
   { "exceedNXDOMAINs", true, "rate, seconds", "get set of addresses that exceed `rate` NXDOMAIN/s over `seconds` seconds" },
   { "exceedQRate", true, "rate, seconds", "get set of address that exceed `rate` queries/s over `seconds` seconds" },
@@ -291,6 +294,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "QNameLabelsCountRule", true, "min, max", "matches if the qname has less than `min` or more than `max` labels" },
   { "QNameWireLengthRule", true, "min, max", "matches if the qname's length on the wire is less than `min` or more than `max` bytes" },
   { "QTypeRule", true, "qtype", "matches queries with the specified qtype" },
+  { "RCodeRule", true, "rcode", "matches responses with the specified rcode" },
   { "setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us" },
   { "setDNSSECPool", true, "pool name", "move queries requesting DNSSEC processing to this pool" },
   { "setECSOverride", true, "bool", "whether to override an existing EDNS Client Subnet value in the query" },
index bd997e197ff207c8a13569407fc13f4327765ba3..75aa60ac309f70fd1ae7acacaeeeb1971efdbdf6 100644 (file)
@@ -141,8 +141,11 @@ vector<std::function<void(void)>> setupLua(bool client, const std::string& confi
     );
 
   g_lua.writeVariable("DNSResponseAction", std::unordered_map<string,int>{
-      {"None",(int)DNSResponseAction::Action::None}}
-    );
+      {"Allow",        (int)DNSResponseAction::Action::Allow        },
+      {"Delay",        (int)DNSResponseAction::Action::Delay        },
+      {"HeaderModify", (int)DNSResponseAction::Action::HeaderModify },
+      {"None",         (int)DNSResponseAction::Action::None         }
+    });
 
   g_lua.writeVariable("DNSClass", std::unordered_map<string,int>{
       {"IN",    QClass::IN    },
@@ -903,6 +906,10 @@ vector<std::function<void(void)>> setupLua(bool client, const std::string& confi
       return std::shared_ptr<DNSRule>(new QNameWireLengthRule(min, max));
     });
 
+  g_lua.writeFunction("RCodeRule", [](int rcode) {
+      return std::shared_ptr<DNSRule>(new RCodeRule(rcode));
+    });
+
   g_lua.writeFunction("addAction", [](luadnsrule_t var, std::shared_ptr<DNSAction> ea)
                      {
                         setLuaSideEffect();
index 12066df03a3e88110cd7dbda32fbdc8fe44d44e5..801a9e4f8ea8a30c45f0c58d21bd07973730e2c9 100644 (file)
@@ -667,6 +667,18 @@ void moreLua(bool client)
     g_lua.writeFunction("setVerboseHealthChecks", [](bool verbose) { g_verboseHealthChecks=verbose; });
     g_lua.writeFunction("setStaleCacheEntriesTTL", [](uint32_t ttl) { g_staleCacheEntriesTTL = ttl; });
 
+    g_lua.writeFunction("DropResponseAction", []() {
+        return std::shared_ptr<DNSResponseAction>(new DropResponseAction);
+      });
+
+    g_lua.writeFunction("AllowResponseAction", []() {
+        return std::shared_ptr<DNSResponseAction>(new AllowResponseAction);
+      });
+
+    g_lua.writeFunction("DelayResponseAction", [](int msec) {
+        return std::shared_ptr<DNSResponseAction>(new DelayResponseAction(msec));
+      });
+
     g_lua.writeFunction("RemoteLogAction", [](std::shared_ptr<RemoteLogger> logger) {
 #ifdef HAVE_PROTOBUF
         return std::shared_ptr<DNSAction>(new RemoteLogAction(logger));
index 35c96d67129a6ef8dc4837f3fd61cfb2afde6fb7..c76cb1e196fdf0497a1a2e2d7774edfe10241408 100644 (file)
@@ -435,7 +435,7 @@ void* tcpClientThread(int pipefd)
 #ifdef HAVE_PROTOBUF
         dr.uniqueId = dq.uniqueId;
 #endif
-        if (!processResponse(localRespRulactions, dr)) {
+        if (!processResponse(localRespRulactions, dr, &delayMsec)) {
           break;
         }
 
index dbefaa85991404aa0893b3a8080f1f18cd3b9623..6a104d83a8d22e450783d194b27d513771a42b38 100644 (file)
@@ -404,8 +404,8 @@ void* responderThread(std::shared_ptr<DownstreamState> state)
 #ifdef HAVE_PROTOBUF
     dr.uniqueId = ids->uniqueId;
 #endif
-    if (!processResponse(localRespRulactions, dr)) {
-      break;
+    if (!processResponse(localRespRulactions, dr, &ids->delayMsec)) {
+      continue;
     }
 
 #ifdef HAVE_DNSCRYPT
@@ -821,14 +821,31 @@ bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& localDynNMGBlock,
   return true;
 }
 
-bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr)
+bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr, int* delayMsec)
 {
+  DNSResponseAction::Action action=DNSResponseAction::Action::None;
   std::string ruleresult;
   for(const auto& lr : *localRespRulactions) {
     if(lr.first->matches(&dr)) {
       lr.first->d_matches++;
-      /* for now we only support actions returning None */
-      (*lr.second)(&dr, &ruleresult);
+      action=(*lr.second)(&dr, &ruleresult);
+      switch(action) {
+      case DNSResponseAction::Action::Allow:
+        return true;
+        break;
+      case DNSResponseAction::Action::Drop:
+        return false;
+        break;
+      case DNSResponseAction::Action::HeaderModify:
+        return true;
+        break;
+        /* non-terminal actions follow */
+      case DNSResponseAction::Action::Delay:
+        *delayMsec = static_cast<int>(pdns_stou(ruleresult)); // sorry
+        break;
+      case DNSResponseAction::Action::None:
+        break;
+      }
     }
   }
 
index 6e9538c1f5b3edd1547944e6bcee3d79093467f3..9de599632daa5ba8e65391ad60dad2ddac95ff1f 100644 (file)
@@ -449,7 +449,7 @@ public:
 class DNSResponseAction
 {
 public:
-  enum class Action { None };
+  enum class Action { Allow, Delay, Drop, HeaderModify, None };
   virtual Action operator()(DNSResponse*, string* ruleresult) const =0;
   virtual string toString() const = 0;
 };
@@ -669,7 +669,7 @@ void resetLuaSideEffect(); // reset to indeterminate state
 bool responseContentMatches(const char* response, const uint16_t responseLen, const DNSName& qname, const uint16_t qtype, const uint16_t qclass, const ComboAddress& remote);
 bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& localDynBlockNMG,
                   LocalStateHolder<SuffixMatchTree<DynBlock> >& localDynBlockSMT, LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSAction> > > >& localRulactions, blockfilter_t blockFilter, DNSQuestion& dq, string& poolname, int* delayMsec, const struct timespec& now);
-bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr);
+bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr, int* delayMsec);
 bool fixUpResponse(char** response, uint16_t* responseLen, size_t* responseSize, const DNSName& qname, uint16_t origFlags, bool ednsAdded, bool ecsAdded, std::vector<uint8_t>& rewrittenResponse, uint16_t addRoom);
 void restoreFlags(struct dnsheader* dh, uint16_t origFlags);
 
index 28575ea510ee07725c32a0bbd02cf23b3d00dbd4..c7a80870d96e3f721afa7f5ef350622b3e0b0d1e 100644 (file)
@@ -528,6 +528,25 @@ private:
   size_t d_max;
 };
 
+class RCodeRule : public DNSRule
+{
+public:
+  RCodeRule(int rcode) : d_rcode(rcode)
+  {
+  }
+  bool matches(const DNSQuestion* dq) const override
+  {
+    return d_rcode == dq->dh->rcode;
+  }
+  string toString() const override
+  {
+    return "rcode=="+RCode::to_s(d_rcode);
+  }
+private:
+  int d_rcode;
+};
+
+
 class DropAction : public DNSAction
 {
 public:
@@ -996,3 +1015,47 @@ public:
 private:
   std::shared_ptr<RemoteLogger> d_logger;
 };
+
+class DropResponseAction : public DNSResponseAction
+{
+public:
+  DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override
+  {
+    return Action::Drop;
+  }
+  string toString() const override
+  {
+    return "drop";
+  }
+};
+
+class AllowResponseAction : public DNSResponseAction
+{
+public:
+  DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override
+  {
+    return Action::Allow;
+  }
+  string toString() const override
+  {
+    return "allow";
+  }
+};
+
+class DelayResponseAction : public DNSResponseAction
+{
+public:
+  DelayResponseAction(int msec) : d_msec(msec)
+  {}
+  DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override
+  {
+    *ruleresult=std::to_string(d_msec);
+    return Action::Delay;
+  }
+  string toString() const override
+  {
+    return "delay by "+std::to_string(d_msec)+ " msec";
+  }
+private:
+  int d_msec;
+};
diff --git a/regression-tests.dnsdist/test_Responses.py b/regression-tests.dnsdist/test_Responses.py
new file mode 100644 (file)
index 0000000..096b09d
--- /dev/null
@@ -0,0 +1,153 @@
+#!/usr/bin/env python
+from datetime import datetime, timedelta
+import time
+import dns
+from dnsdisttests import DNSDistTest
+
+class TestResponseRuleNXDelayed(DNSDistTest):
+
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+    addResponseAction(RCodeRule(dnsdist.NXDOMAIN), DelayResponseAction(1000))
+    """
+
+    def testNXDelayed(self):
+        """
+        Responses: Delayed on NXDomain
+
+        Send an A query to "delayed.responses.tests.powerdns.com.",
+        check that the response delay is longer than 1000 ms
+        for a NXDomain response over UDP, shorter for a NoError one.
+        """
+        name = 'delayed.responses.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        # NX over UDP
+        response.set_rcode(dns.rcode.NXDOMAIN)
+        begin = datetime.now()
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        end = datetime.now()
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        self.assertTrue((end - begin) > timedelta(0, 1))
+
+        # NoError over UDP
+        response.set_rcode(dns.rcode.NOERROR)
+        begin = datetime.now()
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        end = datetime.now()
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        self.assertTrue((end - begin) < timedelta(0, 1))
+
+        # NX over TCP
+        response.set_rcode(dns.rcode.NXDOMAIN)
+        begin = datetime.now()
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        end = datetime.now()
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+        self.assertTrue((end - begin) < timedelta(0, 1))
+
+class TestResponseRuleQNameDropped(DNSDistTest):
+
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+    addResponseAction("drop.responses.tests.powerdns.com.", DropResponseAction())
+    """
+
+    def testDropped(self):
+        """
+        Responses: Dropped on QName
+
+        Send an A query to "drop.responses.tests.powerdns.com.",
+        check that the response (not the query) is dropped.
+        """
+        name = 'drop.responses.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, None)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, None)
+
+    def testNotDropped(self):
+        """
+        Responses: NOT Dropped on QName
+
+        Send an A query to "dontdrop.responses.tests.powerdns.com.",
+        check that the response is not dropped.
+        """
+        name = 'dontdrop.responses.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+
+class TestResponseRuleQNameAllowed(DNSDistTest):
+
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+    addResponseAction("allow.responses.tests.powerdns.com.", AllowResponseAction())
+    addResponseAction(AllRule(), DropResponseAction())
+    """
+
+    def testAllowed(self):
+        """
+        Responses: Allowed on QName
+
+        Send an A query to "allow.responses.tests.powerdns.com.",
+        check that the response is allowed.
+        """
+        name = 'allow.responses.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(response, receivedResponse)
+
+    def testNotAllowed(self):
+        """
+        Responses: Not allowed on QName
+
+        Send an A query to "dontallow.responses.tests.powerdns.com.",
+        check that the response is dropped.
+        """
+        name = 'dontallow.responses.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, None)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEquals(query, receivedQuery)
+        self.assertEquals(receivedResponse, None)