]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
fix IXFR-in record duplication issue by avoiding the query cache 8977/head
authorPeter van Dijk <peter.van.dijk@powerdns.com>
Fri, 27 Mar 2020 13:00:43 +0000 (14:00 +0100)
committerPeter van Dijk <peter.van.dijk@powerdns.com>
Sat, 4 Apr 2020 21:56:49 +0000 (23:56 +0200)
pdns/slavecommunicator.cc
regression-tests.auth-py/authtests.py
regression-tests.auth-py/requirements.txt
regression-tests.auth-py/test_IXFR.py [new file with mode: 0644]

index 8a55e4125ba34e8ebe082c634b982cbfc7b835f7..e88fc24791d5847be3e2a7287535d63e6c8acaf4 100644 (file)
@@ -139,8 +139,8 @@ void CommunicatorClass::ixfrSuck(const DNSName &domain, const TSIGTriplet& tt, c
         vector<DNSRecord> rrset;
         {
           DNSZoneRecord zrr;
-          B.lookup(QType(g.first.second), g.first.first+domain, di.id);
-          while(B.get(zrr)) {
+          di.backend->lookup(QType(g.first.second), g.first.first+domain, di.id);
+          while(di.backend->get(zrr)) {
             zrr.dr.d_name.makeUsRelative(domain);
             rrset.push_back(zrr.dr);
           }
index 6bb1f686752f6498508cb92d08b7f54294f32e01..14a51f6919ee02ab18c8a4d58c39ec47e9be2c62 100644 (file)
@@ -119,6 +119,8 @@ options {
                 bind_dnssec_db=bind_dnssec_db))
             pdnsconf.write(cls._config_template % params)
 
+        os.system("sqlite3 ./configs/auth/powerdns.sqlite < ../modules/gsqlite3backend/schema.sqlite3.sql")
+
         pdnsutilCmd = [os.environ['PDNSUTIL'],
                        '--config-dir=%s' % confdir,
                        'create-bind-db',
@@ -159,16 +161,15 @@ options {
 
     @classmethod
     def generateAllAuthConfig(cls, confdir):
-        if cls._zones:
-            cls.generateAuthConfig(confdir)
-            cls.generateAuthNamedConf(confdir, cls._zones.keys())
+        cls.generateAuthConfig(confdir)
+        cls.generateAuthNamedConf(confdir, cls._zones.keys())
 
-            for zonename, zonecontent in cls._zones.items():
-                cls.generateAuthZone(confdir,
-                                     zonename,
-                                     zonecontent)
-                if cls._zone_keys.get(zonename, None):
-                    cls.secureZone(confdir, zonename, cls._zone_keys.get(zonename))
+        for zonename, zonecontent in cls._zones.items():
+            cls.generateAuthZone(confdir,
+                                 zonename,
+                                 zonecontent)
+            if cls._zone_keys.get(zonename, None):
+                cls.secureZone(confdir, zonename, cls._zone_keys.get(zonename))
 
     @classmethod
     def startAuth(cls, confdir, ipaddress):
@@ -284,7 +285,7 @@ options {
         if timeout:
             sock.settimeout(timeout)
 
-        sock.connect(("127.0.0.1", cls._recursorPort))
+        sock.connect(("127.0.0.1", cls._authPort))
 
         try:
             wire = query.to_wire()
@@ -308,9 +309,8 @@ options {
             message = dns.message.from_wire(data)
         return message
 
-
     @classmethod
-    def sendTCPQuery(cls, query, timeout=2.0):
+    def sendTCPQueryMultiResponse(cls, query, timeout=2.0, count=1):
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         if timeout:
             sock.settimeout(timeout)
@@ -321,23 +321,28 @@ options {
             wire = query.to_wire()
             sock.send(struct.pack("!H", len(wire)))
             sock.send(wire)
-            data = sock.recv(2)
-            if data:
-                (datalen,) = struct.unpack("!H", data)
-                data = sock.recv(datalen)
         except socket.timeout as e:
-            print("Timeout: %s" % (str(e)))
-            data = None
+            raise Exception("Timeout: %s" % (str(e)))
         except socket.error as e:
-            print("Network error: %s" % (str(e)))
-            data = None
-        finally:
-            sock.close()
+            raise Exception("Network error: %s" % (str(e)))
 
-        message = None
-        if data:
-            message = dns.message.from_wire(data)
-        return message
+        messages = []
+        for i in range(count):
+            try:
+                data = sock.recv(2)
+                print("got data", repr(data))
+                if data:
+                    (datalen,) = struct.unpack("!H", data)
+                    data = sock.recv(datalen)
+                    messages.append(dns.message.from_wire(data))
+                else:
+                    break
+            except socket.timeout as e:
+                raise Exception("Timeout: %s" % (str(e)))
+            except socket.error as e:
+                raise Exception("Network error: %s" % (str(e)))
+
+        return messages
 
     def setUp(self):
         # This function is called before every tests
index 2cbba7a7c3005668b87a2213f36a6780af173d2a..27efa323412b0f6077c563c21ec05452789b469c 100644 (file)
@@ -2,3 +2,4 @@ dnspython>=1.11
 nose>=1.3.7
 Twisted>0.15.0
 requests>=2.18.4
+git+https://github.com/PowerDNS/xfrserver.git@0.2
diff --git a/regression-tests.auth-py/test_IXFR.py b/regression-tests.auth-py/test_IXFR.py
new file mode 100644 (file)
index 0000000..0970d92
--- /dev/null
@@ -0,0 +1,244 @@
+import dns
+import os
+import subprocess
+import time
+
+from authtests import AuthTest
+from xfrserver.xfrserver import AXFRServer
+
+zones = {
+    1: ["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 1 2 3 4 5
+@        4242    NS     ns1.example.
+@        4242    NS     ns2.example.
+ns1.example.    4242    A       192.0.2.1
+ns2.example.    4242    A       192.0.2.2
+"""],
+    2: ["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 2 2 3 4 5
+@        4242    NS     ns1.example.
+@        4242    NS     ns2.example.
+ns1.example.    4242    A       192.0.2.1
+ns2.example.    4242    A       192.0.2.2
+newrecord.example.        8484    A       192.0.2.42
+"""],
+    3: ["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 3 2 3 4 5""","""
+@        86400   SOA    foo bar 2 2 3 4 5""","""
+@        86400   SOA    foo bar 3 2 3 4 5""","""
+@        4242    NS     ns3.example.
+"""],
+    5: ["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 5 2 3 4 5""","""
+@        86400   SOA    foo bar 3 2 3 4 5""","""
+@        86400   SOA    foo bar 4 2 3 4 5""","""
+@        86400   SOA    foo bar 4 2 3 4 5""","""
+@        86400   SOA    foo bar 5 2 3 4 5""","""
+@        4242    NS     ns5.example.
+"""],
+    8: ["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 8 2 3 4 5""","""
+@        86400   SOA    foo bar 5 2 3 4 5""","""
+@        86400   SOA    foo bar 6 2 3 4 5""","""
+@        86400   SOA    foo bar 6 2 3 4 5""","""
+@        86400   SOA    foo bar 7 2 3 4 5""","""
+@        86400   SOA    foo bar 7 2 3 4 5""","""
+@        86400   SOA    foo bar 8 2 3 4 5""","""
+"""]
+
+
+}
+
+
+xfrServerPort = 4244
+xfrServer = AXFRServer(xfrServerPort, zones)
+
+class TestIXFR(AuthTest):
+    _config_template = """
+launch=gsqlite3 bind
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-dnssec
+slave
+slave-cycle-interval=1
+query-cache-ttl=20
+negquery-cache-ttl=60
+"""
+
+    _zones = {}
+    global xfrServerPort
+    _xfrDone = 0
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestIXFR, cls).setUpClass()
+        os.system("$PDNSUTIL --config-dir=configs/auth create-slave-zone example. 127.0.0.1:%s" % (xfrServerPort,))
+        os.system("$PDNSUTIL --config-dir=configs/auth set-meta example. IXFR 1")
+
+    def waitUntilCorrectSerialIsLoaded(self, serial, timeout=10):
+        global xfrServer
+
+        xfrServer.moveToSerial(serial)
+
+        attempts = 0
+        while attempts < timeout:
+            print('attempts=%s timeout=%s' % (attempts, timeout))
+            servedSerial = xfrServer.getServedSerial()
+            print('servedSerial=%s' % servedSerial)
+            if servedSerial > serial:
+                raise AssertionError("Expected serial %d, got %d" % (serial, servedSerial))
+            if servedSerial == serial:
+                self._xfrDone = self._xfrDone + 1
+                time.sleep(1)
+                return
+
+            attempts = attempts + 1
+            time.sleep(1)
+
+        raise AssertionError("Waited %d seconds for the serial to be updated to %d but the last served serial is still %d" % (timeout, serial, servedSerial))
+
+    def checkFullZone(self, serial, data=None):
+        global zones
+
+        # FIXME: 90% duplication from _getRecordsForSerial
+        zone = []
+        if not data:
+            data = zones[serial]
+        for i in dns.zone.from_text('\n'.join(data), relativize=False).iterate_rdatasets():
+            n, rds = i
+            rrs=dns.rrset.RRset(n, rds.rdclass, rds.rdtype)
+            rrs.update(rds)
+            zone.append(rrs)
+
+        expected =[[zone[0]], sorted(zone[1:], key=lambda rrset: (rrset.name, rrset.rdtype)), [zone[0]]] # AXFRs are SOA-wrapped
+
+        query = dns.message.make_query('example.', 'AXFR')
+        res = self.sendTCPQueryMultiResponse(query, count=len(expected))
+        answers = [r.answer for r in res]
+        answers[1].sort(key=lambda rrset: (rrset.name, rrset.rdtype))
+        self.assertEqual(answers, expected)
+
+    def checkIXFR(self, fromserial, toserial):
+        global zones, xfrServer
+
+        ixfr = []
+        soa1 = xfrServer._getSOAForSerial(fromserial)
+        soa2 = xfrServer._getSOAForSerial(toserial)
+        newrecord = [r for r in xfrServer._getRecordsForSerial(toserial) if r.name==dns.name.from_text('newrecord.example.')]
+        query = dns.message.make_query('example.', 'IXFR')
+        query.authority = [soa1]
+
+        expected = [[soa2], [soa1], [soa2], newrecord, [soa2]]
+        res = self.sendTCPQueryMultiResponse(query, count=len(expected))
+        answers = [r.answer for r in res]
+
+        # answers[1].sort(key=lambda rrset: (rrset.name, rrset.rdtype))
+        self.assertEqual(answers, expected)
+        # check the TTLs
+        answerPos = 0
+        for expectedAnswer in expected:
+            pos = 0
+            for rec in expectedAnswer:
+                self.assertEquals(rec.ttl, answers[answerPos][pos].ttl)
+                pos = pos + 1
+            answerPos = answerPos + 1
+
+    def test_a_XFR(self):
+        print("x1")
+        self.waitUntilCorrectSerialIsLoaded(1)
+        print("x2")
+        self.checkFullZone(1)
+        print("x3")
+
+        self.waitUntilCorrectSerialIsLoaded(2)
+        print("x4")
+        self.checkFullZone(2)
+        print("x5")
+
+        self.waitUntilCorrectSerialIsLoaded(3)
+        print("x7")
+        self.checkFullZone(3, data=["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 3 2 3 4 5
+@        4242    NS     ns1.example.
+@        4242    NS     ns2.example.
+@        4242    NS     ns3.example.
+ns1.example.    4242    A       192.0.2.1
+ns2.example.    4242    A       192.0.2.2
+newrecord.example.        8484    A       192.0.2.42
+"""])
+        print("x8")
+
+        self.waitUntilCorrectSerialIsLoaded(5)
+        print("x7")
+        self.checkFullZone(5, data=["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 5 2 3 4 5
+@        4242    NS     ns1.example.
+@        4242    NS     ns2.example.
+@        4242    NS     ns3.example.
+@        4242    NS     ns5.example.
+ns1.example.    4242    A       192.0.2.1
+ns2.example.    4242    A       192.0.2.2
+newrecord.example.        8484    A       192.0.2.42
+"""])
+        print("x8")
+
+
+    # _b_ because we expect post-XFR testing state
+    def test_b_UDP_SOA_existing(self):
+        query = dns.message.make_query('example.', 'SOA')
+        expected = dns.message.make_response(query)
+        expected.answer.append(xfrServer._getSOAForSerial(5))
+        expected.flags |= dns.flags.AA
+
+        response = self.sendUDPQuery(query)
+
+        self.assertEquals(expected, response)
+        # check the TTLs
+        pos = 0
+        for rec in expected.answer:
+            self.assertEquals(rec.ttl, response.answer[pos].ttl)
+            pos = pos + 1
+
+    def test_b_UDP_SOA_not_loaded(self):
+        query = dns.message.make_query('example2.', 'SOA')
+        expected = dns.message.make_response(query)
+        expected.set_rcode(dns.rcode.REFUSED)
+
+        response = self.sendUDPQuery(query)
+        self.assertEquals(expected, response)
+
+    def test_b_UDP_SOA_not_configured(self):
+        query = dns.message.make_query('example3.', 'SOA')
+        expected = dns.message.make_response(query)
+        expected.set_rcode(dns.rcode.REFUSED)
+
+        response = self.sendUDPQuery(query)
+        self.assertEquals(expected, response)
+
+    def test_d_XFR(self):
+        self.waitUntilCorrectSerialIsLoaded(8)
+        print("x7")
+        self.checkFullZone(7, data=["""
+$ORIGIN example.""","""
+@        86400   SOA    foo bar 8 2 3 4 5
+@        4242    NS     ns1.example.
+@        4242    NS     ns2.example.
+@        4242    NS     ns3.example.
+@        4242    NS     ns5.example.
+ns1.example.    4242    A       192.0.2.1
+ns2.example.    4242    A       192.0.2.2
+newrecord.example.        8484    A       192.0.2.42
+"""])
+        print("x8")
+        ret = subprocess.check_output([os.environ['PDNSUTIL'],
+                           '--config-dir=configs/auth',
+                           'list-zone', 'example'], stderr=subprocess.STDOUT)
+        rets = ret.split('\n')
+
+        self.assertEqual(1, sum('SOA' in l for l in ret.split('\n')))
\ No newline at end of file