Correctly interpret an empty AXFR response to an IXFR query
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 18 Feb 2019 12:07:14 +0000 (13:07 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 20 Feb 2019 09:33:07 +0000 (10:33 +0100)
pdns/ixfr.cc
pdns/test-ixfr_cc.cc
regression-tests.recursor-dnssec/test_RPZ.py

index c6b2137532c625f273add805ca43a9658f9ae63e..0a7dca9f5638829b783a9dd3af2c686c01c0ad2e 100644 (file)
@@ -59,6 +59,12 @@ vector<pair<vector<DNSRecord>, vector<DNSRecord> > > 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;
     }
index bab31a11c50754501fd5caa2a226247e8cdcaa37..00d795e78f15acd63fabeb718207deee05706bdc 100644 (file)
@@ -211,7 +211,14 @@ BOOST_AUTO_TEST_CASE(test_ixfr_same_serial) {
 
   auto ret = processIXFRRecords(master, zone, records, std::dynamic_pointer_cast<SOARecordContent>(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) {
index 0ed6e08ba0bc3450a7b9042a8e364b4fe86c34e1..beedf56db07e0ea1ae4c51b8e01d5f910a3fa60d 100644 (file)
@@ -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