]> granicus.if.org Git - pdns/commitdiff
dnsdist: add rules for self-answered responses
authorChris Hofstaedtler <chris.hofstaedtler@deduktiva.com>
Mon, 15 Jan 2018 21:40:09 +0000 (22:40 +0100)
committerChris Hofstaedtler <chris.hofstaedtler@deduktiva.com>
Mon, 22 Jan 2018 17:42:25 +0000 (18:42 +0100)
pdns/dnsdist-console.cc
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-lua-rules.cc
pdns/dnsdist-tcp.cc
pdns/dnsdist-web.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/docs/guides/webserver.rst
pdns/dnsdistdist/docs/rules-actions.rst
regression-tests.dnsdist/test_API.py
regression-tests.dnsdist/test_SelfAnsweredResponses.py [new file with mode: 0644]

index 04aa7842295637f32c1ab270f5e3129026c32a97..1f267c0aa9ca4aa9841f8f132b25d561f0272106 100644 (file)
@@ -292,6 +292,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "addLuaResponseAction", true, "x, func [, {uuid=\"UUID\"}]", "where 'x' is all the combinations from `addAction`, 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" },
   { "addCacheHitResponseAction", true, "DNS rule, DNS response action [, {uuid=\"UUID\"}]", "add a cache hit response rule" },
   { "addResponseAction", true, "DNS rule, DNS response action [, {uuid=\"UUID\"}]", "add a response rule" },
+  { "addSelfAnsweredResponseAction", true, "DNS rule, DNS response action [, {uuid=\"UUID\"}]", "add a self-answered response rule" },
   { "addTLSLocal", true, "addr, certFile, keyFile[,params]", "listen to incoming DNS over TLS queries on the specified address using the specified certificate and key. The last parameter is a table" },
   { "AllowAction", true, "", "let these packets go through" },
   { "AllowResponseAction", true, "", "let these packets go through" },
@@ -338,6 +339,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "mvCacheHitResponseRule", true, "from, to", "move cache hit response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" },
   { "mvResponseRule", true, "from, to", "move response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" },
   { "mvRule", true, "from, to", "move rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule, in which case the rule will be moved to the last position" },
+  { "mvSelfAnsweredResponseRule", true, "from, to", "move self-answered response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" },
   { "newDNSName", true, "name", "make a DNSName based on this .-terminated name" },
   { "newPacketCache", true, "maxEntries[, maxTTL=86400, minTTL=0, temporaryFailureTTL=60, staleTTL=60, dontAge=false, numberOfShards=1, deferrableInsertLock=true]", "return a new Packet Cache" },
   { "newQPSLimiter", true, "rate, burst", "configure a QPS limiter with that rate and that burst capacity" },
@@ -356,6 +358,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "rmCacheHitResponseRule", true, "id", "remove cache hit response rule in position 'id', or whose uuid matches if 'id' is an UUID string" },
   { "rmResponseRule", true, "id", "remove response rule in position 'id', or whose uuid matches if 'id' is an UUID string" },
   { "rmRule", true, "id", "remove rule in position 'id', or whose uuid matches if 'id' is an UUID string" },
+  { "rmSelfAnsweredResponseRule", true, "id", "remove self-answered response rule in position 'id', or whose uuid matches if 'id' is an UUID string" },
   { "rmServer", true, "n", "remove server with index n" },
   { "roundrobin", false, "", "Simple round robin over available servers" },
   { "QNameLabelsCountRule", true, "min, max", "matches if the qname has less than `min` or more than `max` labels" },
@@ -409,6 +412,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "showResponseLatency", true, "", "show a plot of the response time latency distribution" },
   { "showResponseRules", true, "[showUUIDs]", "show all defined response rules, optionally with their UUIDs" },
   { "showRules", true, "[showUUIDs]", "show all defined rules, optionally with their UUIDs" },
+  { "showSelfAnsweredResponseRules", true, "[showUUIDs]", "show all defined self-answered response rules, optionally with their UUIDs" },
   { "showServerPolicy", true, "", "show name of currently operational server selection policy" },
   { "showServers", true, "", "output all servers" },
   { "showTCPStats", true, "", "show some statistics regarding TCP" },
