diff --git a/src/loader/mod.rs b/src/loader/mod.rs index 5bb26dd7..b49bbbf2 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -249,7 +249,7 @@ async fn refresh( unreachable!("source-specific loading succeeded and must have filled 'builder'") }); - handle.finish_load(built); + handle.finish_load(built, soa.rdata.serial); } Err(err) => { diff --git a/src/server/mod.rs b/src/server/mod.rs index 0bc99fc6..71c14a8d 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -2,9 +2,9 @@ use std::{fmt, sync::Arc}; -use cascade_api::{ZoneReviewDecision, ZoneReviewResult}; use cascade_zonedata::{LoadedZoneReviewer, SignedZoneReviewer, ZoneViewer}; use domain::base::Serial; +use tracing::{debug, error, info}; use crate::{ center::Center, @@ -12,7 +12,7 @@ use crate::{ manager::Terminated, units::zone_server::{Source, ZoneServer}, util::AbortOnDrop, - zone::Zone, + zone::{UpcomingInstance, Zone, ZoneHandle, machine::ZoneStateMachine}, }; mod request; @@ -62,15 +62,75 @@ impl LoadedReviewServer { ZoneServer::new(Source::Unsigned).on_seek_approval_for_zone(center, zone, zone_serial) } - /// Process a review of a served instance. + /// Process a review of the upcoming loaded instance. + #[tracing::instrument( + level = "trace", + skip_all, + fields(zone = %zone.name, r#type = "loaded", zone_serial, ?decision) + )] pub fn process_review( center: &Arc
, zone: &Arc, zone_serial: Serial, - decision: ZoneReviewDecision, - ) -> ZoneReviewResult { - // TODO: Inline. - ZoneServer::new(Source::Unsigned).on_zone_review(center, zone, zone_serial, decision) + decision: cascade_api::ZoneReviewDecision, + ) -> cascade_api::ZoneReviewResult { + let mut state = zone.state.lock().unwrap(); + let mut handle = ZoneHandle { + zone, + state: &mut state, + center, + }; + + // Ensure the zone is in loader review. + let ZoneStateMachine::LoadedReview(machine) = &mut handle.state.machine else { + debug!("The zone is not in loaded-review state"); + + return Err(cascade_api::ZoneReviewError::NotUnderReview); + }; + if machine.decided { + debug!("The instance has already been reviewed"); + + return Err(cascade_api::ZoneReviewError::NotUnderReview); + } + + // Look up the upcoming instance. + let Some(UpcomingInstance { + loaded: Some(loaded), + signed: None, + }) = &handle.state.instances.upcoming + else { + unreachable!("'UpcomingInstance' is inconsistent with 'LoadedReview'") + }; + + // Ensure the serial number is correct. + if Serial(loaded.serial.into()) != zone_serial { + debug!( + "The upcoming loaded instance has serial '{}', not '{zone_serial}'", + loaded.serial + ); + + return Err(cascade_api::ZoneReviewError::NotUnderReview); + } + + // Remember that a review has been received. + machine.decided = true; + + match decision { + cascade_api::ZoneReviewDecision::Approve => { + info!("Approving the upcoming loaded instance"); + + handle.approve_loaded(); + } + + cascade_api::ZoneReviewDecision::Reject => { + error!("Rejecting the upcoming loaded instance"); + + // TODO: Whether to soft or hard reject should be part of the policy + handle.hard_reject_loaded(); + } + } + + Ok(cascade_api::ZoneReviewOutput {}) } /// Register a new zone. @@ -151,15 +211,75 @@ impl SignedReviewServer { ZoneServer::new(Source::Signed).on_seek_approval_for_zone(center, zone, zone_serial) } - /// Process a review of a served instance. + /// Process a review of the upcoming signed instance. + #[tracing::instrument( + level = "trace", + skip_all, + fields(zone = %zone.name, r#type = "signed", zone_serial, ?decision) + )] pub fn process_review( center: &Arc
, zone: &Arc, zone_serial: Serial, - decision: ZoneReviewDecision, - ) -> ZoneReviewResult { - // TODO: Inline. - ZoneServer::new(Source::Signed).on_zone_review(center, zone, zone_serial, decision) + decision: cascade_api::ZoneReviewDecision, + ) -> cascade_api::ZoneReviewResult { + let mut state = zone.state.lock().unwrap(); + let mut handle = ZoneHandle { + zone, + state: &mut state, + center, + }; + + // Ensure the zone is in signer review. + let ZoneStateMachine::SignedReview(machine) = &mut handle.state.machine else { + debug!("The zone is not in signed-review state"); + + return Err(cascade_api::ZoneReviewError::NotUnderReview); + }; + if machine.decided { + debug!("The instance has already been reviewed"); + + return Err(cascade_api::ZoneReviewError::NotUnderReview); + } + + // Look up the upcoming instance. + let Some(UpcomingInstance { + loaded: _, + signed: Some(signed), + }) = &handle.state.instances.upcoming + else { + unreachable!("'UpcomingInstance' is inconsistent with 'LoadedReview'") + }; + + // Ensure the serial number is correct. + if Serial(signed.serial.into()) != zone_serial { + debug!( + "The upcoming signed instance has serial '{}', not '{zone_serial}'", + signed.serial + ); + + return Err(cascade_api::ZoneReviewError::NotUnderReview); + } + + // Remember that a review has been received. + machine.decided = true; + + match decision { + cascade_api::ZoneReviewDecision::Approve => { + info!("Approving the upcoming signed instance"); + + handle.approve_signed(); + } + + cascade_api::ZoneReviewDecision::Reject => { + error!("Rejecting the upcoming signed instance"); + + // TODO: Whether to soft or hard reject should be part of the policy + handle.hard_reject_signed(); + } + } + + Ok(cascade_api::ZoneReviewOutput {}) } /// Register a new zone. diff --git a/src/signer/mod.rs b/src/signer/mod.rs index 0ad89ca5..f76b0c24 100644 --- a/src/signer/mod.rs +++ b/src/signer/mod.rs @@ -23,7 +23,7 @@ use std::{ }; use cascade_zonedata::SignedZoneBuilder; -use tracing::error; +use tracing::{debug, error}; use crate::units::zone_signer::SignerError; use crate::{ @@ -85,8 +85,16 @@ async fn sign( match result { Ok(()) => { + let soa = builder.next_signed().unwrap().soa().clone(); + + debug!( + zone = %zone.name, + serial = ?soa.rdata.serial, + "Generated a new signed instance of the zone" + ); + let built = builder.finish().unwrap_or_else(|_| unreachable!()); - handle.finish_signing(built); + handle.finish_signing(built, soa.rdata.serial); status.status.finish(true); status.current_action = "Finished".to_string(); } diff --git a/src/units/http_server.rs b/src/units/http_server.rs index 7fbc9873..de9fc530 100644 --- a/src/units/http_server.rs +++ b/src/units/http_server.rs @@ -438,21 +438,18 @@ impl HttpServer { } }); - unsigned_serial = zone_state - .storage - .loaded_review_soa - .as_ref() - .map(|r| Serial::from(u32::from(r.rdata.serial))); - signed_serial = zone_state - .storage - .signed_review_soa - .as_ref() - .map(|r| Serial::from(u32::from(r.rdata.serial))); + let upcoming = zone_state.instances.upcoming.as_ref(); + unsigned_serial = upcoming + .and_then(|i| i.loaded.as_ref()) + .map(|i| Serial(u32::from(i.serial))); + signed_serial = upcoming + .and_then(|i| i.signed.as_ref()) + .map(|i| Serial(u32::from(i.serial))); published_serial = zone_state - .storage - .published_soa + .instances + .current .as_ref() - .map(|r| Serial::from(u32::from(r.rdata.serial))); + .map(|i| Serial(u32::from(i.signed.serial))); progress = match zone_state.machine { ZoneStateMachine::Waiting(..) => Progress::Waiting, @@ -467,11 +464,12 @@ impl HttpServer { }; last_published = zone_state - .last_published + .instances + .current .as_ref() - .map(|p| LastPublishedZone { - loaded_serial: p.loaded_serial, - signed_serial: p.signed_serial, + .map(|i| LastPublishedZone { + loaded_serial: Serial(i.loaded.serial.into()), + signed_serial: Serial(i.signed.serial.into()), }); let mut found_error = None; diff --git a/src/units/zone_server.rs b/src/units/zone_server.rs index 2b105bad..0a41858f 100644 --- a/src/units/zone_server.rs +++ b/src/units/zone_server.rs @@ -26,9 +26,7 @@ use domain::tsig::{Algorithm, KeyStore}; use domain::zonetree::StoredName; use tracing::{debug, error, info, trace, warn}; -use crate::api::{ - ZoneReviewDecision, ZoneReviewError, ZoneReviewOutput, ZoneReviewResult, ZoneReviewStatus, -}; +use crate::api::{ZoneReviewDecision, ZoneReviewStatus}; use crate::center::Center; use crate::config::SocketConfig; use crate::daemon::SocketProvider; @@ -37,10 +35,7 @@ use crate::manager::record_zone_event; use crate::policy::NameserverCommsPolicy; use crate::server::{LoadedReviewServer, PublicationServer, SignedReviewServer}; use crate::util::AbortOnDrop; -use crate::zone::{ - HistoricalEvent, SignedZoneVersionState, UnsignedZoneVersionState, Zone, ZoneHandle, - ZoneVersionReviewState, -}; +use crate::zone::{HistoricalEvent, Zone, ZoneHandle}; /// The source of a zone server. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -315,37 +310,6 @@ impl ZoneServer { "[{unit_name}]: Seeking approval for {zone_type} zone '{zone_name}' at serial {zone_serial}." ); - // Mark this version of the zone as pending approval. - // - // TODO: These entries should have been created a long time ago, but - // not all components use these fields yet. For now, they need to be - // created over here -- hence 'or_insert_with()'. - { - let mut zone_state = zone.state.lock().unwrap(); - match self.source { - Source::Unsigned => { - zone_state - .unsigned - .entry(zone_serial) - .or_insert_with(|| UnsignedZoneVersionState { - review: Default::default(), - }) - .review = ZoneVersionReviewState::Pending; - } - Source::Signed => { - zone_state - .signed - .entry(zone_serial) - .or_insert_with(|| SignedZoneVersionState { - unsigned_serial: Serial::from(0), // TODO - review: Default::default(), - }) - .review = ZoneVersionReviewState::Pending; - } - Source::Published => unreachable!(), - } - } - record_zone_event(center, zone, pending_event, Some(zone_serial)); if review.cmd_hook.is_none() || review_server.is_none() { @@ -451,31 +415,33 @@ impl ZoneServer { "[{unit_name}]: Failed to execute hook '{hook}' for {zone_type} zone '{zone_name}' at serial {zone_serial}: {err}" ); - { - let mut zone_state = zone.state.lock().unwrap(); - match self.source { - Source::Unsigned => { - zone_state.record_event( - HistoricalEvent::UnsignedHookFailed { - err: err.to_string(), - }, - Some(zone_serial), - ); - zone_state.unsigned.get_mut(&zone_serial).unwrap().review = - ZoneVersionReviewState::Rejected; - } - Source::Signed => { - zone_state.record_event( - HistoricalEvent::SignedHookFailed { - err: err.to_string(), - }, - Some(zone_serial), - ); - zone_state.signed.get_mut(&zone_serial).unwrap().review = - ZoneVersionReviewState::Rejected; - } - Source::Published => unreachable!(), + let mut state = zone.state.lock().unwrap(); + let mut handle = ZoneHandle { + zone, + state: &mut state, + center, + }; + + match self.source { + Source::Unsigned => { + handle.state.record_event( + HistoricalEvent::UnsignedHookFailed { + err: err.to_string(), + }, + Some(zone_serial), + ); + handle.hard_reject_loaded(); + } + Source::Signed => { + handle.state.record_event( + HistoricalEvent::SignedHookFailed { + err: err.to_string(), + }, + Some(zone_serial), + ); + handle.hard_reject_signed(); } + Source::Published => unreachable!(), } } } @@ -534,120 +500,6 @@ impl ZoneServer { } Ok(()) } - - pub fn on_zone_review( - &self, - center: &Arc
, - zone: &Arc, - zone_serial: Serial, - decision: ZoneReviewDecision, - ) -> ZoneReviewResult { - let unit_name = self.unit_name(); - let zone_name = &zone.name; - - // Look up the zone. - - let new_review_state = match decision { - ZoneReviewDecision::Approve => ZoneVersionReviewState::Approved, - ZoneReviewDecision::Reject => ZoneVersionReviewState::Rejected, - }; - - // Look up the version of the zone being reviewed. - match self.source { - Source::Unsigned => { - { - let mut zone_state = zone.state.lock().unwrap(); - let Some(version) = zone_state.unsigned.get_mut(&zone_serial) else { - // 'on_seek_approval_for_zone_cmd()' should have created - // this. Since it doesn't exist, the zone is not under - // review. - - debug!( - "[{unit_name}] Got a review for {zone_name}/{zone_serial}, but it was not pending review" - ); - return Err(ZoneReviewError::NotUnderReview); - }; - - // Check that the zone was not already approved. - if matches!(version.review, ZoneVersionReviewState::Approved) { - // This version of the zone is no longer being reviewed. - // - // TODO: Differentiate this from 'NotUnderReview'? - - return Err(ZoneReviewError::NotUnderReview); - } - - version.review = new_review_state; - } - if matches!(decision, ZoneReviewDecision::Approve) { - info!( - "Unsigned zone '{zone_name}' with serial {zone_serial} has been approved." - ); - self.on_unsigned_zone_approved(center, zone, zone_serial); - } else { - error!( - "Unsigned zone '{zone_name}' with serial {zone_serial} has been rejected." - ); - - // TODO: Whether to soft or hard reject should be part of the policy - let mut state = zone.state.lock().unwrap(); - ZoneHandle { - zone, - state: &mut state, - center, - } - .hard_reject_loaded(); - } - } - - Source::Signed => { - { - let mut zone_state = zone.state.lock().unwrap(); - let Some(version) = zone_state.signed.get_mut(&zone_serial) else { - // 'on_seek_approval_for_zone_cmd()' should have created - // this. Since it doesn't exist, the zone is not under - // review. - - debug!( - "[{unit_name}] Got a review for {zone_name}/{zone_serial}, but it was not pending review" - ); - return Err(ZoneReviewError::NotUnderReview); - }; - - // Check that the zone was not already approved. - if matches!(version.review, ZoneVersionReviewState::Approved) { - // This version of the zone is no longer being reviewed. - // - // TODO: Differentiate this from 'NotUnderReview'? - - return Err(ZoneReviewError::NotUnderReview); - } - - version.review = new_review_state; - } - if matches!(decision, ZoneReviewDecision::Approve) { - info!("Signed zone '{zone_name}' with serial {zone_serial} has been approved."); - self.on_signed_zone_approved(center, zone, zone_serial); - } else { - error!( - "Signed zone '{zone_name}' with serial {zone_serial} has been rejected." - ); - // TODO: Whether to soft or hard reject should be part of the policy - let mut state = zone.state.lock().unwrap(); - ZoneHandle { - zone, - state: &mut state, - center, - } - .hard_reject_signed(); - } - } - - Source::Published => unreachable!(), - }; - - Ok(ZoneReviewOutput {}) - } } impl std::fmt::Debug for ZoneServer { diff --git a/src/zone/instance.rs b/src/zone/instance.rs new file mode 100644 index 00000000..8ebda2f6 --- /dev/null +++ b/src/zone/instance.rs @@ -0,0 +1,340 @@ +//! Instances of zones. +//! +//! An _instance_ is a snapshot of the contents of a zone (the specific records +//! within it). Cascade maintains information about various instances, e.g. how +//! they were generated and information needed to re-sign them. +//! +//! Instances can be classified by their origin and purpose: _loaded_ instances +//! are generated by Cascade's zone loader (by loading from a configured zone +//! source) and _signed_ instances are generated by the zone signer (by signing +//! a loaded instance or re-signing a prior signed instance). An "instance" in +//! general refers to a pair of loaded and signed instances (where the signed +//! instance was generated from the loaded instance). +//! +//! Instances (whether loaded or signed) can be in one of four states: +//! +//! - _Upcoming_ (i.e. _Next_): The instance is being generated and reviewed. If +//! all goes well, it will become _Current_. +//! +//! - _Current_ (i.e. _Published_): The instance has been accepted and is now +//! being published by Cascade as the latest authoritative version. +//! +//! - _Obsolete_: The instance was previously _Current_, but has since been +//! replaced by a new _Current_ instance. +//! +//! - _Abandoned_: The instance failed to be accepted (e.g. due to an error in +//! generation or being rejected at review). +//! +//! Instances are tracked by [`Instances`]. At any time, there are (at most) +//! one loaded+signed pair of upcoming instances, and one loaded+signed pair +//! of current instances. There may be any number of obsolete and abandoned +//! instances. + +use domain::new::base::Serial; + +//----------- Instances -------------------------------------------------------- + +/// State regarding instances of the zone. +/// +/// See the module-level documentation for more information. +#[derive(Debug, Default)] +pub struct Instances { + /// The upcoming instance, if any. + /// + /// This is [`Some`] when a new instance is being built (a new instance of + /// the zone is being loaded, or the zone is being re-signed). + pub upcoming: Option, + + /// The current/published instance. + /// + /// In general, this will be [`Some`]; most zones have a current instance. + /// It is [`None`] if an instance of the zone has never been accepted, or + /// the instance could not be restored from disk on startup. + pub current: Option, + // + // TODO: + // - The next usable loaded/signed instance IDs. + // These increase monotonically, even if instances are abandoned. + // - Obsolete instances. + // - Abandoned instances. +} + +/// # Initiating operations +impl Instances { + /// Initiate a new operation. + /// + /// An upcoming instance will be prepared. + /// + /// ## Panics + /// + /// Panics if an upcoming instance already exists. + pub fn start_load(&mut self) { + assert!( + self.upcoming.is_none(), + "Cannot start a load while an upcoming instance exists", + ); + + self.upcoming = Some(UpcomingInstance { + loaded: None, + signed: None, + }); + } + + /// Initiate a re-sign operation. + /// + /// ## Panics + /// + /// Panics if: + /// - An upcoming instance already exists. + /// - There is no current loaded instance of the zone. + pub fn start_resign(&mut self) { + assert!( + self.upcoming.is_none(), + "Cannot start a re-sign while an upcoming instance exists", + ); + + assert!( + self.current.is_some(), + "Cannot start a re-sign without a current loaded instance", + ); + + self.upcoming = Some(UpcomingInstance { + loaded: None, + signed: None, + }); + } + + /// Clear the current instance. + /// + /// This method should be called if the data for the zone is being cleared. + /// The current instance (if any) will be abandoned. + /// + /// ## Panics + /// + /// Panics if there is an upcoming instance of the zone. This method must be + /// called after the upcoming instance is resolved (applied or abandoned). + pub fn clear(&mut self) { + // TODO: Save the current instance to a list of obsolete ones. + + assert!( + self.upcoming.is_none(), + "Cannot modify the current instance while an upcoming one exists" + ); + + self.current = None; + } +} + +/// # Loading operations +impl Instances { + /// Finish an ongoing load. + /// + /// ## Panics + /// + /// Panics if: + /// - There is no upcoming instance of the zone. + /// - The upcoming instance is not empty. + pub fn finish_load(&mut self, serial: Serial) { + let Some(upcoming) = &mut self.upcoming else { + panic!("There is no upcoming instance of the zone"); + }; + + assert!( + upcoming.loaded.is_none() && upcoming.signed.is_none(), + "The upcoming instance is not empty" + ); + + upcoming.loaded = Some(LoadedInstance { serial }); + } +} + +/// # Signing operations +impl Instances { + /// Finish an ongoing (re-)sign. + /// + /// ## Panics + /// + /// Panics if: + /// - There is no upcoming instance of the zone. + /// - An upcoming signed instance already exists. + pub fn finish_sign(&mut self, serial: Serial) { + let Some(upcoming) = &mut self.upcoming else { + panic!("There is no upcoming instance of the zone"); + }; + + assert!( + upcoming.signed.is_none(), + "An upcoming signed instance already exists", + ); + + upcoming.signed = Some(SignedInstance { serial }); + } +} + +/// # Finalizing operations +impl Instances { + /// Switch from the current instance to the upcoming one. + /// + /// The upcoming instance (which must exist) will replace the current one. + /// + /// ## Panics + /// + /// Panics if: + /// - There is no upcoming instance. + /// - The upcoming instance is malformed. + /// - A re-sign took place but there is no current loaded instance. + pub fn switch(&mut self) { + // TODO: Save the current instance to a list of obsolete ones. + + let upcoming = self + .upcoming + .take() + .unwrap_or_else(|| panic!("There is no upcoming instance")); + let current = self.current.take(); + + match upcoming { + // A re-sign took place. + UpcomingInstance { + loaded: None, + signed: Some(signed), + } => { + let CurrentInstance { loaded, signed: _ } = current.unwrap_or_else(|| { + panic!("A re-sign took place but there is no current loaded instance") + }); + + self.current = Some(CurrentInstance { loaded, signed }); + } + + // A new loaded instance has appeared. + UpcomingInstance { + loaded: Some(loaded), + signed: Some(signed), + } => { + self.current = Some(CurrentInstance { loaded, signed }); + } + + // The upcoming instance is malformed. + _ => { + panic!("The upcoming instance is malformed") + } + } + } + + /// Abandon the upcoming instance entirely. + /// + /// ## Panics + /// + /// Panics if an upcoming instance does not exist. + pub fn abandon(&mut self) { + // TODO: Add the upcoming instance to a list of abandoned ones. + + assert!(self.upcoming.is_some(), "Nothing to abandon"); + + self.upcoming = None; + } +} + +//----------- CurrentInstance -------------------------------------------------- + +/// The current/published instance of a zone. +/// +/// This type holds information about the current (loaded and signed) instance +/// of a zone, which is published by Cascade. +#[derive(Debug)] +pub struct CurrentInstance { + /// The current loaded instance. + /// + /// This describes the instance loaded from the zone source. + pub loaded: LoadedInstance, + + /// The current signed instance. + /// + /// This describes the signed instance generated by Cascade. + pub signed: SignedInstance, + // + // TODO: + // - When the instance was published. +} + +//----------- UpcomingInstance ------------------------------------------------- + +/// An upcoming instance of a zone. +/// +/// This type holds information about an upcoming (loaded and signed) instance +/// of a zone, which is being generated by Cascade to replace the current one. +/// In case of a re-sign operation, only the signed instance is being replaced. +/// +/// There are two reasons an upcoming instance may exist: +/// +/// - A new instance of the zone is being loaded. The current instance can be +/// in any state. An upcoming loaded instance will exist, and an upcoming +/// signed instance might exist (if signing is enabled). +/// +/// - The current instance of the zone is being re-signed. A current loaded +/// instance exists, but a current signed instance might not (e.g. if signing +/// was disabled and has now been enabled). An upcoming loaded instance will +/// not exist, but an upcoming signed instance will exist. +#[derive(Debug)] +pub struct UpcomingInstance { + /// The upcoming loaded instance, if any. + /// + /// This describes the instance loaded from the zone source. It is [`Some`] + /// if/when loading is complete; if a re-sign operation is happening, it is + /// only [`None`]. + pub loaded: Option, + + /// The upcoming signed instance, if any. + /// + /// This describes the signed instance generated by Cascade. It is [`Some`] + /// if/when signing is complete. + pub signed: Option, + // + // TODO: + // - When the instance was initiated. + // - The cause of initiation (e.g. reception of DNS NOTIFY). + // - The policy used for generating the instance. + // (with such a field, policy changes during generation can be ignored.) +} + +//----------- LoadedInstance --------------------------------------------------- + +/// A loaded instance of a zone. +#[derive(Debug)] +pub struct LoadedInstance { + /// The SOA serial of this instance. + pub serial: Serial, + // + // TODO: + // - Instance ID. + // - How the instance was loaded (time, source, duration, size). + // - Whether the zone contains DNSSEC records (for pass-through mode). + // - Basic details of the zone: + // - How many records it contains (especially NS, DS, glue records). + // - Total memory usage. +} + +//----------- SignedInstance --------------------------------------------------- + +/// A signed instance of a zone. +#[derive(Debug)] +pub struct SignedInstance { + /// The SOA serial of this instance. + pub serial: Serial, + // + // TODO: + // - Instance ID. + // - How the instance was signed: + // - time, source, duration, size + // - Whether re-signing occurred, and what was re-signed. + // - Basic details of the signed zone: + // - How many records it contains (especially RRSIG, NSEC/NSEC3 records). + // - How many records from the loaded instance are included. + // - Total memory usage (excluding the loaded instance). + // - DNSSEC details: + // - NSEC or NSEC3 records (incl. rollovers). + // - ZONEMD (incl. algorithms and rollovers). + // - Zone signing keys (incl. rollovers). + // - The earliest signature expiration time. + // - Extensions/overrides used during signing + // - See 'apex_{remove,extra}'. +} diff --git a/src/zone/machine.rs b/src/zone/machine.rs index 0d6860ae..6d57d9fb 100644 --- a/src/zone/machine.rs +++ b/src/zone/machine.rs @@ -1,5 +1,5 @@ use cascade_api::ZoneReviewStatus; -use cascade_zonedata::{LoadedZoneBuilder, LoadedZoneBuilt, SignedZoneBuilder}; +use domain::new::base::Serial; use tracing::{info, trace}; use crate::{ @@ -100,7 +100,7 @@ impl ZoneStateMachine { /// # Initiating operations impl<'a> ZoneHandle<'a> { - pub(crate) fn try_start_load(&mut self) -> Option { + pub(crate) fn try_start_load(&mut self) -> Option { // It's important that we first check the storage here instead of the // zone state machine. The reason is that while the zone state machine // is in the waiting state, the storage might still be persisting or @@ -120,12 +120,14 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::Loading(waiting.start_load())); + self.state.instances.start_load(); + self.state.record_event(HistoricalEvent::StartedLoad, None); Some(builder) } - pub(crate) fn try_start_resign(&mut self) -> Option { + pub(crate) fn try_start_resign(&mut self) -> Option { // It's important that we first check the storage here instead of the // zone state machine. The reason is that while the zone state machine // is in the waiting state, the storage might still be persisting or @@ -146,6 +148,8 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::Signing(waiting.start_resign())); + self.state.instances.start_resign(); + self.state.record_event(HistoricalEvent::StartedLoad, None); Some(builder) @@ -154,7 +158,7 @@ impl<'a> ZoneHandle<'a> { /// # Loading operations impl<'a> ZoneHandle<'a> { - pub(crate) fn abandon_load(&mut self, builder: LoadedZoneBuilder) { + pub(crate) fn abandon_load(&mut self, builder: cascade_zonedata::LoadedZoneBuilder) { let (transition, state) = self.state.machine.transition(); let ZoneStateMachine::Loading(loaded) = state else { @@ -164,9 +168,12 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::Waiting(loaded.abandon_load())); self.storage().abandon_load(builder); + + // Abandon the entire upcoming instance. + self.state.instances.abandon(); } - pub(crate) fn finish_load(&mut self, built: LoadedZoneBuilt) { + pub(crate) fn finish_load(&mut self, built: cascade_zonedata::LoadedZoneBuilt, serial: Serial) { let (transition, state) = self.state.machine.transition(); let ZoneStateMachine::Loading(loaded) = state else { @@ -176,6 +183,8 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::LoadedReview(loaded.finish_load())); self.storage().finish_load(built); + + self.state.instances.finish_load(serial); } } @@ -215,6 +224,9 @@ impl<'a> ZoneHandle<'a> { }; transition.move_to(ZoneStateMachine::Waiting(loaded.soft_reject())); + + // Abandon the entire upcoming instance. + self.state.instances.abandon(); } pub(crate) fn hard_reject_loaded(&mut self) { @@ -275,14 +287,17 @@ impl<'a> ZoneHandle<'a> { self.signer().enqueue_new_sign(builder); } - pub(crate) fn finish_signing(&mut self, built: cascade_zonedata::SignedZoneBuilt) { + pub(crate) fn finish_signing( + &mut self, + built: cascade_zonedata::SignedZoneBuilt, + serial: Serial, + ) { self.state.record_event( // TODO: Get the right trigger. HistoricalEvent::SigningSucceeded { trigger: SigningTrigger::Load.into(), }, - // TODO: Get the serial in here. - None, + Some(u32::from(serial).into()), ); let (transition, state) = self.state.machine.transition(); @@ -294,10 +309,12 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::SignedReview(signing.finish_signing())); self.storage().finish_sign(built); + + self.state.instances.finish_sign(serial); } - // Abandon the ongoing signing operation (but not due to failure). - pub(crate) fn abandon_signing(&mut self, builder: SignedZoneBuilder) { + /// Abandon the ongoing signing operation (but not due to failure). + pub(crate) fn abandon_signing(&mut self, builder: cascade_zonedata::SignedZoneBuilder) { let (transition, state) = self.state.machine.transition(); let ZoneStateMachine::Signing(signing) = state else { @@ -309,9 +326,16 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::Waiting(signing.abandon())); self.storage().abandon_sign(builder); + + // Abandon the entire upcoming instance. + self.state.instances.abandon(); } - pub(crate) fn signing_failed(&mut self, builder: SignedZoneBuilder, err: SignerError) { + pub(crate) fn signing_failed( + &mut self, + builder: cascade_zonedata::SignedZoneBuilder, + err: SignerError, + ) { let (transition, state) = self.state.machine.transition(); let ZoneStateMachine::Signing(signing) = state else { @@ -321,6 +345,9 @@ impl<'a> ZoneHandle<'a> { transition.move_to(ZoneStateMachine::SigningFailed(signing.signing_failed(err))); self.storage().abandon_sign(builder); + + // Abandon the entire upcoming instance. + self.state.instances.abandon(); } } @@ -359,6 +386,9 @@ impl<'a> ZoneHandle<'a> { }; transition.move_to(ZoneStateMachine::Waiting(signed.soft_reject())); + + // Abandon the entire upcoming instance. + self.state.instances.abandon(); } pub(crate) fn hard_reject_signed(&mut self) { @@ -376,6 +406,9 @@ impl<'a> ZoneHandle<'a> { }; transition.move_to(ZoneStateMachine::HaltSigned(review.hard_reject())); + + // Abandon the entire upcoming instance. + self.state.instances.abandon(); } } @@ -401,6 +434,8 @@ impl<'a> ZoneHandle<'a> { }; transition.move_to(ZoneStateMachine::Waiting(signed.approve())); + self.state.instances.switch(); + self.storage().start_cleanup(cleaner); } } @@ -415,21 +450,24 @@ impl<'a> ZoneHandle<'a> { let waiting = halt_loaded.reset(); transition.move_to(ZoneStateMachine::Waiting(waiting)); self.storage().abandon_loaded_review(); + self.state.instances.abandon(); } ZoneStateMachine::HaltSigned(halt_signed) => { let waiting = halt_signed.reset(); transition.move_to(ZoneStateMachine::Waiting(waiting)); self.storage().abandon_signed_review(); + self.state.instances.abandon(); } ZoneStateMachine::SigningFailed(signing_failed) => { let waiting = signing_failed.reset(); transition.move_to(ZoneStateMachine::Waiting(waiting)); + self.state.instances.abandon(); } _ => { transition.move_to(state); return Err(()); } - }; + } Ok(()) } @@ -553,7 +591,7 @@ pub struct Loading {} impl Loading { fn finish_load(self) -> LoadedReview { - LoadedReview {} + LoadedReview { decided: false } } fn abandon_load(self) -> Waiting { @@ -561,8 +599,15 @@ impl Loading { } } +/// An upcoming loaded instance is being reviewed. #[derive(Debug)] -pub struct LoadedReview {} +pub struct LoadedReview { + /// Whether a review decision has been received. + /// + /// If this is `true`, an approval/rejection for the zone has been received; + /// it is being processed. + pub decided: bool, +} impl LoadedReview { fn approve(self) -> Signing { @@ -596,7 +641,7 @@ pub struct Signing {} impl Signing { fn finish_signing(self) -> SignedReview { - SignedReview {} + SignedReview { decided: false } } /// Abandon the signing operation (but not due to failure). @@ -620,8 +665,15 @@ impl SigningFailed { } } +/// An upcoming signed instance is being reviewed. #[derive(Debug)] -pub struct SignedReview {} +pub struct SignedReview { + /// Whether a review decision has been received. + /// + /// If this is `true`, an approval/rejection for the zone has been received; + /// it is being processed. + pub decided: bool, +} impl SignedReview { fn approve(self) -> Waiting { diff --git a/src/zone/mod.rs b/src/zone/mod.rs index 7a4d635d..cfb9d215 100644 --- a/src/zone/mod.rs +++ b/src/zone/mod.rs @@ -36,6 +36,9 @@ use crate::units::zone_signer::faketime_or_now; mod storage; pub use storage::{StorageState, StorageZoneHandle}; +mod instance; +pub use instance::{CurrentInstance, Instances, LoadedInstance, SignedInstance, UpcomingInstance}; + pub mod machine; pub mod state; @@ -181,9 +184,6 @@ pub struct ZoneState { /// The policy (version) used by the zone. pub policy: Option>, - /// Metadata related to the last published zone version. - pub last_published: Option, - /// An enqueued save of this state. /// /// The enqueued save operation will persist the current state in a short @@ -239,11 +239,8 @@ pub struct ZoneState { /// serial for the Increment serial policy. pub previous_serial: Option, - /// Unsigned versions of the zone. - pub unsigned: foldhash::HashMap, - - /// Signed versions of the zone. - pub signed: foldhash::HashMap, + /// Instances of the zone. + pub instances: Instances, /// History of interesting events that occurred for this zone. pub history: Vec, @@ -293,7 +290,6 @@ impl Default for ZoneState { Self { machine: Default::default(), policy: Default::default(), - last_published: Default::default(), enqueued_save: Default::default(), min_expiration: Default::default(), next_min_expiration: Default::default(), @@ -303,8 +299,7 @@ impl Default for ZoneState { key_roll: Default::default(), last_signature_refresh: faketime_or_now(), previous_serial: Default::default(), - unsigned: Default::default(), - signed: Default::default(), + instances: Default::default(), history: Default::default(), loader: Default::default(), signer: Default::default(), @@ -314,55 +309,6 @@ impl Default for ZoneState { } } -#[derive(Debug)] -pub struct LastPublished { - pub loaded_serial: Serial, - pub signed_serial: Serial, - // TODO: - // - time of publish - // - number of records - // - size in bytes -} - -/// The state of an unsigned version of a zone. -#[derive(Clone, Debug)] -pub struct UnsignedZoneVersionState { - /// The review state of the zone version. - pub review: ZoneVersionReviewState, -} - -/// The state of a signed version of a zone. -#[derive(Clone, Debug)] -pub struct SignedZoneVersionState { - /// The serial number of the corresponding unsigned version of the zone. - pub unsigned_serial: Serial, - - /// The review state of the zone version. - pub review: ZoneVersionReviewState, -} - -/// The review state of a version of a zone. -#[derive(Clone, Debug, Default)] -pub enum ZoneVersionReviewState { - /// The zone is pending review. - /// - /// If a review script has been configured, it is running now. Otherwise, - /// the zone must be manually reviewed. - #[default] - Pending, - - /// The zone has been approved. - /// - /// This is a terminal state. The zone may have progressed further through - /// the pipeline, so it is no longer possible to reject it. - Approved, - - /// The zone has been rejected. - /// - /// The zone has not yet been approved; it can be approved at any time. - Rejected, -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct HistoryItem { pub when: SystemTime, diff --git a/src/zone/storage.rs b/src/zone/storage.rs index aa8013d5..06a55ab0 100644 --- a/src/zone/storage.rs +++ b/src/zone/storage.rs @@ -29,16 +29,15 @@ use cascade_zonedata::{ LoadedZoneBuilder, LoadedZoneBuilt, LoadedZonePersisted, LoadedZonePersister, LoadedZoneRestored, LoadedZoneRestorer, LoadedZoneReviewer, SignedZoneBuilder, SignedZoneBuilt, SignedZonePersisted, SignedZonePersister, SignedZoneRestored, SignedZoneRestorer, - SignedZoneReviewer, SoaRecord, ZoneCleaner, ZoneDataStorage, + SignedZoneReviewer, ZoneCleaner, ZoneDataStorage, }; -use domain::base::Serial; use tracing::{info, trace, trace_span, warn}; use crate::{ center::Center, server::{LoadedReviewServer, PublicationServer, SignedReviewServer}, util::BackgroundTasks, - zone::{HistoricalEvent, LastPublished, Zone, ZoneHandle, ZoneState}, + zone::{HistoricalEvent, Zone, ZoneHandle, ZoneState}, }; //----------- StorageZoneHandle ------------------------------------------------ @@ -185,8 +184,6 @@ impl StorageZoneHandle<'_> { fields(zone = %self.zone.name), )] fn start_loaded_review(&mut self, loaded_reviewer: LoadedZoneReviewer) { - self.state.storage.loaded_review_soa = loaded_reviewer.read().map(|r| r.soa().clone()); - let zone = self.zone.clone(); let center = self.center.clone(); let span = trace_span!("start_loaded_review"); @@ -269,8 +266,6 @@ impl StorageZoneHandle<'_> { info!("The loaded instance has been rejected; cleaning it up"); let (s, loaded_reviewer) = s.give_up(); - self.state.storage.loaded_review_soa = - loaded_reviewer.read().map(|r| r.soa().clone()); transition.move_to(ZoneDataStorage::CleanLoadedPending(s)); loaded_reviewer } @@ -396,8 +391,6 @@ impl StorageZoneHandle<'_> { trace!("Abandoning the ongoing sign operation"); let (s, loaded_reviewer) = s.give_up(builder); - self.state.storage.loaded_review_soa = - loaded_reviewer.read().map(|r| r.soa().clone()); transition.move_to(ZoneDataStorage::CleanLoadedPending(s)); loaded_reviewer } @@ -421,8 +414,6 @@ impl StorageZoneHandle<'_> { fields(zone = %self.zone.name), )] fn start_signed_review(&mut self, signed_reviewer: SignedZoneReviewer) { - self.state.storage.signed_review_soa = signed_reviewer.read().map(|r| r.soa().clone()); - let zone = self.zone.clone(); let center = self.center.clone(); let span = trace_span!("start_signed_review"); @@ -504,10 +495,6 @@ impl StorageZoneHandle<'_> { let new_s; (new_s, loaded_reviewer, signed_reviewer) = s.give_up(); transition.move_to(ZoneDataStorage::CleanWholePending(new_s)); - self.state.storage.loaded_review_soa = - loaded_reviewer.read().map(|r| r.soa().clone()); - self.state.storage.signed_review_soa = - signed_reviewer.read().map(|r| r.soa().clone()); } _ => panic!("The zone is not undergoing signer review"), @@ -736,9 +723,6 @@ impl StorageZoneHandle<'_> { ), }; - self.state.storage.published_soa = viewer.read().map(|r| r.soa().clone()); - self.state.storage.published_loaded_soa = viewer.read().map(|r| r.loaded().soa().clone()); - // Spawn a background task to update the publication server. let span = trace_span!("switch_publication_server"); let zone = self.zone.clone(); @@ -768,31 +752,6 @@ impl StorageZoneHandle<'_> { _ => unreachable!("just transitioned to 'Switching'"), }; - handle.state.last_published = Some(LastPublished { - loaded_serial: Serial( - handle - .state - .storage - .published_loaded_soa - .as_ref() - .unwrap() - .rdata - .serial - .into(), - ), - signed_serial: Serial( - handle - .state - .storage - .published_soa - .as_ref() - .unwrap() - .rdata - .serial - .into(), - ), - }); - handle.finish_switch(cleaner); handle.state.storage.background_tasks.finish(); @@ -898,33 +857,6 @@ pub struct StorageState { /// passed to [`StorageZoneHandle::abandon_loaded_restoration()`]. pub restorer: Option, - /// The SOA record of the loaded instance of the zone being reviewed, if - /// any. - // - // TODO: This should move into a component of 'ZoneState' tracking the - // upcoming zone instance. - pub loaded_review_soa: Option, - - /// The SOA record of the signed instance of the zone being reviewed, if - /// any. - // - // TODO: This should move into a component of 'ZoneState' tracking the - // upcoming zone instance. - pub signed_review_soa: Option, - - /// The SOA record of the published instance of the zone, if any. - // - // TODO: This should move into a component of 'ZoneState' tracking the - // current i.e. published zone instance. - pub published_soa: Option, - - /// The SOA record of the loaded instance underlying the published instance - /// of the zone, if any. - // - // TODO: This should move into a component of 'ZoneState' tracking the - // current i.e. published zone instance. - pub published_loaded_soa: Option, - /// Ongoing background tasks. /// /// When the zone data needs to be cleaned or persisted, a background task @@ -940,10 +872,6 @@ impl StorageState { Self { machine, restorer: Some(restorer), - loaded_review_soa: None, - signed_review_soa: None, - published_soa: None, - published_loaded_soa: None, background_tasks: Default::default(), } }