From 2c1f6068924d3590a027df2c3b3fb8915222b043 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:48:43 +0200 Subject: [PATCH 01/44] Add limited initial IXFR out support (AXFR fallback only) and a minimal system test. --- .../scripts/manage-test-environment.sh | 3 +- integration-tests/system-tests.yml | 15 +++ integration-tests/tests/ixfr-out/action.yml | 92 +++++++++++++++++++ src/server/request.rs | 1 - src/server/service.rs | 75 ++++++++++++++- 5 files changed, 178 insertions(+), 8 deletions(-) create mode 100644 integration-tests/tests/ixfr-out/action.yml diff --git a/integration-tests/scripts/manage-test-environment.sh b/integration-tests/scripts/manage-test-environment.sh index 2b811cd19..733d8b7e3 100755 --- a/integration-tests/scripts/manage-test-environment.sh +++ b/integration-tests/scripts/manage-test-environment.sh @@ -293,8 +293,7 @@ pattern: name: secondary zonefile: "%s.secondary-zone" allow-notify: 127.0.0.1 NOKEY - # Until Cascade supports IXFR we always use AXFR - request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY + request-xfr: 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY zone: diff --git a/integration-tests/system-tests.yml b/integration-tests/system-tests.yml index c9d3592af..cdc590e49 100644 --- a/integration-tests/system-tests.yml +++ b/integration-tests/system-tests.yml @@ -166,6 +166,21 @@ jobs: with: log-level: ${{ inputs.log-level }} + ixfr-out: + name: Serve a zone via IXFR to the NSD secondary. + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/set-build-profile + with: + build-profile: ${{ inputs.build-profile }} + - uses: ./integration-tests/tests/ixfr-out + with: + log-level: ${{ inputs.log-level }} + incremental-signing: name: Test incremental signing. runs-on: ubuntu-latest diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml new file mode 100644 index 000000000..1adadce51 --- /dev/null +++ b/integration-tests/tests/ixfr-out/action.yml @@ -0,0 +1,92 @@ +# Making reusable composite actions documented at +# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository +name: 'Serve a zone via IXFR to the NSD secondary.' +description: 'Serve a zone via IXFR to the NSD secondary.' +defaults: + # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell + run: + shell: bash --noprofile --norc -eo pipefail -x {0} +inputs: + log-level: + description: The level of logging that Cascade should output. + required: false + default: debug + type: choice + options: + - error + - warning + - info + - debug + - trace +runs: + using: "composite" + steps: + - uses: ./.github/actions/prepare-systest-env + - uses: ./.github/actions/setup-and-start-cascade + with: + log-level: ${{ inputs.log-level }} + + - name: Add a NOTIFY with TSIG outbound policy + run: | + POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) + cascade template policy | grep -Ev '(send-notify-to|accept-xfr-from)' > "${POLICY_DIR}/custom.toml" + + sed -i -e 's/serial-policy = "date-counter"/serial-policy = "keep"/' "${POLICY_DIR}/custom.toml" + echo 'send-notify-to = ["127.0.0.1:1054"]' >> "${POLICY_DIR}/custom.toml" + + # TODO: Extend the test to use TSIG (driven by inputs ala the tsig-downstream test) + # once PRs #564 and #587 have been merged. + # echo 'send-notify-to = ["127.0.0.1:1054^tsig-key"]' >> "${POLICY_DIR}/custom.toml" + # cascade tsig add tsig-key hmac-sha256 "COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" + cascade policy reload + + - name: Make an initial zone based on RFC 1995 section 7 + run: | + tee example.test.zone <<'EOF' + example.test. IN SOA ns.example.test. mail.example.test. ( + 1 60 60 3600 5) + IN NS ns.example.test. + ns.example.test. IN A 133.69.136.1 + nezu.example.test. IN A 133.69.136.5 + EOF + + - name: Add the zone using the custom policy + run: | + cascade zone add --policy custom --source $PWD/example.test.zone example.test + + - name: Check zone status + run: | + timeout=10 # seconds + start=$(date +%s) + until cascade zone status example.test | grep -q "Published zone available"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Check the SOA SERIAL at the NSD secondary + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 example.test SOA | grep -q 'ns.example.test. mail.example.test. 1 60 60 3600 5'; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 1054 example.test SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + # Note: There is no NSD metric that I am aware of that we can use to + # verify that it received IXFR from Cascade instead of AXFR, nor is there + # any Cascade Prometheus metric or CLI command that we can use to check + # this either. Increasing the NSD log level to 9 doesn't cause it to + # report whether it received IXFR or AXFR either. + + - name: Print log files on any failure in this job + uses: ./.github/actions/print-logfiles + if: failure() diff --git a/src/server/request.rs b/src/server/request.rs index 44c4a248a..078026d8a 100644 --- a/src/server/request.rs +++ b/src/server/request.rs @@ -139,7 +139,6 @@ pub enum ZoneRequestKind { Axfr, /// An IXFR request. - #[expect(dead_code)] Ixfr { /// The SOA record known to the client. known_soa: Record<(), Soa>>, diff --git a/src/server/service.rs b/src/server/service.rs index b6faecf37..6a334c10b 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -79,10 +79,14 @@ mod compat { message::Request, service::{CallResult, Service, ServiceResult}, }, - new::base::wire::ParseBytesZC, + new::{ + base::{name::Name, wire::ParseBytesZC}, + rdata::Soa, + }, tsig, }; use futures::Stream; + use tracing::trace; use crate::server::request::{RequestKind, ZoneRequestKind}; @@ -142,10 +146,9 @@ mod compat { } // TODO: Support IXFR. - ZoneRequestKind::Ixfr { .. } => Box::pin(std::future::ready(error( - old_request.message(), - Rcode::NOTIMP, - ))), + ZoneRequestKind::Ixfr { known_soa } => { + Box::pin(ixfr(old_request, known_soa.rdata, zone.clone())) as Response + } } } } @@ -252,6 +255,68 @@ mod compat { Box::new(stream) as _ } + async fn ixfr( + request: Request, Option>>, + client_soa: Soa>, + zone: ServedZone, + ) -> ResponseStream { + // Refuse IXFR requests over UDP. + if request.transport_ctx().is_udp() { + return error(request.message(), Rcode::NOTIMP); + } + + // Save a cheap clone of the zone to avoid a borrow checker error. + let zone_clone = zone.clone(); + + // Obtain a read lock to read the zone for an extended duration. + let viewer = zone.viewer.read_owned().await; + + if viewer.is_empty() { + // The zone is known to exist, but we don't have any data for it. + return error(request.message(), Rcode::NOTAUTH); + } + + // https://datatracker.ietf.org/doc/html/rfc1995#section-4 + // 4. Response Format + // "If incremental zone transfer is not available, the entire zone + // is returned. The first and the last RR of the response is the + // SOA record of the zone. I.e. the behavior is the same as an + // AXFR response except the query type is IXFR." + + // https://datatracker.ietf.org/doc/html/rfc1995#section-2 + // 2. Brief Description of the Protocol + // "If an IXFR query with the same or newer version number than that + // of the server is received, it is replied to with a single SOA + // record of the server's current version, just as in AXFR." + // ^^^^^^^^^^^^^^^ + // Errata https://www.rfc-editor.org/errata/eid3196 points out that + // this is NOT "just as in AXFR" as AXFR does not do that. + let our_soa_serial = { viewer.soa().rdata.serial }; + + if client_soa.serial >= our_soa_serial { + trace!("Responding to IXFR with single SOA because query serial >= zone serial"); + return soa(request.message(), &*viewer); + } else { + // TODO: implement retrieval and serving of diffs. + + // TODO: Add something like the Bind `max-ixfr-ratio` option that + // "sets the size threshold (expressed as a percentage of the + // size of the full zone) beyond which named chooses to use an + // AXFR response rather than IXFR when answering zone transfer + // requests"? + + // Note: Unlike RFC 5936 for AXFR, neither RFC 1995 nor RFC 9103 + // say anything about whether an IXFR response can consist of + // more than one response message, but given the 2^16 byte maximum + // response size of a TCP DNS message and the 2^16 maximum number + // of ANSWER RRs allowed per DNS response, large zones may not + // fit in a single response message and will have to be split into + // multiple response messages. + + axfr(request, zone_clone).await + } + } + fn error(request: &Message>, rcode: Rcode) -> ResponseStream { let response = MessageBuilder::new_stream_vec() .start_error(request, rcode) From 5dd79aa27a861246005ce4c500a17bc13f79365e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 29 Apr 2026 00:14:59 +0200 Subject: [PATCH 02/44] PoC IXFR-out. Will need changing based on PR #589. --- crates/zonedata/src/persister.rs | 8 +- integration-tests/tests/ixfr-out/action.yml | 96 +++++++++++++++- src/server/service.rs | 119 +++++++++++++++++--- src/zone/storage.rs | 24 +++- 4 files changed, 217 insertions(+), 30 deletions(-) diff --git a/crates/zonedata/src/persister.rs b/crates/zonedata/src/persister.rs index ae19663dc..373564cb2 100644 --- a/crates/zonedata/src/persister.rs +++ b/crates/zonedata/src/persister.rs @@ -116,17 +116,17 @@ impl SignedZonePersister { impl SignedZonePersister { /// Perform the actual persisting. - pub fn persist(self) -> SignedZonePersisted { + pub fn persist(self) -> (SignedZonePersisted, Arc) { let SignedZonePersister { data, - signed_index, + signed_index: _, signed_diff, } = self; // TODO - let _ = (signed_index, signed_diff); + // let _ = (signed_index, signed_diff); - SignedZonePersisted { data } + (SignedZonePersisted { data }, signed_diff) } } diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index 1adadce51..21d2aecdc 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -43,11 +43,11 @@ runs: - name: Make an initial zone based on RFC 1995 section 7 run: | tee example.test.zone <<'EOF' - example.test. IN SOA ns.example.test. mail.example.test. ( + EXAMPLE.TEST. IN SOA NS.EXAMPLE.TEST. mail.example.test. ( 1 60 60 3600 5) - IN NS ns.example.test. - ns.example.test. IN A 133.69.136.1 - nezu.example.test. IN A 133.69.136.5 + IN NS NS.EXAMPLE.TEST. + NS.EXAMPLE.TEST. IN A 133.69.136.1 + NEZU.EXAMPLE.TEST. IN A 133.69.136.5 EOF - name: Add the zone using the custom policy @@ -81,12 +81,100 @@ runs: sleep 1 done + - name: Edit the source zone and tell Cascade to reload it. + run: | + tee example.test.zone <<'EOF' + example.test. IN SOA ns.example.test. mail.example.test. ( + 2 60 60 3600 5) + IN NS NS.EXAMPLE.TEST. + NS.EXAMPLE.TEST. IN A 133.69.136.1 + JAIN-BB.EXAMPLE.TEST. IN A 133.69.136.4 + IN A 192.41.197.2 + EOF + cascade zone reload example.test + + - name: Check zone status + run: | + timeout=10 # seconds + start=$(date +%s) + until cascade zone status example.test | grep -q "Published zone available"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Check the SOA SERIAL at the NSD secondary + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 example.test SOA | grep -q 'ns.example.test. mail.example.test. 2 60 60 3600 5'; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 1054 example.test SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + - name: Edit the source zone and tell Cascade to reload it. + run: | + tee example.test.zone <<'EOF' + EXAMPLE.TEST. IN SOA ns.example.test. mail.example.test. ( + 3 60 60 3600 5) + IN NS NS.EXAMPLE.TEST. + NS.EXAMPLE.TEST. IN A 133.69.136.1 + JAIN-BB.EXAMPLE.TEST. IN A 133.69.136.3 + IN A 192.41.197.2 + EOF + cascade zone reload example.test + + - name: Check zone status + run: | + timeout=10 # seconds + start=$(date +%s) + until cascade zone status example.test | grep -q "Published zone available"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Check the SOA SERIAL at the NSD secondary + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 example.test SOA | grep -q 'ns.example.test. mail.example.test. 3 60 60 3600 5'; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 1054 example.test SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + # Note: There is no NSD metric that I am aware of that we can use to # verify that it received IXFR from Cascade instead of AXFR, nor is there # any Cascade Prometheus metric or CLI command that we can use to check # this either. Increasing the NSD log level to 9 doesn't cause it to # report whether it received IXFR or AXFR either. + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test AXFR + + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=1 + + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=2 + + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=3 + + - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 example.test AXFR + - name: Print log files on any failure in this job uses: ./.github/actions/print-logfiles if: failure() diff --git a/src/server/service.rs b/src/server/service.rs index 6a334c10b..72f163af8 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -262,6 +262,7 @@ mod compat { ) -> ResponseStream { // Refuse IXFR requests over UDP. if request.transport_ctx().is_udp() { + tracing::warn!("Reject IXFR over UDP"); return error(request.message(), Rcode::NOTIMP); } @@ -276,6 +277,9 @@ mod compat { return error(request.message(), Rcode::NOTAUTH); } + // Remember the latest SOA. + let new_soa = viewer.soa().clone(); + // https://datatracker.ietf.org/doc/html/rfc1995#section-4 // 4. Response Format // "If incremental zone transfer is not available, the entire zone @@ -296,25 +300,104 @@ mod compat { if client_soa.serial >= our_soa_serial { trace!("Responding to IXFR with single SOA because query serial >= zone serial"); return soa(request.message(), &*viewer); - } else { - // TODO: implement retrieval and serving of diffs. - - // TODO: Add something like the Bind `max-ixfr-ratio` option that - // "sets the size threshold (expressed as a percentage of the - // size of the full zone) beyond which named chooses to use an - // AXFR response rather than IXFR when answering zone transfer - // requests"? - - // Note: Unlike RFC 5936 for AXFR, neither RFC 1995 nor RFC 9103 - // say anything about whether an IXFR response can consist of - // more than one response message, but given the 2^16 byte maximum - // response size of a TCP DNS message and the 2^16 maximum number - // of ANSWER RRs allowed per DNS response, large zones may not - // fit in a single response message and will have to be split into - // multiple response messages. - - axfr(request, zone_clone).await } + + let diffs = zone.handle.state.lock().unwrap().storage.diffs.clone(); + + // TODO: Add something like the Bind `max-ixfr-ratio` option that + // "sets the size threshold (expressed as a percentage of the size of + // the full zone) beyond which named chooses to use an AXFR response + // rather than IXFR when answering zone transfer requests"? + + // Note: Unlike RFC 5936 for AXFR, neither RFC 1995 nor RFC 9103 say + // anything about whether an IXFR response can consist of more than + // one response message, but given the 2^16 byte maximum response + // size of a TCP DNS message and the 2^16 maximum number of ANSWER + // RRs allowed per DNS response, large zones may not fit in a single + // response message and will have to be split into multiple response + // messages. + + // Currently we have only a single diff available. + for (i, d) in diffs.iter().enumerate() { + tracing::info!( + "Diff {i} for zone '{}': serial {} => serial {}", + zone.handle.name, + d.removed_soa.as_ref().unwrap().0.rdata.serial, + d.added_soa.as_ref().unwrap().0.rdata.serial, + ); + } + + // Find the diff, if we have it, that removes the SOA serial number + // that the client currently has. That will be the start of the diff + // that we need to serve. + let start_idx = diffs.iter().position(|d| { + d.removed_soa.as_ref().map(|rr| rr.0.rdata.serial) == Some(client_soa.serial) + }); + + let Some(start_idx) = start_idx else { + tracing::trace!( + "Falling back from IXFR to AXFR because no diff is available for zone '{}' from serial {}", + zone.handle.name, + client_soa.serial, + ); + return axfr(request, zone_clone).await; + }; + + let (tx, mut rx) = tokio::sync::mpsc::channel(1024); + + // Stream the records in the background. + tokio::task::spawn(async move { + // Collect the sequence of IXFR output records.. + let mut rrs = vec![new_soa.clone().into()]; + for diff in &diffs[start_idx..] { + rrs.push(diff.removed_soa.clone().unwrap().into()); + rrs.extend(diff.removed_records.clone()); + rrs.push(diff.added_soa.clone().unwrap().into()); + rrs.extend(diff.added_records.clone()); + } + rrs.push(new_soa.into()); + + // Divide the records into DNS messages. + let mut rr_iter = rrs.into_iter().peekable(); + let mut max_message_size = u16::MAX; // TCP + max_message_size -= request.num_reserved_bytes(); + let messages = std::iter::from_fn(move || { + rr_iter.peek()?; + + let mut builder = MessageBuilder::new_stream_vec(); + builder.set_push_limit(max_message_size as usize); + let mut builder = builder + .start_answer(request.message(), Rcode::NOERROR) + .unwrap(); + builder.header_mut().set_aa(true); + + while let Some(record) = rr_iter.peek() { + match builder.push(OldRecord::from(record.clone())) { + // On success, consume the record. + Ok(()) => { + let _ = rr_iter.next(); + } + + // Once the message runs out of space, stop. + Err(_) => break, + } + } + + let response = builder.additional(); + Some(CallResult::new(response)) + }); + + for message in messages { + if tx.send(message).await.is_err() { + // The channel has closed; stop. + break; + } + } + }); + + let stream = futures::stream::poll_fn(move |cx| rx.poll_recv(cx).map(|m| m.map(Ok))); + + return Box::new(stream) as _; } fn error(request: &Message>, rcode: Rcode) -> ResponseStream { diff --git a/src/zone/storage.rs b/src/zone/storage.rs index 5574d26ab..48b4f5de0 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -26,9 +26,9 @@ use std::{fmt, sync::Arc}; use cascade_zonedata::{ - LoadedZoneBuilder, LoadedZoneBuilt, LoadedZonePersister, LoadedZoneReviewer, SignedZoneBuilder, - SignedZoneBuilt, SignedZonePersister, SignedZoneReviewer, SoaRecord, ZoneCleaner, - ZoneDataStorage, ZoneViewer, + DiffData, LoadedZoneBuilder, LoadedZoneBuilt, LoadedZonePersister, LoadedZoneReviewer, + SignedZoneBuilder, SignedZoneBuilt, SignedZonePersister, SignedZoneReviewer, SoaRecord, + ZoneCleaner, ZoneDataStorage, ZoneViewer, }; use domain::base::Serial; use tracing::{info, trace, trace_span, warn}; @@ -648,12 +648,24 @@ impl StorageZoneHandle<'_> { trace!("Persisting the signed instance"); // Perform the persisting. - let persisted = tokio::task::spawn_blocking(move || persister.persist()).await.unwrap(); + let (persisted, signed_diff) = tokio::task::spawn_blocking(move || persister.persist()).await.unwrap(); // Mark persistence as completed. let viewer = { let mut state = zone.state.lock().unwrap(); let state = &mut *state; + + // Store the signed diff in-memory for serving IXFR. + // Only push a diff if a SOA was removed, otherwise this is + // not a diff to a previous version of the zone but actually + // the entire new zone content compared to an empty zone. Also + // don't store a diff to the same serial. + if signed_diff.removed_soa.is_some() { + if signed_diff.removed_soa != signed_diff.added_soa { + state.storage.diffs.push(signed_diff); + } + } + match transition(&mut state.storage.machine) { (transition, ZoneDataStorage::PersistingSigned(s)) => { let (s, viewer) = s.mark_complete(persisted); @@ -836,6 +848,9 @@ pub struct StorageState { // current i.e. published zone instance. pub published_loaded_soa: Option, + /// Diffs from one serial to another. + pub diffs: Vec>, + /// Ongoing background tasks. /// /// When the zone data needs to be cleaned or persisted, a background task @@ -857,6 +872,7 @@ impl StorageState { signed_review_soa: None, published_soa: None, published_loaded_soa: None, + diffs: Default::default(), background_tasks: Default::default(), } } From 942120319487da8f4e4c292102c1f1e0994483cc Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:14:16 +0200 Subject: [PATCH 03/44] Clippy. --- src/persistence/persist.rs | 8 +++----- src/server/service.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/persistence/persist.rs b/src/persistence/persist.rs index 38c372693..bf9f9dda0 100644 --- a/src/persistence/persist.rs +++ b/src/persistence/persist.rs @@ -44,11 +44,9 @@ pub fn persist_signed( // compared to an empty zone. Also don't store a diff to the same serial. let signed_diff = persister.signed_diff(); - if signed_diff.removed_soa.is_some() { - if signed_diff.removed_soa != signed_diff.added_soa { - let mut state = zone.state.lock().unwrap(); - state.storage.diffs.push(signed_diff.clone()); - } + if signed_diff.removed_soa.is_some() && signed_diff.removed_soa != signed_diff.added_soa { + let mut state = zone.state.lock().unwrap(); + state.storage.diffs.push(signed_diff.clone()); } persister.mark_complete() diff --git a/src/server/service.rs b/src/server/service.rs index c856c3d2a..8582ace00 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -439,7 +439,7 @@ mod compat { let stream = futures::stream::poll_fn(move |cx| rx.poll_recv(cx).map(|m| m.map(Ok))); - return Box::new(stream) as _; + Box::new(stream) as _ } fn error(request: &Message>, rcode: Rcode) -> ResponseStream { From 5345a2b9beece2ee46a7a4497f8279c92578a7f9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:30:25 +0200 Subject: [PATCH 04/44] De-clutter ixfr-out system test output. --- integration-tests/tests/ixfr-out/action.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index 21d2aecdc..d6394463f 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -165,15 +165,18 @@ runs: # this either. Increasing the NSD log level to 9 doesn't cause it to # report whether it received IXFR or AXFR either. - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test AXFR + # TODO: Verify the AXFR and IXFR response content is as expected / + # matching that of NSD. - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=1 + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test AXFR > cascade-axfr.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=2 + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=1 >cascade-ixfr-1.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=3 + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=2 >cascade-ixfr-2.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 example.test AXFR + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=3 >cascade-ixfr-3.log + + - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 example.test AXFR >nsd-axfr.log - name: Print log files on any failure in this job uses: ./.github/actions/print-logfiles From 39b0b201a00b77cc9c16990a203fdda54c6768ab Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 6 May 2026 13:53:45 +0200 Subject: [PATCH 05/44] Save last serial and key tags in zone state. --- src/signer/incremental.rs | 20 ++++---- src/units/zone_signer.rs | 104 ++++++++++++++++++++------------------ 2 files changed, 65 insertions(+), 59 deletions(-) diff --git a/src/signer/incremental.rs b/src/signer/incremental.rs index c587616d0..5d2aa3c57 100644 --- a/src/signer/incremental.rs +++ b/src/signer/incremental.rs @@ -2216,18 +2216,18 @@ impl IncrementalSigningState { } /// Load the state varibles we need at the start and then update state at the end. -struct LocalState { - apex_remove: HashSet, - apex_extra: Vec, - last_signature_refresh: UnixTime, - key_tags: HashSet, - key_roll: Option, - previous_serial: Option, - next_min_expiration: Option, +pub struct LocalState { + pub apex_remove: HashSet, + pub apex_extra: Vec, + pub last_signature_refresh: UnixTime, + pub key_tags: HashSet, + pub key_roll: Option, + pub previous_serial: Option, + pub next_min_expiration: Option, } impl LocalState { - fn new(zone: &Arc) -> Result { + pub fn new(zone: &Arc) -> Result { let zone_state = zone .state .lock() @@ -2244,7 +2244,7 @@ impl LocalState { }) } - fn save(self, center: &Arc
, zone: &Arc) -> Result<(), SignerError> { + pub fn save(self, center: &Arc
, zone: &Arc) -> Result<(), SignerError> { let mut modified = false; let mut zone_state = zone diff --git a/src/units/zone_signer.rs b/src/units/zone_signer.rs index d3e48f5c9..8dd0cc9c6 100644 --- a/src/units/zone_signer.rs +++ b/src/units/zone_signer.rs @@ -1,5 +1,5 @@ use std::cmp::{Ordering, min}; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::env::{self, VarError}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, RwLock}; @@ -7,6 +7,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bytes::Bytes; use cascade_zonedata::{OldRecord, RegularRecord, SignedZoneBuilder}; +use domain::base::Serial; use domain::base::iana::SecurityAlgorithm; use domain::base::name::FlattenInto; use domain::base::{CanonicalOrd, Name, Record}; @@ -23,7 +24,7 @@ use domain::dnssec::sign::keys::SigningKey; use domain::dnssec::sign::keys::keyset::{KeySet, KeyType, UnixTime}; use domain::dnssec::sign::records::RecordsIter; use domain::dnssec::sign::signatures::rrsigs::{GenerateRrsigConfig, sign_sorted_zone_records}; -use domain::new::base::{RType, Serial}; +use domain::new::base::{RType, Serial as NewBaseSerial}; use domain::new::rdata::RecordData; use domain::rdata::dnssec::Timestamp; use domain::rdata::{Dnskey, Nsec3param, ZoneRecordData}; @@ -50,7 +51,7 @@ use crate::api::{ use crate::center::Center; use crate::manager::{Terminated, record_zone_event}; use crate::policy::{PolicyVersion, SignerDenialPolicy, SignerSerialPolicy}; -use crate::signer::incremental::sign_incrementally; +use crate::signer::incremental::{LocalState, sign_incrementally}; use crate::signer::{ResigningTrigger, SigningTrigger}; use crate::units::http_server::KmipServerState; use crate::units::key_manager::{ @@ -60,7 +61,7 @@ use crate::util::{ AbortOnDrop, serialize_duration_as_secs, serialize_instant_as_duration_secs, serialize_opt_duration_as_secs, }; -use crate::zone::{HistoricalEvent, HistoricalEventType, Zone, ZoneHandle}; +use crate::zone::{HistoricalEvent, Zone, ZoneHandle}; // Re-signing zones before signatures expire works as follows: // - compute when the first zone needs to be re-signed. Loop over unsigned @@ -329,16 +330,15 @@ impl ZoneSigner { info!("[ZS]: Starting signing operation for zone '{zone_name}'"); let start = Instant::now(); - let (last_signed_serial, policy) = { + let mut local_state = LocalState::new(zone)?; + + let policy = { // Use a block to make sure that the mutex is clearly dropped. let zone_state = zone.state.lock().unwrap(); - let last_signed_serial = zone_state - .find_last_event(HistoricalEventType::SigningSucceeded, None) - .and_then(|item| item.serial) - .map(|serial| Serial::from(serial.0)); - (last_signed_serial, zone_state.policy.clone().unwrap()) + zone_state.policy.clone().unwrap() }; + let previous_serial = local_state.previous_serial; // // Lookup the zone to sign. @@ -351,9 +351,10 @@ impl ZoneSigner { .expect("a non-empty loaded instance must exist"); let loaded_serial = loaded.soa().rdata.serial; - let serial = match policy.signer.serial_policy { + let serial: Serial = match policy.signer.serial_policy { SignerSerialPolicy::Keep => { - if let Some(previous_serial) = last_signed_serial + let loaded_serial = Serial::from(Into::::into(loaded_serial)); + if let Some(previous_serial) = previous_serial && loaded_serial <= previous_serial { return Err(SignerError::KeepSerialPolicyViolated); @@ -362,26 +363,22 @@ impl ZoneSigner { loaded_serial } SignerSerialPolicy::Counter => { - // Select the maximum of 'last_signed_serial + 1' and - // 'loaded_serial'. - // - // TODO: This is a partial workaround to help users starting - // out with counter mode. For ongoing discussion, see - // . - let mut serial = loaded_serial; - if let Some(previous_serial) = last_signed_serial - && serial <= previous_serial - { - serial = previous_serial.inc(1); - } - serial + // Always increment the serial number, ignore the serial + // number in the unsigned zone. + let previous_serial = if let Some(serial) = previous_serial { + serial + } else { + Serial::from(0) + }; + + previous_serial.add(1) } SignerSerialPolicy::UnixTime => { - let mut serial = Serial::unix_time(); - if let Some(previous_serial) = last_signed_serial + let mut serial = Serial::now(); + if let Some(previous_serial) = previous_serial && serial <= previous_serial { - serial = previous_serial.inc(1); + serial = previous_serial.add(1); } serial @@ -394,15 +391,17 @@ impl ZoneSigner { * 100; let mut serial: Serial = serial.into(); - if let Some(previous_serial) = last_signed_serial + if let Some(previous_serial) = previous_serial && serial <= previous_serial { - serial = previous_serial.inc(1); + serial = previous_serial.add(1); } serial } }; + local_state.previous_serial = Some(serial); + let serial = NewBaseSerial::from(serial.into_int()); let new_soa = { let mut soa = loaded.soa().clone(); soa.rdata.serial = serial; @@ -410,7 +409,7 @@ impl ZoneSigner { }; info!( - "[ZS]: Serials for zone '{zone_name}': last signed={last_signed_serial:?}, current={loaded_serial}, serial policy={}, new={serial}", + "[ZS]: Serials for zone '{zone_name}': last signed={previous_serial:?}, current={loaded_serial}, serial policy={}, new={serial}", policy.signer.serial_policy ); @@ -504,6 +503,25 @@ impl ZoneSigner { )); } + // Save the current zone signing keys and clear key_roll + let mut key_tags = HashSet::new(); + for v in state.keyset.keys().values() { + let signer = match v.keytype() { + KeyType::Ksk(_) => false, + KeyType::Zsk(key_state) => key_state.signer(), + KeyType::Csk(_, key_state) => key_state.signer(), + KeyType::Include(_) => false, + }; + + if !signer { + continue; + } + + key_tags.insert(v.key_tag()); + } + local_state.key_tags = key_tags; + local_state.key_roll = None; + // // Sort them into DNSSEC order ready for NSEC(3) generation. // @@ -714,22 +732,7 @@ impl ZoneSigner { min_expiration.add(u32::from(sig.expiration).into()); } - - // Save the minimum of the expiration times. - { - // Use a block to make sure that the mutex is clearly dropped. - let mut zone_state = zone.state.lock().unwrap(); - - // Save as next_min_expiration. After the signed zone is approved - // this value should be move to min_expiration. - zone_state.next_min_expiration = saved_min_expiration.get(); - debug!( - "SIGNER: Determined min expiration time: {:?}", - zone_state.next_min_expiration - ); - - zone.mark_dirty(&mut zone_state, center); - } + local_state.next_min_expiration = saved_min_expiration.get(); let total_time = start.elapsed(); @@ -768,6 +771,9 @@ impl ZoneSigner { Some(domain::base::Serial(serial.into())), ); + local_state.last_signature_refresh = UnixTime::now(); + local_state.save(center, zone)?; + Ok(()) } @@ -1041,7 +1047,7 @@ pub struct InProgressStatus { } impl InProgressStatus { - fn new(requested_status: RequestedStatus, zone_serial: Serial) -> Self { + fn new(requested_status: RequestedStatus, zone_serial: NewBaseSerial) -> Self { Self { requested_at: requested_status.requested_at, zone_serial: domain::base::Serial(zone_serial.into()), @@ -1125,7 +1131,7 @@ impl ZoneSigningStatus { Self::Requested(RequestedStatus::new()) } - fn start(&mut self, zone_serial: Serial) -> Result<(), ()> { + fn start(&mut self, zone_serial: NewBaseSerial) -> Result<(), ()> { match *self { ZoneSigningStatus::Requested(s) => { *self = Self::InProgress(InProgressStatus::new(s, zone_serial)); From 5c3c6cf950de8e833f31c23271b8f74a81f09902 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 6 May 2026 14:43:55 +0200 Subject: [PATCH 06/44] Switch to apex_remove and apex_extra. --- src/main.rs | 2 +- src/signer/incremental.rs | 9 ++------- src/units/zone_signer.rs | 26 ++++++++++---------------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/main.rs b/src/main.rs index daa71076f..ffdb23924 100644 --- a/src/main.rs +++ b/src/main.rs @@ -426,7 +426,7 @@ fn check_dnst_version(config: &Config) -> bool { }; // Change this string and the match pattern to whatever version we require in the future - let required_version = ">0.1.0"; + let required_version = ">0.2.0"; let res = match (major, minor, patch) { // major = 0; minor >= 1; patch = * (0, 1.., ..) => true, diff --git a/src/signer/incremental.rs b/src/signer/incremental.rs index 5d2aa3c57..2844df041 100644 --- a/src/signer/incremental.rs +++ b/src/signer/incremental.rs @@ -523,10 +523,7 @@ impl WorkSpace<'_> { pub fn handle_keyset_changed(&mut self) -> bool { let mut apex_changed = false; - // Check the apex RRtypes that need to be removed. We - // should get that from keyset, but currently we don't. - // Just have a fixed list. - let apex_remove: HashSet = [Rtype::DNSKEY, Rtype::CDS, Rtype::CDNSKEY].into(); + let apex_remove = self.keyset_state.apex_remove.clone(); let curr_apex_remove = &self.local_state.apex_remove; if apex_remove != *curr_apex_remove { @@ -536,9 +533,7 @@ impl WorkSpace<'_> { } // Check records that need to be added to the apex. - let mut apex_extra = vec![]; - apex_extra.extend_from_slice(&self.keyset_state.dnskey_rrset); - apex_extra.extend_from_slice(&self.keyset_state.cds_rrset); + let mut apex_extra = self.keyset_state.apex_extra.clone(); apex_extra.sort(); let curr_apex_extra = &self.local_state.apex_extra; diff --git a/src/units/zone_signer.rs b/src/units/zone_signer.rs index 8dd0cc9c6..773152f0d 100644 --- a/src/units/zone_signer.rs +++ b/src/units/zone_signer.rs @@ -7,6 +7,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bytes::Bytes; use cascade_zonedata::{OldRecord, RegularRecord, SignedZoneBuilder}; +use domain::base::Rtype; use domain::base::Serial; use domain::base::iana::SecurityAlgorithm; use domain::base::name::FlattenInto; @@ -464,21 +465,15 @@ impl ZoneSigner { let state = std::fs::read_to_string(&state_path) .map_err(|_| SignerError::CannotReadStateFile(state_path.into_string()))?; let state: KeySetState = serde_json::from_str(&state).unwrap(); - for dnskey_rr in &state.dnskey_rrset { - let mut zonefile = Zonefile::new(); - zonefile.extend_from_slice(dnskey_rr.as_bytes()); - zonefile.extend_from_slice(b"\n"); - if let Ok(Some(Entry::Record(rec))) = zonefile.next_entry() { - let record: OldRecord = rec.flatten_into(); - new_records.push(record.clone().into()); - records.push(record); - } - } - // Also add CDS and CDNSKEY records plus their signatures. - for cds_rr in &state.cds_rrset { + local_state.apex_remove = state.apex_remove.clone(); + let mut apex_extra = state.apex_extra.clone(); + apex_extra.sort(); + local_state.apex_extra = apex_extra; + + for rr in &state.apex_extra { let mut zonefile = Zonefile::new(); - zonefile.extend_from_slice(cds_rr.as_bytes()); + zonefile.extend_from_slice(rr.as_bytes()); zonefile.extend_from_slice(b"\n"); if let Ok(Some(Entry::Record(rec))) = zonefile.next_entry() { let record: OldRecord = rec.flatten_into(); @@ -955,10 +950,9 @@ pub struct KeySetState { /// Domain KeySet state. pub keyset: KeySet, - pub dnskey_rrset: Vec, pub ds_rrset: Vec, - pub cds_rrset: Vec, - pub ns_rrset: Vec, + pub apex_remove: HashSet, + pub apex_extra: Vec, } pub struct MinTimestamp(Mutex>); From ce23145416188065f60c3b379c3a57004525feb0 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 6 May 2026 15:06:35 +0200 Subject: [PATCH 07/44] Fmt. --- src/units/zone_signer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/units/zone_signer.rs b/src/units/zone_signer.rs index 773152f0d..ada3dc1d9 100644 --- a/src/units/zone_signer.rs +++ b/src/units/zone_signer.rs @@ -466,10 +466,10 @@ impl ZoneSigner { .map_err(|_| SignerError::CannotReadStateFile(state_path.into_string()))?; let state: KeySetState = serde_json::from_str(&state).unwrap(); - local_state.apex_remove = state.apex_remove.clone(); - let mut apex_extra = state.apex_extra.clone(); - apex_extra.sort(); - local_state.apex_extra = apex_extra; + local_state.apex_remove = state.apex_remove.clone(); + let mut apex_extra = state.apex_extra.clone(); + apex_extra.sort(); + local_state.apex_extra = apex_extra; for rr in &state.apex_extra { let mut zonefile = Zonefile::new(); From 538cc286d3ec9ca420d04d9d4b1eba4601c24f86 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 May 2026 21:58:57 +0200 Subject: [PATCH 08/44] Review feedback: Remove outdated TODO comment. --- src/server/service.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/service.rs b/src/server/service.rs index 8582ace00..f65b4b57a 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -152,7 +152,6 @@ mod compat { Box::pin(axfr(old_request, zone.clone())) as Response } - // TODO: Support IXFR. ZoneRequestKind::Ixfr { known_soa } => { Box::pin(ixfr(old_request, known_soa.rdata, zone.clone())) as Response } From ddfdc2ec65dafa4820c8c585dcdb740d78350fe4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 May 2026 21:59:40 +0200 Subject: [PATCH 09/44] Review feedback: Better log message. --- src/server/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/service.rs b/src/server/service.rs index f65b4b57a..df36db263 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -303,7 +303,7 @@ mod compat { ) -> ResponseStream { // Refuse IXFR requests over UDP. if request.transport_ctx().is_udp() { - tracing::warn!("Reject IXFR over UDP"); + tracing::warn!("Rejecting IXFR over UDP"); return error(request.message(), Rcode::NOTIMP); } From 941bf19389a8007574cac1c6ee79ffb1cc3f0729 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 May 2026 22:00:58 +0200 Subject: [PATCH 10/44] Review feedback: Remove unnecessary braces. --- src/server/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/service.rs b/src/server/service.rs index df36db263..e9e65b2a8 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -336,7 +336,7 @@ mod compat { // ^^^^^^^^^^^^^^^ // Errata https://www.rfc-editor.org/errata/eid3196 points out that // this is NOT "just as in AXFR" as AXFR does not do that. - let our_soa_serial = { viewer.soa().rdata.serial }; + let our_soa_serial = viewer.soa().rdata.serial; if client_soa.serial >= our_soa_serial { trace!("Responding to IXFR with single SOA because query serial >= zone serial"); From 92da4ff7ceb644542606228ddd4be381e287f842 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 May 2026 22:04:21 +0200 Subject: [PATCH 11/44] Review feedback: Log at wrong level. Also add a guard and improve the actual logged output. --- src/server/service.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index e9e65b2a8..2beefc97c 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -86,7 +86,7 @@ mod compat { tsig, }; use futures::Stream; - use tracing::trace; + use tracing::{Level, trace}; use crate::server::request::{RequestKind, ZoneRequestKind}; @@ -359,13 +359,19 @@ mod compat { // messages. // Currently we have only a single diff available. - for (i, d) in diffs.iter().enumerate() { - tracing::info!( - "Diff {i} for zone '{}': serial {} => serial {}", - zone.handle.name, - d.removed_soa.as_ref().unwrap().0.rdata.serial, - d.added_soa.as_ref().unwrap().0.rdata.serial, + if tracing::enabled!(Level::TRACE) { + trace!( + "IXFR out: {} diffs availablei for zone {}:", + diffs.len(), + zone.handle.name ); + for (i, d) in diffs.iter().enumerate() { + trace!( + "IXFR out: Diff #{i}: serial {} => serial {}", + d.removed_soa.as_ref().unwrap().0.rdata.serial, + d.added_soa.as_ref().unwrap().0.rdata.serial, + ); + } } // Find the diff, if we have it, that removes the SOA serial number From 54de82507b441b725df0e0b4040afb08f6c63f2c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 May 2026 22:05:08 +0200 Subject: [PATCH 12/44] Review feedback: Errant period in comment. --- src/server/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/service.rs b/src/server/service.rs index 2beefc97c..769006ac8 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -394,7 +394,7 @@ mod compat { // Stream the records in the background. tokio::task::spawn(async move { - // Collect the sequence of IXFR output records.. + // Collect the sequence of IXFR output records. let mut rrs = vec![new_soa.clone().into()]; for diff in &diffs[start_idx..] { rrs.push(diff.removed_soa.clone().unwrap().into()); From a7120d2027fe722574a7efc21295d35d4b3125d1 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 May 2026 22:40:28 +0200 Subject: [PATCH 13/44] Review feedback: Added some fn RustDocs. Primarily to cover the wider than perhaps expected use of the soa() fn. --- src/server/service.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/server/service.rs b/src/server/service.rs index 769006ac8..cb72310e8 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -196,9 +196,14 @@ mod compat { true } + /// Generate a SOA DNS message response stream for the given zone viewer. + /// + /// Note: Also used by [`axfr()`] and [`ixfr()`] as well as in response to + /// a direct SOA query. + /// + /// Returns an NXDOMAIN response if we have the zone but no data for it. fn soa(request: &Message>, viewer: &V) -> ResponseStream { if viewer.is_empty() { - // The zone is known to exist, but we don't have any data for it. return error(request, Rcode::NXDOMAIN); } let soa = viewer.soa().clone(); @@ -213,6 +218,7 @@ mod compat { Box::new(futures::stream::once(std::future::ready(result))) as _ } + /// Generate an AXFR DNS message response stream for the given zone. async fn axfr( request: Request, Option>>, zone: ServedZone, @@ -296,6 +302,7 @@ mod compat { Box::new(stream) as _ } + /// Generate an IXFR DNS message response stream for the given zone. async fn ixfr( request: Request, Option>>, client_soa: Soa>, From 87ff6a736c9653d415f8d278c7ee4c0ee6fe01ce Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 00:28:47 +0200 Subject: [PATCH 14/44] Extend IXFR-out system test to use TSIG too. --- .../scripts/manage-test-environment.sh | 6 +- integration-tests/system-tests.yml | 3 +- integration-tests/tests/ixfr-out/action.yml | 140 ++++++++++++------ 3 files changed, 97 insertions(+), 52 deletions(-) diff --git a/integration-tests/scripts/manage-test-environment.sh b/integration-tests/scripts/manage-test-environment.sh index 01054b35f..32180fc8c 100755 --- a/integration-tests/scripts/manage-test-environment.sh +++ b/integration-tests/scripts/manage-test-environment.sh @@ -309,21 +309,21 @@ zone: name: notify-tsig.test zonefile: "notify-tsig.test.secondary-zone" allow-notify: 127.0.0.1 tsig-key - request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY + request-xfr: 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY zone: name: xfr-tsig.test zonefile: "xfr-tsig.test.secondary-zone" allow-notify: 127.0.0.1 NOKEY - request-xfr: AXFR 127.0.0.1@${_cascade_port} tsig-key + request-xfr: 127.0.0.1@${_cascade_port} tsig-key provide-xfr: 127.0.0.1 NOKEY zone: name: notify-and-xfr-tsig.test zonefile: "notify-and-xfr-tsig.test.secondary-zone" allow-notify: 127.0.0.1 tsig-key - request-xfr: AXFR 127.0.0.1@${_cascade_port} tsig-key + request-xfr: 127.0.0.1@${_cascade_port} tsig-key provide-xfr: 127.0.0.1 NOKEY EOF diff --git a/integration-tests/system-tests.yml b/integration-tests/system-tests.yml index 184399852..8bf0bc696 100644 --- a/integration-tests/system-tests.yml +++ b/integration-tests/system-tests.yml @@ -171,7 +171,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - rust: [stable] + with-tsig: [true, false] steps: - uses: actions/checkout@v4 - uses: ./.github/actions/set-build-profile @@ -180,6 +180,7 @@ jobs: - uses: ./integration-tests/tests/ixfr-out with: log-level: ${{ inputs.log-level }} + with-tsig: ${{ inputs.with-tsig }} incremental-signing: name: Test incremental signing. diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index d6394463f..757a60ce4 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -18,6 +18,10 @@ inputs: - info - debug - trace + with-tsig: + description: Whether or not TSIG should be required for XFR out. + required: true + type: boolean runs: using: "composite" steps: @@ -29,38 +33,64 @@ runs: - name: Add a NOTIFY with TSIG outbound policy run: | POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) - cascade template policy | grep -Ev '(send-notify-to|accept-xfr-from)' > "${POLICY_DIR}/custom.toml" + cascade template policy | grep -Ev '(send-notify-to|accept-xfr-from)' > "${POLICY_DIR}/without-tsig.toml" + cascade template policy | grep -Ev '(send-notify-to|accept-xfr-from)' > "${POLICY_DIR}/with-tsig.toml" - sed -i -e 's/serial-policy = "date-counter"/serial-policy = "keep"/' "${POLICY_DIR}/custom.toml" - echo 'send-notify-to = ["127.0.0.1:1054"]' >> "${POLICY_DIR}/custom.toml" + sed -i -e 's/serial-policy = "date-counter"/serial-policy = "keep"/' "${POLICY_DIR}/without-tsig.toml" + sed -i -e 's/serial-policy = "date-counter"/serial-policy = "keep"/' "${POLICY_DIR}/with-tsig.toml" + + echo 'send-notify-to = ["127.0.0.1:1054"]' >> "${POLICY_DIR}/without-tsig.toml" + echo 'send-notify-to = ["127.0.0.1:1054^tsig-key"]' >> "${POLICY_DIR}/with-tsig.toml" - # TODO: Extend the test to use TSIG (driven by inputs ala the tsig-downstream test) - # once PRs #564 and #587 have been merged. - # echo 'send-notify-to = ["127.0.0.1:1054^tsig-key"]' >> "${POLICY_DIR}/custom.toml" - # cascade tsig add tsig-key hmac-sha256 "COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" + cascade tsig add tsig-key hmac-sha256 "COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" cascade policy reload - + + - name: Determine the downstrem secondary zone config to use + id: set-vars + run: | + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + ZONE_NAME="notify-and-xfr-tsig.test." + POLICY_NAME="with-tsig" + else + ZONE_NAME="example.test." + POLICY_NAME="without-tsig" + fi + echo "ZONE_NAME=${ZONE_NAME}" >> "$GITHUB_OUTPUT" + echo "POLICY_NAME=${POLICY_NAME}" >> "$GITHUB_OUTPUT" + + # The zone name has to match a zone configured in the downstream NSD + # nameserver. Zone 'notify-and-xfr-tsig.test' is a zone in the downstream + # NSD nameserver that is configured to expect TSIG to be used for the + # NOTIFY message it is sent, to use IXFR (as opposed to only AXFR) and for + # it to need to use TSIG to request an IXFR transfer from Cascade. - name: Make an initial zone based on RFC 1995 section 7 + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | - tee example.test.zone <<'EOF' - EXAMPLE.TEST. IN SOA NS.EXAMPLE.TEST. mail.example.test. ( + cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status example.test + cascade zone status ${ZONE_NAME} echo "timeout: zone status did not report published zone available" >&2 exit 1 fi @@ -68,13 +98,15 @@ runs: done - name: Check the SOA SERIAL at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | timeout=10 # seconds start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 example.test SOA | grep -q 'ns.example.test. mail.example.test. 1 60 60 3600 5'; do + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 1 60 60 3600 5"; do if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 1054 example.test SOA + cascade zone status ${ZONE_NAME} + dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA echo "::error:: timeout: NSD did not acquire the zone changes" exit 1 fi @@ -82,24 +114,28 @@ runs: done - name: Edit the source zone and tell Cascade to reload it. + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | - tee example.test.zone <<'EOF' - example.test. IN SOA ns.example.test. mail.example.test. ( + cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status example.test + cascade zone status ${ZONE_NAME} echo "timeout: zone status did not report published zone available" >&2 exit 1 fi @@ -107,52 +143,60 @@ runs: done - name: Check the SOA SERIAL at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | timeout=10 # seconds start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 example.test SOA | grep -q 'ns.example.test. mail.example.test. 2 60 60 3600 5'; do + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 2 60 60 3600 5"; do if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 1054 example.test SOA + dig @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + cascade zone status ${ZONE_NAME} echo "::error:: timeout: NSD did not acquire the zone changes" exit 1 fi sleep 1 done - - name: Edit the source zone and tell Cascade to reload it. + - name: Edit the source zone again and tell Cascade to reload it. + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | - tee example.test.zone <<'EOF' - EXAMPLE.TEST. IN SOA ns.example.test. mail.example.test. ( + cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status example.test + cascade zone status ${ZONE_NAME} echo "timeout: zone status did not report published zone available" >&2 exit 1 fi sleep 1 done - - name: Check the SOA SERIAL at the NSD secondary + - name: Check the SOA SERIAL again at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | timeout=10 # seconds start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 example.test SOA | grep -q 'ns.example.test. mail.example.test. 3 60 60 3600 5'; do + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5"; do if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 1054 example.test SOA + cascade zone status ${ZONE_NAME} + dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA echo "::error:: timeout: NSD did not acquire the zone changes" exit 1 fi @@ -168,15 +212,15 @@ runs: # TODO: Verify the AXFR and IXFR response content is as expected / # matching that of NSD. - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test AXFR > cascade-axfr.log + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} AXFR > cascade-axfr.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=1 >cascade-ixfr-1.log + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=1 >cascade-ixfr-1.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=2 >cascade-ixfr-2.log + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=2 >cascade-ixfr-2.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 example.test -t IXFR=3 >cascade-ixfr-3.log + - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=3 >cascade-ixfr-3.log - - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 example.test AXFR >nsd-axfr.log + - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${{ steps.set-vars.outputs.ZONE_NAME }} AXFR >nsd-axfr.log - name: Print log files on any failure in this job uses: ./.github/actions/print-logfiles From c2d1a7bf55d922cd7c69e1ed3256a19935033f79 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 13:35:30 +0200 Subject: [PATCH 15/44] FIX: system-test should pass the `with-tsig` input correctly to the ixfr-out test. --- integration-tests/system-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/system-tests.yml b/integration-tests/system-tests.yml index 8bf0bc696..1b9d54989 100644 --- a/integration-tests/system-tests.yml +++ b/integration-tests/system-tests.yml @@ -180,7 +180,7 @@ jobs: - uses: ./integration-tests/tests/ixfr-out with: log-level: ${{ inputs.log-level }} - with-tsig: ${{ inputs.with-tsig }} + with-tsig: ${{ matrix.with-tsig }} incremental-signing: name: Test incremental signing. From cd989d17f86b2078464aa11abddc93bf3ca680ef Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 13:36:33 +0200 Subject: [PATCH 16/44] (WIP) Extend the ixfr-out test to actually verify produced output. Currently fails due to an unrelated TSIG bug causing dig to fail. --- .../keys/Kexample.test.+015+00580.key | 1 + .../keys/Kexample.test.+015+00580.private | 3 + .../Knotify-and-xfr-tsig.test.+015+10995.key | 1 + ...otify-and-xfr-tsig.test.+015+10995.private | 3 + .../ixfr-out/policies/with-tsig.toml | 32 +++++ .../ixfr-out/policies/without-tsig.toml | 23 ++++ .../reference-output/example.test.axfr | 17 +++ .../reference-output/example.test.ixfr-1-3 | 73 +++++++++++ .../notify-and-xfr-tsig.test.axfr | 17 +++ .../notify-and-xfr-tsig.test.ixfr-1-3 | 27 ++++ integration-tests/tests/ixfr-out/action.yml | 115 +++++++++++++----- 11 files changed, 280 insertions(+), 32 deletions(-) create mode 100644 integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key create mode 100644 integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private create mode 100644 integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key create mode 100644 integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private create mode 100644 integration-tests/ixfr-out/policies/with-tsig.toml create mode 100644 integration-tests/ixfr-out/policies/without-tsig.toml create mode 100644 integration-tests/ixfr-out/reference-output/example.test.axfr create mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 diff --git a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key new file mode 100644 index 000000000..224ce50d5 --- /dev/null +++ b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key @@ -0,0 +1 @@ +example.test. IN DNSKEY 256 3 15 u7Tg6xf6Xgt0y+yUT8CQi1Nyy+tRopVHnZOFQz0zQ5A= diff --git a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private new file mode 100644 index 000000000..c6ba81b21 --- /dev/null +++ b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: qCwt/EY9s1vGn0uj+4dlMQ5waOuFfIYpZZVrX43jdcc= diff --git a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key new file mode 100644 index 000000000..9ef332abc --- /dev/null +++ b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key @@ -0,0 +1 @@ +notify-and-xfr-tsig.test. IN DNSKEY 256 3 15 0efFuDXcYUyhaTUWgir/RaWrzAJWAa58VuEF+391Taw= diff --git a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private new file mode 100644 index 000000000..b385fd508 --- /dev/null +++ b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: Nx9EHQ1RBhngQMCRQUxMsiD2ybaWRCiEdcDlW5isKoo= diff --git a/integration-tests/ixfr-out/policies/with-tsig.toml b/integration-tests/ixfr-out/policies/with-tsig.toml new file mode 100644 index 000000000..cc32f479d --- /dev/null +++ b/integration-tests/ixfr-out/policies/with-tsig.toml @@ -0,0 +1,32 @@ +# Setup incremental signing to produce predictable outputs so that we can +# easily compare the generated zone against expected output. +# +# Enable sending of NOTIFY messages using TSIG authentication to the NSD +# secondary. +version = "v1" + +[signer.denial] +type = "nsec" + +[signer] +serial-policy = "keep" +signature-inception-offset = 0 +signature-lifetime = 100000000 + +[key-manager.records] +dnskey.signature-inception-offset = 0 +cds.signature-inception-offset = 0 +dnskey.signature-lifetime = 100000000 +cds.signature-lifetime = 100000000 + +[server.outbound] +# NSD secondary listens on port 1054. +send-notify-to = ["127.0.0.1:1054^tsig-key"] + +# Require that the XFR request from the NSD secondary be signed with the +# specified TSIG key. Without this, a TSIG signed XFR request with a known +# key will still be accepted, but an XFR request that is NOT TSIG signed +# would also be accepted. In the test we verify that TSIG is required by +# Cascade by attempting a dig without the required TSIG key and showing +# that it fails. +accept-xfr-from = ["127.0.0.1^tsig-key"] diff --git a/integration-tests/ixfr-out/policies/without-tsig.toml b/integration-tests/ixfr-out/policies/without-tsig.toml new file mode 100644 index 000000000..07c1d9359 --- /dev/null +++ b/integration-tests/ixfr-out/policies/without-tsig.toml @@ -0,0 +1,23 @@ +# Setup incremental signing to produce predictable outputs so that we can +# easily compare the generated zone against expected output. +# +# Enable sending of NOTIFY messages to the NSD secondary. +version = "v1" + +[signer.denial] +type = "nsec" + +[signer] +serial-policy = "keep" +signature-inception-offset = 0 +signature-lifetime = 100000000 + +[key-manager.records] +dnskey.signature-inception-offset = 0 +cds.signature-inception-offset = 0 +dnskey.signature-lifetime = 100000000 +cds.signature-lifetime = 100000000 + +[server.outbound] +# NSD secondary listens on port 1054. +send-notify-to = ["127.0.0.1:1054"] diff --git a/integration-tests/ixfr-out/reference-output/example.test.axfr b/integration-tests/ixfr-out/reference-output/example.test.axfr new file mode 100644 index 000000000..c9577dd58 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.axfr @@ -0,0 +1,17 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== +example.test. 3600 IN NS NS.example.test. +example.test. 3600 IN RRSIG NS 15 2 3600 20231114221320 20200913122640 580 example.test. hUD4MZwiVtXmHs+VueFIIrTUKYzWfOiNWm2nTR1gtsHTYnGjRrVEH/ic 3XeWDtKK0EedUE5iOKIEfQyYpaTLAg== +example.test. 3600 IN DNSKEY 256 3 15 u7Tg6xf6Xgt0y+yUT8CQi1Nyy+tRopVHnZOFQz0zQ5A= +example.test. 3600 IN RRSIG DNSKEY 15 2 3600 20231114221320 20200913122640 580 example.test. xGC6Pj6iN7tOjmhnLxmw4yQt7CCxCVouXvdB1P3lsQwN0iFfxgamkU7j EqIQcPpl20erYZ4XoWwtOdoeaXwtAg== +example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. P/YK6hlW/FOz43gKHqT4zoxRBdKFinx8GktOFDKuM+CWNyfBkjUZEI04 e3pa5/GUNcLKwRYAyWlNQnONPuusCQ== +NS.example.test. 3600 IN A 133.69.136.1 +NS.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. EsKLrDOszkF8dHi6K8XYed5EbqU9fSlNO2+25pBO/qZk1nZm1RyDO15X KWoCf1jUUNsTR4sZkiu3OGV7oazcAA== +NS.example.test. 5 IN NSEC example.test. A RRSIG NSEC +NS.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. hO9nn4+rxV08CFGkyXYzqGrRYNVOptJXZOaQLp6OSCMtte2iSQCq7UxJ p2KK2Q7aegqYKm6nNFoABmEYjZNlDQ== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== +JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. /S/FZGh/OBQjwaVRPXJChDNkraEeD7qtVvIAa8db4bYmylI4KWOsH991 XDjye2aOv1+qPZxXIfzuX/3ER8JUBA== diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 new file mode 100644 index 000000000..56beb1143 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 @@ -0,0 +1,73 @@ +; The input zone was based on RFC 1995 section 7. This diff should look +; similar to "the following incremental message" from that section, but with +; DNSSEC changes present as well. Our zone name differs to that of the example +; in RFC 1995 as we have to match the zone name configured in our secondary NSD +; instance. + +; === Start of IXFR +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 + +; [DIFF 1]: "NEZU.JAIN.AD.JP. is removed and JAIN-BB.JAIN.AD.JP. is added." +; [DIFF 1]: Records from serial 1 to be removed. + +; Changing the SOA serial requires that we regenerate the RRSIG, so the old +; RRSIG has to be removed. +example.test. 3600 IN SOA NS.example.test. mail.example.test. 1 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. TlLDWFfpydqCQjy1oDPBXugGWraoyvvk83KdTsiq441t0/hYKmL2mFEH 2nwJ6fcuZT2hLpvl9gwk3bJCXrNdAQ== + +; Remove the A record and its associated RRSIG. +NEZU.example.test. 3600 IN A 133.69.136.5 +NEZU.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. k232/sABO752SeaFp3jHkIJMUGlDA0NG+iQh1gpGVsi07XqDce+JzNX4 NiWmz06kVu+SKu1HFHNvGJ3enh5/DQ== + +; Remove the A record from the NSEC type bitmap too, and also remove NSEC +; RRSIG as a new one will be added to replace it. +NEZU.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. BCpvhGfe+GW4GnyJBBiqjMpiVZhCXv9ZMdh+RF7qf07V3jIePWJThAy2 yV5O4I7NhKgl6rqpOyv2+aSoW8tVDw== +NEZU.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC + +; As the entire NEZU.example.test. label was removed this also requires the +; next owner name in the NSEC chain to be changed so we have to remove the +; current NSEC on the previous owner and its associated RRSIG, new ones will +; be added. +example.test. 5 IN NSEC NEZU.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. Hyrs5CKpXfCCNC9OK9+07Ei5qhRjQ9EQZK3up84BCwlflhOCGQppddzM eHK0Klgl0ywKaD7dfV2w3SWvP7vTDA== + +; [DIFF 1]: Records to be added for serial 2. + +; The new SOA with the updated SERIAL and its associated RRSIG. +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== + +; A changed NSEC and associated RRSIG due to changes in the NSEC chain. +example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. P/YK6hlW/FOz43gKHqT4zoxRBdKFinx8GktOFDKuM+CWNyfBkjUZEI04 e3pa5/GUNcLKwRYAyWlNQnONPuusCQ== + +; Add new label JAIN-BB.example.test with two A records, associated NSEC and RRSIGs. +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. /S/FZGh/OBQjwaVRPXJChDNkraEeD7qtVvIAa8db4bYmylI4KWOsH991 XDjye2aOv1+qPZxXIfzuX/3ER8JUBA== +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== + +; [DIFF 2]: "One of the IP addresses of JAIN-BB.JAIN.AD.JP. is changed." +; [DIFF 2]: Records from serial 2 to be removed. + +; The old SOA and associated RRSIG. +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== + +; The old A record and associated RRSIG. +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== + +; [DIFF 2]: Records to be added for serial 3. + +; The new SOA and associated RRSIG. +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== + +; The updated A record and associated RRSIG. +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== + +; End of IXFR +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr new file mode 100644 index 000000000..08e54a056 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr @@ -0,0 +1,17 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== +notify-and-xfr-tsig.test. 3600 IN NS NS.notify-and-xfr-tsig.test. +notify-and-xfr-tsig.test. 3600 IN RRSIG NS 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. YDqsJSHx81jK/GSqDdnNDG3JN9v9SH6jgmsgbQNElcaSCXqMrR2Q3QFq wrYW06FgnadRgTySRZ2evzS3dYGaDw== +notify-and-xfr-tsig.test. 3600 IN DNSKEY 256 3 15 0efFuDXcYUyhaTUWgir/RaWrzAJWAa58VuEF+391Taw= +notify-and-xfr-tsig.test. 3600 IN RRSIG DNSKEY 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. CIiRWX2DYaZRidc0nDl4zE0vmUCK2XFX2ekrJc1DVljIestEaJBAR2V1 ZQYOtCtb8y8AA/RCH8UFN2DYSM6+CQ== +notify-and-xfr-tsig.test. 5 IN NSEC JAIN-BB.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY +notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. z7aQTz7VOeeSwwftFsHr+f2BG1k1mVTGztiqZNUAYBkT5R+IlhWpzNdR KvpUSMcdgSNSsjKLQciFq/vYSqg7DA== +NS.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.1 +NS.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. ziPT0QEZ/gGcVltE17tqgf3jqo9SfesIffux+pIZwC3vO8eg1MnzPJOv Kmdl9vJQqS03FjwT+6r3xtjIVIfwAA== +NS.notify-and-xfr-tsig.test. 5 IN NSEC notify-and-xfr-tsig.test. A RRSIG NSEC +NS.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. nLNfo5CuJQd8qYbDywdefe0pFQ881jn3W6WEfNnkMg1daJk2ENcZUkiJ aeR7Hv7FPzTyavE2SQbuHfHrYDKoAA== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 192.41.197.2 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== +JAIN-BB.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC +JAIN-BB.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Kn1CEC2iYr/uSRAZvM9SV2HRR0B8l8ObkvqYufiGO0en+OSLRmfgzviT Mdhdai9onUYr7ndQFHI8bSav5MBoCQ== diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 new file mode 100644 index 000000000..33df5ea3e --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 @@ -0,0 +1,27 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN SOA NS.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 1 60 60 3600 5 +NEZU.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.5 +notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0BtPWx9VOtcE1BMD3cLk1FR1YaT/1v32VuuxXh/XQgS97Nj80H+Qm57V ORpk/V6wkw2exuvz3ko2s2BfkaQqAQ== +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. xzwl8jSWvas1L2Uqzfq8kGkPZzvTQyvrmv8Q3q+jVdTj7FIYCOXN3moZ MvasxJgo6euVesldGA0WzQ0UaIxyBw== +notify-and-xfr-tsig.test. 5 IN NSEC NEZU.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY +NEZU.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. kNDs5mxnK6tKyDu1fBIAg1SnynqRQM7afK+hmPkLJD0SkvKEItomMbui gKlgDdtA7YWA8zTh4j7bwUq/tBlYAQ== +NEZU.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Etgfp3BL11ReT8j0UHQ5epj6l+mAH2+1Fmv/p74EQ3EsbpPiGx3c+96u XeRbWRCVVY/yCqH7E5j0NPHvFYhBAg== +NEZU.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 192.41.197.2 +notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. z7aQTz7VOeeSwwftFsHr+f2BG1k1mVTGztiqZNUAYBkT5R+IlhWpzNdR KvpUSMcdgSNSsjKLQciFq/vYSqg7DA== +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== +notify-and-xfr-tsig.test. 5 IN NSEC JAIN-BB.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY +JAIN-BB.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Kn1CEC2iYr/uSRAZvM9SV2HRR0B8l8ObkvqYufiGO0en+OSLRmfgzviT Mdhdai9onUYr7ndQFHI8bSav5MBoCQ== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== +JAIN-BB.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index 757a60ce4..e120fb45c 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -29,21 +29,7 @@ runs: - uses: ./.github/actions/setup-and-start-cascade with: log-level: ${{ inputs.log-level }} - - - name: Add a NOTIFY with TSIG outbound policy - run: | - POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) - cascade template policy | grep -Ev '(send-notify-to|accept-xfr-from)' > "${POLICY_DIR}/without-tsig.toml" - cascade template policy | grep -Ev '(send-notify-to|accept-xfr-from)' > "${POLICY_DIR}/with-tsig.toml" - - sed -i -e 's/serial-policy = "date-counter"/serial-policy = "keep"/' "${POLICY_DIR}/without-tsig.toml" - sed -i -e 's/serial-policy = "date-counter"/serial-policy = "keep"/' "${POLICY_DIR}/with-tsig.toml" - - echo 'send-notify-to = ["127.0.0.1:1054"]' >> "${POLICY_DIR}/without-tsig.toml" - echo 'send-notify-to = ["127.0.0.1:1054^tsig-key"]' >> "${POLICY_DIR}/with-tsig.toml" - - cascade tsig add tsig-key hmac-sha256 "COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" - cascade policy reload + fake-time: 1600000000 - name: Determine the downstrem secondary zone config to use id: set-vars @@ -51,19 +37,35 @@ runs: if [[ "${{ inputs.with-tsig }}" == "true" ]]; then ZONE_NAME="notify-and-xfr-tsig.test." POLICY_NAME="with-tsig" + KEY_NAME="Knotify-and-xfr-tsig.test.+015+10995.key" else ZONE_NAME="example.test." POLICY_NAME="without-tsig" + KEY_NAME="Kexample.test.+015+00580.key" fi echo "ZONE_NAME=${ZONE_NAME}" >> "$GITHUB_OUTPUT" echo "POLICY_NAME=${POLICY_NAME}" >> "$GITHUB_OUTPUT" + echo "KEY_NAME=${KEY_NAME}" >> "$GITHUB_OUTPUT" + echo "TSIG_SPEC=hmac-sha256:tsig-key:COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" >> "$GITHUB_OUTPUT" + + - name: Add the TSIG key + if: ${{ inputs.with-tsig }} + run: | + cascade tsig add ${{ steps.set-vars.outputs.TSIG_SPEC }} + + - name: Add policy ${{ steps.set-vars.outputs.POLICY_NAME }} and reload + run: | + POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) + TEST_DIR="${PWD}/integration-tests/ixfr-out" + cp "${TEST_DIR}/policies/${{ steps.set-vars.outputs.POLICY_NAME }}.toml" "${POLICY_DIR}/" + cascade policy reload # The zone name has to match a zone configured in the downstream NSD # nameserver. Zone 'notify-and-xfr-tsig.test' is a zone in the downstream # NSD nameserver that is configured to expect TSIG to be used for the # NOTIFY message it is sent, to use IXFR (as opposed to only AXFR) and for # it to need to use TSIG to request an IXFR transfer from Cascade. - - name: Make an initial zone based on RFC 1995 section 7 + - name: Make a ${{ steps.set-vars.outputs.ZONE_NAME }} zone based on RFC 1995 section 7 env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | @@ -75,14 +77,19 @@ runs: NEZU.${ZONE_NAME} IN A 133.69.136.5 EOF - - name: Add the zone using the custom policy + - name: Add the ${{ steps.set-vars.outputs.ZONE_NAME }} zone using policy ${{ steps.set-vars.outputs.POLICY_NAME }} env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} POLICY_NAME: ${{ steps.set-vars.outputs.POLICY_NAME }} + KEY_NAME: ${{ steps.set-vars.outputs.KEY_NAME }} run: | - cascade zone add --policy ${POLICY_NAME} --source "${ZONE_NAME}zone" ${ZONE_NAME} + TEST_DIR="${PWD}/integration-tests/ixfr-out" + KEY_FILE="${TEST_DIR}/keys/${KEY_NAME}" + # Use a pre-created key so that generated signatures can be matched + # against reference output. + cascade zone add --policy ${POLICY_NAME} --source "${ZONE_NAME}zone" --import-csk-file "${KEY_FILE}" ${ZONE_NAME} - - name: Check zone status + - name: Check zone ${{ steps.set-vars.outputs.ZONE_NAME }} status env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | @@ -97,7 +104,7 @@ runs: sleep 1 done - - name: Check the SOA SERIAL at the NSD secondary + - name: Expect serial 1 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | @@ -113,21 +120,21 @@ runs: sleep 1 done - - name: Edit the source zone and tell Cascade to reload it. + - name: Edit and reload serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | cat >"${ZONE_NAME}zone" <"${ZONE_NAME}zone" < cascade-axfr.log + - name: Verify that TSIG is required by Cascade + if: ${{ inputs.with-tsig }} + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + # This should not succeed as it lacks the required TSIG key. + # Note that dig will exit with code 0 even if the request fails, + # because it considers an error response still to be successful + # receipt of a response, so we have to check the actual output. + dig @127.0.0.1 -p 4542 ${ZONE_NAME} AXFR &> dig-no-tsig.log + if grep -Fq 'Transfer failed' dig-no-tsig.log; then + exit 0 + else + echo "::error:: dig unexpectedly was able to AXFR the zone without a TSIG key" + exit 1 + fi + + - name: Verify Cascade AXFR out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + echo "EXTRA_ARGS: ${EXTRA_ARGS}" + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${EXTRA_ARGS} ${ZONE_NAME} AXFR &> cascade-axfr.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-axfr.log > output.sorted + diff -u -w output.sorted reference.output.sorted - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=1 >cascade-ixfr-1.log + - name: Verify Cascade IXFR serial 1..=3 out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=1 ${EXTRA_ARGS} &> cascade-ixfr-1-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-ixfr-1-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=2 >cascade-ixfr-2.log @@ -222,6 +271,8 @@ runs: - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${{ steps.set-vars.outputs.ZONE_NAME }} AXFR >nsd-axfr.log + - run: exit 1 + - name: Print log files on any failure in this job uses: ./.github/actions/print-logfiles if: failure() From c733215a5aaeafb4bf55f96c905415eab34cd4ba Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 13:37:08 +0200 Subject: [PATCH 17/44] Add more logging of request handling w.r.t. ACL processing. --- src/server/service.rs | 73 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index cb72310e8..5d5a1f46f 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -86,7 +86,7 @@ mod compat { tsig, }; use futures::Stream; - use tracing::{Level, trace}; + use tracing::{Level, debug, trace}; use crate::server::request::{RequestKind, ZoneRequestKind}; @@ -167,6 +167,21 @@ mod compat { ) -> bool { let zone_state = zone.handle.state.lock().unwrap(); + if tracing::enabled!(Level::TRACE) { + let tsig_key = old_request.metadata().as_ref().map(|key| key.name()); + trace!( + "Received request {} from {} for {} in zone {} with TSIG key {tsig_key:?}", + old_request.message().header().id(), + old_request.client_addr().ip(), + old_request + .message() + .qtype() + .map(|rtype| rtype.to_string()) + .unwrap_or("".to_string()), + zone.handle.name, + ); + } + if let Some(acls) = zone_state .policy .as_ref() @@ -188,6 +203,27 @@ mod compat { } // No ACL matched, reject the request. + if tracing::enabled!(Level::DEBUG) { + let extra = if tracing::enabled!(Level::TRACE) { + &format!( + " (TSIG key={wanted_tsig_key_name:?}) [no matching ACL found: {acls:?}]" + ) + } else { + "" + }; + debug!( + "Rejecting request {} from {} for {} in zone {}: access denied{extra}", + old_request.message().header().id(), + old_request.client_addr().ip(), + old_request + .message() + .qtype() + .map(|rtype| rtype.to_string()) + .unwrap_or("".to_string()), + zone.handle.name, + ); + } + return false; } } @@ -365,27 +401,32 @@ mod compat { // response message and will have to be split into multiple response // messages. - // Currently we have only a single diff available. if tracing::enabled!(Level::TRACE) { trace!( - "IXFR out: {} diffs availablei for zone {}:", + "IXFR out: {} diffs available for zone {}:", diffs.len(), zone.handle.name ); - for (i, d) in diffs.iter().enumerate() { + for (i, (loaded_diff, signed_diff)) in diffs.iter().enumerate() { trace!( - "IXFR out: Diff #{i}: serial {} => serial {}", - d.removed_soa.as_ref().unwrap().0.rdata.serial, - d.added_soa.as_ref().unwrap().0.rdata.serial, + "IXFR out: Diff #{i}: serial {} => serial {}, loaded -{}+{}, signed -{}+{}", + signed_diff.removed_soa.as_ref().unwrap().0.rdata.serial, + signed_diff.added_soa.as_ref().unwrap().0.rdata.serial, + loaded_diff.removed_records.len(), + loaded_diff.added_records.len(), + signed_diff.removed_records.len(), + signed_diff.added_records.len(), ); } } // Find the diff, if we have it, that removes the SOA serial number // that the client currently has. That will be the start of the diff - // that we need to serve. - let start_idx = diffs.iter().position(|d| { - d.removed_soa.as_ref().map(|rr| rr.0.rdata.serial) == Some(client_soa.serial) + // that we need to serve. The SOA serial has to match the one seen by + // the client, i.e. the one we published in the signed zone, not the + // one from the loaded zone which could be completely different. + let start_idx = diffs.iter().position(|(_, signed_diff)| { + signed_diff.removed_soa.as_ref().map(|rr| rr.0.rdata.serial) == Some(client_soa.serial) }); let Some(start_idx) = start_idx else { @@ -403,11 +444,13 @@ mod compat { tokio::task::spawn(async move { // Collect the sequence of IXFR output records. let mut rrs = vec![new_soa.clone().into()]; - for diff in &diffs[start_idx..] { - rrs.push(diff.removed_soa.clone().unwrap().into()); - rrs.extend(diff.removed_records.clone()); - rrs.push(diff.added_soa.clone().unwrap().into()); - rrs.extend(diff.added_records.clone()); + for (loaded_diff, signed_diff) in &diffs[start_idx..] { + rrs.push(signed_diff.removed_soa.clone().unwrap().into()); + rrs.extend(loaded_diff.removed_records.clone()); + rrs.extend(signed_diff.removed_records.clone()); + rrs.push(signed_diff.added_soa.clone().unwrap().into()); + rrs.extend(loaded_diff.added_records.clone()); + rrs.extend(signed_diff.added_records.clone()); } rrs.push(new_soa.into()); From 4e4827090a5b9b9a9e33c2686685d92c30f6400b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 13:37:39 +0200 Subject: [PATCH 18/44] FIX: include loaded RRs in the IXFR out, not just signed RRs. --- src/persistence/persist.rs | 32 ++++++++++++++++++++++++++------ src/zone/storage.rs | 5 +++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/persistence/persist.rs b/src/persistence/persist.rs index bf9f9dda0..67be2aadf 100644 --- a/src/persistence/persist.rs +++ b/src/persistence/persist.rs @@ -38,15 +38,35 @@ pub fn persist_signed( // TODO let _ = center; - // Store the signed diff in-memory for serving IXFR. - // Only push a diff if a SOA was removed, otherwise this is not a diff to - // a previous version of the zone but actually the entire new zone content - // compared to an empty zone. Also don't store a diff to the same serial. + // Store the diffs in-memory for serving IXFR. + // + // Only store a diff if the SOA from the previous version of the signed + // zone was removed and a new one added, otherwise this is not a diff to a + // previous version of the zone but actually a snapshot of the zone after + // having been signed for the first time. + let loaded_diff = persister.loaded_diff(); let signed_diff = persister.signed_diff(); - if signed_diff.removed_soa.is_some() && signed_diff.removed_soa != signed_diff.added_soa { + if let Some(loaded_diff) = loaded_diff + && signed_diff.removed_soa.is_some() + && signed_diff.removed_soa != signed_diff.added_soa + { let mut state = zone.state.lock().unwrap(); - state.storage.diffs.push(signed_diff.clone()); + + // Store anything that changed when the zone was re-loaded, i.e. + // unsigned zone content changes. Note that the SOA SERIAL is not + // required to change unless using 'keep' policy and so we should not + // require the SOA to have been removed and a new one added. + + // Store anything that changed when the zone was re-signed, i.e. + // changes DNSSEC RRs that can be caused by unsigned content changes + // or changing from NSEC <-> NSEC3 or using a new key to sign with or + // just regenerating signatures to avoid them expiring. Signed zones + // MUST always have a new SOA SERIAL compared to the previous version + // of the signed zone. + + let complete_diff = (loaded_diff.clone(), signed_diff.clone()); + state.storage.diffs.push(complete_diff); } persister.mark_complete() diff --git a/src/zone/storage.rs b/src/zone/storage.rs index a8538e5d1..370dcc9f8 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -925,8 +925,9 @@ pub struct StorageState { // current i.e. published zone instance. pub published_loaded_soa: Option, - /// Diffs from one serial to another. - pub diffs: Vec>, + /// Diffs from one serial to another. Each diff consists of changes in the + /// loaded part and changes in the signed part. + pub diffs: Vec<(Arc, Arc)>, /// Ongoing background tasks. /// From 570415c6d1b84957f0d2497e271f1340f3dde4c2 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 8 May 2026 16:39:58 +0200 Subject: [PATCH 19/44] Review feedback. --- src/main.rs | 4 ++-- src/units/zone_signer.rs | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index ffdb23924..d64c8c8cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -428,8 +428,8 @@ fn check_dnst_version(config: &Config) -> bool { // Change this string and the match pattern to whatever version we require in the future let required_version = ">0.2.0"; let res = match (major, minor, patch) { - // major = 0; minor >= 1; patch = * - (0, 1.., ..) => true, + // major = 0; minor >= 2; patch = * + (0, 2.., ..) => true, _ => false, }; diff --git a/src/units/zone_signer.rs b/src/units/zone_signer.rs index ada3dc1d9..ab503edef 100644 --- a/src/units/zone_signer.rs +++ b/src/units/zone_signer.rs @@ -366,12 +366,7 @@ impl ZoneSigner { SignerSerialPolicy::Counter => { // Always increment the serial number, ignore the serial // number in the unsigned zone. - let previous_serial = if let Some(serial) = previous_serial { - serial - } else { - Serial::from(0) - }; - + let previous_serial = previous_serial.unwrap_or(Serial::from(0)); previous_serial.add(1) } SignerSerialPolicy::UnixTime => { From d399f1dbd4ebf1cdc6a172bdbf1c0b91f5b11b03 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 20:22:23 +0200 Subject: [PATCH 20/44] Adjust test condition. As step.if doesn't seem to be honoured by nektos/act. --- integration-tests/tests/ixfr-out/action.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index e120fb45c..270e6be6a 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -220,21 +220,19 @@ runs: # matching that of NSD. - name: Verify that TSIG is required by Cascade - if: ${{ inputs.with-tsig }} env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} run: | + if [[ "${{ inputs.with-tsig }}" == "false" ]]; then + exit 0 + fi + # This should not succeed as it lacks the required TSIG key. # Note that dig will exit with code 0 even if the request fails, # because it considers an error response still to be successful # receipt of a response, so we have to check the actual output. - dig @127.0.0.1 -p 4542 ${ZONE_NAME} AXFR &> dig-no-tsig.log - if grep -Fq 'Transfer failed' dig-no-tsig.log; then - exit 0 - else - echo "::error:: dig unexpectedly was able to AXFR the zone without a TSIG key" - exit 1 - fi + dig @127.0.0.1 +yaml -p 4542 ${ZONE_NAME} AXFR &> dig-no-tsig.log + grep -Fq "status: REFUSED" dig-no-tsig.log - name: Verify Cascade AXFR out env: @@ -244,7 +242,6 @@ runs: if [[ "${{ inputs.with-tsig }}" == "true" ]]; then EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" fi - echo "EXTRA_ARGS: ${EXTRA_ARGS}" dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${EXTRA_ARGS} ${ZONE_NAME} AXFR &> cascade-axfr.log TEST_DIR="${PWD}/integration-tests/ixfr-out" LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted @@ -271,8 +268,6 @@ runs: - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${{ steps.set-vars.outputs.ZONE_NAME }} AXFR >nsd-axfr.log - - run: exit 1 - - name: Print log files on any failure in this job uses: ./.github/actions/print-logfiles if: failure() From cfd91bead084cb4f554bb8a3045026689eeac407 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 22:40:08 +0200 Subject: [PATCH 21/44] Review feedback: signal upgrade to TCP on UDP IXFR request. Not via TCP but via single SOA response, per RFC 1995. Also adds a system test using dig. --- integration-tests/tests/ixfr-out/action.yml | 18 ++++++++++--- src/server/service.rs | 28 ++++++++++++++------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index 270e6be6a..c06801133 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -216,9 +216,6 @@ runs: # this either. Increasing the NSD log level to 9 doesn't cause it to # report whether it received IXFR or AXFR either. - # TODO: Verify the AXFR and IXFR response content is as expected / - # matching that of NSD. - - name: Verify that TSIG is required by Cascade env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} @@ -248,6 +245,21 @@ runs: LC_COLLATE=C sort cascade-axfr.log > output.sorted diff -u -w output.sorted reference.output.sorted + # A UDP IXFR request should result in a single SOA response "to inform the + # client that a TCP query should be initiated". + - name: Verify that UDP is not supported + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + SOA_RR=$(dig +short +notcp +ignore @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=1 ${EXTRA_ARGS} 2>&1 | tee dig-udp-ixfr.log) + if [[ "${SOA_RR}" != "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5" ]]; then + exit 1 + fi + - name: Verify Cascade IXFR serial 1..=3 out env: ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} diff --git a/src/server/service.rs b/src/server/service.rs index 5d5a1f46f..974343f64 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -344,12 +344,6 @@ mod compat { client_soa: Soa>, zone: ServedZone, ) -> ResponseStream { - // Refuse IXFR requests over UDP. - if request.transport_ctx().is_udp() { - tracing::warn!("Rejecting IXFR over UDP"); - return error(request.message(), Rcode::NOTIMP); - } - // Save a cheap clone of the zone to avoid a borrow checker error. let zone_clone = zone.clone(); @@ -361,6 +355,23 @@ mod compat { return error(request.message(), Rcode::NOTAUTH); } + // UDP is unlikely to work for any but the smallest of diffs, + // especially with a DNSSEC signed zone because even removal of a + // single A record can result in many RRs being changed due to the + // impact on the NSEC(3) chain, plus any change has to include a SOA + // SERIAL bump causing both the SOA RR and its RRSIG to also be in + // every IXFR diff. However, RFC 1995 says that "Transport of a query + // may be by either UDP or TCP" so we can't refuse UDP entirely. We + // can however return a single SOA record per RFC 1995 "to inform the + // client that a TCP query should be initiated". + if request.transport_ctx().is_udp() { + trace!( + "Signalling UDP IXR client at {} to retry by TCP", + request.client_addr().ip() + ); + return soa(request.message(), &*viewer); + } + // Remember the latest SOA. let new_soa = viewer.soa().clone(); @@ -430,10 +441,9 @@ mod compat { }); let Some(start_idx) = start_idx else { - tracing::trace!( + trace!( "Falling back from IXFR to AXFR because no diff is available for zone '{}' from serial {}", - zone.handle.name, - client_soa.serial, + zone.handle.name, client_soa.serial, ); return axfr(request, zone_clone).await; }; From 01fac00a0d3b919917e60b720c281d815f421f30 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 22:42:36 +0200 Subject: [PATCH 22/44] Review feedback: Duplicate spawn comment. --- src/server/service.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/server/service.rs b/src/server/service.rs index 974343f64..fa67a8a3c 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -280,7 +280,7 @@ mod compat { // prepare the messages in an async function (as a Tokio task) and send // them over a channel from there. // - // In the future, AXFRs could be implemented by spawning an OS thread + // In the future, IXFRs could be implemented by spawning an OS thread // and doing all the work there. This is incompatible with the API of // `domain::net::server`, as the underlying TCP connection cannot be // extracted, but we plan to stop using that API anyway. @@ -450,6 +450,19 @@ mod compat { let (tx, mut rx) = tokio::sync::mpsc::channel(1024); + // NOTE: The following code is a bit tricky. Ideally, we would elide + // the channel and return the `messages` iterator as an async `Stream`; + // but the iterator borrows from `viewer` via `.non_soa_records()`, and + // this prevents the iterator from satisfying `'static`. Rust actually + // _does_ have machinery to work around this, in async functions, so we + // prepare the messages in an async function (as a Tokio task) and send + // them over a channel from there. + // + // In the future, AXFRs could be implemented by spawning an OS thread + // and doing all the work there. This is incompatible with the API of + // `domain::net::server`, as the underlying TCP connection cannot be + // extracted, but we plan to stop using that API anyway. + // Stream the records in the background. tokio::task::spawn(async move { // Collect the sequence of IXFR output records. From 79059904c8a62b6a11dc80bbc64088d1eb0da3e3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 22:45:07 +0200 Subject: [PATCH 23/44] Review feedback: Note the potential to use flat_map() to avoid a vec allocation. --- src/server/service.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/service.rs b/src/server/service.rs index fa67a8a3c..127215b56 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -467,6 +467,8 @@ mod compat { tokio::task::spawn(async move { // Collect the sequence of IXFR output records. let mut rrs = vec![new_soa.clone().into()]; + // TODO: Use diffs[..].iter().flat_map() here to avoid the + // intermediate Vec allocation? for (loaded_diff, signed_diff) in &diffs[start_idx..] { rrs.push(signed_diff.removed_soa.clone().unwrap().into()); rrs.extend(loaded_diff.removed_records.clone()); From 02efdfcb2d6d2e041fc1026fa3ea0464f7af7c75 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 22:48:36 +0200 Subject: [PATCH 24/44] Review feedback: Remove irrelevant comment. --- src/server/service.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index 127215b56..a7a79226a 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -387,9 +387,6 @@ mod compat { // "If an IXFR query with the same or newer version number than that // of the server is received, it is replied to with a single SOA // record of the server's current version, just as in AXFR." - // ^^^^^^^^^^^^^^^ - // Errata https://www.rfc-editor.org/errata/eid3196 points out that - // this is NOT "just as in AXFR" as AXFR does not do that. let our_soa_serial = viewer.soa().rdata.serial; if client_soa.serial >= our_soa_serial { From baf4c64c60d8fd9ffe0bcd4411e7738549ffad63 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 May 2026 23:35:55 +0200 Subject: [PATCH 25/44] Verify more XFR outputs against expectations. --- .../reference-output/example.test.ixfr-2-3 | 10 +++++ .../reference-output/example.test.ixfr-3-3 | 1 + .../notify-and-xfr-tsig.test.ixfr-2-3 | 10 +++++ .../notify-and-xfr-tsig.test.ixfr-3-3 | 1 + integration-tests/tests/ixfr-out/action.yml | 45 +++++++++++++++++-- 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 create mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 new file mode 100644 index 000000000..56f9a7289 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 @@ -0,0 +1,10 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 new file mode 100644 index 000000000..ebe4c34bc --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 @@ -0,0 +1 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 new file mode 100644 index 000000000..9b3966fe2 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 @@ -0,0 +1,10 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 new file mode 100644 index 000000000..1808ea302 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 @@ -0,0 +1 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index c06801133..a627272be 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -274,11 +274,50 @@ runs: LC_COLLATE=C sort cascade-ixfr-1-3.log > output.sorted diff -u -w output.sorted reference.output.sorted - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=2 >cascade-ixfr-2.log + - name: Verify Cascade IXFR serial 2..=3 out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=2 ${EXTRA_ARGS} &> cascade-ixfr-2-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-ixfr-2-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify Cascade IXFR serial 3..=3 out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=3 ${EXTRA_ARGS} &> cascade-ixfr-3-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-ixfr-3-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted - - run: dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${{ steps.set-vars.outputs.ZONE_NAME }} -t IXFR=3 >cascade-ixfr-3.log + - name: Verify NSD AXFR out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${ZONE_NAME} AXFR &> nsd-axfr.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" - - run: dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${{ steps.set-vars.outputs.ZONE_NAME }} AXFR >nsd-axfr.log + # Pass -f to sort and -i to diff to normalize case in the Cascade and + # NSD output being compared, as NSD outputs lower case labels while + # Cascade preserves label case. -f tells sort not to order differently + # based on case, and -i tells diff to consider differing case to be + # equal. + + LC_COLLATE=C sort -f "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort -f nsd-axfr.log > output.sorted + diff -u -w -i output.sorted reference.output.sorted - name: Print log files on any failure in this job uses: ./.github/actions/print-logfiles From 8f95fd45dcdb70bf89dbc31b1cd63ddfb3374c42 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 May 2026 11:02:49 +0200 Subject: [PATCH 26/44] Review feedback: Move IXFR diff logging from TRACE to DEBUG level. --- src/server/service.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index a7a79226a..469bab253 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -409,14 +409,14 @@ mod compat { // response message and will have to be split into multiple response // messages. - if tracing::enabled!(Level::TRACE) { - trace!( + if tracing::enabled!(Level::DEBUG) { + debug!( "IXFR out: {} diffs available for zone {}:", diffs.len(), zone.handle.name ); for (i, (loaded_diff, signed_diff)) in diffs.iter().enumerate() { - trace!( + debug!( "IXFR out: Diff #{i}: serial {} => serial {}, loaded -{}+{}, signed -{}+{}", signed_diff.removed_soa.as_ref().unwrap().0.rdata.serial, signed_diff.added_soa.as_ref().unwrap().0.rdata.serial, @@ -438,7 +438,7 @@ mod compat { }); let Some(start_idx) = start_idx else { - trace!( + debug!( "Falling back from IXFR to AXFR because no diff is available for zone '{}' from serial {}", zone.handle.name, client_soa.serial, ); From 3781eae2098a942d496035d0d4ad3fb931f97444 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 May 2026 11:23:09 +0200 Subject: [PATCH 27/44] FIX: Also store signed-only diffs. Incremental signing doesn't change the loaded zone so don't expect a non-empty loaded diff. --- src/persistence/persist.rs | 7 ++----- src/server/service.rs | 18 ++++++++++++++---- src/zone/storage.rs | 5 ++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/persistence/persist.rs b/src/persistence/persist.rs index 67be2aadf..1cc929129 100644 --- a/src/persistence/persist.rs +++ b/src/persistence/persist.rs @@ -47,10 +47,7 @@ pub fn persist_signed( let loaded_diff = persister.loaded_diff(); let signed_diff = persister.signed_diff(); - if let Some(loaded_diff) = loaded_diff - && signed_diff.removed_soa.is_some() - && signed_diff.removed_soa != signed_diff.added_soa - { + if signed_diff.removed_soa.is_some() && signed_diff.removed_soa != signed_diff.added_soa { let mut state = zone.state.lock().unwrap(); // Store anything that changed when the zone was re-loaded, i.e. @@ -65,7 +62,7 @@ pub fn persist_signed( // MUST always have a new SOA SERIAL compared to the previous version // of the signed zone. - let complete_diff = (loaded_diff.clone(), signed_diff.clone()); + let complete_diff = (loaded_diff.cloned(), signed_diff.clone()); state.storage.diffs.push(complete_diff); } diff --git a/src/server/service.rs b/src/server/service.rs index 469bab253..30f3febb0 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -420,8 +420,14 @@ mod compat { "IXFR out: Diff #{i}: serial {} => serial {}, loaded -{}+{}, signed -{}+{}", signed_diff.removed_soa.as_ref().unwrap().0.rdata.serial, signed_diff.added_soa.as_ref().unwrap().0.rdata.serial, - loaded_diff.removed_records.len(), - loaded_diff.added_records.len(), + loaded_diff + .as_ref() + .map(|d| d.removed_records.len()) + .unwrap_or(0), + loaded_diff + .as_ref() + .map(|d| d.added_records.len()) + .unwrap_or(0), signed_diff.removed_records.len(), signed_diff.added_records.len(), ); @@ -468,10 +474,14 @@ mod compat { // intermediate Vec allocation? for (loaded_diff, signed_diff) in &diffs[start_idx..] { rrs.push(signed_diff.removed_soa.clone().unwrap().into()); - rrs.extend(loaded_diff.removed_records.clone()); + if let Some(d) = loaded_diff { + rrs.extend(d.removed_records.clone()); + } rrs.extend(signed_diff.removed_records.clone()); rrs.push(signed_diff.added_soa.clone().unwrap().into()); - rrs.extend(loaded_diff.added_records.clone()); + if let Some(d) = loaded_diff { + rrs.extend(d.added_records.clone()); + } rrs.extend(signed_diff.added_records.clone()); } rrs.push(new_soa.into()); diff --git a/src/zone/storage.rs b/src/zone/storage.rs index 370dcc9f8..60c06a26f 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -927,7 +927,10 @@ pub struct StorageState { /// Diffs from one serial to another. Each diff consists of changes in the /// loaded part and changes in the signed part. - pub diffs: Vec<(Arc, Arc)>, + /// + /// A loaded diff is not available if the zone was re-signed due to + /// changes in signing key or to refresh expiring signatures. + pub diffs: Vec<(Option>, Arc)>, /// Ongoing background tasks. /// From 8b52b2bccc411e012ab4a6945d7bc6f29af6ac1d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 May 2026 14:00:51 +0200 Subject: [PATCH 28/44] Ignore NOTIFY if the zone was sourced from a file. --- src/units/zone_server.rs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/units/zone_server.rs b/src/units/zone_server.rs index 1be6517a9..ea2fac760 100644 --- a/src/units/zone_server.rs +++ b/src/units/zone_server.rs @@ -745,10 +745,36 @@ impl Notifiable for LoaderNotifier { // able to learn the used TSIG key. let center = &self.center; if let Some(zone) = crate::center::get_zone(center, apex_name) { - info!("Instructing zone loader to refresh zone '{apex_name}"); - center.loader.on_refresh_zone(center, &zone); + // Don't allow NOTIFY to trigger re-signing of a zone loaded + // from disk because the operator may be in the middle + // of editing the file and thus the zone may not be ready + // to reload. Zones sourced from files should be reloaded + // explicitly using the `cascade zone reload` CLI command. + // Also ignore NOTIFY for a zone that has no source at all. + + // Clone the source so that we don't hold the zone state lock + // when calling on_refresh_zone(). + let zone_source = zone.state.lock().unwrap().loader.source.clone(); + match zone_source { + crate::loader::Source::Server { .. } => { + info!("Instructing zone loader to refresh zone '{apex_name}"); + center.loader.on_refresh_zone(center, &zone); + } + + crate::loader::Source::Zonefile { .. } => { + warn!( + "Ignoring NOTIFY for zone '{apex_name}': zone source is not an upstream nameserver" + ); + } + + crate::loader::Source::None => { + // Nothing to do. + } + } } else { - warn!("Ignoring NOTIFY for unknown zone '{apex_name}'"); + warn!( + "Ignoring NOTIFY for zone '{apex_name}': zone is not registered with Cascade" + ); } } From 4ed1029d3fa0c1939ef5faae1864b081e524d033 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 May 2026 17:38:46 +0200 Subject: [PATCH 29/44] Merge loaded-review and signed-review IXFR out RR selection code. --- src/server/service.rs | 251 +++++++++++++++++++----------------------- 1 file changed, 116 insertions(+), 135 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index 96f36658c..f68cbb132 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -72,7 +72,7 @@ impl Clone for ZoneService { mod compat { use std::{pin::Pin, sync::Arc}; - use cascade_zonedata::OldRecord; + use cascade_zonedata::{DiffData, OldRecord}; use domain::{ base::{Message, MessageBuilder, iana::Rcode}, net::server::{ @@ -102,7 +102,8 @@ mod compat { fn call(&self, old_request: Request, Option>>) -> Response { // Parse the request. - let message = old_request.message().as_slice(); + let slice = old_request.message().as_slice(); + let message = slice; let message = domain::new::base::Message::parse_bytes_by_ref(message) .expect("'message' was already checked to be a valid DNS message"); let request = match crate::server::request::parse(message) { @@ -511,164 +512,100 @@ mod compat { // Collect the sequence of IXFR output records. let mut rrs = vec![new_soa.clone().into()]; - if viewer.is_loaded_viewer() { - // Serve a loaded zone diff. - let mut last_removed_soa = None; - let mut last_added_soa = None; - for (i, (loaded_diff, _)) in diffs[start_idx..].iter().enumerate() { - // Skip a change that only occured in the signed zone e.g. - // due to incremental re-signing. - let Some(loaded_diff) = &loaded_diff else { - continue; - }; - if loaded_diff.is_empty() { - continue; - } - trace!( - "Serving diff #{} for loaded review server IXFR out", - start_idx + i - ); + // Serve the correct records: + // - to a client of the loaded review server, who shouldn't + // receive any signed records in the IXFR response, OR + // - to a client of the signed review or publication server, + // who should be served differences in both loaded and signed + // records in the IXFR response. + // + // There is a lot of overlap between these two branches but I + // find it much harder to understand if the code is merged so I + // kept it separate like this. + let mut last_removed_soa = None; + let mut last_added_soa = None; + + for (i, (loaded_diff, signed_diff)) in diffs[start_idx..].iter().enumerate() { + let abs_idx = start_idx + i; + + // Select the appropriate diff as the SOA source to use. + let candidate_diff = if viewer.is_loaded_viewer() { + loaded_diff + } else { + signed_diff + }; - // The loaded zone SOA serial may not have changed, e.g. - // if serial policy is NOT set to keep then the operator - // expects the signed zone SOA serial to get bumped even - // if the input zone has changed RRs but not a changed SOA - // serial. However, for IXFR out we MUST have a change in - // SOA RR in order to construct the IXFR out. Such a diff - // should not have been stored and it is a serious error - // if we get such a diff here. - assert!(loaded_diff.removed_soa.is_some()); - assert!(loaded_diff.added_soa.is_some()); - let removed_soa = loaded_diff.removed_soa.as_ref().unwrap(); - let added_soa = loaded_diff.added_soa.as_ref().unwrap(); - - // Stop iterating if we encounter the same SOA serial - // again which happens if the loaded zone remained - // unchanged but incremental signing caused changes in the - // signed part only to occur. + // Skip diffs that didn't change the key part of the zone, + // loaded or signed, being served, i.e. + // - For a loaded review server, skip a change that only + // occured in the signed zone e.g. due to incremental + // re-signing. + // - For a signed review / publication server, skip a change + // that only occured in the loaded zone and not (yet) in + // the signed zone. + let soa_source_diff = match require_diff(candidate_diff) { + Some(diff) => diff, + None => continue, + }; + + // To serve an IXFR we must have a change in serial number. + assert!(soa_source_diff.removed_soa.is_some()); + assert!(soa_source_diff.added_soa.is_some()); + let removed_soa = soa_source_diff.removed_soa.as_ref().unwrap(); + let added_soa = soa_source_diff.added_soa.as_ref().unwrap(); + + // Stop iterating if we encounter the same SOA serial + // again which happens if the loaded zone remained + // unchanged but incremental signing caused changes in the + // signed part only to occur. + if viewer.is_loaded_viewer() { if let Some(last_removed_soa) = last_removed_soa { if removed_soa == last_removed_soa { - trace!("Stopping at last unique loaded diff #{}.", start_idx + i); + trace!("Stopping at last unique loaded diff #{abs_idx}."); break; } } + } - // Ensure that the sequence of diffs has no SOA serial - // gaps, each last added SOA should be the SOA that the - // next diff sequence removes. - if let Some(last_added_soa) = last_added_soa { - assert_eq!(removed_soa, last_added_soa); - } + // Ensure that the sequence of diffs has no SOA serial + // gaps, each last added SOA should be the SOA that the + // next diff sequence removes. + if let Some(last_added_soa) = last_added_soa { + assert_eq!(removed_soa, last_added_soa); + } + + if tracing::enabled!(Level::TRACE) { + trace_diff_pair(abs_idx, loaded_diff, signed_diff); + } + + trace!("Serving diff #{abs_idx} for loaded review server IXFR out",); + if viewer.is_loaded_viewer() { // Remove old records. rrs.push(removed_soa.clone().into()); - rrs.extend(loaded_diff.removed_records.clone()); + rrs.extend(soa_source_diff.removed_records.clone()); // Add new records. rrs.push(added_soa.clone().into()); - rrs.extend(loaded_diff.added_records.clone()); - - last_removed_soa = Some(removed_soa); - last_added_soa = Some(added_soa); - } - } else { - // Serve a diff based on both the loaded diff (if the source - // zone changed) and the signed diff. - let mut last_added_soa = None; - for (i, (loaded_diff, signed_diff)) in diffs[start_idx..].iter().enumerate() { - // Skip a change that only occured in the loaded zone and - // not (yet) in the signed zone. - let Some(signed_diff) = &signed_diff else { - continue; - }; - if signed_diff.is_empty() { - continue; - } - trace!( - "Serving diff #{} for signed / publication server IXFR out", - start_idx + i - ); - - // The signed zone SOA serial MUST have been bumped. - let removed_soa = signed_diff.removed_soa.as_ref().unwrap(); - let added_soa = signed_diff.added_soa.as_ref().unwrap(); - - // Ensure that the sequence of diffs has no SOA serial - // gaps, each last added SOA should be the SOA that the - // next diff sequence removes. - if let Some(last_added_soa) = last_added_soa { - assert_eq!(removed_soa, last_added_soa); - } - - // trace!( - // "Loaded diff #{}: {:?}->{:?}: {:?}->{:?}", - // start_idx + i, - // loaded_diff - // .as_ref() - // .map(|d| d.removed_soa.as_ref().map(|s| s.rdata.serial)) - // .flatten(), - // loaded_diff - // .as_ref() - // .map(|d| d.added_soa.as_ref().map(|s| s.rdata.serial)) - // .flatten(), - // loaded_diff.as_ref().map(|d| d - // .removed_records - // .iter() - // .map(|r| format!("{:?}", r.rtype)) - // .collect::>() - // .join(",")), - // loaded_diff.as_ref().map(|d| d - // .added_records - // .iter() - // .map(|r| format!("{:?}", r.rtype)) - // .collect::>() - // .join(",")), - // ); - - // trace!( - // "Signed diff #{}: {:?}->{:?}: {:?}->{:?}", - // start_idx + i, - // signed_diff.removed_soa.as_ref().map(|s| s.rdata.serial), - // signed_diff.added_soa.as_ref().map(|s| s.rdata.serial), - // signed_diff - // .as_ref() - // .removed_records - // .iter() - // .map(|r| format!("{:?}", r.rtype)) - // .collect::>() - // .join(","), - // signed_diff - // .as_ref() - // .added_records - // .iter() - // .map(|r| format!("{:?}", r.rtype)) - // .collect::>() - // .join(","), - // ); - + rrs.extend(soa_source_diff.added_records.clone()); + } else { // Remove old records. rrs.push(removed_soa.clone().into()); if let Some(d) = loaded_diff { rrs.extend(d.removed_records.clone()); } - rrs.extend(signed_diff.removed_records.clone()); + rrs.extend(soa_source_diff.removed_records.clone()); // Add new records. rrs.push(added_soa.clone().into()); if let Some(d) = loaded_diff { rrs.extend(d.added_records.clone()); } - rrs.extend(signed_diff.added_records.clone()); - - // Ensure that the sequence of diffs has no SOA serial - // gaps, each last added SOA should be the SOA that the - // next diff sequence removes. - if let Some(last_added_soa) = last_added_soa { - assert_eq!(removed_soa, last_added_soa); - } - - last_added_soa = Some(added_soa); + rrs.extend(soa_source_diff.added_records.clone()); } + + last_removed_soa = Some(removed_soa); + last_added_soa = Some(added_soa); } rrs.push(new_soa.into()); @@ -715,6 +652,50 @@ mod compat { Box::new(stream) as _ } + fn require_diff(d: &Option>) -> Option<&Arc> { + let Some(d) = &d else { + return None; + }; + if d.is_empty() { + return None; + } + Some(d) + } + + fn trace_diff_pair( + diff_idx: usize, + loaded_diff: &Option>, + signed_diff: &Option>, + ) { + fn trace_diff(prefix: &str, diff_idx: usize, d: &Option>) { + trace!( + "{prefix} diff #{}: {:?}->{:?}: {:?}->{:?}", + diff_idx, + d.as_ref() + .map(|d| d.removed_soa.as_ref().map(|s| s.rdata.serial)) + .flatten(), + d.as_ref() + .map(|d| d.added_soa.as_ref().map(|s| s.rdata.serial)) + .flatten(), + d.as_ref().map(|d| d + .removed_records + .iter() + .map(|r| format!("{:?}", r.rtype)) + .collect::>() + .join(",")), + d.as_ref().map(|d| d + .added_records + .iter() + .map(|r| format!("{:?}", r.rtype)) + .collect::>() + .join(",")), + ); + } + + trace_diff("Loaded", diff_idx, loaded_diff); + trace_diff("Signed", diff_idx, signed_diff); + } + fn error(request: &Message>, rcode: Rcode) -> ResponseStream { let response = MessageBuilder::new_stream_vec() .start_error(request, rcode) From 8312b8b41e8ad26ea644aea734fd3ac2c846abb3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 May 2026 19:19:25 +0200 Subject: [PATCH 30/44] Remove unnecessary Options. --- src/persistence/persist.rs | 7 +-- src/server/service.rs | 108 ++++++++++++------------------------- src/zone/storage.rs | 2 +- 3 files changed, 38 insertions(+), 79 deletions(-) diff --git a/src/persistence/persist.rs b/src/persistence/persist.rs index bb78dc103..983ae2c2b 100644 --- a/src/persistence/persist.rs +++ b/src/persistence/persist.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use cascade_zonedata::{ - LoadedZonePersisted, LoadedZonePersister, SignedZonePersisted, SignedZonePersister, + DiffData, LoadedZonePersisted, LoadedZonePersister, SignedZonePersisted, SignedZonePersister, }; use crate::{center::Center, zone::Zone}; @@ -39,7 +39,7 @@ pub fn persist_loaded( // require the SOA to have been removed and a new one added. let mut state = zone.state.lock().unwrap(); - let loaded_only_diff = (Some(persister.loaded_diff().clone()), None); + let loaded_only_diff = (persister.loaded_diff().clone(), DiffData::new().into()); state.storage.diffs.push(loaded_only_diff); } @@ -83,7 +83,8 @@ pub fn persist_signed( // MUST always have a new SOA SERIAL compared to the previous version // of the signed zone. - let complete_diff = (loaded_diff.cloned(), Some(signed_diff.clone())); + let loaded_diff = loaded_diff.cloned().unwrap_or(DiffData::new().into()); + let complete_diff = (loaded_diff, signed_diff.clone()); state.storage.diffs.push(complete_diff); } diff --git a/src/server/service.rs b/src/server/service.rs index f68cbb132..daa828c27 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -425,37 +425,25 @@ mod compat { debug!( "IXFR out: Diff #{i}: loaded serial -{:?}+{:?} => signed serial -{:?}+{:?}, loaded -{}+{}, signed -{}+{}", loaded_diff + .removed_soa .as_ref() - .map(|d| d.removed_soa.as_ref().map(|soa_rr| soa_rr.0.rdata.serial)) - .flatten(), + .map(|soa_rr| soa_rr.0.rdata.serial), loaded_diff + .added_soa .as_ref() - .map(|d| d.added_soa.as_ref().map(|soa_rr| soa_rr.0.rdata.serial)) - .flatten(), + .map(|soa_rr| soa_rr.0.rdata.serial), signed_diff + .removed_soa .as_ref() - .map(|d| d.removed_soa.as_ref().map(|soa_rr| soa_rr.0.rdata.serial)) - .flatten(), + .map(|soa_rr| soa_rr.0.rdata.serial), signed_diff + .added_soa .as_ref() - .map(|d| d.added_soa.as_ref().map(|soa_rr| soa_rr.0.rdata.serial)) - .flatten(), - loaded_diff - .as_ref() - .map(|d| d.removed_records.len()) - .unwrap_or(0), - loaded_diff - .as_ref() - .map(|d| d.added_records.len()) - .unwrap_or(0), - signed_diff - .as_ref() - .map(|d| d.removed_records.len()) - .unwrap_or(0), - signed_diff - .as_ref() - .map(|d| d.added_records.len()) - .unwrap_or(0), + .map(|soa_rr| soa_rr.0.rdata.serial), + loaded_diff.removed_records.len(), + loaded_diff.added_records.len(), + signed_diff.removed_records.len(), + signed_diff.added_records.len(), ); } } @@ -476,11 +464,7 @@ mod compat { } else { signed_diff }; - if let Some(d) = d { - d.removed_soa.as_ref().map(|rr| rr.0.rdata.serial) == Some(client_soa.serial) - } else { - false - } + d.removed_soa.as_ref().map(|rr| rr.0.rdata.serial) == Some(client_soa.serial) }) }; @@ -529,7 +513,7 @@ mod compat { let abs_idx = start_idx + i; // Select the appropriate diff as the SOA source to use. - let candidate_diff = if viewer.is_loaded_viewer() { + let soa_source_diff = if viewer.is_loaded_viewer() { loaded_diff } else { signed_diff @@ -543,10 +527,9 @@ mod compat { // - For a signed review / publication server, skip a change // that only occured in the loaded zone and not (yet) in // the signed zone. - let soa_source_diff = match require_diff(candidate_diff) { - Some(diff) => diff, - None => continue, - }; + if soa_source_diff.is_empty() { + continue; + } // To serve an IXFR we must have a change in serial number. assert!(soa_source_diff.removed_soa.is_some()); @@ -558,13 +541,12 @@ mod compat { // again which happens if the loaded zone remained // unchanged but incremental signing caused changes in the // signed part only to occur. - if viewer.is_loaded_viewer() { - if let Some(last_removed_soa) = last_removed_soa { - if removed_soa == last_removed_soa { - trace!("Stopping at last unique loaded diff #{abs_idx}."); - break; - } - } + if viewer.is_loaded_viewer() + && let Some(last_removed_soa) = last_removed_soa + && removed_soa == last_removed_soa + { + trace!("Stopping at last unique loaded diff #{abs_idx}."); + break; } // Ensure that the sequence of diffs has no SOA serial @@ -591,16 +573,12 @@ mod compat { } else { // Remove old records. rrs.push(removed_soa.clone().into()); - if let Some(d) = loaded_diff { - rrs.extend(d.removed_records.clone()); - } + rrs.extend(loaded_diff.removed_records.clone()); rrs.extend(soa_source_diff.removed_records.clone()); // Add new records. rrs.push(added_soa.clone().into()); - if let Some(d) = loaded_diff { - rrs.extend(d.added_records.clone()); - } + rrs.extend(loaded_diff.added_records.clone()); rrs.extend(soa_source_diff.added_records.clone()); } @@ -652,43 +630,23 @@ mod compat { Box::new(stream) as _ } - fn require_diff(d: &Option>) -> Option<&Arc> { - let Some(d) = &d else { - return None; - }; - if d.is_empty() { - return None; - } - Some(d) - } - - fn trace_diff_pair( - diff_idx: usize, - loaded_diff: &Option>, - signed_diff: &Option>, - ) { - fn trace_diff(prefix: &str, diff_idx: usize, d: &Option>) { + fn trace_diff_pair(diff_idx: usize, loaded_diff: &Arc, signed_diff: &Arc) { + fn trace_diff(prefix: &str, diff_idx: usize, d: &Arc) { trace!( "{prefix} diff #{}: {:?}->{:?}: {:?}->{:?}", diff_idx, - d.as_ref() - .map(|d| d.removed_soa.as_ref().map(|s| s.rdata.serial)) - .flatten(), - d.as_ref() - .map(|d| d.added_soa.as_ref().map(|s| s.rdata.serial)) - .flatten(), - d.as_ref().map(|d| d - .removed_records + d.removed_soa.as_ref().map(|s| s.rdata.serial), + d.added_soa.as_ref().map(|s| s.rdata.serial), + d.removed_records .iter() .map(|r| format!("{:?}", r.rtype)) .collect::>() - .join(",")), - d.as_ref().map(|d| d - .added_records + .join(","), + d.added_records .iter() .map(|r| format!("{:?}", r.rtype)) .collect::>() - .join(",")), + .join(","), ); } diff --git a/src/zone/storage.rs b/src/zone/storage.rs index 1d86d0d4b..8dc10fc7d 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -933,7 +933,7 @@ pub struct StorageState { /// /// A signed diff is not available if the zone has been re-loaded and has /// not yet been signed, e.g. is held at review or signing is in-progress. - pub diffs: Vec<(Option>, Option>)>, + pub diffs: Vec<(Arc, Arc)>, /// Ongoing background tasks. /// From 06d4bf0477fb7bbb68eafd8d42bb957ae7514a54 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 13:22:36 +0200 Subject: [PATCH 31/44] Add IXFR out review server tests, and fix a bug that was found. --- .../reference-output/loaded-review.ixfr-1-3 | 11 + .../reference-output/loaded-review.ixfr-2-3 | 6 + .../reference-output/loaded-review.ixfr-3-3 | 1 + .../reference-output/signed-review.ixfr-1-3 | 27 ++ .../reference-output/signed-review.ixfr-2-3 | 10 + .../reference-output/signed-review.ixfr-3-3 | 1 + integration-tests/system-tests.yml | 12 + .../tests/ixfr-out-review/action.yml | 239 ++++++++++++++++++ src/server/service.rs | 8 +- 9 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 create mode 100644 integration-tests/tests/ixfr-out-review/action.yml diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 new file mode 100644 index 000000000..fe72b7b79 --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 @@ -0,0 +1,11 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN SOA NS.example.test. mail.example.test. 1 60 60 3600 5 +NEZU.example.test. 3600 IN A 133.69.136.5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 new file mode 100644 index 000000000..aa673c63b --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 @@ -0,0 +1,6 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 new file mode 100644 index 000000000..ebe4c34bc --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 @@ -0,0 +1 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 new file mode 100644 index 000000000..e9ec16609 --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 @@ -0,0 +1,27 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN SOA NS.example.test. mail.example.test. 2026051200 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. 0pT73Y+9z9QOkhX9UjRncbR6KyXgSqfxoCviwNhyN8BijAf3oJLVueZR zFNCkNkwg+goMKF9acAmRasdHmA/Dw== +example.test. 5 IN NSEC NEZU.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20200927122640 20200912122640 580 example.test. ijL6SFOFQhPOfSQsTtV9I39aKWkJo6orbMXKcUQ6Woa/Ze+4q5oNK2R7 t8kgL8GTUySY3h3ljlA3SMp+zPjuBg== +NEZU.example.test. 3600 IN A 133.69.136.5 +NEZU.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. eP6CmrELzTgLGgki4TDyqAjNzl535SfnIVNYkjlXV/sDh9iJcNg9QCl8 Na8LR4HRM6RIEe3pw4aZCZg0SqCFDw== +NEZU.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +NEZU.example.test. 5 IN RRSIG NSEC 15 3 5 20200927122640 20200912122640 580 example.test. moXs8yosAlQXN+OQborKKgMp0wBmPVN/9nM0lx61ZK2FTRdXSbRi349q CIuQ6bi2SmOD8pfmgSFRSVOiFTmPDg== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== +example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20200927122640 20200912122640 580 example.test. XUPmDdnhSI7IFMQWMJPewxEN6dqhwj2b/Q/ah74BCvS5GCv4p5Tu2HlY lkDVRE93xZW4mdvb+P8CMcBPLDeBBQ== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== +JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20200927122640 20200912122640 580 example.test. bjeFIBVSXyNxaGnWElV61PhoVo0/C7ztRmk/WLJ4R84tmBbIbSLN1bo0 1EkRvlbHJj/HNKHtUNZkGhDUe8SgAA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. lw2qqnFjRO9k0FmtLHgcJPvzLPQidUMUW8Cx7l03eA6SxVVvfBr3nn5w sIfP4yw09HvKazQl10Xc47SfJNMKCw== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. xTVteJjlrzkL8U1vqPTE7mfOXjUrsnbdOiLOvjVJFeH20wV18HEjnHl8 PWsDtSVhvf9PemqDbjmjlP7HnPIjAA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 new file mode 100644 index 000000000..b3b56e6fc --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 @@ -0,0 +1,10 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. lw2qqnFjRO9k0FmtLHgcJPvzLPQidUMUW8Cx7l03eA6SxVVvfBr3nn5w sIfP4yw09HvKazQl10Xc47SfJNMKCw== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. xTVteJjlrzkL8U1vqPTE7mfOXjUrsnbdOiLOvjVJFeH20wV18HEjnHl8 PWsDtSVhvf9PemqDbjmjlP7HnPIjAA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 new file mode 100644 index 000000000..32a65570b --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 @@ -0,0 +1 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/system-tests.yml b/integration-tests/system-tests.yml index 0ff73a840..b3d27ed86 100644 --- a/integration-tests/system-tests.yml +++ b/integration-tests/system-tests.yml @@ -194,6 +194,18 @@ jobs: with: log-level: ${{ inputs.log-level }} + ixfr-out-review: + name: Serve manual zone edits via IXFR for review. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/set-build-profile + with: + build-profile: ${{ inputs.build-profile }} + - uses: ./integration-tests/tests/ixfr-out-review + with: + log-level: ${{ inputs.log-level }} + incremental-signing: name: Test incremental signing. runs-on: ubuntu-latest diff --git a/integration-tests/tests/ixfr-out-review/action.yml b/integration-tests/tests/ixfr-out-review/action.yml new file mode 100644 index 000000000..9b5d71caf --- /dev/null +++ b/integration-tests/tests/ixfr-out-review/action.yml @@ -0,0 +1,239 @@ +# Making reusable composite actions documented at +# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository +name: 'Serve manual zone modifications via IXFR for review.' +description: 'Serve a zone via IXFR to the NSD secondary.' +defaults: + # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell + run: + shell: bash --noprofile --norc -eo pipefail -x {0} +inputs: + log-level: + description: The level of logging that Cascade should output. + required: false + default: debug + type: choice + options: + - error + - warning + - info + - debug + - trace +runs: + using: "composite" + steps: + - uses: ./.github/actions/setup-and-start-cascade + with: + log-level: ${{ inputs.log-level }} + fake-time: 1600000000 + + - name: Determine expected published serial number + id: set-vars + run: | + EXPECTED_SERIAL="$(date +'%Y%m%d00')" + echo "EXPECTED_SERIAL=${EXPECTED_SERIAL}" >>"$GITHUB_OUTPUT" + + - name: Make a zone based on RFC 1995 section 7 + run: | + cat >"example.test.zone" < (start + timeout))); then + cascade zone status example.test + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Expect serial 1 of the zone at the loaded review server + run: | + timeout=3 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 4540 example.test SOA | grep -qi "ns.example.test. mail.example.test. 1 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4540 example.test SOA + echo "::error:: timeout: loaded review server is not serving serial 1 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 1 of the zone at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4541 example.test SOA + echo "::error:: timeout: signed review server is not serving serial 1 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Edit and reload serial 2 of the zone + run: | + cat >"example.test.zone" < (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4540 example.test SOA + echo "::error:: timeout: loaded review server is not serving serial 2 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 2 of the zone at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + timeout=10 # seconds + start=$(date +%s) + let EXPECTED_SERIAL=$(( EXPECTED_SERIAL + 1 )) + until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4541 example.test SOA + echo "::error:: timeout: signed review server is not serving serial 2 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Edit and reload serial 3 of the zone + run: | + cat >"example.test.zone" < (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4540 example.test SOA + echo "::error:: timeout: loaded review server is not serving serial 3 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 3 of the zone at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + timeout=10 # seconds + start=$(date +%s) + let EXPECTED_SERIAL=$(( EXPECTED_SERIAL + 2 )) + until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4541 example.test SOA + echo "::error:: timeout: signed review server is not serving serial 3 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Verify IXFR of serial 1..=3 at the loaded review server + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=1 &> loaded-review-ixfr-1-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort loaded-review-ixfr-1-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 2..=3 at the loaded review server + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=2 &> loaded-review-ixfr-2-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort loaded-review-ixfr-2-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 3..=3 at the loaded review server + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=3 &> loaded-review-ixfr-3-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort loaded-review-ixfr-3-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 1..=3 at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=${EXPECTED_SERIAL} &> signed-review-ixfr-1-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort signed-review-ixfr-1-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 2..=3 at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=$(( EXPECTED_SERIAL + 1 )) &> signed-review-ixfr-2-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort signed-review-ixfr-2-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 3..=3 at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=$(( EXPECTED_SERIAL + 2 )) &> signed-review-ixfr-3-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort signed-review-ixfr-3-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Print log files on any failure in this job + uses: ./.github/actions/print-logfiles + if: failure() diff --git a/src/server/service.rs b/src/server/service.rs index daa828c27..2c99791ed 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -537,16 +537,16 @@ mod compat { let removed_soa = soa_source_diff.removed_soa.as_ref().unwrap(); let added_soa = soa_source_diff.added_soa.as_ref().unwrap(); - // Stop iterating if we encounter the same SOA serial - // again which happens if the loaded zone remained + // Skip a diff if we encounter the same SOA serial + // again which can happen if the loaded zone remained // unchanged but incremental signing caused changes in the // signed part only to occur. if viewer.is_loaded_viewer() && let Some(last_removed_soa) = last_removed_soa && removed_soa == last_removed_soa { - trace!("Stopping at last unique loaded diff #{abs_idx}."); - break; + trace!("Skipping unchanged loaded diff #{abs_idx}."); + continue; } // Ensure that the sequence of diffs has no SOA serial From 393fb4d33405ec3bb1a392ee9e6dc9f001914715 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 13:27:13 +0200 Subject: [PATCH 32/44] Typo correction. --- integration-tests/tests/downstream-tsig/action.yml | 2 +- integration-tests/tests/ixfr-out-incremental/action.yml | 2 +- integration-tests/tests/ixfr-out/action.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-tests/tests/downstream-tsig/action.yml b/integration-tests/tests/downstream-tsig/action.yml index 006c5a54d..f8d01bc9f 100644 --- a/integration-tests/tests/downstream-tsig/action.yml +++ b/integration-tests/tests/downstream-tsig/action.yml @@ -44,7 +44,7 @@ runs: with: log-level: ${{ inputs.log-level }} - - name: Determine the downstrem secondary zone config to use + - name: Determine the downstream secondary zone config to use id: set-zone-name run: | if [[ ${{ inputs.downstream-expects-tsig }} -eq 0 && ${{ inputs.downstream-uses-tsig }} -eq 0 ]]; then diff --git a/integration-tests/tests/ixfr-out-incremental/action.yml b/integration-tests/tests/ixfr-out-incremental/action.yml index d789a512f..47fa37e4b 100644 --- a/integration-tests/tests/ixfr-out-incremental/action.yml +++ b/integration-tests/tests/ixfr-out-incremental/action.yml @@ -26,7 +26,7 @@ runs: with: log-level: ${{ inputs.log-level }} - - name: Determine the downstrem secondary zone config to use + - name: Determine the downstream secondary zone config to use id: set-vars run: | ZONE_NAME="example.test." diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml index 6a4a509b1..0cdab4edf 100644 --- a/integration-tests/tests/ixfr-out/action.yml +++ b/integration-tests/tests/ixfr-out/action.yml @@ -31,7 +31,7 @@ runs: log-level: ${{ inputs.log-level }} fake-time: 1600000000 - - name: Determine the downstrem secondary zone config to use + - name: Determine the downstream secondary zone config to use id: set-vars run: | if [[ "${{ inputs.with-tsig }}" == "true" ]]; then From 0c692c6666e8cc6a4c381c9df750e9bc23d63435 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 13:33:33 +0200 Subject: [PATCH 33/44] Remove system tests, they will be moved to a separate PR. By request. --- .../policies/fast-incremental.toml | 25 ------- .../reference-output/loaded-review.ixfr-1-3 | 11 --- .../reference-output/loaded-review.ixfr-2-3 | 6 -- .../reference-output/loaded-review.ixfr-3-3 | 1 - .../reference-output/signed-review.ixfr-1-3 | 27 ------- .../reference-output/signed-review.ixfr-2-3 | 10 --- .../reference-output/signed-review.ixfr-3-3 | 1 - .../keys/Kexample.test.+015+00580.key | 1 - .../keys/Kexample.test.+015+00580.private | 3 - .../Knotify-and-xfr-tsig.test.+015+10995.key | 1 - ...otify-and-xfr-tsig.test.+015+10995.private | 3 - .../ixfr-out/policies/with-tsig.toml | 32 -------- .../ixfr-out/policies/without-tsig.toml | 23 ------ .../reference-output/example.test.axfr | 17 ----- .../reference-output/example.test.ixfr-1-3 | 73 ------------------- .../reference-output/example.test.ixfr-2-3 | 10 --- .../reference-output/example.test.ixfr-3-3 | 1 - .../notify-and-xfr-tsig.test.axfr | 17 ----- .../notify-and-xfr-tsig.test.ixfr-1-3 | 27 ------- .../notify-and-xfr-tsig.test.ixfr-2-3 | 10 --- .../notify-and-xfr-tsig.test.ixfr-3-3 | 1 - integration-tests/system-tests.yml | 40 ---------- 22 files changed, 340 deletions(-) delete mode 100644 integration-tests/ixfr-out-incremental/policies/fast-incremental.toml delete mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 delete mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 delete mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 delete mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 delete mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 delete mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 delete mode 100644 integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key delete mode 100644 integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private delete mode 100644 integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key delete mode 100644 integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private delete mode 100644 integration-tests/ixfr-out/policies/with-tsig.toml delete mode 100644 integration-tests/ixfr-out/policies/without-tsig.toml delete mode 100644 integration-tests/ixfr-out/reference-output/example.test.axfr delete mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 delete mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 delete mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 delete mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr delete mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 delete mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 delete mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 diff --git a/integration-tests/ixfr-out-incremental/policies/fast-incremental.toml b/integration-tests/ixfr-out-incremental/policies/fast-incremental.toml deleted file mode 100644 index 156a5cb1f..000000000 --- a/integration-tests/ixfr-out-incremental/policies/fast-incremental.toml +++ /dev/null @@ -1,25 +0,0 @@ -# Setup incremental signing to produce predictable outputs so that we can -# easily compare the generated zone against expected output. -# -# Enable sending of NOTIFY messages to the NSD secondary. -version = "v1" - -[signer.denial] -type = "nsec" - -[signer] -serial-policy = "counter" -signature-inception-offset = 0 -signature-lifetime = 100 -signature-remain-time = 90 -signature-refresh-interval = 1 - -[key-manager.records] -dnskey.signature-inception-offset = 0 -cds.signature-inception-offset = 0 -dnskey.signature-lifetime = 10 -cds.signature-lifetime = 10 - -[server.outbound] -# NSD secondary listens on port 1054. -send-notify-to = ["127.0.0.1:1054"] diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 deleted file mode 100644 index fe72b7b79..000000000 --- a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 +++ /dev/null @@ -1,11 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -example.test. 3600 IN SOA NS.example.test. mail.example.test. 1 60 60 3600 5 -NEZU.example.test. 3600 IN A 133.69.136.5 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN A 192.41.197.2 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 deleted file mode 100644 index aa673c63b..000000000 --- a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 +++ /dev/null @@ -1,6 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 deleted file mode 100644 index ebe4c34bc..000000000 --- a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 +++ /dev/null @@ -1 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 deleted file mode 100644 index e9ec16609..000000000 --- a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 +++ /dev/null @@ -1,27 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 -example.test. 3600 IN SOA NS.example.test. mail.example.test. 2026051200 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. 0pT73Y+9z9QOkhX9UjRncbR6KyXgSqfxoCviwNhyN8BijAf3oJLVueZR zFNCkNkwg+goMKF9acAmRasdHmA/Dw== -example.test. 5 IN NSEC NEZU.example.test. NS SOA RRSIG NSEC DNSKEY -example.test. 5 IN RRSIG NSEC 15 2 5 20200927122640 20200912122640 580 example.test. ijL6SFOFQhPOfSQsTtV9I39aKWkJo6orbMXKcUQ6Woa/Ze+4q5oNK2R7 t8kgL8GTUySY3h3ljlA3SMp+zPjuBg== -NEZU.example.test. 3600 IN A 133.69.136.5 -NEZU.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. eP6CmrELzTgLGgki4TDyqAjNzl535SfnIVNYkjlXV/sDh9iJcNg9QCl8 Na8LR4HRM6RIEe3pw4aZCZg0SqCFDw== -NEZU.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC -NEZU.example.test. 5 IN RRSIG NSEC 15 3 5 20200927122640 20200912122640 580 example.test. moXs8yosAlQXN+OQborKKgMp0wBmPVN/9nM0lx61ZK2FTRdXSbRi349q CIuQ6bi2SmOD8pfmgSFRSVOiFTmPDg== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== -example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY -example.test. 5 IN RRSIG NSEC 15 2 5 20200927122640 20200912122640 580 example.test. XUPmDdnhSI7IFMQWMJPewxEN6dqhwj2b/Q/ah74BCvS5GCv4p5Tu2HlY lkDVRE93xZW4mdvb+P8CMcBPLDeBBQ== -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN A 192.41.197.2 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== -JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC -JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20200927122640 20200912122640 580 example.test. bjeFIBVSXyNxaGnWElV61PhoVo0/C7ztRmk/WLJ4R84tmBbIbSLN1bo0 1EkRvlbHJj/HNKHtUNZkGhDUe8SgAA== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. lw2qqnFjRO9k0FmtLHgcJPvzLPQidUMUW8Cx7l03eA6SxVVvfBr3nn5w sIfP4yw09HvKazQl10Xc47SfJNMKCw== -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. xTVteJjlrzkL8U1vqPTE7mfOXjUrsnbdOiLOvjVJFeH20wV18HEjnHl8 PWsDtSVhvf9PemqDbjmjlP7HnPIjAA== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 deleted file mode 100644 index b3b56e6fc..000000000 --- a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 +++ /dev/null @@ -1,10 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. lw2qqnFjRO9k0FmtLHgcJPvzLPQidUMUW8Cx7l03eA6SxVVvfBr3nn5w sIfP4yw09HvKazQl10Xc47SfJNMKCw== -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. xTVteJjlrzkL8U1vqPTE7mfOXjUrsnbdOiLOvjVJFeH20wV18HEjnHl8 PWsDtSVhvf9PemqDbjmjlP7HnPIjAA== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 deleted file mode 100644 index 32a65570b..000000000 --- a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 +++ /dev/null @@ -1 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key deleted file mode 100644 index 224ce50d5..000000000 --- a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key +++ /dev/null @@ -1 +0,0 @@ -example.test. IN DNSKEY 256 3 15 u7Tg6xf6Xgt0y+yUT8CQi1Nyy+tRopVHnZOFQz0zQ5A= diff --git a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private deleted file mode 100644 index c6ba81b21..000000000 --- a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 15 (ED25519) -PrivateKey: qCwt/EY9s1vGn0uj+4dlMQ5waOuFfIYpZZVrX43jdcc= diff --git a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key deleted file mode 100644 index 9ef332abc..000000000 --- a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key +++ /dev/null @@ -1 +0,0 @@ -notify-and-xfr-tsig.test. IN DNSKEY 256 3 15 0efFuDXcYUyhaTUWgir/RaWrzAJWAa58VuEF+391Taw= diff --git a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private deleted file mode 100644 index b385fd508..000000000 --- a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 15 (ED25519) -PrivateKey: Nx9EHQ1RBhngQMCRQUxMsiD2ybaWRCiEdcDlW5isKoo= diff --git a/integration-tests/ixfr-out/policies/with-tsig.toml b/integration-tests/ixfr-out/policies/with-tsig.toml deleted file mode 100644 index cc32f479d..000000000 --- a/integration-tests/ixfr-out/policies/with-tsig.toml +++ /dev/null @@ -1,32 +0,0 @@ -# Setup incremental signing to produce predictable outputs so that we can -# easily compare the generated zone against expected output. -# -# Enable sending of NOTIFY messages using TSIG authentication to the NSD -# secondary. -version = "v1" - -[signer.denial] -type = "nsec" - -[signer] -serial-policy = "keep" -signature-inception-offset = 0 -signature-lifetime = 100000000 - -[key-manager.records] -dnskey.signature-inception-offset = 0 -cds.signature-inception-offset = 0 -dnskey.signature-lifetime = 100000000 -cds.signature-lifetime = 100000000 - -[server.outbound] -# NSD secondary listens on port 1054. -send-notify-to = ["127.0.0.1:1054^tsig-key"] - -# Require that the XFR request from the NSD secondary be signed with the -# specified TSIG key. Without this, a TSIG signed XFR request with a known -# key will still be accepted, but an XFR request that is NOT TSIG signed -# would also be accepted. In the test we verify that TSIG is required by -# Cascade by attempting a dig without the required TSIG key and showing -# that it fails. -accept-xfr-from = ["127.0.0.1^tsig-key"] diff --git a/integration-tests/ixfr-out/policies/without-tsig.toml b/integration-tests/ixfr-out/policies/without-tsig.toml deleted file mode 100644 index 07c1d9359..000000000 --- a/integration-tests/ixfr-out/policies/without-tsig.toml +++ /dev/null @@ -1,23 +0,0 @@ -# Setup incremental signing to produce predictable outputs so that we can -# easily compare the generated zone against expected output. -# -# Enable sending of NOTIFY messages to the NSD secondary. -version = "v1" - -[signer.denial] -type = "nsec" - -[signer] -serial-policy = "keep" -signature-inception-offset = 0 -signature-lifetime = 100000000 - -[key-manager.records] -dnskey.signature-inception-offset = 0 -cds.signature-inception-offset = 0 -dnskey.signature-lifetime = 100000000 -cds.signature-lifetime = 100000000 - -[server.outbound] -# NSD secondary listens on port 1054. -send-notify-to = ["127.0.0.1:1054"] diff --git a/integration-tests/ixfr-out/reference-output/example.test.axfr b/integration-tests/ixfr-out/reference-output/example.test.axfr deleted file mode 100644 index c9577dd58..000000000 --- a/integration-tests/ixfr-out/reference-output/example.test.axfr +++ /dev/null @@ -1,17 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== -example.test. 3600 IN NS NS.example.test. -example.test. 3600 IN RRSIG NS 15 2 3600 20231114221320 20200913122640 580 example.test. hUD4MZwiVtXmHs+VueFIIrTUKYzWfOiNWm2nTR1gtsHTYnGjRrVEH/ic 3XeWDtKK0EedUE5iOKIEfQyYpaTLAg== -example.test. 3600 IN DNSKEY 256 3 15 u7Tg6xf6Xgt0y+yUT8CQi1Nyy+tRopVHnZOFQz0zQ5A= -example.test. 3600 IN RRSIG DNSKEY 15 2 3600 20231114221320 20200913122640 580 example.test. xGC6Pj6iN7tOjmhnLxmw4yQt7CCxCVouXvdB1P3lsQwN0iFfxgamkU7j EqIQcPpl20erYZ4XoWwtOdoeaXwtAg== -example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY -example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. P/YK6hlW/FOz43gKHqT4zoxRBdKFinx8GktOFDKuM+CWNyfBkjUZEI04 e3pa5/GUNcLKwRYAyWlNQnONPuusCQ== -NS.example.test. 3600 IN A 133.69.136.1 -NS.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. EsKLrDOszkF8dHi6K8XYed5EbqU9fSlNO2+25pBO/qZk1nZm1RyDO15X KWoCf1jUUNsTR4sZkiu3OGV7oazcAA== -NS.example.test. 5 IN NSEC example.test. A RRSIG NSEC -NS.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. hO9nn4+rxV08CFGkyXYzqGrRYNVOptJXZOaQLp6OSCMtte2iSQCq7UxJ p2KK2Q7aegqYKm6nNFoABmEYjZNlDQ== -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -JAIN-BB.example.test. 3600 IN A 192.41.197.2 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== -JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC -JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. /S/FZGh/OBQjwaVRPXJChDNkraEeD7qtVvIAa8db4bYmylI4KWOsH991 XDjye2aOv1+qPZxXIfzuX/3ER8JUBA== diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 deleted file mode 100644 index 56beb1143..000000000 --- a/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 +++ /dev/null @@ -1,73 +0,0 @@ -; The input zone was based on RFC 1995 section 7. This diff should look -; similar to "the following incremental message" from that section, but with -; DNSSEC changes present as well. Our zone name differs to that of the example -; in RFC 1995 as we have to match the zone name configured in our secondary NSD -; instance. - -; === Start of IXFR -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 - -; [DIFF 1]: "NEZU.JAIN.AD.JP. is removed and JAIN-BB.JAIN.AD.JP. is added." -; [DIFF 1]: Records from serial 1 to be removed. - -; Changing the SOA serial requires that we regenerate the RRSIG, so the old -; RRSIG has to be removed. -example.test. 3600 IN SOA NS.example.test. mail.example.test. 1 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. TlLDWFfpydqCQjy1oDPBXugGWraoyvvk83KdTsiq441t0/hYKmL2mFEH 2nwJ6fcuZT2hLpvl9gwk3bJCXrNdAQ== - -; Remove the A record and its associated RRSIG. -NEZU.example.test. 3600 IN A 133.69.136.5 -NEZU.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. k232/sABO752SeaFp3jHkIJMUGlDA0NG+iQh1gpGVsi07XqDce+JzNX4 NiWmz06kVu+SKu1HFHNvGJ3enh5/DQ== - -; Remove the A record from the NSEC type bitmap too, and also remove NSEC -; RRSIG as a new one will be added to replace it. -NEZU.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. BCpvhGfe+GW4GnyJBBiqjMpiVZhCXv9ZMdh+RF7qf07V3jIePWJThAy2 yV5O4I7NhKgl6rqpOyv2+aSoW8tVDw== -NEZU.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC - -; As the entire NEZU.example.test. label was removed this also requires the -; next owner name in the NSEC chain to be changed so we have to remove the -; current NSEC on the previous owner and its associated RRSIG, new ones will -; be added. -example.test. 5 IN NSEC NEZU.example.test. NS SOA RRSIG NSEC DNSKEY -example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. Hyrs5CKpXfCCNC9OK9+07Ei5qhRjQ9EQZK3up84BCwlflhOCGQppddzM eHK0Klgl0ywKaD7dfV2w3SWvP7vTDA== - -; [DIFF 1]: Records to be added for serial 2. - -; The new SOA with the updated SERIAL and its associated RRSIG. -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== - -; A changed NSEC and associated RRSIG due to changes in the NSEC chain. -example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY -example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. P/YK6hlW/FOz43gKHqT4zoxRBdKFinx8GktOFDKuM+CWNyfBkjUZEI04 e3pa5/GUNcLKwRYAyWlNQnONPuusCQ== - -; Add new label JAIN-BB.example.test with two A records, associated NSEC and RRSIGs. -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN A 192.41.197.2 -JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC -JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. /S/FZGh/OBQjwaVRPXJChDNkraEeD7qtVvIAa8db4bYmylI4KWOsH991 XDjye2aOv1+qPZxXIfzuX/3ER8JUBA== -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== - -; [DIFF 2]: "One of the IP addresses of JAIN-BB.JAIN.AD.JP. is changed." -; [DIFF 2]: Records from serial 2 to be removed. - -; The old SOA and associated RRSIG. -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== - -; The old A record and associated RRSIG. -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== - -; [DIFF 2]: Records to be added for serial 3. - -; The new SOA and associated RRSIG. -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== - -; The updated A record and associated RRSIG. -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== - -; End of IXFR -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 deleted file mode 100644 index 56f9a7289..000000000 --- a/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 +++ /dev/null @@ -1,10 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== -JAIN-BB.example.test. 3600 IN A 133.69.136.4 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 -example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== -JAIN-BB.example.test. 3600 IN A 133.69.136.3 -JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 deleted file mode 100644 index ebe4c34bc..000000000 --- a/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 +++ /dev/null @@ -1 +0,0 @@ -example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr deleted file mode 100644 index 08e54a056..000000000 --- a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr +++ /dev/null @@ -1,17 +0,0 @@ -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== -notify-and-xfr-tsig.test. 3600 IN NS NS.notify-and-xfr-tsig.test. -notify-and-xfr-tsig.test. 3600 IN RRSIG NS 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. YDqsJSHx81jK/GSqDdnNDG3JN9v9SH6jgmsgbQNElcaSCXqMrR2Q3QFq wrYW06FgnadRgTySRZ2evzS3dYGaDw== -notify-and-xfr-tsig.test. 3600 IN DNSKEY 256 3 15 0efFuDXcYUyhaTUWgir/RaWrzAJWAa58VuEF+391Taw= -notify-and-xfr-tsig.test. 3600 IN RRSIG DNSKEY 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. CIiRWX2DYaZRidc0nDl4zE0vmUCK2XFX2ekrJc1DVljIestEaJBAR2V1 ZQYOtCtb8y8AA/RCH8UFN2DYSM6+CQ== -notify-and-xfr-tsig.test. 5 IN NSEC JAIN-BB.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY -notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. z7aQTz7VOeeSwwftFsHr+f2BG1k1mVTGztiqZNUAYBkT5R+IlhWpzNdR KvpUSMcdgSNSsjKLQciFq/vYSqg7DA== -NS.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.1 -NS.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. ziPT0QEZ/gGcVltE17tqgf3jqo9SfesIffux+pIZwC3vO8eg1MnzPJOv Kmdl9vJQqS03FjwT+6r3xtjIVIfwAA== -NS.notify-and-xfr-tsig.test. 5 IN NSEC notify-and-xfr-tsig.test. A RRSIG NSEC -NS.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. nLNfo5CuJQd8qYbDywdefe0pFQ881jn3W6WEfNnkMg1daJk2ENcZUkiJ aeR7Hv7FPzTyavE2SQbuHfHrYDKoAA== -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 192.41.197.2 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== -JAIN-BB.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC -JAIN-BB.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Kn1CEC2iYr/uSRAZvM9SV2HRR0B8l8ObkvqYufiGO0en+OSLRmfgzviT Mdhdai9onUYr7ndQFHI8bSav5MBoCQ== diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 deleted file mode 100644 index 33df5ea3e..000000000 --- a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 +++ /dev/null @@ -1,27 +0,0 @@ -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 -notify-and-xfr-tsig.test. 3600 IN SOA NS.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 1 60 60 3600 5 -NEZU.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.5 -notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0BtPWx9VOtcE1BMD3cLk1FR1YaT/1v32VuuxXh/XQgS97Nj80H+Qm57V ORpk/V6wkw2exuvz3ko2s2BfkaQqAQ== -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. xzwl8jSWvas1L2Uqzfq8kGkPZzvTQyvrmv8Q3q+jVdTj7FIYCOXN3moZ MvasxJgo6euVesldGA0WzQ0UaIxyBw== -notify-and-xfr-tsig.test. 5 IN NSEC NEZU.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY -NEZU.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. kNDs5mxnK6tKyDu1fBIAg1SnynqRQM7afK+hmPkLJD0SkvKEItomMbui gKlgDdtA7YWA8zTh4j7bwUq/tBlYAQ== -NEZU.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Etgfp3BL11ReT8j0UHQ5epj6l+mAH2+1Fmv/p74EQ3EsbpPiGx3c+96u XeRbWRCVVY/yCqH7E5j0NPHvFYhBAg== -NEZU.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 192.41.197.2 -notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. z7aQTz7VOeeSwwftFsHr+f2BG1k1mVTGztiqZNUAYBkT5R+IlhWpzNdR KvpUSMcdgSNSsjKLQciFq/vYSqg7DA== -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== -notify-and-xfr-tsig.test. 5 IN NSEC JAIN-BB.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY -JAIN-BB.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Kn1CEC2iYr/uSRAZvM9SV2HRR0B8l8ObkvqYufiGO0en+OSLRmfgzviT Mdhdai9onUYr7ndQFHI8bSav5MBoCQ== -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== -JAIN-BB.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 deleted file mode 100644 index 9b3966fe2..000000000 --- a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 +++ /dev/null @@ -1,10 +0,0 @@ -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 -notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 -JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 deleted file mode 100644 index 1808ea302..000000000 --- a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 +++ /dev/null @@ -1 +0,0 @@ -notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/system-tests.yml b/integration-tests/system-tests.yml index b3d27ed86..7e3fb8755 100644 --- a/integration-tests/system-tests.yml +++ b/integration-tests/system-tests.yml @@ -166,46 +166,6 @@ jobs: with: log-level: ${{ inputs.log-level }} - ixfr-out: - name: Serve manual zone edits via IXFR to the NSD secondary. - runs-on: ubuntu-latest - strategy: - matrix: - with-tsig: [true, false] - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/set-build-profile - with: - build-profile: ${{ inputs.build-profile }} - - uses: ./integration-tests/tests/ixfr-out - with: - log-level: ${{ inputs.log-level }} - with-tsig: ${{ matrix.with-tsig }} - - ixfr-out-incremental: - name: Serve incremental signing changes via IXFR to the NSD secondary. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/set-build-profile - with: - build-profile: ${{ inputs.build-profile }} - - uses: ./integration-tests/tests/ixfr-out-incremental - with: - log-level: ${{ inputs.log-level }} - - ixfr-out-review: - name: Serve manual zone edits via IXFR for review. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/set-build-profile - with: - build-profile: ${{ inputs.build-profile }} - - uses: ./integration-tests/tests/ixfr-out-review - with: - log-level: ${{ inputs.log-level }} - incremental-signing: name: Test incremental signing. runs-on: ubuntu-latest From c2fcd2f43c8ec0bf4a170c1a74d75a6ab0d394f8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 13:36:09 +0200 Subject: [PATCH 34/44] Revert "Remove system tests, they will be moved to a separate PR." This reverts commit 0c692c6666e8cc6a4c381c9df750e9bc23d63435. --- .../policies/fast-incremental.toml | 25 +++++++ .../reference-output/loaded-review.ixfr-1-3 | 11 +++ .../reference-output/loaded-review.ixfr-2-3 | 6 ++ .../reference-output/loaded-review.ixfr-3-3 | 1 + .../reference-output/signed-review.ixfr-1-3 | 27 +++++++ .../reference-output/signed-review.ixfr-2-3 | 10 +++ .../reference-output/signed-review.ixfr-3-3 | 1 + .../keys/Kexample.test.+015+00580.key | 1 + .../keys/Kexample.test.+015+00580.private | 3 + .../Knotify-and-xfr-tsig.test.+015+10995.key | 1 + ...otify-and-xfr-tsig.test.+015+10995.private | 3 + .../ixfr-out/policies/with-tsig.toml | 32 ++++++++ .../ixfr-out/policies/without-tsig.toml | 23 ++++++ .../reference-output/example.test.axfr | 17 +++++ .../reference-output/example.test.ixfr-1-3 | 73 +++++++++++++++++++ .../reference-output/example.test.ixfr-2-3 | 10 +++ .../reference-output/example.test.ixfr-3-3 | 1 + .../notify-and-xfr-tsig.test.axfr | 17 +++++ .../notify-and-xfr-tsig.test.ixfr-1-3 | 27 +++++++ .../notify-and-xfr-tsig.test.ixfr-2-3 | 10 +++ .../notify-and-xfr-tsig.test.ixfr-3-3 | 1 + integration-tests/system-tests.yml | 40 ++++++++++ 22 files changed, 340 insertions(+) create mode 100644 integration-tests/ixfr-out-incremental/policies/fast-incremental.toml create mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 create mode 100644 integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 create mode 100644 integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key create mode 100644 integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private create mode 100644 integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key create mode 100644 integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private create mode 100644 integration-tests/ixfr-out/policies/with-tsig.toml create mode 100644 integration-tests/ixfr-out/policies/without-tsig.toml create mode 100644 integration-tests/ixfr-out/reference-output/example.test.axfr create mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 create mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 create mode 100644 integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 create mode 100644 integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 diff --git a/integration-tests/ixfr-out-incremental/policies/fast-incremental.toml b/integration-tests/ixfr-out-incremental/policies/fast-incremental.toml new file mode 100644 index 000000000..156a5cb1f --- /dev/null +++ b/integration-tests/ixfr-out-incremental/policies/fast-incremental.toml @@ -0,0 +1,25 @@ +# Setup incremental signing to produce predictable outputs so that we can +# easily compare the generated zone against expected output. +# +# Enable sending of NOTIFY messages to the NSD secondary. +version = "v1" + +[signer.denial] +type = "nsec" + +[signer] +serial-policy = "counter" +signature-inception-offset = 0 +signature-lifetime = 100 +signature-remain-time = 90 +signature-refresh-interval = 1 + +[key-manager.records] +dnskey.signature-inception-offset = 0 +cds.signature-inception-offset = 0 +dnskey.signature-lifetime = 10 +cds.signature-lifetime = 10 + +[server.outbound] +# NSD secondary listens on port 1054. +send-notify-to = ["127.0.0.1:1054"] diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 new file mode 100644 index 000000000..fe72b7b79 --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-1-3 @@ -0,0 +1,11 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN SOA NS.example.test. mail.example.test. 1 60 60 3600 5 +NEZU.example.test. 3600 IN A 133.69.136.5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 new file mode 100644 index 000000000..aa673c63b --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-2-3 @@ -0,0 +1,6 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 new file mode 100644 index 000000000..ebe4c34bc --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/loaded-review.ixfr-3-3 @@ -0,0 +1 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 new file mode 100644 index 000000000..e9ec16609 --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-1-3 @@ -0,0 +1,27 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN SOA NS.example.test. mail.example.test. 2026051200 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. 0pT73Y+9z9QOkhX9UjRncbR6KyXgSqfxoCviwNhyN8BijAf3oJLVueZR zFNCkNkwg+goMKF9acAmRasdHmA/Dw== +example.test. 5 IN NSEC NEZU.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20200927122640 20200912122640 580 example.test. ijL6SFOFQhPOfSQsTtV9I39aKWkJo6orbMXKcUQ6Woa/Ze+4q5oNK2R7 t8kgL8GTUySY3h3ljlA3SMp+zPjuBg== +NEZU.example.test. 3600 IN A 133.69.136.5 +NEZU.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. eP6CmrELzTgLGgki4TDyqAjNzl535SfnIVNYkjlXV/sDh9iJcNg9QCl8 Na8LR4HRM6RIEe3pw4aZCZg0SqCFDw== +NEZU.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +NEZU.example.test. 5 IN RRSIG NSEC 15 3 5 20200927122640 20200912122640 580 example.test. moXs8yosAlQXN+OQborKKgMp0wBmPVN/9nM0lx61ZK2FTRdXSbRi349q CIuQ6bi2SmOD8pfmgSFRSVOiFTmPDg== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== +example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20200927122640 20200912122640 580 example.test. XUPmDdnhSI7IFMQWMJPewxEN6dqhwj2b/Q/ah74BCvS5GCv4p5Tu2HlY lkDVRE93xZW4mdvb+P8CMcBPLDeBBQ== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== +JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20200927122640 20200912122640 580 example.test. bjeFIBVSXyNxaGnWElV61PhoVo0/C7ztRmk/WLJ4R84tmBbIbSLN1bo0 1EkRvlbHJj/HNKHtUNZkGhDUe8SgAA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. lw2qqnFjRO9k0FmtLHgcJPvzLPQidUMUW8Cx7l03eA6SxVVvfBr3nn5w sIfP4yw09HvKazQl10Xc47SfJNMKCw== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. xTVteJjlrzkL8U1vqPTE7mfOXjUrsnbdOiLOvjVJFeH20wV18HEjnHl8 PWsDtSVhvf9PemqDbjmjlP7HnPIjAA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 new file mode 100644 index 000000000..b3b56e6fc --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-2-3 @@ -0,0 +1,10 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051201 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. h7+N8Sk54Vq/GBLi4TaoLkkjgfXpNHJJ+otwK/0twaKXcywWJEHQKVge R7kBOu89QcWJTY5yUYTdoVCPw7aOCg== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. SzU26WJGMDBev8rx6KcOrUreSX4hVp2jwbTKNWaHxvbzrdd0nDn52dw5 k1HTZsL2+aPuHBxGDYnUJYWlM1ZNDQ== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20200927122640 20200912122640 580 example.test. lw2qqnFjRO9k0FmtLHgcJPvzLPQidUMUW8Cx7l03eA6SxVVvfBr3nn5w sIfP4yw09HvKazQl10Xc47SfJNMKCw== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20200927122640 20200912122640 580 example.test. xTVteJjlrzkL8U1vqPTE7mfOXjUrsnbdOiLOvjVJFeH20wV18HEjnHl8 PWsDtSVhvf9PemqDbjmjlP7HnPIjAA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 new file mode 100644 index 000000000..32a65570b --- /dev/null +++ b/integration-tests/ixfr-out-review/reference-output/signed-review.ixfr-3-3 @@ -0,0 +1 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2026051202 60 60 3600 5 diff --git a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key new file mode 100644 index 000000000..224ce50d5 --- /dev/null +++ b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.key @@ -0,0 +1 @@ +example.test. IN DNSKEY 256 3 15 u7Tg6xf6Xgt0y+yUT8CQi1Nyy+tRopVHnZOFQz0zQ5A= diff --git a/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private new file mode 100644 index 000000000..c6ba81b21 --- /dev/null +++ b/integration-tests/ixfr-out/keys/Kexample.test.+015+00580.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: qCwt/EY9s1vGn0uj+4dlMQ5waOuFfIYpZZVrX43jdcc= diff --git a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key new file mode 100644 index 000000000..9ef332abc --- /dev/null +++ b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.key @@ -0,0 +1 @@ +notify-and-xfr-tsig.test. IN DNSKEY 256 3 15 0efFuDXcYUyhaTUWgir/RaWrzAJWAa58VuEF+391Taw= diff --git a/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private new file mode 100644 index 000000000..b385fd508 --- /dev/null +++ b/integration-tests/ixfr-out/keys/Knotify-and-xfr-tsig.test.+015+10995.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: Nx9EHQ1RBhngQMCRQUxMsiD2ybaWRCiEdcDlW5isKoo= diff --git a/integration-tests/ixfr-out/policies/with-tsig.toml b/integration-tests/ixfr-out/policies/with-tsig.toml new file mode 100644 index 000000000..cc32f479d --- /dev/null +++ b/integration-tests/ixfr-out/policies/with-tsig.toml @@ -0,0 +1,32 @@ +# Setup incremental signing to produce predictable outputs so that we can +# easily compare the generated zone against expected output. +# +# Enable sending of NOTIFY messages using TSIG authentication to the NSD +# secondary. +version = "v1" + +[signer.denial] +type = "nsec" + +[signer] +serial-policy = "keep" +signature-inception-offset = 0 +signature-lifetime = 100000000 + +[key-manager.records] +dnskey.signature-inception-offset = 0 +cds.signature-inception-offset = 0 +dnskey.signature-lifetime = 100000000 +cds.signature-lifetime = 100000000 + +[server.outbound] +# NSD secondary listens on port 1054. +send-notify-to = ["127.0.0.1:1054^tsig-key"] + +# Require that the XFR request from the NSD secondary be signed with the +# specified TSIG key. Without this, a TSIG signed XFR request with a known +# key will still be accepted, but an XFR request that is NOT TSIG signed +# would also be accepted. In the test we verify that TSIG is required by +# Cascade by attempting a dig without the required TSIG key and showing +# that it fails. +accept-xfr-from = ["127.0.0.1^tsig-key"] diff --git a/integration-tests/ixfr-out/policies/without-tsig.toml b/integration-tests/ixfr-out/policies/without-tsig.toml new file mode 100644 index 000000000..07c1d9359 --- /dev/null +++ b/integration-tests/ixfr-out/policies/without-tsig.toml @@ -0,0 +1,23 @@ +# Setup incremental signing to produce predictable outputs so that we can +# easily compare the generated zone against expected output. +# +# Enable sending of NOTIFY messages to the NSD secondary. +version = "v1" + +[signer.denial] +type = "nsec" + +[signer] +serial-policy = "keep" +signature-inception-offset = 0 +signature-lifetime = 100000000 + +[key-manager.records] +dnskey.signature-inception-offset = 0 +cds.signature-inception-offset = 0 +dnskey.signature-lifetime = 100000000 +cds.signature-lifetime = 100000000 + +[server.outbound] +# NSD secondary listens on port 1054. +send-notify-to = ["127.0.0.1:1054"] diff --git a/integration-tests/ixfr-out/reference-output/example.test.axfr b/integration-tests/ixfr-out/reference-output/example.test.axfr new file mode 100644 index 000000000..c9577dd58 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.axfr @@ -0,0 +1,17 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== +example.test. 3600 IN NS NS.example.test. +example.test. 3600 IN RRSIG NS 15 2 3600 20231114221320 20200913122640 580 example.test. hUD4MZwiVtXmHs+VueFIIrTUKYzWfOiNWm2nTR1gtsHTYnGjRrVEH/ic 3XeWDtKK0EedUE5iOKIEfQyYpaTLAg== +example.test. 3600 IN DNSKEY 256 3 15 u7Tg6xf6Xgt0y+yUT8CQi1Nyy+tRopVHnZOFQz0zQ5A= +example.test. 3600 IN RRSIG DNSKEY 15 2 3600 20231114221320 20200913122640 580 example.test. xGC6Pj6iN7tOjmhnLxmw4yQt7CCxCVouXvdB1P3lsQwN0iFfxgamkU7j EqIQcPpl20erYZ4XoWwtOdoeaXwtAg== +example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. P/YK6hlW/FOz43gKHqT4zoxRBdKFinx8GktOFDKuM+CWNyfBkjUZEI04 e3pa5/GUNcLKwRYAyWlNQnONPuusCQ== +NS.example.test. 3600 IN A 133.69.136.1 +NS.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. EsKLrDOszkF8dHi6K8XYed5EbqU9fSlNO2+25pBO/qZk1nZm1RyDO15X KWoCf1jUUNsTR4sZkiu3OGV7oazcAA== +NS.example.test. 5 IN NSEC example.test. A RRSIG NSEC +NS.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. hO9nn4+rxV08CFGkyXYzqGrRYNVOptJXZOaQLp6OSCMtte2iSQCq7UxJ p2KK2Q7aegqYKm6nNFoABmEYjZNlDQ== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== +JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. /S/FZGh/OBQjwaVRPXJChDNkraEeD7qtVvIAa8db4bYmylI4KWOsH991 XDjye2aOv1+qPZxXIfzuX/3ER8JUBA== diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 new file mode 100644 index 000000000..56beb1143 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.ixfr-1-3 @@ -0,0 +1,73 @@ +; The input zone was based on RFC 1995 section 7. This diff should look +; similar to "the following incremental message" from that section, but with +; DNSSEC changes present as well. Our zone name differs to that of the example +; in RFC 1995 as we have to match the zone name configured in our secondary NSD +; instance. + +; === Start of IXFR +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 + +; [DIFF 1]: "NEZU.JAIN.AD.JP. is removed and JAIN-BB.JAIN.AD.JP. is added." +; [DIFF 1]: Records from serial 1 to be removed. + +; Changing the SOA serial requires that we regenerate the RRSIG, so the old +; RRSIG has to be removed. +example.test. 3600 IN SOA NS.example.test. mail.example.test. 1 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. TlLDWFfpydqCQjy1oDPBXugGWraoyvvk83KdTsiq441t0/hYKmL2mFEH 2nwJ6fcuZT2hLpvl9gwk3bJCXrNdAQ== + +; Remove the A record and its associated RRSIG. +NEZU.example.test. 3600 IN A 133.69.136.5 +NEZU.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. k232/sABO752SeaFp3jHkIJMUGlDA0NG+iQh1gpGVsi07XqDce+JzNX4 NiWmz06kVu+SKu1HFHNvGJ3enh5/DQ== + +; Remove the A record from the NSEC type bitmap too, and also remove NSEC +; RRSIG as a new one will be added to replace it. +NEZU.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. BCpvhGfe+GW4GnyJBBiqjMpiVZhCXv9ZMdh+RF7qf07V3jIePWJThAy2 yV5O4I7NhKgl6rqpOyv2+aSoW8tVDw== +NEZU.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC + +; As the entire NEZU.example.test. label was removed this also requires the +; next owner name in the NSEC chain to be changed so we have to remove the +; current NSEC on the previous owner and its associated RRSIG, new ones will +; be added. +example.test. 5 IN NSEC NEZU.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. Hyrs5CKpXfCCNC9OK9+07Ei5qhRjQ9EQZK3up84BCwlflhOCGQppddzM eHK0Klgl0ywKaD7dfV2w3SWvP7vTDA== + +; [DIFF 1]: Records to be added for serial 2. + +; The new SOA with the updated SERIAL and its associated RRSIG. +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== + +; A changed NSEC and associated RRSIG due to changes in the NSEC chain. +example.test. 5 IN NSEC JAIN-BB.example.test. NS SOA RRSIG NSEC DNSKEY +example.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 580 example.test. P/YK6hlW/FOz43gKHqT4zoxRBdKFinx8GktOFDKuM+CWNyfBkjUZEI04 e3pa5/GUNcLKwRYAyWlNQnONPuusCQ== + +; Add new label JAIN-BB.example.test with two A records, associated NSEC and RRSIGs. +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN A 192.41.197.2 +JAIN-BB.example.test. 5 IN NSEC NS.example.test. A RRSIG NSEC +JAIN-BB.example.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 580 example.test. /S/FZGh/OBQjwaVRPXJChDNkraEeD7qtVvIAa8db4bYmylI4KWOsH991 XDjye2aOv1+qPZxXIfzuX/3ER8JUBA== +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== + +; [DIFF 2]: "One of the IP addresses of JAIN-BB.JAIN.AD.JP. is changed." +; [DIFF 2]: Records from serial 2 to be removed. + +; The old SOA and associated RRSIG. +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== + +; The old A record and associated RRSIG. +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== + +; [DIFF 2]: Records to be added for serial 3. + +; The new SOA and associated RRSIG. +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== + +; The updated A record and associated RRSIG. +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== + +; End of IXFR +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 new file mode 100644 index 000000000..56f9a7289 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.ixfr-2-3 @@ -0,0 +1,10 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN SOA ns.example.test. mail.example.test. 2 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. fYRpxmxV5HQl7wIPmuN+lrYQJalFuxi9iI0DX2/mM4kdh7q9GGutFTOZ kbdY4D+AZf25s08CZR1CPpd9o2Q3Dg== +JAIN-BB.example.test. 3600 IN A 133.69.136.4 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. XLFJlxjJDzmdwqF2BgxWN7Swfty8HD3ILb6Mo7fHKrfYt6x3uxXIUIjO JFVwFBhavLTrOlOcXdBGoNTvceqTDg== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 +example.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 580 example.test. iVSTY7uTaal9mRd44phalz9+oHlQtNDGPrQb4rvx3SkdTMT21l9PDCVL BxomZOiC51WhuQX+8FxSiLQcN+sfAw== +JAIN-BB.example.test. 3600 IN A 133.69.136.3 +JAIN-BB.example.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 580 example.test. gPZnGC1IeAz1VtEvYEV9jMLw7kFhwPKX9kMtH32/xC7zyYNOfmcWqffZ xbobEY8c/oS9z4WIiK8k4uFi90rQCA== +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 b/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 new file mode 100644 index 000000000..ebe4c34bc --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/example.test.ixfr-3-3 @@ -0,0 +1 @@ +example.test. 3600 IN SOA ns.example.test. mail.example.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr new file mode 100644 index 000000000..08e54a056 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.axfr @@ -0,0 +1,17 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== +notify-and-xfr-tsig.test. 3600 IN NS NS.notify-and-xfr-tsig.test. +notify-and-xfr-tsig.test. 3600 IN RRSIG NS 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. YDqsJSHx81jK/GSqDdnNDG3JN9v9SH6jgmsgbQNElcaSCXqMrR2Q3QFq wrYW06FgnadRgTySRZ2evzS3dYGaDw== +notify-and-xfr-tsig.test. 3600 IN DNSKEY 256 3 15 0efFuDXcYUyhaTUWgir/RaWrzAJWAa58VuEF+391Taw= +notify-and-xfr-tsig.test. 3600 IN RRSIG DNSKEY 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. CIiRWX2DYaZRidc0nDl4zE0vmUCK2XFX2ekrJc1DVljIestEaJBAR2V1 ZQYOtCtb8y8AA/RCH8UFN2DYSM6+CQ== +notify-and-xfr-tsig.test. 5 IN NSEC JAIN-BB.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY +notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. z7aQTz7VOeeSwwftFsHr+f2BG1k1mVTGztiqZNUAYBkT5R+IlhWpzNdR KvpUSMcdgSNSsjKLQciFq/vYSqg7DA== +NS.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.1 +NS.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. ziPT0QEZ/gGcVltE17tqgf3jqo9SfesIffux+pIZwC3vO8eg1MnzPJOv Kmdl9vJQqS03FjwT+6r3xtjIVIfwAA== +NS.notify-and-xfr-tsig.test. 5 IN NSEC notify-and-xfr-tsig.test. A RRSIG NSEC +NS.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. nLNfo5CuJQd8qYbDywdefe0pFQ881jn3W6WEfNnkMg1daJk2ENcZUkiJ aeR7Hv7FPzTyavE2SQbuHfHrYDKoAA== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 192.41.197.2 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== +JAIN-BB.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC +JAIN-BB.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Kn1CEC2iYr/uSRAZvM9SV2HRR0B8l8ObkvqYufiGO0en+OSLRmfgzviT Mdhdai9onUYr7ndQFHI8bSav5MBoCQ== diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 new file mode 100644 index 000000000..33df5ea3e --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-1-3 @@ -0,0 +1,27 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN SOA NS.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 1 60 60 3600 5 +NEZU.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.5 +notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0BtPWx9VOtcE1BMD3cLk1FR1YaT/1v32VuuxXh/XQgS97Nj80H+Qm57V ORpk/V6wkw2exuvz3ko2s2BfkaQqAQ== +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. xzwl8jSWvas1L2Uqzfq8kGkPZzvTQyvrmv8Q3q+jVdTj7FIYCOXN3moZ MvasxJgo6euVesldGA0WzQ0UaIxyBw== +notify-and-xfr-tsig.test. 5 IN NSEC NEZU.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY +NEZU.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. kNDs5mxnK6tKyDu1fBIAg1SnynqRQM7afK+hmPkLJD0SkvKEItomMbui gKlgDdtA7YWA8zTh4j7bwUq/tBlYAQ== +NEZU.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Etgfp3BL11ReT8j0UHQ5epj6l+mAH2+1Fmv/p74EQ3EsbpPiGx3c+96u XeRbWRCVVY/yCqH7E5j0NPHvFYhBAg== +NEZU.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 192.41.197.2 +notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 2 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. z7aQTz7VOeeSwwftFsHr+f2BG1k1mVTGztiqZNUAYBkT5R+IlhWpzNdR KvpUSMcdgSNSsjKLQciFq/vYSqg7DA== +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== +notify-and-xfr-tsig.test. 5 IN NSEC JAIN-BB.notify-and-xfr-tsig.test. NS SOA RRSIG NSEC DNSKEY +JAIN-BB.notify-and-xfr-tsig.test. 5 IN RRSIG NSEC 15 3 5 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. Kn1CEC2iYr/uSRAZvM9SV2HRR0B8l8ObkvqYufiGO0en+OSLRmfgzviT Mdhdai9onUYr7ndQFHI8bSav5MBoCQ== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== +JAIN-BB.notify-and-xfr-tsig.test. 5 IN NSEC NS.notify-and-xfr-tsig.test. A RRSIG NSEC +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 new file mode 100644 index 000000000..9b3966fe2 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-2-3 @@ -0,0 +1,10 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 2 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0Qn9RoFrnZrhs8qy/GFWrSQ0ZUx11s60vVAzhBR0MhX9k8731t6Is5ic VwajxP8l3NM1PSvB0Eqamo0cyy6rCA== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.4 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. SRcvE4FT1NE9uVZrrFv1f+xinA37/CZWac6bJ62REhIE2e90rbDr1i/e n31YVDZQOHeVTKZCuueVKXgPYyztCw== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 +notify-and-xfr-tsig.test. 3600 IN RRSIG SOA 15 2 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. caF70ZxIDm7UIXcj4RmkY5G+rcNFJp+wKpg17pUEz+N4P8+uxp3tYjYj 145UTqd36AUv4jsJgqEVN9roaZAbAw== +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN A 133.69.136.3 +JAIN-BB.notify-and-xfr-tsig.test. 3600 IN RRSIG A 15 3 3600 20231114221320 20200913122640 10995 notify-and-xfr-tsig.test. 0X5PtAfOES8cyYc9kFoXfFB5w5AHBtTSlw8V2wd+UehtSgHMDX0ebUig C65dQahRoe6oELz7UNY7rv+BLOMrAA== +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 new file mode 100644 index 000000000..1808ea302 --- /dev/null +++ b/integration-tests/ixfr-out/reference-output/notify-and-xfr-tsig.test.ixfr-3-3 @@ -0,0 +1 @@ +notify-and-xfr-tsig.test. 3600 IN SOA ns.notify-and-xfr-tsig.test. mail.notify-and-xfr-tsig.test. 3 60 60 3600 5 diff --git a/integration-tests/system-tests.yml b/integration-tests/system-tests.yml index 7e3fb8755..b3d27ed86 100644 --- a/integration-tests/system-tests.yml +++ b/integration-tests/system-tests.yml @@ -166,6 +166,46 @@ jobs: with: log-level: ${{ inputs.log-level }} + ixfr-out: + name: Serve manual zone edits via IXFR to the NSD secondary. + runs-on: ubuntu-latest + strategy: + matrix: + with-tsig: [true, false] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/set-build-profile + with: + build-profile: ${{ inputs.build-profile }} + - uses: ./integration-tests/tests/ixfr-out + with: + log-level: ${{ inputs.log-level }} + with-tsig: ${{ matrix.with-tsig }} + + ixfr-out-incremental: + name: Serve incremental signing changes via IXFR to the NSD secondary. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/set-build-profile + with: + build-profile: ${{ inputs.build-profile }} + - uses: ./integration-tests/tests/ixfr-out-incremental + with: + log-level: ${{ inputs.log-level }} + + ixfr-out-review: + name: Serve manual zone edits via IXFR for review. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/set-build-profile + with: + build-profile: ${{ inputs.build-profile }} + - uses: ./integration-tests/tests/ixfr-out-review + with: + log-level: ${{ inputs.log-level }} + incremental-signing: name: Test incremental signing. runs-on: ubuntu-latest From 394b53477d5d5bff0f8142e29ec165c0a4bf2e18 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 13:45:50 +0200 Subject: [PATCH 35/44] Review feedback: Return SERVFAIL instead of NOTAUTH for empty zone. And log an error. --- src/server/service.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index 2c99791ed..f61775977 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -86,7 +86,7 @@ mod compat { tsig, }; use futures::Stream; - use tracing::{Level, debug, trace}; + use tracing::{Level, debug, trace, warn}; use crate::server::request::{RequestKind, ZoneRequestKind}; @@ -270,7 +270,12 @@ mod compat { if viewer.is_empty() { // The zone is known to exist, but we don't have any data for it. - return error(request.message(), Rcode::NOTAUTH); + warn!( + "Returning SERVFAIL for AXFR request from client '{}' for zone '{}': zone is empty", + request.client_addr().ip(), + zone.handle.name, + ); + return error(request.message(), Rcode::SERVFAIL); } // NOTE: The following code is a bit tricky. Ideally, we would elide @@ -357,7 +362,12 @@ mod compat { if viewer.is_empty() { // The zone is known to exist, but we don't have any data for it. - return error(request.message(), Rcode::NOTAUTH); + warn!( + "Returning SERVFAIL for AXFR request from client '{}' for zone '{}': zone is empty", + request.client_addr().ip(), + zone.handle.name, + ); + return error(request.message(), Rcode::SERVFAIL); } // UDP is unlikely to work for any but the smallest of diffs, From 1f47a286dd7510074b7cd2a735f308d33f211182 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 13:59:21 +0200 Subject: [PATCH 36/44] Really remove the system tests. --- .../tests/ixfr-out-incremental/action.yml | 162 --------- .../tests/ixfr-out-review/action.yml | 239 ------------- integration-tests/tests/ixfr-out/action.yml | 324 ------------------ 3 files changed, 725 deletions(-) delete mode 100644 integration-tests/tests/ixfr-out-incremental/action.yml delete mode 100644 integration-tests/tests/ixfr-out-review/action.yml delete mode 100644 integration-tests/tests/ixfr-out/action.yml diff --git a/integration-tests/tests/ixfr-out-incremental/action.yml b/integration-tests/tests/ixfr-out-incremental/action.yml deleted file mode 100644 index 47fa37e4b..000000000 --- a/integration-tests/tests/ixfr-out-incremental/action.yml +++ /dev/null @@ -1,162 +0,0 @@ -# Making reusable composite actions documented at -# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository -name: 'Serve incremental signing changes via IXFR to the NSD secondary.' -description: 'Serve a zone via IXFR to the NSD secondary.' -defaults: - # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell - run: - shell: bash --noprofile --norc -eo pipefail -x {0} -inputs: - log-level: - description: The level of logging that Cascade should output. - required: false - default: debug - type: choice - options: - - error - - warning - - info - - debug - - trace -runs: - using: "composite" - steps: - - uses: ./.github/actions/prepare-systest-env - - uses: ./.github/actions/setup-and-start-cascade - with: - log-level: ${{ inputs.log-level }} - - - name: Determine the downstream secondary zone config to use - id: set-vars - run: | - ZONE_NAME="example.test." - POLICY_NAME="fast-incremental" - KEY_NAME="Kexample.test.+015+00580.key" - echo "ZONE_NAME=${ZONE_NAME}" >> "$GITHUB_OUTPUT" - echo "POLICY_NAME=${POLICY_NAME}" >> "$GITHUB_OUTPUT" - echo "KEY_NAME=${KEY_NAME}" >> "$GITHUB_OUTPUT" - - - name: Add policy ${{ steps.set-vars.outputs.POLICY_NAME }} and reload - run: | - POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) - TEST_DIR="${PWD}/integration-tests/ixfr-out-incremental" - cp "${TEST_DIR}/policies/${{ steps.set-vars.outputs.POLICY_NAME }}.toml" "${POLICY_DIR}/" - cascade policy reload - - # The zone name has to match a zone configured in the downstream NSD - # nameserver. Zone 'notify-and-xfr-tsig.test' is a zone in the downstream - # NSD nameserver that is configured to expect TSIG to be used for the - # NOTIFY message it is sent, to use IXFR (as opposed to only AXFR) and for - # it to need to use TSIG to request an IXFR transfer from Cascade. - - name: Make a ${{ steps.set-vars.outputs.ZONE_NAME }} zone based on RFC 1995 section 7 - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status ${ZONE_NAME} - echo "timeout: zone status did not report published zone available" >&2 - exit 1 - fi - sleep 1 - done - - - name: Expect serial 1 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 1 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status ${ZONE_NAME} - dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA - echo "::error:: timeout: NSD did not acquire the zone changes" - exit 1 - fi - sleep 1 - done - - - name: Expect serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 2 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - dig @127.0.0.1 -p 1054 ${ZONE_NAME} SOA - cascade zone status ${ZONE_NAME} - echo "::error:: timeout: NSD did not acquire the zone changes" - exit 1 - fi - sleep 1 - done - - - name: Expect serial 3 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status ${ZONE_NAME} - dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA - echo "::error:: timeout: NSD did not acquire the zone changes" - exit 1 - fi - sleep 1 - done - - - name: Capture Cascade AXFR out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} AXFR &> cascade-axfr.log - - - name: Compare NSD AXFR out to Cascade AXFR out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${ZONE_NAME} AXFR &> nsd-axfr.log - - # Pass -f to sort and -i to diff to normalize case in the Cascade and - # NSD output being compared, as NSD outputs lower case labels while - # Cascade preserves label case. -f tells sort not to order differently - # based on case, and -i tells diff to consider differing case to be - # equal. - - LC_COLLATE=C sort -f cascade-axfr.log > cascade-axfr.sorted - LC_COLLATE=C sort -f nsd-axfr.log > nsd-axfr.sorted - diff -u -w -i cascade-axfr.sorted nsd-axfr.sorted - - - name: Print log files on any failure in this job - uses: ./.github/actions/print-logfiles - if: failure() diff --git a/integration-tests/tests/ixfr-out-review/action.yml b/integration-tests/tests/ixfr-out-review/action.yml deleted file mode 100644 index 9b5d71caf..000000000 --- a/integration-tests/tests/ixfr-out-review/action.yml +++ /dev/null @@ -1,239 +0,0 @@ -# Making reusable composite actions documented at -# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository -name: 'Serve manual zone modifications via IXFR for review.' -description: 'Serve a zone via IXFR to the NSD secondary.' -defaults: - # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell - run: - shell: bash --noprofile --norc -eo pipefail -x {0} -inputs: - log-level: - description: The level of logging that Cascade should output. - required: false - default: debug - type: choice - options: - - error - - warning - - info - - debug - - trace -runs: - using: "composite" - steps: - - uses: ./.github/actions/setup-and-start-cascade - with: - log-level: ${{ inputs.log-level }} - fake-time: 1600000000 - - - name: Determine expected published serial number - id: set-vars - run: | - EXPECTED_SERIAL="$(date +'%Y%m%d00')" - echo "EXPECTED_SERIAL=${EXPECTED_SERIAL}" >>"$GITHUB_OUTPUT" - - - name: Make a zone based on RFC 1995 section 7 - run: | - cat >"example.test.zone" < (start + timeout))); then - cascade zone status example.test - echo "timeout: zone status did not report published zone available" >&2 - exit 1 - fi - sleep 1 - done - - - name: Expect serial 1 of the zone at the loaded review server - run: | - timeout=3 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 4540 example.test SOA | grep -qi "ns.example.test. mail.example.test. 1 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 4540 example.test SOA - echo "::error:: timeout: loaded review server is not serving serial 1 of the zone" - exit 1 - fi - sleep 1 - done - - - name: Expect serial 1 of the zone at the signed review server - env: - EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 4541 example.test SOA - echo "::error:: timeout: signed review server is not serving serial 1 of the zone" - exit 1 - fi - sleep 1 - done - - - name: Edit and reload serial 2 of the zone - run: | - cat >"example.test.zone" < (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 4540 example.test SOA - echo "::error:: timeout: loaded review server is not serving serial 2 of the zone" - exit 1 - fi - sleep 1 - done - - - name: Expect serial 2 of the zone at the signed review server - env: - EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} - run: | - timeout=10 # seconds - start=$(date +%s) - let EXPECTED_SERIAL=$(( EXPECTED_SERIAL + 1 )) - until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 4541 example.test SOA - echo "::error:: timeout: signed review server is not serving serial 2 of the zone" - exit 1 - fi - sleep 1 - done - - - name: Edit and reload serial 3 of the zone - run: | - cat >"example.test.zone" < (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 4540 example.test SOA - echo "::error:: timeout: loaded review server is not serving serial 3 of the zone" - exit 1 - fi - sleep 1 - done - - - name: Expect serial 3 of the zone at the signed review server - env: - EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} - run: | - timeout=10 # seconds - start=$(date +%s) - let EXPECTED_SERIAL=$(( EXPECTED_SERIAL + 2 )) - until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status example.test - dig +short @127.0.0.1 -p 4541 example.test SOA - echo "::error:: timeout: signed review server is not serving serial 3 of the zone" - exit 1 - fi - sleep 1 - done - - - name: Verify IXFR of serial 1..=3 at the loaded review server - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=1 &> loaded-review-ixfr-1-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out-review" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort loaded-review-ixfr-1-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify IXFR of serial 2..=3 at the loaded review server - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=2 &> loaded-review-ixfr-2-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out-review" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort loaded-review-ixfr-2-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify IXFR of serial 3..=3 at the loaded review server - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=3 &> loaded-review-ixfr-3-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out-review" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort loaded-review-ixfr-3-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify IXFR of serial 1..=3 at the signed review server - env: - EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=${EXPECTED_SERIAL} &> signed-review-ixfr-1-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out-review" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort signed-review-ixfr-1-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify IXFR of serial 2..=3 at the signed review server - env: - EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=$(( EXPECTED_SERIAL + 1 )) &> signed-review-ixfr-2-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out-review" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort signed-review-ixfr-2-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify IXFR of serial 3..=3 at the signed review server - env: - EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=$(( EXPECTED_SERIAL + 2 )) &> signed-review-ixfr-3-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out-review" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort signed-review-ixfr-3-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Print log files on any failure in this job - uses: ./.github/actions/print-logfiles - if: failure() diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml deleted file mode 100644 index 0cdab4edf..000000000 --- a/integration-tests/tests/ixfr-out/action.yml +++ /dev/null @@ -1,324 +0,0 @@ -# Making reusable composite actions documented at -# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository -name: 'Serve manual zone modifications via IXFR to the NSD secondary.' -description: 'Serve a zone via IXFR to the NSD secondary.' -defaults: - # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell - run: - shell: bash --noprofile --norc -eo pipefail -x {0} -inputs: - log-level: - description: The level of logging that Cascade should output. - required: false - default: debug - type: choice - options: - - error - - warning - - info - - debug - - trace - with-tsig: - description: Whether or not TSIG should be required for XFR out. - required: true - type: boolean -runs: - using: "composite" - steps: - - uses: ./.github/actions/prepare-systest-env - - uses: ./.github/actions/setup-and-start-cascade - with: - log-level: ${{ inputs.log-level }} - fake-time: 1600000000 - - - name: Determine the downstream secondary zone config to use - id: set-vars - run: | - if [[ "${{ inputs.with-tsig }}" == "true" ]]; then - ZONE_NAME="notify-and-xfr-tsig.test." - POLICY_NAME="with-tsig" - KEY_NAME="Knotify-and-xfr-tsig.test.+015+10995.key" - else - ZONE_NAME="example.test." - POLICY_NAME="without-tsig" - KEY_NAME="Kexample.test.+015+00580.key" - fi - echo "ZONE_NAME=${ZONE_NAME}" >> "$GITHUB_OUTPUT" - echo "POLICY_NAME=${POLICY_NAME}" >> "$GITHUB_OUTPUT" - echo "KEY_NAME=${KEY_NAME}" >> "$GITHUB_OUTPUT" - echo "TSIG_SPEC=hmac-sha256:tsig-key:COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" >> "$GITHUB_OUTPUT" - - - name: Add the TSIG key - if: ${{ inputs.with-tsig }} - run: | - cascade tsig add ${{ steps.set-vars.outputs.TSIG_SPEC }} - - - name: Add policy ${{ steps.set-vars.outputs.POLICY_NAME }} and reload - run: | - POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) - TEST_DIR="${PWD}/integration-tests/ixfr-out" - cp "${TEST_DIR}/policies/${{ steps.set-vars.outputs.POLICY_NAME }}.toml" "${POLICY_DIR}/" - cascade policy reload - - # The zone name has to match a zone configured in the downstream NSD - # nameserver. Zone 'notify-and-xfr-tsig.test' is a zone in the downstream - # NSD nameserver that is configured to expect TSIG to be used for the - # NOTIFY message it is sent, to use IXFR (as opposed to only AXFR) and for - # it to need to use TSIG to request an IXFR transfer from Cascade. - - name: Make a ${{ steps.set-vars.outputs.ZONE_NAME }} zone based on RFC 1995 section 7 - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status ${ZONE_NAME} - echo "timeout: zone status did not report published zone available" >&2 - exit 1 - fi - sleep 1 - done - - - name: Expect serial 1 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 1 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status ${ZONE_NAME} - dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA - echo "::error:: timeout: NSD did not acquire the zone changes" - exit 1 - fi - sleep 1 - done - - - name: Edit and reload serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status ${ZONE_NAME} - echo "timeout: zone status did not report published zone available" >&2 - exit 1 - fi - sleep 1 - done - - - name: Expect serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 2 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - dig @127.0.0.1 -p 1054 ${ZONE_NAME} SOA - cascade zone status ${ZONE_NAME} - echo "::error:: timeout: NSD did not acquire the zone changes" - exit 1 - fi - sleep 1 - done - - - name: Edit and reload serial 3 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - cat >"${ZONE_NAME}zone" < (start + timeout))); then - cascade zone status ${ZONE_NAME} - echo "timeout: zone status did not report published zone available" >&2 - exit 1 - fi - sleep 1 - done - - - name: Expect serial 3 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - timeout=10 # seconds - start=$(date +%s) - until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5"; do - if (($(date +%s) > (start + timeout))); then - cascade zone status ${ZONE_NAME} - dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA - echo "::error:: timeout: NSD did not acquire the zone changes" - exit 1 - fi - sleep 1 - done - - # Note: There is no NSD metric that I am aware of that we can use to - # verify that it received IXFR from Cascade instead of AXFR, nor is there - # any Cascade Prometheus metric or CLI command that we can use to check - # this either. Increasing the NSD log level to 9 doesn't cause it to - # report whether it received IXFR or AXFR either. - - - name: Verify that TSIG is required by Cascade - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - if [[ "${{ inputs.with-tsig }}" == "false" ]]; then - exit 0 - fi - - # This should not succeed as it lacks the required TSIG key. - # Note that dig will exit with code 0 even if the request fails, - # because it considers an error response still to be successful - # receipt of a response, so we have to check the actual output. - dig @127.0.0.1 +yaml -p 4542 ${ZONE_NAME} AXFR &> dig-no-tsig.log - grep -Fq "status: REFUSED" dig-no-tsig.log - - - name: Verify Cascade AXFR out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - EXTRA_ARGS= - if [[ "${{ inputs.with-tsig }}" == "true" ]]; then - EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" - fi - dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${EXTRA_ARGS} ${ZONE_NAME} AXFR &> cascade-axfr.log - TEST_DIR="${PWD}/integration-tests/ixfr-out" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort cascade-axfr.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - # A UDP IXFR request should result in a single SOA response "to inform the - # client that a TCP query should be initiated". - - name: Verify that UDP is not supported - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - EXTRA_ARGS= - if [[ "${{ inputs.with-tsig }}" == "true" ]]; then - EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" - fi - SOA_RR=$(dig +short +notcp +ignore @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=1 ${EXTRA_ARGS} 2>&1 | tee dig-udp-ixfr.log) - if [[ "${SOA_RR}" != "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5" ]]; then - exit 1 - fi - - - name: Verify Cascade IXFR serial 1..=3 out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - EXTRA_ARGS= - if [[ "${{ inputs.with-tsig }}" == "true" ]]; then - EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" - fi - dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=1 ${EXTRA_ARGS} &> cascade-ixfr-1-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort cascade-ixfr-1-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify Cascade IXFR serial 2..=3 out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - EXTRA_ARGS= - if [[ "${{ inputs.with-tsig }}" == "true" ]]; then - EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" - fi - dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=2 ${EXTRA_ARGS} &> cascade-ixfr-2-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort cascade-ixfr-2-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify Cascade IXFR serial 3..=3 out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - EXTRA_ARGS= - if [[ "${{ inputs.with-tsig }}" == "true" ]]; then - EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" - fi - dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=3 ${EXTRA_ARGS} &> cascade-ixfr-3-3.log - TEST_DIR="${PWD}/integration-tests/ixfr-out" - LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort cascade-ixfr-3-3.log > output.sorted - diff -u -w output.sorted reference.output.sorted - - - name: Verify NSD AXFR out - env: - ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} - run: | - dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${ZONE_NAME} AXFR &> nsd-axfr.log - TEST_DIR="${PWD}/integration-tests/ixfr-out" - - # Pass -f to sort and -i to diff to normalize case in the Cascade and - # NSD output being compared, as NSD outputs lower case labels while - # Cascade preserves label case. -f tells sort not to order differently - # based on case, and -i tells diff to consider differing case to be - # equal. - - LC_COLLATE=C sort -f "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted - LC_COLLATE=C sort -f nsd-axfr.log > output.sorted - diff -u -w -i output.sorted reference.output.sorted - - - name: Print log files on any failure in this job - uses: ./.github/actions/print-logfiles - if: failure() From cb936e5ccfab828dd8d9f9d635207795d7819cfa Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 14:01:59 +0200 Subject: [PATCH 37/44] Remove change related to now removed system tests. --- integration-tests/scripts/manage-test-environment.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/integration-tests/scripts/manage-test-environment.sh b/integration-tests/scripts/manage-test-environment.sh index 32180fc8c..cca593f46 100755 --- a/integration-tests/scripts/manage-test-environment.sh +++ b/integration-tests/scripts/manage-test-environment.sh @@ -298,7 +298,8 @@ pattern: name: secondary zonefile: "%s.secondary-zone" allow-notify: 127.0.0.1 NOKEY - request-xfr: 127.0.0.1@${_cascade_port} NOKEY + # Until Cascade supports IXFr we always use AXFR + request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY zone: @@ -309,21 +310,21 @@ zone: name: notify-tsig.test zonefile: "notify-tsig.test.secondary-zone" allow-notify: 127.0.0.1 tsig-key - request-xfr: 127.0.0.1@${_cascade_port} NOKEY + request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY zone: name: xfr-tsig.test zonefile: "xfr-tsig.test.secondary-zone" allow-notify: 127.0.0.1 NOKEY - request-xfr: 127.0.0.1@${_cascade_port} tsig-key + request-xfr: AXFR 127.0.0.1@${_cascade_port} tsig-key provide-xfr: 127.0.0.1 NOKEY zone: name: notify-and-xfr-tsig.test zonefile: "notify-and-xfr-tsig.test.secondary-zone" allow-notify: 127.0.0.1 tsig-key - request-xfr: 127.0.0.1@${_cascade_port} tsig-key + request-xfr: AXFR 127.0.0.1@${_cascade_port} tsig-key provide-xfr: 127.0.0.1 NOKEY EOF From 719574555cae287663ff23f79e979710a0349244 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 14:02:28 +0200 Subject: [PATCH 38/44] I can't type... --- integration-tests/scripts/manage-test-environment.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/scripts/manage-test-environment.sh b/integration-tests/scripts/manage-test-environment.sh index cca593f46..349ce5d69 100755 --- a/integration-tests/scripts/manage-test-environment.sh +++ b/integration-tests/scripts/manage-test-environment.sh @@ -298,7 +298,7 @@ pattern: name: secondary zonefile: "%s.secondary-zone" allow-notify: 127.0.0.1 NOKEY - # Until Cascade supports IXFr we always use AXFR + # Until Cascade supports IXFR we always use AXFR request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY From 6f5f566dd64f9b81bfc9b9326c15ca93bb68e312 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 14:25:23 +0200 Subject: [PATCH 39/44] Review feedback: remove unnecessary step / temporary variable. --- src/server/service.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index f61775977..3093afb01 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -102,8 +102,7 @@ mod compat { fn call(&self, old_request: Request, Option>>) -> Response { // Parse the request. - let slice = old_request.message().as_slice(); - let message = slice; + let message = old_request.message().as_slice(); let message = domain::new::base::Message::parse_bytes_by_ref(message) .expect("'message' was already checked to be a valid DNS message"); let request = match crate::server::request::parse(message) { From 10b717df71c9118f612f8c7e0fe1aeae20ecf1cd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 14:50:56 +0200 Subject: [PATCH 40/44] Enforce ACLs only on the publication server. (fixes #648) Not on the review servers. --- src/server/mod.rs | 6 ++--- src/server/service.rs | 60 ++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/server/mod.rs b/src/server/mod.rs index 0bc99fc64..94c1e41d1 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -34,7 +34,7 @@ pub struct LoadedReviewServer { impl LoadedReviewServer { /// Construct a new [`LoadedReviewServer`]. pub fn new() -> Self { - let (service, handle) = ZoneService::new(); + let (service, handle) = ZoneService::new(service::ServiceMode::LoadedReview); Self { service, handle } } @@ -123,7 +123,7 @@ pub struct SignedReviewServer { impl SignedReviewServer { /// Construct a new [`SignedReviewServer`]. pub fn new() -> Self { - let (service, handle) = ZoneService::new(); + let (service, handle) = ZoneService::new(service::ServiceMode::SignedReview); Self { service, handle } } @@ -212,7 +212,7 @@ pub struct PublicationServer { impl PublicationServer { /// Construct a new [`PublicationServer`]. pub fn new() -> Self { - let (service, handle) = ZoneService::new(); + let (service, handle) = ZoneService::new(service::ServiceMode::Publication); Self { service, handle } } diff --git a/src/server/service.rs b/src/server/service.rs index 3093afb01..64c259ae4 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -39,6 +39,16 @@ pub struct ZoneService { // of the limitations of 'domain::net::server'; that architecture should be // gradually replaced locally for better flexibility and efficiency. state: Arc>>, + + /// What mode of operation is intended? + mode: ServiceMode, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ServiceMode { + LoadedReview, + SignedReview, + Publication, } impl ZoneService { @@ -46,10 +56,11 @@ impl ZoneService { /// /// In addition to the service, a [`ZoneServiceHandle`] is returned through /// which the service can be interacted with. - pub fn new() -> (ZoneService, ZoneServiceHandle) { + pub fn new(server_mode: ServiceMode) -> (ZoneService, ZoneServiceHandle) { let state = Arc::new(std::sync::RwLock::default()); let service = ZoneService { state: state.clone(), + mode: server_mode, }; let handle = ZoneServiceHandle { state }; (service, handle) @@ -60,6 +71,7 @@ impl Clone for ZoneService { fn clone(&self) -> Self { Self { state: self.state.clone(), + mode: self.mode, } } } @@ -88,7 +100,10 @@ mod compat { use futures::Stream; use tracing::{Level, debug, trace, warn}; - use crate::server::request::{RequestKind, ZoneRequestKind}; + use crate::server::{ + request::{RequestKind, ZoneRequestKind}, + service::ServiceMode, + }; use super::{ServedZone, Viewer, ZoneService}; @@ -132,7 +147,7 @@ mod compat { return Box::pin(std::future::ready(error(old_request.message(), rcode))); }; - if !is_permitted(zone, &old_request) { + if self.mode == ServiceMode::Publication && !is_permitted(zone, &old_request) { return Box::pin(std::future::ready(error( old_request.message(), Rcode::REFUSED, @@ -153,7 +168,8 @@ mod compat { } ZoneRequestKind::Ixfr { known_soa } => { - Box::pin(ixfr(old_request, known_soa.rdata, zone.clone())) as Response + Box::pin(ixfr(old_request, known_soa.rdata, zone.clone(), self.mode)) + as Response } } } @@ -352,6 +368,7 @@ mod compat { request: Request, Option>>, client_soa: Soa>, zone: ServedZone, + mode: ServiceMode, ) -> ResponseStream { // Save a cheap clone of the zone to avoid a borrow checker error. let zone_clone = zone.clone(); @@ -468,7 +485,7 @@ mod compat { // SOA serial. let start_idx = { diffs.iter().position(|(loaded_diff, signed_diff)| { - let d = if viewer.is_loaded_viewer() { + let d = if mode == ServiceMode::LoadedReview { loaded_diff } else { signed_diff @@ -522,7 +539,7 @@ mod compat { let abs_idx = start_idx + i; // Select the appropriate diff as the SOA source to use. - let soa_source_diff = if viewer.is_loaded_viewer() { + let soa_source_diff = if mode == ServiceMode::LoadedReview { loaded_diff } else { signed_diff @@ -550,7 +567,7 @@ mod compat { // again which can happen if the loaded zone remained // unchanged but incremental signing caused changes in the // signed part only to occur. - if viewer.is_loaded_viewer() + if mode == ServiceMode::LoadedReview && let Some(last_removed_soa) = last_removed_soa && removed_soa == last_removed_soa { @@ -571,7 +588,7 @@ mod compat { trace!("Serving diff #{abs_idx} for loaded review server IXFR out",); - if viewer.is_loaded_viewer() { + if mode == ServiceMode::LoadedReview { // Remove old records. rrs.push(removed_soa.clone().into()); rrs.extend(soa_source_diff.removed_records.clone()); @@ -682,21 +699,6 @@ trait Viewer { /// Whether the zone instance is empty. fn is_empty(&self) -> bool; - /// Whether the records being viewed are at the loaded stage or the signed - /// stage/publication stage. - /// - /// IXFR out needs to know this to know whether or not to check the loaded - /// SOA or the signed SOA in a diff against the client SOA. The Service - /// impl that servers the IXFR out only has a Viewer trait to work with, - /// it doesn't know whether it is being used by a PublicationServer or - /// SignedReviewServer or LoadedReviewServer. - /// - /// The name is a bit verbose to distinguish between the presence of - /// signed records vs the stage at which the viewer sees the records - /// in Cascade, because in passthrough mode the loaded zone could also - /// contain DNSSEC records, i.e. be a "signed" zone). - fn is_loaded_viewer(&self) -> bool; - /// Return the SOA record. fn soa(&self) -> &SoaRecord; @@ -709,10 +711,6 @@ impl Viewer for LoadedZoneReviewer { self.read().is_none() } - fn is_loaded_viewer(&self) -> bool { - true - } - fn soa(&self) -> &SoaRecord { self.read().unwrap().soa() } @@ -727,10 +725,6 @@ impl Viewer for SignedZoneReviewer { self.read().is_none() } - fn is_loaded_viewer(&self) -> bool { - false - } - fn soa(&self) -> &SoaRecord { self.read().unwrap().soa() } @@ -748,10 +742,6 @@ impl Viewer for ZoneViewer { self.read().is_none() } - fn is_loaded_viewer(&self) -> bool { - false - } - fn soa(&self) -> &SoaRecord { self.read().unwrap().soa() } From 7bdc3ee5d2e148452a0ed1723a69b1995daebaf7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 15:18:34 +0200 Subject: [PATCH 41/44] Query the transient diff at review servers. (fixes #652) --- crates/zonedata/src/storage/mod.rs | 27 ++++++++++++++++++++++++++- src/server/service.rs | 24 +++++++++++++++++++++++- src/zone/storage.rs | 10 ++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/zonedata/src/storage/mod.rs b/crates/zonedata/src/storage/mod.rs index ea92d84d4..d979c46de 100644 --- a/crates/zonedata/src/storage/mod.rs +++ b/crates/zonedata/src/storage/mod.rs @@ -7,7 +7,9 @@ use std::sync::Arc; -use crate::{LoadedZoneRestorer, LoadedZoneReviewer, SignedZoneReviewer, ZoneViewer, data::Data}; +use crate::{ + DiffData, LoadedZoneRestorer, LoadedZoneReviewer, SignedZoneReviewer, ZoneViewer, data::Data, +}; mod states; pub use states::{ @@ -135,4 +137,27 @@ impl ZoneDataStorage { ZoneDataStorage::Poisoned => "Poisoned", } } + + /// Get the current loaded diff, if any. + pub fn loaded_diff(&self) -> Option> { + match self { + ZoneDataStorage::ReviewLoadedPending(s) => Some(s.loaded_diff.clone()), + ZoneDataStorage::ReviewingLoaded(s) => Some(s.loaded_diff.clone()), + ZoneDataStorage::PersistingLoaded(s) => Some(s.loaded_diff.clone()), + _ => None, + } + } + + /// Get the current signed diff, if any. + pub fn signed_diff(&self) -> Option> { + match self { + ZoneDataStorage::ReviewSignedPending(s) => Some(s.signed_diff.clone()), + ZoneDataStorage::ReviewingSigned(s) => Some(s.signed_diff.clone()), + ZoneDataStorage::PersistingSigned(_) => { + // PersistingSigned has no diff unlike PersistingLoaded + None + } + _ => None, + } + } } diff --git a/src/server/service.rs b/src/server/service.rs index 64c259ae4..8bef675f7 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -425,7 +425,29 @@ mod compat { return soa(request.message(), &*viewer); } - let diffs = zone.handle.state.lock().unwrap().storage.diffs.clone(); + let diffs = { + let zone_state = zone.handle.state.lock().unwrap(); + + match mode { + ServiceMode::LoadedReview => { + let mut diffs = Vec::with_capacity(1); + if let Some(loaded_diff) = zone_state.storage.current_loaded_diff() { + let empty_signed_diff = Arc::new(DiffData::new()); + diffs.push((loaded_diff, empty_signed_diff)); + } + diffs + } + ServiceMode::SignedReview => { + let mut diffs = Vec::with_capacity(1); + if let Some(signed_diff) = zone_state.storage.current_signed_diff() { + let empty_loaded_diff = Arc::new(DiffData::new()); + diffs.push((empty_loaded_diff, signed_diff)); + } + diffs + } + ServiceMode::Publication => zone_state.storage.diffs.clone(), + } + }; // TODO: Add something like the Bind `max-ixfr-ratio` option that // "sets the size threshold (expressed as a percentage of the size of diff --git a/src/zone/storage.rs b/src/zone/storage.rs index 8dc10fc7d..418c3f2b0 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -958,6 +958,16 @@ impl StorageState { background_tasks: Default::default(), } } + + /// Get the current loaded diff, if any. + pub fn current_loaded_diff(&self) -> Option> { + self.machine.loaded_diff() + } + + /// Get the current signed diff, if any. + pub fn current_signed_diff(&self) -> Option> { + self.machine.signed_diff() + } } impl Default for StorageState { From cd14c9b13a1c1c0b479c27b1f763bcfdc0f156bb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 16:03:11 +0200 Subject: [PATCH 42/44] Remove unwanted changes in this branch. --- src/server/service.rs | 87 ++++++++++++++++++++++++------------------- src/zone/storage.rs | 10 +++++ 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/server/service.rs b/src/server/service.rs index f61775977..8bef675f7 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -39,6 +39,16 @@ pub struct ZoneService { // of the limitations of 'domain::net::server'; that architecture should be // gradually replaced locally for better flexibility and efficiency. state: Arc>>, + + /// What mode of operation is intended? + mode: ServiceMode, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ServiceMode { + LoadedReview, + SignedReview, + Publication, } impl ZoneService { @@ -46,10 +56,11 @@ impl ZoneService { /// /// In addition to the service, a [`ZoneServiceHandle`] is returned through /// which the service can be interacted with. - pub fn new() -> (ZoneService, ZoneServiceHandle) { + pub fn new(server_mode: ServiceMode) -> (ZoneService, ZoneServiceHandle) { let state = Arc::new(std::sync::RwLock::default()); let service = ZoneService { state: state.clone(), + mode: server_mode, }; let handle = ZoneServiceHandle { state }; (service, handle) @@ -60,6 +71,7 @@ impl Clone for ZoneService { fn clone(&self) -> Self { Self { state: self.state.clone(), + mode: self.mode, } } } @@ -88,7 +100,10 @@ mod compat { use futures::Stream; use tracing::{Level, debug, trace, warn}; - use crate::server::request::{RequestKind, ZoneRequestKind}; + use crate::server::{ + request::{RequestKind, ZoneRequestKind}, + service::ServiceMode, + }; use super::{ServedZone, Viewer, ZoneService}; @@ -102,8 +117,7 @@ mod compat { fn call(&self, old_request: Request, Option>>) -> Response { // Parse the request. - let slice = old_request.message().as_slice(); - let message = slice; + let message = old_request.message().as_slice(); let message = domain::new::base::Message::parse_bytes_by_ref(message) .expect("'message' was already checked to be a valid DNS message"); let request = match crate::server::request::parse(message) { @@ -133,7 +147,7 @@ mod compat { return Box::pin(std::future::ready(error(old_request.message(), rcode))); }; - if !is_permitted(zone, &old_request) { + if self.mode == ServiceMode::Publication && !is_permitted(zone, &old_request) { return Box::pin(std::future::ready(error( old_request.message(), Rcode::REFUSED, @@ -154,7 +168,8 @@ mod compat { } ZoneRequestKind::Ixfr { known_soa } => { - Box::pin(ixfr(old_request, known_soa.rdata, zone.clone())) as Response + Box::pin(ixfr(old_request, known_soa.rdata, zone.clone(), self.mode)) + as Response } } } @@ -353,6 +368,7 @@ mod compat { request: Request, Option>>, client_soa: Soa>, zone: ServedZone, + mode: ServiceMode, ) -> ResponseStream { // Save a cheap clone of the zone to avoid a borrow checker error. let zone_clone = zone.clone(); @@ -409,7 +425,29 @@ mod compat { return soa(request.message(), &*viewer); } - let diffs = zone.handle.state.lock().unwrap().storage.diffs.clone(); + let diffs = { + let zone_state = zone.handle.state.lock().unwrap(); + + match mode { + ServiceMode::LoadedReview => { + let mut diffs = Vec::with_capacity(1); + if let Some(loaded_diff) = zone_state.storage.current_loaded_diff() { + let empty_signed_diff = Arc::new(DiffData::new()); + diffs.push((loaded_diff, empty_signed_diff)); + } + diffs + } + ServiceMode::SignedReview => { + let mut diffs = Vec::with_capacity(1); + if let Some(signed_diff) = zone_state.storage.current_signed_diff() { + let empty_loaded_diff = Arc::new(DiffData::new()); + diffs.push((empty_loaded_diff, signed_diff)); + } + diffs + } + ServiceMode::Publication => zone_state.storage.diffs.clone(), + } + }; // TODO: Add something like the Bind `max-ixfr-ratio` option that // "sets the size threshold (expressed as a percentage of the size of @@ -469,7 +507,7 @@ mod compat { // SOA serial. let start_idx = { diffs.iter().position(|(loaded_diff, signed_diff)| { - let d = if viewer.is_loaded_viewer() { + let d = if mode == ServiceMode::LoadedReview { loaded_diff } else { signed_diff @@ -523,7 +561,7 @@ mod compat { let abs_idx = start_idx + i; // Select the appropriate diff as the SOA source to use. - let soa_source_diff = if viewer.is_loaded_viewer() { + let soa_source_diff = if mode == ServiceMode::LoadedReview { loaded_diff } else { signed_diff @@ -551,7 +589,7 @@ mod compat { // again which can happen if the loaded zone remained // unchanged but incremental signing caused changes in the // signed part only to occur. - if viewer.is_loaded_viewer() + if mode == ServiceMode::LoadedReview && let Some(last_removed_soa) = last_removed_soa && removed_soa == last_removed_soa { @@ -572,7 +610,7 @@ mod compat { trace!("Serving diff #{abs_idx} for loaded review server IXFR out",); - if viewer.is_loaded_viewer() { + if mode == ServiceMode::LoadedReview { // Remove old records. rrs.push(removed_soa.clone().into()); rrs.extend(soa_source_diff.removed_records.clone()); @@ -683,21 +721,6 @@ trait Viewer { /// Whether the zone instance is empty. fn is_empty(&self) -> bool; - /// Whether the records being viewed are at the loaded stage or the signed - /// stage/publication stage. - /// - /// IXFR out needs to know this to know whether or not to check the loaded - /// SOA or the signed SOA in a diff against the client SOA. The Service - /// impl that servers the IXFR out only has a Viewer trait to work with, - /// it doesn't know whether it is being used by a PublicationServer or - /// SignedReviewServer or LoadedReviewServer. - /// - /// The name is a bit verbose to distinguish between the presence of - /// signed records vs the stage at which the viewer sees the records - /// in Cascade, because in passthrough mode the loaded zone could also - /// contain DNSSEC records, i.e. be a "signed" zone). - fn is_loaded_viewer(&self) -> bool; - /// Return the SOA record. fn soa(&self) -> &SoaRecord; @@ -710,10 +733,6 @@ impl Viewer for LoadedZoneReviewer { self.read().is_none() } - fn is_loaded_viewer(&self) -> bool { - true - } - fn soa(&self) -> &SoaRecord { self.read().unwrap().soa() } @@ -728,10 +747,6 @@ impl Viewer for SignedZoneReviewer { self.read().is_none() } - fn is_loaded_viewer(&self) -> bool { - false - } - fn soa(&self) -> &SoaRecord { self.read().unwrap().soa() } @@ -749,10 +764,6 @@ impl Viewer for ZoneViewer { self.read().is_none() } - fn is_loaded_viewer(&self) -> bool { - false - } - fn soa(&self) -> &SoaRecord { self.read().unwrap().soa() } diff --git a/src/zone/storage.rs b/src/zone/storage.rs index 8dc10fc7d..418c3f2b0 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -958,6 +958,16 @@ impl StorageState { background_tasks: Default::default(), } } + + /// Get the current loaded diff, if any. + pub fn current_loaded_diff(&self) -> Option> { + self.machine.loaded_diff() + } + + /// Get the current signed diff, if any. + pub fn current_signed_diff(&self) -> Option> { + self.machine.signed_diff() + } } impl Default for StorageState { From 586c4a96fbf1a24738dcc5d1a6fd91ee5f393511 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 16:06:36 +0200 Subject: [PATCH 43/44] Add missing test files. --- .../tests/ixfr-out-incremental/action.yml | 162 +++++++++ .../tests/ixfr-out-review/action.yml | 239 +++++++++++++ integration-tests/tests/ixfr-out/action.yml | 324 ++++++++++++++++++ 3 files changed, 725 insertions(+) create mode 100644 integration-tests/tests/ixfr-out-incremental/action.yml create mode 100644 integration-tests/tests/ixfr-out-review/action.yml create mode 100644 integration-tests/tests/ixfr-out/action.yml diff --git a/integration-tests/tests/ixfr-out-incremental/action.yml b/integration-tests/tests/ixfr-out-incremental/action.yml new file mode 100644 index 000000000..47fa37e4b --- /dev/null +++ b/integration-tests/tests/ixfr-out-incremental/action.yml @@ -0,0 +1,162 @@ +# Making reusable composite actions documented at +# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository +name: 'Serve incremental signing changes via IXFR to the NSD secondary.' +description: 'Serve a zone via IXFR to the NSD secondary.' +defaults: + # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell + run: + shell: bash --noprofile --norc -eo pipefail -x {0} +inputs: + log-level: + description: The level of logging that Cascade should output. + required: false + default: debug + type: choice + options: + - error + - warning + - info + - debug + - trace +runs: + using: "composite" + steps: + - uses: ./.github/actions/prepare-systest-env + - uses: ./.github/actions/setup-and-start-cascade + with: + log-level: ${{ inputs.log-level }} + + - name: Determine the downstream secondary zone config to use + id: set-vars + run: | + ZONE_NAME="example.test." + POLICY_NAME="fast-incremental" + KEY_NAME="Kexample.test.+015+00580.key" + echo "ZONE_NAME=${ZONE_NAME}" >> "$GITHUB_OUTPUT" + echo "POLICY_NAME=${POLICY_NAME}" >> "$GITHUB_OUTPUT" + echo "KEY_NAME=${KEY_NAME}" >> "$GITHUB_OUTPUT" + + - name: Add policy ${{ steps.set-vars.outputs.POLICY_NAME }} and reload + run: | + POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) + TEST_DIR="${PWD}/integration-tests/ixfr-out-incremental" + cp "${TEST_DIR}/policies/${{ steps.set-vars.outputs.POLICY_NAME }}.toml" "${POLICY_DIR}/" + cascade policy reload + + # The zone name has to match a zone configured in the downstream NSD + # nameserver. Zone 'notify-and-xfr-tsig.test' is a zone in the downstream + # NSD nameserver that is configured to expect TSIG to be used for the + # NOTIFY message it is sent, to use IXFR (as opposed to only AXFR) and for + # it to need to use TSIG to request an IXFR transfer from Cascade. + - name: Make a ${{ steps.set-vars.outputs.ZONE_NAME }} zone based on RFC 1995 section 7 + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + cat >"${ZONE_NAME}zone" < (start + timeout))); then + cascade zone status ${ZONE_NAME} + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Expect serial 1 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 1 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status ${ZONE_NAME} + dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 2 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + dig @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + cascade zone status ${ZONE_NAME} + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 3 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status ${ZONE_NAME} + dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + - name: Capture Cascade AXFR out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} AXFR &> cascade-axfr.log + + - name: Compare NSD AXFR out to Cascade AXFR out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${ZONE_NAME} AXFR &> nsd-axfr.log + + # Pass -f to sort and -i to diff to normalize case in the Cascade and + # NSD output being compared, as NSD outputs lower case labels while + # Cascade preserves label case. -f tells sort not to order differently + # based on case, and -i tells diff to consider differing case to be + # equal. + + LC_COLLATE=C sort -f cascade-axfr.log > cascade-axfr.sorted + LC_COLLATE=C sort -f nsd-axfr.log > nsd-axfr.sorted + diff -u -w -i cascade-axfr.sorted nsd-axfr.sorted + + - name: Print log files on any failure in this job + uses: ./.github/actions/print-logfiles + if: failure() diff --git a/integration-tests/tests/ixfr-out-review/action.yml b/integration-tests/tests/ixfr-out-review/action.yml new file mode 100644 index 000000000..9b5d71caf --- /dev/null +++ b/integration-tests/tests/ixfr-out-review/action.yml @@ -0,0 +1,239 @@ +# Making reusable composite actions documented at +# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository +name: 'Serve manual zone modifications via IXFR for review.' +description: 'Serve a zone via IXFR to the NSD secondary.' +defaults: + # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell + run: + shell: bash --noprofile --norc -eo pipefail -x {0} +inputs: + log-level: + description: The level of logging that Cascade should output. + required: false + default: debug + type: choice + options: + - error + - warning + - info + - debug + - trace +runs: + using: "composite" + steps: + - uses: ./.github/actions/setup-and-start-cascade + with: + log-level: ${{ inputs.log-level }} + fake-time: 1600000000 + + - name: Determine expected published serial number + id: set-vars + run: | + EXPECTED_SERIAL="$(date +'%Y%m%d00')" + echo "EXPECTED_SERIAL=${EXPECTED_SERIAL}" >>"$GITHUB_OUTPUT" + + - name: Make a zone based on RFC 1995 section 7 + run: | + cat >"example.test.zone" < (start + timeout))); then + cascade zone status example.test + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Expect serial 1 of the zone at the loaded review server + run: | + timeout=3 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 4540 example.test SOA | grep -qi "ns.example.test. mail.example.test. 1 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4540 example.test SOA + echo "::error:: timeout: loaded review server is not serving serial 1 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 1 of the zone at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4541 example.test SOA + echo "::error:: timeout: signed review server is not serving serial 1 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Edit and reload serial 2 of the zone + run: | + cat >"example.test.zone" < (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4540 example.test SOA + echo "::error:: timeout: loaded review server is not serving serial 2 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 2 of the zone at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + timeout=10 # seconds + start=$(date +%s) + let EXPECTED_SERIAL=$(( EXPECTED_SERIAL + 1 )) + until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4541 example.test SOA + echo "::error:: timeout: signed review server is not serving serial 2 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Edit and reload serial 3 of the zone + run: | + cat >"example.test.zone" < (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4540 example.test SOA + echo "::error:: timeout: loaded review server is not serving serial 3 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Expect serial 3 of the zone at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + timeout=10 # seconds + start=$(date +%s) + let EXPECTED_SERIAL=$(( EXPECTED_SERIAL + 2 )) + until dig +short @127.0.0.1 -p 4541 example.test SOA | grep -qi "ns.example.test. mail.example.test. ${EXPECTED_SERIAL} 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status example.test + dig +short @127.0.0.1 -p 4541 example.test SOA + echo "::error:: timeout: signed review server is not serving serial 3 of the zone" + exit 1 + fi + sleep 1 + done + + - name: Verify IXFR of serial 1..=3 at the loaded review server + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=1 &> loaded-review-ixfr-1-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort loaded-review-ixfr-1-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 2..=3 at the loaded review server + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=2 &> loaded-review-ixfr-2-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort loaded-review-ixfr-2-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 3..=3 at the loaded review server + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4540 example.test -t IXFR=3 &> loaded-review-ixfr-3-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/loaded-review.ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort loaded-review-ixfr-3-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 1..=3 at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=${EXPECTED_SERIAL} &> signed-review-ixfr-1-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort signed-review-ixfr-1-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 2..=3 at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=$(( EXPECTED_SERIAL + 1 )) &> signed-review-ixfr-2-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort signed-review-ixfr-2-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify IXFR of serial 3..=3 at the signed review server + env: + EXPECTED_SERIAL: ${{ steps.set-vars.outputs.EXPECTED_SERIAL }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 4541 example.test -t IXFR=$(( EXPECTED_SERIAL + 2 )) &> signed-review-ixfr-3-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out-review" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/signed-review.ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort signed-review-ixfr-3-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Print log files on any failure in this job + uses: ./.github/actions/print-logfiles + if: failure() diff --git a/integration-tests/tests/ixfr-out/action.yml b/integration-tests/tests/ixfr-out/action.yml new file mode 100644 index 000000000..0cdab4edf --- /dev/null +++ b/integration-tests/tests/ixfr-out/action.yml @@ -0,0 +1,324 @@ +# Making reusable composite actions documented at +# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action#creating-a-composite-action-within-the-same-repository +name: 'Serve manual zone modifications via IXFR to the NSD secondary.' +description: 'Serve a zone via IXFR to the NSD secondary.' +defaults: + # see: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#defaultsrunshell + run: + shell: bash --noprofile --norc -eo pipefail -x {0} +inputs: + log-level: + description: The level of logging that Cascade should output. + required: false + default: debug + type: choice + options: + - error + - warning + - info + - debug + - trace + with-tsig: + description: Whether or not TSIG should be required for XFR out. + required: true + type: boolean +runs: + using: "composite" + steps: + - uses: ./.github/actions/prepare-systest-env + - uses: ./.github/actions/setup-and-start-cascade + with: + log-level: ${{ inputs.log-level }} + fake-time: 1600000000 + + - name: Determine the downstream secondary zone config to use + id: set-vars + run: | + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + ZONE_NAME="notify-and-xfr-tsig.test." + POLICY_NAME="with-tsig" + KEY_NAME="Knotify-and-xfr-tsig.test.+015+10995.key" + else + ZONE_NAME="example.test." + POLICY_NAME="without-tsig" + KEY_NAME="Kexample.test.+015+00580.key" + fi + echo "ZONE_NAME=${ZONE_NAME}" >> "$GITHUB_OUTPUT" + echo "POLICY_NAME=${POLICY_NAME}" >> "$GITHUB_OUTPUT" + echo "KEY_NAME=${KEY_NAME}" >> "$GITHUB_OUTPUT" + echo "TSIG_SPEC=hmac-sha256:tsig-key:COzoVsYQmXeXiyq1Quhp0bbVnMyxjPxsaGSoIWR98i0=" >> "$GITHUB_OUTPUT" + + - name: Add the TSIG key + if: ${{ inputs.with-tsig }} + run: | + cascade tsig add ${{ steps.set-vars.outputs.TSIG_SPEC }} + + - name: Add policy ${{ steps.set-vars.outputs.POLICY_NAME }} and reload + run: | + POLICY_DIR=$(integration-tests/scripts/get-default-path.sh policy-dir) + TEST_DIR="${PWD}/integration-tests/ixfr-out" + cp "${TEST_DIR}/policies/${{ steps.set-vars.outputs.POLICY_NAME }}.toml" "${POLICY_DIR}/" + cascade policy reload + + # The zone name has to match a zone configured in the downstream NSD + # nameserver. Zone 'notify-and-xfr-tsig.test' is a zone in the downstream + # NSD nameserver that is configured to expect TSIG to be used for the + # NOTIFY message it is sent, to use IXFR (as opposed to only AXFR) and for + # it to need to use TSIG to request an IXFR transfer from Cascade. + - name: Make a ${{ steps.set-vars.outputs.ZONE_NAME }} zone based on RFC 1995 section 7 + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + cat >"${ZONE_NAME}zone" < (start + timeout))); then + cascade zone status ${ZONE_NAME} + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Expect serial 1 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 1 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status ${ZONE_NAME} + dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + - name: Edit and reload serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + cat >"${ZONE_NAME}zone" < (start + timeout))); then + cascade zone status ${ZONE_NAME} + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Expect serial 2 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 2 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + dig @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + cascade zone status ${ZONE_NAME} + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + - name: Edit and reload serial 3 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + cat >"${ZONE_NAME}zone" < (start + timeout))); then + cascade zone status ${ZONE_NAME} + echo "timeout: zone status did not report published zone available" >&2 + exit 1 + fi + sleep 1 + done + + - name: Expect serial 3 of zone ${{ steps.set-vars.outputs.ZONE_NAME }} at the NSD secondary + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + timeout=10 # seconds + start=$(date +%s) + until dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA | grep -q "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5"; do + if (($(date +%s) > (start + timeout))); then + cascade zone status ${ZONE_NAME} + dig +short @127.0.0.1 -p 1054 ${ZONE_NAME} SOA + echo "::error:: timeout: NSD did not acquire the zone changes" + exit 1 + fi + sleep 1 + done + + # Note: There is no NSD metric that I am aware of that we can use to + # verify that it received IXFR from Cascade instead of AXFR, nor is there + # any Cascade Prometheus metric or CLI command that we can use to check + # this either. Increasing the NSD log level to 9 doesn't cause it to + # report whether it received IXFR or AXFR either. + + - name: Verify that TSIG is required by Cascade + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + if [[ "${{ inputs.with-tsig }}" == "false" ]]; then + exit 0 + fi + + # This should not succeed as it lacks the required TSIG key. + # Note that dig will exit with code 0 even if the request fails, + # because it considers an error response still to be successful + # receipt of a response, so we have to check the actual output. + dig @127.0.0.1 +yaml -p 4542 ${ZONE_NAME} AXFR &> dig-no-tsig.log + grep -Fq "status: REFUSED" dig-no-tsig.log + + - name: Verify Cascade AXFR out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${EXTRA_ARGS} ${ZONE_NAME} AXFR &> cascade-axfr.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-axfr.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + # A UDP IXFR request should result in a single SOA response "to inform the + # client that a TCP query should be initiated". + - name: Verify that UDP is not supported + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + SOA_RR=$(dig +short +notcp +ignore @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=1 ${EXTRA_ARGS} 2>&1 | tee dig-udp-ixfr.log) + if [[ "${SOA_RR}" != "ns.${ZONE_NAME} mail.${ZONE_NAME} 3 60 60 3600 5" ]]; then + exit 1 + fi + + - name: Verify Cascade IXFR serial 1..=3 out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=1 ${EXTRA_ARGS} &> cascade-ixfr-1-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-1-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-ixfr-1-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify Cascade IXFR serial 2..=3 out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=2 ${EXTRA_ARGS} &> cascade-ixfr-2-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-2-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-ixfr-2-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify Cascade IXFR serial 3..=3 out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + EXTRA_ARGS= + if [[ "${{ inputs.with-tsig }}" == "true" ]]; then + EXTRA_ARGS="-y ${{ steps.set-vars.outputs.TSIG_SPEC }}" + fi + dig +onesoa +noall +answer @127.0.0.1 -p 4542 ${ZONE_NAME} -t IXFR=3 ${EXTRA_ARGS} &> cascade-ixfr-3-3.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + LC_COLLATE=C sort "${TEST_DIR}/reference-output/${ZONE_NAME}ixfr-3-3" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort cascade-ixfr-3-3.log > output.sorted + diff -u -w output.sorted reference.output.sorted + + - name: Verify NSD AXFR out + env: + ZONE_NAME: ${{ steps.set-vars.outputs.ZONE_NAME }} + run: | + dig +onesoa +noall +answer @127.0.0.1 -p 1054 ${ZONE_NAME} AXFR &> nsd-axfr.log + TEST_DIR="${PWD}/integration-tests/ixfr-out" + + # Pass -f to sort and -i to diff to normalize case in the Cascade and + # NSD output being compared, as NSD outputs lower case labels while + # Cascade preserves label case. -f tells sort not to order differently + # based on case, and -i tells diff to consider differing case to be + # equal. + + LC_COLLATE=C sort -f "${TEST_DIR}/reference-output/${ZONE_NAME}axfr" | egrep -v '^;|^$' > reference.output.sorted + LC_COLLATE=C sort -f nsd-axfr.log > output.sorted + diff -u -w -i output.sorted reference.output.sorted + + - name: Print log files on any failure in this job + uses: ./.github/actions/print-logfiles + if: failure() From 3305ff5d1ecd6db6fbc5ecd89790ed4b19914f07 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 May 2026 16:28:20 +0200 Subject: [PATCH 44/44] Add missing NSD config change. --- integration-tests/scripts/manage-test-environment.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-tests/scripts/manage-test-environment.sh b/integration-tests/scripts/manage-test-environment.sh index 349ce5d69..7ef29328f 100755 --- a/integration-tests/scripts/manage-test-environment.sh +++ b/integration-tests/scripts/manage-test-environment.sh @@ -299,7 +299,7 @@ pattern: zonefile: "%s.secondary-zone" allow-notify: 127.0.0.1 NOKEY # Until Cascade supports IXFR we always use AXFR - request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY + request-xfr: 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY zone: @@ -310,21 +310,21 @@ zone: name: notify-tsig.test zonefile: "notify-tsig.test.secondary-zone" allow-notify: 127.0.0.1 tsig-key - request-xfr: AXFR 127.0.0.1@${_cascade_port} NOKEY + request-xfr: 127.0.0.1@${_cascade_port} NOKEY provide-xfr: 127.0.0.1 NOKEY zone: name: xfr-tsig.test zonefile: "xfr-tsig.test.secondary-zone" allow-notify: 127.0.0.1 NOKEY - request-xfr: AXFR 127.0.0.1@${_cascade_port} tsig-key + request-xfr: 127.0.0.1@${_cascade_port} tsig-key provide-xfr: 127.0.0.1 NOKEY zone: name: notify-and-xfr-tsig.test zonefile: "notify-and-xfr-tsig.test.secondary-zone" allow-notify: 127.0.0.1 tsig-key - request-xfr: AXFR 127.0.0.1@${_cascade_port} tsig-key + request-xfr: 127.0.0.1@${_cascade_port} tsig-key provide-xfr: 127.0.0.1 NOKEY EOF