@@ -432,8 +436,10 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "topClients", true, "n", "show top-`n` clients sending the most queries over length of ringbuffer" },
   { "topQueries", true, "n[, labels]", "show top 'n' queries, as grouped when optionally cut down to 'labels' labels" },
   { "topResponses", true, "n, kind[, labels]", "show top 'n' responses with RCODE=kind (0=NO Error, 2=ServFail, 3=ServFail), as grouped when optionally cut down to 'labels' labels" },
+  { "topCacheHitResponseRule", true, "", "move the last cache hit response rule to the first position" },
   { "topResponseRule", true, "", "move the last response rule to the first position" },
   { "topRule", true, "", "move the last rule to the first position" },
+  { "topSelfAnsweredResponseRule", true, "", "move the last self-answered response rule to the first position" },
   { "topSlow", true, "[top][, limit][, labels]", "show `top` queries slower than `limit` milliseconds, grouped by last `labels` labels" },
   { "truncateTC", true, "bool", "if set (defaults to no starting with dnsdist 1.2.0) truncate TC=1 answers so they are actually empty. Fixes an issue for PowerDNS Authoritative Server 2.9.22. Note: turning this on breaks compatibility with RFC 6891." },
   { "unregisterDynBPFFilter", true, "DynBPFFilter", "unregister this dynamic BPF filter" },
index 9b86a89974abd27b3b3de2058345a711202250f0..351d407fb3a9d925082528c1119f4f6effe46e99 100644 (file)
@@ -864,6 +864,14 @@ void setupLuaActions()
       addAction(&g_cachehitresprulactions, var, boost::get<std::shared_ptr<DNSResponseAction> >(era), params);
     });
 
+  g_lua.writeFunction("addSelfAnsweredResponseAction", [](luadnsrule_t var, boost::variant<std::shared_ptr<DNSAction>, std::shared_ptr<DNSResponseAction>> era, boost::optional<luaruleparams_t> params) {
+      if (era.type() != typeid(std::shared_ptr<DNSResponseAction>)) {
+        throw std::runtime_error("addSelfAnsweredResponseAction() can only be called with response-related actions, not query-related ones. Are you looking for addAction()?");
+      }
+
+      addAction(&g_selfansweredresprulactions, var, boost::get<std::shared_ptr<DNSResponseAction> >(era), params);
+    });
+
   g_lua.registerFunction<void(DNSAction::*)()>("printStats", [](const DNSAction& ta) {
       setLuaNoSideEffect();
       auto stats = ta.getStats();
index 4e2e53c8cf58c2a664bab9d2ec1c37c97549fe45..a6ffe645545bbef7adb7bc0c978820a196263140 100644 (file)
@@ -1034,6 +1034,22 @@ void setupLuaRules()
       mvRule(&g_cachehitresprulactions, from, to);
     });
 
+  g_lua.writeFunction("showSelfAnsweredResponseRules", [](boost::optional<bool> showUUIDs) {
+      showRules(&g_selfansweredresprulactions, showUUIDs);
+    });
+
+  g_lua.writeFunction("rmSelfAnsweredResponseRule", [](boost::variant<unsigned int, std::string> id) {
+      rmRule(&g_selfansweredresprulactions, id);
+    });
+
+  g_lua.writeFunction("topSelfAnsweredResponseRule", []() {
+      topRule(&g_selfansweredresprulactions);
+    });
+
+  g_lua.writeFunction("mvSelfAnsweredResponseRule", [](unsigned int from, unsigned int to) {
+      mvRule(&g_selfansweredresprulactions, from, to);
+    });
+
   g_lua.writeFunction("rmRule", [](boost::variant<unsigned int, std::string> id) {
       rmRule(&g_rulactions, id);
     });
