diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c index 1491e20f5a65b958870a59ea6ae6a580a2d91f9f..8bcbc8de2dbf1ec10a66eada64f73ab8f3d868cf 100644 --- a/src/knot/nameserver/ixfr.c +++ b/src/knot/nameserver/ixfr.c @@ -51,17 +51,20 @@ static int ixfr_put_chg_part(knot_pkt_t *pkt, struct ixfr_proc *ixfr, if (!knot_rrset_empty(&ixfr->cur_rr)) { IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); - if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA) { - ixfr->in_remove_section = !ixfr->in_remove_section; - } journal_read_clear_rrset(&ixfr->cur_rr); } while (journal_read_rrset(read, &ixfr->cur_rr, true)) { - if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA && - !ixfr->in_remove_section && - knot_soa_serial(ixfr->cur_rr.rrs.rdata) == ixfr->soa_to) { - break; + if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA) { + ixfr->in_remove_section = !ixfr->in_remove_section; + + if (ixfr->in_remove_section) { + if (knot_soa_serial(ixfr->cur_rr.rrs.rdata) == ixfr->soa_to) { + break; + } + } else { + ixfr->soa_last = knot_soa_serial(ixfr->cur_rr.rrs.rdata); + } } if (pkt->size > KNOT_WIRE_PTR_MAX) { @@ -71,9 +74,6 @@ static int ixfr_put_chg_part(knot_pkt_t *pkt, struct ixfr_proc *ixfr, } IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); - if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA) { - ixfr->in_remove_section = !ixfr->in_remove_section; - } journal_read_clear_rrset(&ixfr->cur_rr); } @@ -207,6 +207,7 @@ static int ixfr_answer_init(knotd_qdata_t *qdata, uint32_t *serial_from) xfer->soa_from = knot_soa_serial(their_soa->rrs.rdata); xfer->soa_to = zone_contents_serial(qdata->extra->contents); + xfer->soa_last = xfer->soa_from; qdata->extra->ext = xfer; qdata->extra->ext_cleanup = &ixfr_answer_cleanup; @@ -306,6 +307,10 @@ int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */ return KNOT_STATE_PRODUCE; /* Check for more. */ case KNOT_EOK: /* Last response. */ + if (ixfr->soa_last != ixfr->soa_to) { + IXFROUT_LOG(LOG_ERR, qdata, "failed (inconsistent history)"); + return KNOT_STATE_FAIL; + } xfr_stats_end(&ixfr->proc.stats); xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_IXFR, LOG_DIRECTION_OUT, REMOTE(qdata), false, &ixfr->proc.stats); diff --git a/src/knot/nameserver/ixfr.h b/src/knot/nameserver/ixfr.h index 50b88e7d600f815d6a0d52d33ff8994cc64e4ed8..3012be123ee350d0390a3ddcca011d9bc4c77877 100644 --- a/src/knot/nameserver/ixfr.h +++ b/src/knot/nameserver/ixfr.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -50,6 +50,7 @@ struct ixfr_proc { knot_mm_t *mm; uint32_t soa_from; uint32_t soa_to; + uint32_t soa_last; }; /*! diff --git a/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone b/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone new file mode 100644 index 0000000000000000000000000000000000000000..8a1af1e3ce59d11842d931232be145137267007d --- /dev/null +++ b/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone @@ -0,0 +1,16 @@ +$ORIGIN example.com. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111201 10800 3600 1209600 7200 + NS dns1 + NS dns2 + MX 10 mail + +dns1 A 192.0.2.1 + AAAA 2001:DB8::1 + +dns2 A 192.0.2.2 + AAAA 2001:DB8::2 + +mail A 192.0.2.3 + AAAA 2001:DB8::3 diff --git a/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone.1 b/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone.1 new file mode 100644 index 0000000000000000000000000000000000000000..dc0da7af21b6586dfa6ad992e8067cdecb31101a --- /dev/null +++ b/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone.1 @@ -0,0 +1,16 @@ +$ORIGIN example.com. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111202 10800 3600 1209600 7200 + NS dns1 + NS dns2 + MX 10 mail + +dns1 A 192.0.2.2 + AAAA 2001:DB8::1 + +dns2 A 192.0.2.2 + AAAA 2001:DB8::2 + +mail A 192.0.2.3 + AAAA 2001:DB8::3 diff --git a/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone.2 b/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone.2 new file mode 100644 index 0000000000000000000000000000000000000000..4f077599a3182a5867bd7154a35dacee5c47462e --- /dev/null +++ b/tests-extra/tests/ixfr/inconsistent_history/data/example.com.zone.2 @@ -0,0 +1,16 @@ +$ORIGIN example.com. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111203 10800 3600 1209600 7200 + NS dns1 + NS dns2 + MX 10 mail + +dns1 A 192.0.2.3 + AAAA 2001:DB8::1 + +dns2 A 192.0.2.2 + AAAA 2001:DB8::2 + +mail A 192.0.2.3 + AAAA 2001:DB8::3 diff --git a/tests-extra/tests/ixfr/inconsistent_history/test.py b/tests-extra/tests/ixfr/inconsistent_history/test.py new file mode 100644 index 0000000000000000000000000000000000000000..55472ed3717b06be90255ed087356bf0b388eb9a --- /dev/null +++ b/tests-extra/tests/ixfr/inconsistent_history/test.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +'''Test for failed IXFR with inconsistent history''' + +from dnstest.test import Test + +t = Test() + +master = t.server("knot") +slave = t.server("knot") +zone = t.zone("example.com.", storage=".") +t.link(zone, master, slave, ixfr=True) + +master.disable_notify = True +slave.disable_notify = True + +t.start() + +# Start both servers with the initial zone +serial = master.zone_wait(zone) +slave.zone_wait(zone) + +# Add some zone history on the master only +master.update_zonefile(zone, version=1) +master.reload() +serial = master.zone_wait(zone, serial) + +# Update the zone in a wrong way (zonefile-load: difference, journal-contents: changes, restart) +# -> missing a changeset in the journal +master.update_zonefile(zone, version=2) +master.stop() +master.start() + +# Try to refresh slave, IXFR should fail, AXFR ok +slave.ctl("zone-refresh", wait=True) + +master.zone_wait(zone, serial) +slave.zone_wait(zone, serial) + +# Check that slave has the actual zone +resp = slave.dig("dns1.example.com.", "A") +resp.check() +resp.check_record(name="dns1.example.com.", rtype="A", rdata="192.0.2.3") + +t.end()