]> granicus.if.org Git - pdns/commitdiff
dnsdist: Make dnsdist {A,I}XFR aware, document possible issues
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 3 Jun 2016 09:54:58 +0000 (11:54 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 10 Jun 2016 13:52:56 +0000 (15:52 +0200)
pdns/README-dnsdist.md
pdns/dnsdist-tcp.cc
regression-tests.dnsdist/dnsdisttests.py
regression-tests.dnsdist/test_AXFR.py [new file with mode: 0644]

index 83cc56d17bf95cd5d085ce4811eb20f6206fba00..b0d03f85137a6747897611a78fb8c43f2f5d7235 100644 (file)
@@ -987,6 +987,45 @@ If you forgot to write down the provider fingerprint value after generating the
 Provider fingerprint is: E1D7:2108:9A59:BF8D:F101:16FA:ED5E:EA6A:9F6C:C78F:7F91:AF6B:027E:62F4:69C3:B1AA
 ```
 
+AXFR, IXFR and NOTIFY
+---------------------
+When `dnsdist` is deployed in front of a master authoritative server, it might
+receive AXFR or IXFR queries destined to this master. There are two issues
+that can arise in this kind of setup:
+
+ * If the master is part of a pool of servers, the first SOA query can be directed
+   by `dnsdist` to a different server than the following AXFR/IXFR one. If all servers are not
+   perfectly synchronised at all times, it might to synchronisation issues.
+ * If the master only allows AXFR/IXFR based on the source address of the requestor,
+   it might be confused by the fact that the source address will be the one from
+   the `dnsdist` server.
+
+The first issue can be solved by routing SOA, AXFR and IXFR requests explicitely
+to the master:
+
+```
+> newServer({address="192.168.1.2", name="master", pool={"master", "otherpool"}})
+> addAction(OrRule({QTypeRule(dnsdist.SOA), QTypeRule(dnsdist.AXFR), QTypeRule(dnsdist.IXFR)}), PoolAction("master"))
+```
+
+The second one might requires allowing AXFR/IXFR from the `dnsdist` source address
+and moving the source address check on `dnsdist`'s side:
+
+```
+> addAction(AndRule({OrRule({QTypeRule(dnsdist.AXFR), QTypeRule(dnsdist.IXFR)}), NotRule(makeRule("192.168.1.0/24"))}), RCodeAction(dnsdist.REFUSED))
+```
+
+When `dnsdist` is deployed in front of slaves, however, an issue might arise with NOTIFY
+queries, because the slave will receive a notification coming from the `dnsdist` address,
+and not the master's one. One way to fix this issue is to allow NOTIFY from the `dnsdist`
+address on the slave side (for example with PowerDNS's `trusted-notification-proxy`) and
+move the address check on `dnsdist`'s side:
+
+```
+> addAction(AndRule({OpcodeRule(DNSOpcode.Notify), NotRule(makeRule("192.168.1.0/24"))}), RCodeAction(dnsdist.REFUSED))
+```
+
+
 All functions and types
 -----------------------
 Within `dnsdist` several core object types exist:
index 4adb768b78c3ba7cce0e43053ca6062cffab4482..c7b30984cce5d901a35e29757563bb32a1dd9e84 100644 (file)
@@ -22,6 +22,7 @@
 
 #include "dnsdist.hh"
 #include "dnsdist-ecs.hh"
+#include "dnsparser.hh"
 #include "ednsoptions.hh"
 #include "dolog.hh"
 #include "lock.hh"
@@ -283,9 +284,6 @@ void* tcpClientThread(int pipefd)
          goto drop;
        }
 
-       if(dq.qtype == QType::AXFR || dq.qtype == QType::IXFR)  // XXX fixme we really need to do better
-         break;
-
         std::shared_ptr<ServerPool> serverPool = getPool(*localPools, poolname);
         std::shared_ptr<DNSDistPacketCache> packetCache = nullptr;
        {
@@ -383,6 +381,14 @@ void* tcpClientThread(int pipefd)
           goto retry;
         }
 
+        bool xfrStarted = false;
+        bool isXFR = (dq.qtype == QType::AXFR || dq.qtype == QType::IXFR);
+        if (isXFR) {
+          dq.skipCache = true;
+        }
+
+      getpacket:;
+
         if(!getNonBlockingMsgLen(dsock, &rlen, ds->tcpRecvTimeout)) {
          vinfolog("Downstream connection to %s died on us phase 2, getting a new one!", ds->getName());
           close(dsock);
@@ -390,6 +396,9 @@ void* tcpClientThread(int pipefd)
           sockets.erase(ds->remote);
           sockets[ds->remote]=dsock=setupTCPDownstream(ds);
           downstream_failures++;
+          if(xfrStarted) {
+            goto drop;
+          }
           goto retry;
         }
 
@@ -442,6 +451,18 @@ void* tcpClientThread(int pipefd)
           break;
         }
 
+        if (isXFR && dh->rcode == 0 && dh->ancount != 0) {
+          if (xfrStarted == false) {
+            xfrStarted = true;
+            if (getRecordsOfTypeCount(response, responseLen, 1, QType::SOA) == 1) {
+              goto getpacket;
+            }
+          }
+          else if (getRecordsOfTypeCount(response, responseLen, 1, QType::SOA) == 0) {
+            goto getpacket;
+          }
+        }
+
         g_stats.responses++;
         struct timespec answertime;
         gettime(&answertime);
index 8184eed4a0e799a45edc89e4737572f5567f983c..6ba23c672d884a65768e3a5ddd39294b69d065e3 100644 (file)
@@ -162,7 +162,7 @@ class DNSDistTest(unittest.TestCase):
         sock.close()
 
     @classmethod
-    def TCPResponder(cls, port, ignoreTrailing=False):
+    def TCPResponder(cls, port, ignoreTrailing=False, multipleResponses=False):
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
         try:
@@ -182,12 +182,29 @@ class DNSDistTest(unittest.TestCase):
             response = cls._getResponse(request)
 
             if not response:
+                conn.close()
                 continue
 
             wire = response.to_wire()
             conn.send(struct.pack("!H", len(wire)))
             conn.send(wire)
+
+            while multipleResponses:
+                if cls._toResponderQueue.empty():
+                    break
+
+                response = cls._toResponderQueue.get(True, cls._queueTimeout)
+                if not response:
+                    break
+
+                response = copy.copy(response)
+                response.id = request.id
+                wire = response.to_wire()
+                conn.send(struct.pack("!H", len(wire)))
+                conn.send(wire)
+
             conn.close()
+
         sock.close()
 
     @classmethod
@@ -256,6 +273,46 @@ class DNSDistTest(unittest.TestCase):
             message = dns.message.from_wire(data)
         return (receivedQuery, message)
 
+    @classmethod
+    def sendTCPQueryWithMultipleResponses(cls, query, responses, useQueue=True, timeout=2.0, rawQuery=False):
+        if useQueue:
+            for response in responses:
+                cls._toResponderQueue.put(response, True, timeout)
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        if timeout:
+            sock.settimeout(timeout)
+
+        sock.connect(("127.0.0.1", cls._dnsDistPort))
+        messages = []
+
+        try:
+            if not rawQuery:
+                wire = query.to_wire()
+            else:
+                wire = query
+
+            sock.send(struct.pack("!H", len(wire)))
+            sock.send(wire)
+            while True:
+                data = sock.recv(2)
+                if not data:
+                    break
+                (datalen,) = struct.unpack("!H", data)
+                data = sock.recv(datalen)
+                messages.append(dns.message.from_wire(data))
+
+        except socket.timeout as e:
+            print("Timeout: %s" % (str(e)))
+        except socket.error as e:
+            print("Network error: %s" % (str(e)))
+        finally:
+            sock.close()
+
+        receivedQuery = None
+        if useQueue and not cls._fromResponderQueue.empty():
+            receivedQuery = cls._fromResponderQueue.get(True, timeout)
+        return (receivedQuery, messages)
+
     def setUp(self):
         # This function is called before every tests
 
diff --git a/regression-tests.dnsdist/test_AXFR.py b/regression-tests.dnsdist/test_AXFR.py
new file mode 100644 (file)
index 0000000..6f54dc3
--- /dev/null
@@ -0,0 +1,279 @@
+#!/usr/bin/env python
+import threading
+import dns
+from dnsdisttests import DNSDistTest
+
+class TestAXFR(DNSDistTest):
+
+    # this test suite uses a different responder port
+    # because, contrary to the other ones, its
+    # TCP responder allows multiple responses and we don't want
+    # to mix things up.
+    _testServerPort = 5370
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+    """
+    @classmethod
+    def startResponders(cls):
+        print("Launching responders..")
+
+        cls._UDPResponder = threading.Thread(name='UDP Responder', target=cls.UDPResponder, args=[cls._testServerPort])
+        cls._UDPResponder.setDaemon(True)
+        cls._UDPResponder.start()
+        cls._TCPResponder = threading.Thread(name='TCP Responder', target=cls.TCPResponder, args=[cls._testServerPort, False, True])
+        cls._TCPResponder.setDaemon(True)
+        cls._TCPResponder.start()
+
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testOneMessageAXFR(self):
+        """
+        AXFR: One message
+        """
+        name = 'one.axfr.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AXFR', 'IN')
+        response = dns.message.make_response(query)
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+        response.answer.append(soa)
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.A,
+                                                   '192.0.2.1'))
+        response.answer.append(soa)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(response, receivedResponse)
+
+    def testOneNoSOAAXFR(self):
+        """
+        AXFR: One message, no SOA
+        """
+        name = 'onenosoa.axfr.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AXFR', 'IN')
+        response = dns.message.make_response(query)
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.A,
+                                                   '192.0.2.1'))
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(response, receivedResponse)
+
+    def testFourMessagesAXFR(self):
+        """
+        AXFR: Four messages
+        """
+        name = 'four.axfr.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AXFR', 'IN')
+        responses = []
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+        response = dns.message.make_response(query)
+        response.answer.append(soa)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.A,
+                                                   '192.0.2.1'))
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    'dummy')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        response.answer.append(soa)
+        responses.append(response)
+
+        (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(len(receivedResponses), len(responses))
+
+    def testFourNoFinalSOAAXFR(self):
+        """
+        AXFR: Four messages, no final SOA
+        """
+        name = 'fournosoa.axfr.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AXFR', 'IN')
+        responses = []
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+        response = dns.message.make_response(query)
+        response.answer.append(soa)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.A,
+                                                   '192.0.2.1'))
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    'dummy')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(len(receivedResponses), len(responses))
+
+    def testFourNoFirstSOAAXFR(self):
+        """
+        AXFR: Four messages, no SOA in the first one
+        """
+        name = 'fournosoainfirst.axfr.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AXFR', 'IN')
+        responses = []
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+        response = dns.message.make_response(query)
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.A,
+                                                   '192.0.2.1'))
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        response.answer.append(soa)
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text('dummy.' + name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    'dummy')
+        response.answer.append(rrset)
+        response.answer.append(soa)
+        responses.append(response)
+
+        (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(len(receivedResponses), 1)
+
+    def testFourLastSOAInSecondAXFR(self):
+        """
+        AXFR: Four messages, SOA in the first one and the second one
+        """
+        name = 'foursecondsoainsecond.axfr.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'AXFR', 'IN')
+        responses = []
+        soa = dns.rrset.from_text(name,
+                                  60,
+                                  dns.rdataclass.IN,
+                                  dns.rdatatype.SOA,
+                                  'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60')
+
+        response = dns.message.make_response(query)
+        response.answer.append(soa)
+        response.answer.append(dns.rrset.from_text(name,
+                                                   60,
+                                                   dns.rdataclass.IN,
+                                                   dns.rdatatype.A,
+                                                   '192.0.2.1'))
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        response.answer.append(soa)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text('dummy.' + name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '2001:DB8::1')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    'dummy')
+        response.answer.append(rrset)
+        responses.append(response)
+
+        (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses)
+        receivedQuery.id = query.id
+        self.assertEqual(query, receivedQuery)
+        self.assertEqual(len(receivedResponses), 2)