index f23b4c15845a4b177e62a09ee53b6eb15bd5785d..6fa56882db623710dd0b310edb43e11bf9909b0a 100644 (file)
@@ -361,6 +361,17 @@ void* tcpClientThread(int pipefd)
 
        if(dq.dh->qr) { // something turned it into a response
           restoreFlags(dh, origFlags);
+
+          DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast<dnsheader*>(query), dq.size, dq.len, true, &queryRealTime);
+#ifdef HAVE_PROTOBUF
+          dr.uniqueId = dq.uniqueId;
+#endif
+          dr.qTag = dq.qTag;
+
+          if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) {
+            goto drop;
+          }
+
 #ifdef HAVE_DNSCRYPT
           if (!encryptResponse(query, &dq.len, dq.size, true, dnsCryptQuery, nullptr, nullptr)) {
             goto drop;
@@ -428,6 +439,16 @@ void* tcpClientThread(int pipefd)
             dq.dh->rcode = RCode::ServFail;
             dq.dh->qr = true;
 
+            DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast<dnsheader*>(query), dq.size, dq.len, false, &queryRealTime);
+#ifdef HAVE_PROTOBUF
+            dr.uniqueId = dq.uniqueId;
+#endif
+            dr.qTag = dq.qTag;
+
+            if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) {
+              goto drop;
+            }
+
 #ifdef HAVE_DNSCRYPT
             if (!encryptResponse(query, &dq.len, dq.size, true, dnsCryptQuery, nullptr, nullptr)) {
               goto drop;
index 6af354d562eb7e4eae8412fee6510e2a285863d0..b2bca433514cbd52292c94488c1b5bc8d61efa8b 100644 (file)
@@ -479,6 +479,7 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
       
       auto responseRules = someResponseRulesToJson(&g_resprulactions);
       auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitresprulactions);
+      auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredresprulactions);
 
       string acl;
 
@@ -504,6 +505,7 @@ static void connectionThread(int sock, ComboAddress remote, string password, str
         { "rules", rules},
         { "response-rules", responseRules},
         { "cache-hit-response-rules", cacheHitResponseRules},
+        { "self-answered-response-rules", selfAnsweredResponseRules},
         { "acl", acl},
         { "local", localaddresses}
       };
index 1ff98fd3677c4767f798a09bdbe8d3ac47c6350d..decd88d75cb5ece1d1969b74b346b0b7317955d3 100644 (file)
@@ -135,6 +135,7 @@ DNSDistSNMPAgent* g_snmpAgent{nullptr};
 GlobalStateHolder<vector<DNSDistRuleAction> > g_rulactions;
 GlobalStateHolder<vector<DNSDistResponseRuleAction> > g_resprulactions;
 GlobalStateHolder<vector<DNSDistResponseRuleAction> > g_cachehitresprulactions;
+GlobalStateHolder<vector<DNSDistResponseRuleAction> > g_selfansweredresprulactions;
 
 Rings g_rings;
 QueryCount g_qcount;
@@ -1236,6 +1237,16 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct
         char* response = query;
         uint16_t responseLen = dq.len;
 
