--- /dev/null
+--- a/ixfr.cc
++++ b/ixfr.cc
+@@ -123,7 +123,7 @@ vector<pair<vector<DNSRecord>, vector<DN
+ }
+
+ // 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+
+ 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+ 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+ // 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+ 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+
+ 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+
+ 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+ 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 @@ vector<pair<vector<DNSRecord>, vector<DN
+ }
+ }
+
+- // 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);
+ }