From 98b3317609b2783d1bfd5da99edc81af28f4e1d6 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 18 Feb 2019 13:07:14 +0100 Subject: [PATCH] Correctly interpret an empty AXFR response to an IXFR query --- pdns/ixfr.cc | 6 ++++++ pdns/test-ixfr_cc.cc | 9 ++++++++- regression-tests.recursor-dnssec/test_RPZ.py | 21 +++++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pdns/ixfr.cc b/pdns/ixfr.cc index c6b213753..0a7dca9f5 100644 --- a/pdns/ixfr.cc +++ b/pdns/ixfr.cc @@ -59,6 +59,12 @@ vector, vector > > processIXFRRecords(const Co // the serial of this SOA record is the serial of the // zone before the removals and updates of this sequence if (sr->d_st.serial == masterSOA->d_st.serial) { + if (records.size() == 2) { + // if the entire update is two SOAs records with the same + // serial, this is actually an empty AXFR! + return {{remove, records}}; + } + // if it's the final SOA, there is nothing for us to see break; } diff --git a/pdns/test-ixfr_cc.cc b/pdns/test-ixfr_cc.cc index bab31a11c..00d795e78 100644 --- a/pdns/test-ixfr_cc.cc +++ b/pdns/test-ixfr_cc.cc @@ -211,7 +211,14 @@ BOOST_AUTO_TEST_CASE(test_ixfr_same_serial) { auto ret = processIXFRRecords(master, zone, records, std::dynamic_pointer_cast(masterSOA)); - BOOST_CHECK_EQUAL(ret.size(), 0); + // this is actually an empty AXFR + BOOST_CHECK_EQUAL(ret.size(), 1); + // nothing in the deletion part then + BOOST_CHECK_EQUAL(ret.at(0).first.size(), 0); + // and the two SOAs in the addition part + BOOST_CHECK_EQUAL(ret.at(0).second.size(), 2); + BOOST_CHECK_EQUAL(ret.at(0).second.at(0).d_type, QType(QType::SOA).getCode()); + BOOST_CHECK_EQUAL(ret.at(0).second.at(1).d_type, QType(QType::SOA).getCode()); } BOOST_AUTO_TEST_CASE(test_ixfr_invalid_no_records) { diff --git a/regression-tests.recursor-dnssec/test_RPZ.py b/regression-tests.recursor-dnssec/test_RPZ.py index 0ed6e08ba..beedf56db 100644 --- a/regression-tests.recursor-dnssec/test_RPZ.py +++ b/regression-tests.recursor-dnssec/test_RPZ.py @@ -118,6 +118,12 @@ class RPZServer(object): dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) ] + elif newSerial == 8: + # this one is a bit special too, we are answering with a full AXFR and the new zone is empty + records = [ + dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), + dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) + ] response.answer = records return (newSerial, response) @@ -200,6 +206,7 @@ webserver-port=%d webserver-address=127.0.0.1 webserver-password=%s api-key=%s +log-rpz-changes=yes """ % (_confdir, _wsPort, _wsPassword, _apiKey) @classmethod @@ -256,7 +263,7 @@ api-key=%s self.assertRcodeEqual(res, dns.rcode.NOERROR) self.assertEqual(len(res.answer), 0) - def checkNXD(self, qname, qtype): + def checkNXD(self, qname, qtype='A'): query = dns.message.make_query(qname, qtype, want_dnssec=True) query.flags |= dns.flags.CD for method in ("sendUDPQuery", "sendTCPQuery"): @@ -436,6 +443,18 @@ e 3600 IN A 192.0.2.42 self.checkTruncated('tc.example.') self.checkDropped('drop.example.') + # eighth zone, all entries should be gone + self.waitUntilCorrectSerialIsLoaded(8) + self.checkRPZStats(8, 0, 3, self._xfrDone) + self.checkNotBlocked('a.example.') + self.checkNotBlocked('b.example.') + self.checkNotBlocked('c.example.') + self.checkNotBlocked('d.example.') + self.checkNotBlocked('e.example.') + self.checkNXD('f.example.') + self.checkNXD('tc.example.') + self.checkNXD('drop.example.') + class RPZFileRecursorTest(RPZRecursorTest): """ This test makes sure that we correctly load RPZ zones from a file -- 2.40.0