+        DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast<dnsheader*>(response), dq.size, responseLen, false, &realTime);
+#ifdef HAVE_PROTOBUF
+        dr.uniqueId = dq.uniqueId;
+#endif
+        dr.qTag = dq.qTag;
+
+        if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) {
+          return;
+        }
+
 #ifdef HAVE_DNSCRYPT
         if (!encryptResponse(response, &responseLen, dq.size, false, dnsCryptQuery, nullptr, nullptr)) {
           return;
@@ -1330,6 +1341,16 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct
         dq.dh->rcode = RCode::ServFail;
         dq.dh->qr = true;
 
+        DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast<dnsheader*>(response), dq.size, responseLen, false, &realTime);
+#ifdef HAVE_PROTOBUF
+        dr.uniqueId = dq.uniqueId;
+#endif
+        dr.qTag = dq.qTag;
+
+        if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) {
+          return;
+        }
+
 #ifdef HAVE_DNSCRYPT
         if (!encryptResponse(response, &responseLen, dq.size, false, dnsCryptQuery, nullptr, nullptr)) {
           return;
index aff57f207486e37e7bf6d0ca83633102970ecdb6..c7b68e154f6fc555fbd66922b0031d713e102343 100644 (file)
@@ -749,6 +749,7 @@ extern GlobalStateHolder<pools_t> g_pools;
 extern GlobalStateHolder<vector<DNSDistRuleAction> > g_rulactions;
 extern GlobalStateHolder<vector<DNSDistResponseRuleAction> > g_resprulactions;
 extern GlobalStateHolder<vector<DNSDistResponseRuleAction> > g_cachehitresprulactions;
+extern GlobalStateHolder<vector<DNSDistResponseRuleAction> > g_selfansweredresprulactions;
 extern GlobalStateHolder<NetmaskGroup> g_ACL;
 
 extern ComboAddress g_serverControl; // not changed during runtime
@@ -807,7 +808,7 @@ extern std::vector<std::shared_ptr<DynBPFFilter> > g_dynBPFFilters;
 
 struct LocalHolders
 {
-  LocalHolders(): acl(g_ACL.getLocal()), policy(g_policy.getLocal()), rulactions(g_rulactions.getLocal()), cacheHitRespRulactions(g_cachehitresprulactions.getLocal()), servers(g_dstates.getLocal()), dynNMGBlock(g_dynblockNMG.getLocal()), dynSMTBlock(g_dynblockSMT.getLocal()), pools(g_pools.getLocal())
+  LocalHolders(): acl(g_ACL.getLocal()), policy(g_policy.getLocal()), rulactions(g_rulactions.getLocal()), cacheHitRespRulactions(g_cachehitresprulactions.getLocal()), selfAnsweredRespRulactions(g_selfansweredresprulactions.getLocal()), servers(g_dstates.getLocal()), dynNMGBlock(g_dynblockNMG.getLocal()), dynSMTBlock(g_dynblockSMT.getLocal()), pools(g_pools.getLocal())
   {
   }
 
@@ -815,6 +816,7 @@ struct LocalHolders
   LocalStateHolder<ServerPolicy> policy;
   LocalStateHolder<vector<DNSDistRuleAction> > rulactions;
   LocalStateHolder<vector<DNSDistResponseRuleAction> > cacheHitRespRulactions;
+  LocalStateHolder<vector<DNSDistResponseRuleAction> > selfAnsweredRespRulactions;
   LocalStateHolder<servers_t> servers;
   LocalStateHolder<NetmaskTree<DynBlock> > dynNMGBlock;
   LocalStateHolder<SuffixMatchTree<DynBlock> > dynSMTBlock;
index cdcd5f296e8758025b36b1ecfcf97fdc16a3613a..9bfc57216b4d71d6f07c8fe8cf3b83339494bb07 100644 (file)
@@ -103,6 +103,7 @@ URL Endpoints
 
   :>json string acl: A string of comma-separated netmasks currently allowed by the :ref:`ACL <ACL>`.
   :>json list cache-hit-response-rules: A list of :json:object:`ResponseRule` objects applied on cache hits
+  :>json list self-answered-response-rules: A list of :json:object:`ResponseRule` objects applied on self-answered queries
   :>json string daemon_type: The type of daemon, always "dnsdist"
   :>json list frontends: A list of :json:object:`Frontend` objects
   :>json list pools: A list of :json:object:`Pool` objects
index 9223ed97dfdc466ed1424850a303d765b5c45efe..50652a13ec1144ff2a31685c9e6a406d153d4425 100644 (file)
@@ -365,9 +365,9 @@ For Rules related to responses:
 
   Move the last response rule to the first position.
 
-Functions for manipulation Cache Hit Rules:
+Functions for manipulating Cache Hit Respone Rules:
 
-.. function:: addCacheHitResponseAction(DNSRule, action)
+.. function:: addCacheHitResponseAction(DNSRule, action [, options])
 
   .. versionadded:: 1.2.0
 
@@ -417,6 +417,49 @@ Functions for manipulation Cache Hit Rules:
 
   Move the last cache hit response rule to the first position.
 
+Functions for manipulating Self-Answered Response Rules:
+
+.. function:: addSelfAnsweredResponseAction(DNSRule, action [, options])
+
+  .. versionadded:: 1.3.0
+
+  Add a Rule and Action for Self-Answered queries to the existing rules.
+
+  :param DNSRule: A DNSRule, e.g. an :func:`allRule` or a compounded bunch of rules using e.g. :func:`AndRule`
+  :param action: The action to take
+
+.. function:: mvSelfAnsweredResponseRule(from, to)
+
+  .. versionadded:: 1.3.0
+
+  Move self answered response rule ``from`` to a position where it is in front of ``to``.
+  ``to`` can be one larger than the largest rule, in which case the rule will be moved to the last position.
+
+  :param int from: Rule number to move
+  :param int to: Location to more the Rule to
+
+.. function:: rmSelfAnsweredResponseRule(id)
+
+  .. versionadded:: 1.3.0
+
+  Remove self answered response rule ``id``.
+
+  :param int id: The UUID of the rule to remove if ``id`` is an UUID, its position otherwise
+
+.. function:: showSelfAnsweredResponseRules([showUUIDs])
+
+  .. versionadded:: 1.3.0
+
+  Show all defined self answered response rules, optionally displaying their UUIDs.
+
+  :param bool showUUIDs: Whether to display the UUIDs, defaults to false
+
+.. function:: topSelfAnsweredResponseRule()
+
+  .. versionadded:: 1.3.0
+
+  Move the last self answered response rule to the first position.
+
 .. _RulesIntro:
 
 Matching Packets (Selectors)
@@ -635,8 +678,8 @@ Combining Rules
 
   :param {Rule} selector: A table of Rules
 
-Convience Functions
-~~~~~~~~~~~~~~~~~~~
+Convenience Functions
+~~~~~~~~~~~~~~~~~~~~~
 
 .. function:: makeRule(rule)
 
index 942dfbed1d31aad75f91d7d6b8349c6d1c08dd5f..caf2ff6ad7cb3616e5f706905187f1595a0fa867 100644 (file)
@@ -78,7 +78,7 @@ class TestAPIBasics(DNSDistTest):
 
         self.assertEquals(content['daemon_type'], 'dnsdist')
 
-        rule_groups = ['response-rules', 'cache-hit-response-rules']
+        rule_groups = ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules']
         for key in ['version', 'acl', 'local', 'rules', 'servers', 'frontends', 'pools'] + rule_groups:
             self.assertIn(key, content)
 
diff --git a/regression-tests.dnsdist/test_SelfAnsweredResponses.py b/regression-tests.dnsdist/test_SelfAnsweredResponses.py
new file mode 100644 (file)
index 0000000..843e2bd
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+import dns
+from dnsdisttests import DNSDistTest
+
+class TestSelfAnsweredResponses(DNSDistTest):
+
+    _config_template = """
+    -- this is a silly test config, please do not do this in production.
+    addAction(makeRule("udp.selfanswered.tests.powerdns.com."), SpoofAction("192.0.2.1"))
+    addSelfAnsweredResponseAction(AndRule({makeRule("udp.selfanswered.tests.powerdns.com."), NotRule(MaxQPSRule(1))}), DropResponseAction())
+    addAction(makeRule("tcp.selfanswered.tests.powerdns.com."), SpoofAction("192.0.2.1"))
+    addSelfAnsweredResponseAction(AndRule({makeRule("tcp.selfanswered.tests.powerdns.com."), NotRule(MaxQPSRule(1))}), DropResponseAction())
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testSelfAnsweredUDP(self):
+        """
+        CacheHitResponse: Drop when served from the cache
+        """
+        ttl = 60
+        name = 'udp.selfanswered.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+        response.flags |= dns.flags.RA
+
+        # self-answered, but no SelfAnswered rule matches.
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        self.assertEquals(receivedResponse, response)
+
+        # self-answered, AND SelfAnswered rule matches. Should not see a reply.
+        (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
+        self.assertIsNone(receivedResponse)
+
+    def testSelfAnsweredTCP(self):
+        """
+        testSelfAnsweredTCP: Drop after exceeding QPS
+        """
+        ttl = 60
+        name = 'tcp.selfanswered.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    ttl,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        response.answer.append(rrset)
+        response.flags |= dns.flags.RA
+
+        # self-answered, but no SelfAnswered rule matches.
+        (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        self.assertEquals(receivedResponse, response)
+
+        # self-answered, AND SelfAnswered rule matches. Should not see a reply.
+        (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False)
+        self.assertIsNone(receivedResponse)