Skip to content

IXFR-in fails with error "a patchset did not change the SOA record" #493

@ximon18

Description

@ximon18

Note: I believe this issue does NOT affect the alpha-5 release as it is caused by code changes since alpha-5 in the Git main branch, specifically the new zone loader.


Given the following IXFR served by NSD:

example.test.		5	IN	SOA	ns1.example.test. mail.example.test. 2 60 60 3600 5 <- Start of IXFR for serial 2
example.test.		5	IN	SOA	ns1.example.test. mail.example.test. 1 60 60 3600 5 <- Start of deletions from serial 1
www         A   169.254.1.1
example.test.		5	IN	SOA	ns1.example.test. mail.example.test. 2 60 60 3600 5 <- Start of addiitions in serial 2
www         A   192.168.0.1
example.test.		5	IN	SOA	ns1.example.test. mail.example.test. 2 60 60 3600 5 <- End of IXFR for serial 2

Cascade incorrectly rejects the IXFR with:

| 2026-03-02T14:35:07.112263Z  INFO refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}: cascaded::loader: Refreshing Name(example.test.)
| 2026-03-02T14:35:07.112284Z DEBUG refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}: cascaded::loader::server: Refreshing Name(example.test.) from server 127.0.0.1:1055
| 2026-03-02T14:35:07.112298Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}: cascaded::loader::server: Attempting an IXFR against 127.0.0.1:1055 for Name(example.test.)
| 2026-03-02T14:35:07.112335Z DEBUG refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: cascaded::loader::server: Attempting an IXFR against 127.0.0.1:1055 for Name(example.test.)
| 2026-03-02T14:35:07.112634Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::client::dgram: Received 230 bytes of message
| 2026-03-02T14:35:07.112654Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::client::dgram: Received message is accepted
| 2026-03-02T14:35:07.112686Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::xfr::protocol::iterator: XFR record 0: Record { owner: ParsedName(example.test.), class: Class::IN, ttl: Ttl(5), data: ZoneRecordData::Soa(Soa { mname: ParsedName(ns1.example.test.), rname: ParsedName(mail.example.test.), serial: Serial(2), refresh: Ttl(60), retry: Ttl(60), expire: Ttl(3600), minimum: Ttl(5) }) }
| 2026-03-02T14:35:07.112710Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::xfr::protocol::iterator: XFR record 1: Record { owner: ParsedName(example.test.), class: Class::IN, ttl: Ttl(5), data: ZoneRecordData::Soa(Soa { mname: ParsedName(ns1.example.test.), rname: ParsedName(mail.example.test.), serial: Serial(1), refresh: Ttl(60), retry: Ttl(60), expire: Ttl(3600), minimum: Ttl(5) }) }
| 2026-03-02T14:35:07.112728Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::xfr::protocol::iterator: XFR record 2: Record { owner: ParsedName(www.example.test.), class: Class::IN, ttl: Ttl(5), data: ZoneRecordData::A(A { addr: 169.254.1.1 }) }
| 2026-03-02T14:35:07.112752Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::xfr::protocol::iterator: XFR record 3: Record { owner: ParsedName(example.test.), class: Class::IN, ttl: Ttl(5), data: ZoneRecordData::Soa(Soa { mname: ParsedName(ns1.example.test.), rname: ParsedName(mail.example.test.), serial: Serial(2), refresh: Ttl(60), retry: Ttl(60), expire: Ttl(3600), minimum: Ttl(5) }) }
| 2026-03-02T14:35:07.112773Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::xfr::protocol::iterator: XFR record 4: Record { owner: ParsedName(www.example.test.), class: Class::IN, ttl: Ttl(5), data: ZoneRecordData::A(A { addr: 192.168.0.1 }) }
| 2026-03-02T14:35:07.112784Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}:refresh{zone=example.test addr=127.0.0.1:1055}:ixfr{zone=example.test addr=127.0.0.1:1055}: domain::net::xfr::protocol::iterator: XFR record 5: Record { owner: ParsedName(example.test.), class: Class::IN, ttl: Ttl(5), data: ZoneRecordData::Soa(Soa { mname: ParsedName(ns1.example.test.), rname: ParsedName(mail.example.test.), serial: Serial(2), refresh: Ttl(60), retry: Ttl(60), expire: Ttl(3600), minimum: Ttl(5) }) }
| 2026-03-02T14:35:07.112821Z DEBUG refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}: cascaded::loader::refresh: Updating the scheduling of 'Name(example.test.)' from Some(Instant { tv_sec: 4148565, tv_nsec: 80942976 }) to Some(Instant { tv_sec: 4148565, tv_nsec: 382927134 })
| 2026-03-02T14:35:07.112859Z ERROR refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}: cascaded::loader: Could not load the zone: the IXFR failed: could not write the zone data: a patchset did not change the SOA record zone=example.test
| 2026-03-02T14:35:07.112869Z TRACE refresh{zone=example.test source=Server { addr: 127.0.0.1:1055, tsig_key: None }}: cascaded::zone::storage: Giving up on the ongoing load zone=example.test

This appears to be because of this code (from

Some(Ok(ZoneUpdate::BeginBatchDelete(_))) => {
):

        Some(Ok(ZoneUpdate::BeginBatchDelete(_))) => {
            // This is an IXFR.
            let mut writer = builder.patch().unwrap();
            process_ixfr(&mut writer, updates, metrics)?;
            if !interpreter.is_finished() {
                // Fail: UDP-based IXFR returned a partial IXFR
                return Err(IxfrError::IncompleteResponse);
            }

The issue is that process_ixfr() expects to receive the initial SOA that begins the batch delete, but that was already consumed. Note that the consumed SOA is still available as the unnamed _ value of BeginBatchDelete(_), but is no longer available via updates.next().

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions