From: Peter van Dijk Date: Fri, 27 Mar 2020 13:00:43 +0000 (+0100) Subject: fix IXFR-in record duplication issue by avoiding the query cache X-Git-Tag: dnsdist-1.5.0-rc1~16^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F8977%2Fhead;p=thirdparty%2Fpdns.git fix IXFR-in record duplication issue by avoiding the query cache --- diff --git a/pdns/slavecommunicator.cc b/pdns/slavecommunicator.cc index 8a55e4125b..e88fc24791 100644 --- a/pdns/slavecommunicator.cc +++ b/pdns/slavecommunicator.cc @@ -139,8 +139,8 @@ void CommunicatorClass::ixfrSuck(const DNSName &domain, const TSIGTriplet& tt, c vector 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); } diff --git a/regression-tests.auth-py/authtests.py b/regression-tests.auth-py/authtests.py index 6bb1f68675..14a51f6919 100644 --- a/regression-tests.auth-py/authtests.py +++ b/regression-tests.auth-py/authtests.py @@ -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 diff --git a/regression-tests.auth-py/requirements.txt b/regression-tests.auth-py/requirements.txt index 2cbba7a7c3..27efa32341 100644 --- a/regression-tests.auth-py/requirements.txt +++ b/regression-tests.auth-py/requirements.txt @@ -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 index 0000000000..0970d92321 --- /dev/null +++ b/regression-tests.auth-py/test_IXFR.py @@ -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