--- pdns-recursor-4.4.7/ixfr.cc	2021-11-03 15:52:43.000000000 +0000
+++ pdns-recursor-4.4.8/ixfr.cc	2022-03-16 16:03:58.000000000 +0000
@@ -123,7 +123,7 @@
 }
 
 // Returns pairs of "remove & add" vectors. If you get an empty remove, it means you got an AXFR!
-vector<pair<vector<DNSRecord>, vector<DNSRecord> > > getIXFRDeltas(const ComboAddress& master, const DNSName& zone, const DNSRecord& oursr, 
+vector<pair<vector<DNSRecord>, vector<DNSRecord> > > getIXFRDeltas(const ComboAddress& primary, const DNSName& zone, const DNSRecord& oursr, 
                                                                    const TSIGTriplet& tt, const ComboAddress* laddr, size_t maxReceivedBytes)
 {
   vector<pair<vector<DNSRecord>, vector<DNSRecord> > >  ret;
@@ -137,7 +137,7 @@
 
   pw.commit();
   TSIGRecordContent trc;
-  TSIGTCPVerifier tsigVerifier(tt, master, trc);
+  TSIGTCPVerifier tsigVerifier(tt, primary, trc);
   if(!tt.algo.empty()) {
     TSIGHashEnum the;
     getTSIGHashEnum(tt.algo, the);
@@ -156,11 +156,11 @@
   string msg((const char*)&len, 2);
   msg.append((const char*)&packet[0], packet.size());
 
-  Socket s(master.sin4.sin_family, SOCK_STREAM);
+  Socket s(primary.sin4.sin_family, SOCK_STREAM);
   //  cout<<"going to connect"<<endl;
   if(laddr)
     s.bind(*laddr);
-  s.connect(master);
+  s.connect(primary);
   //  cout<<"Connected"<<endl;
   s.writen(msg);
 
@@ -171,16 +171,24 @@
   //   SOA WHERE THIS DELTA GOES
   //   RECORDS TO ADD
   // CURRENT MASTER SOA 
-  std::shared_ptr<SOARecordContent> masterSOA = nullptr;
+  std::shared_ptr<SOARecordContent> primarySOA = nullptr;
   vector<DNSRecord> records;
   size_t receivedBytes = 0;
-  int8_t ixfrInProgress = -2;
   std::string reply;
 
+  enum transferStyle { Unknown, AXFR, IXFR } style = Unknown;
+  const unsigned int expectedSOAForAXFR = 2;
+  const unsigned int expectedSOAForIXFR = 3;
+  unsigned int primarySOACount = 0;
+
   for(;;) {
-    // IXFR end
-    if (ixfrInProgress >= 0)
+    // IXFR or AXFR style end reached? We don't want to process trailing data after the closing SOA
+    if (style == AXFR && primarySOACount == expectedSOAForAXFR) {
+      break;
+    }
+    else if (style == IXFR && primarySOACount == expectedSOAForIXFR) {
       break;
+    }
 
     if(s.read((char*)&len, sizeof(len)) != sizeof(len))
       break;
@@ -191,7 +199,7 @@
       break;
 
     if (maxReceivedBytes > 0 && (maxReceivedBytes - receivedBytes) < (size_t) len)
-      throw std::runtime_error("Reached the maximum number of received bytes in an IXFR delta for zone '"+zone.toLogString()+"' from master "+master.toStringWithPort());
+      throw std::runtime_error("Reached the maximum number of received bytes in an IXFR delta for zone '"+zone.toLogString()+"' from primary "+primary.toStringWithPort());
 
     reply.resize(len);
     readn2(s.getHandle(), &reply.at(0), len);
@@ -199,7 +207,7 @@
 
     MOADNSParser mdp(false, reply);
     if(mdp.d_header.rcode) 
-      throw std::runtime_error("Got an error trying to IXFR zone '"+zone.toLogString()+"' from master '"+master.toStringWithPort()+"': "+RCode::to_s(mdp.d_header.rcode));
+      throw std::runtime_error("Got an error trying to IXFR zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"': "+RCode::to_s(mdp.d_header.rcode));
 
     //    cout<<"Got a response, rcode: "<<mdp.d_header.rcode<<", got "<<mdp.d_answers.size()<<" answers"<<endl;
 
@@ -209,32 +217,47 @@
 
     for(auto& r: mdp.d_answers) {
       //      cout<<r.first.d_name<< " " <<r.first.d_content->getZoneRepresentation()<<endl;
-      if(!masterSOA) {
+      if(!primarySOA) {
         // we have not seen the first SOA record yet
         if (r.first.d_type != QType::SOA) {
-          throw std::runtime_error("The first record of the IXFR answer for zone '"+zone.toLogString()+"' from master '"+master.toStringWithPort()+"' is not a SOA ("+QType(r.first.d_type).getName()+")");
+          throw std::runtime_error("The first record of the IXFR answer for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"' is not a SOA ("+QType(r.first.d_type).getName()+")");
         }
 
         auto sr = getRR<SOARecordContent>(r.first);
         if (!sr) {
-          throw std::runtime_error("Error getting the content of the first SOA record of the IXFR answer for zone '"+zone.toLogString()+"' from master '"+master.toStringWithPort()+"'");
+          throw std::runtime_error("Error getting the content of the first SOA record of the IXFR answer for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"'");
         }
 
         if(sr->d_st.serial == std::dynamic_pointer_cast<SOARecordContent>(oursr.d_content)->d_st.serial) {
           // we are up to date
           return ret;
         }
-        masterSOA = sr;
+        primarySOA = sr;
+        ++primarySOACount;
       } else if (r.first.d_type == QType::SOA) {
         auto sr = getRR<SOARecordContent>(r.first);
         if (!sr) {
-          throw std::runtime_error("Error getting the content of SOA record of IXFR answer for zone '"+zone.toLogString()+"' from master '"+master.toStringWithPort()+"'");
+          throw std::runtime_error("Error getting the content of SOA record of IXFR answer for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"'");
         }
 
-        // we hit the last SOA record
-        // IXFR is considered to be done if we hit the last SOA record twice
-        if (masterSOA->d_st.serial == sr->d_st.serial) {
-          ixfrInProgress++;
+        // we hit a marker SOA record
+        if (primarySOA->d_st.serial == sr->d_st.serial) {
+          ++primarySOACount;
+        }
+      }
+      // When we see the 2nd record, we can decide what the style is
+      if (records.size() == 1 && style == Unknown) {
+        if (r.first.d_type != QType::SOA) {
+          // Non-empty AXFR style has a non-SOA record following the first SOA
+          style = AXFR;
+        }
+        else if (primarySOACount == expectedSOAForAXFR) {
+          // Empty zone AXFR style: start SOA is immediately followed by end marker SOA
+          style = AXFR;
+        }
+        else {
+          // IXFR has a 2nd SOA (with different serial) following the first
+          style = IXFR;
         }
       }
 
@@ -245,7 +268,7 @@
         if(r.first.d_type == QType::OPT)
           continue;
 
-        throw std::runtime_error("Unexpected record (" +QType(r.first.d_type).getName()+") in non-answer section ("+std::to_string(r.first.d_place)+")in IXFR response for zone '"+zone.toLogString()+"' from master '"+master.toStringWithPort());
+        throw std::runtime_error("Unexpected record (" +QType(r.first.d_type).getName()+") in non-answer section ("+std::to_string(r.first.d_place)+")in IXFR response for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort());
       }
 
       r.first.d_name.makeUsRelative(zone);
@@ -253,7 +276,21 @@
     }
   }
 
-  //  cout<<"Got "<<records.size()<<" records"<<endl;
+  switch (style) {
+  case IXFR:
+    if (primarySOACount != expectedSOAForIXFR) {
+      throw std::runtime_error("Incomplete IXFR transfer for '" + zone.toLogString() + "' from primary '" + primary.toStringWithPort());
+    }
+    break;
+  case AXFR:
+    if (primarySOACount != expectedSOAForAXFR){
+      throw std::runtime_error("Incomplete AXFR style transfer for '" + zone.toLogString() + "' from primary '" + primary.toStringWithPort());
+    }
+    break;
+  case Unknown:
+    throw std::runtime_error("Incomplete XFR for '" + zone.toLogString() + "' from primary '" + primary.toStringWithPort());
+    break;
+  }
 
-  return processIXFRRecords(master, zone, records, masterSOA);
+  return processIXFRRecords(primary, zone, records, primarySOA);
 }
