From 4f1625fc220159d2ff29ce16bfbc65cd848d0548 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 5 Oct 2026 11:49:13 +0200 Subject: [PATCH] Initial changes. Not complete yet. --- src/commands/keyset/cmd.rs | 764 ++++++++++++++++++++++++++++++++++--- 1 file changed, 714 insertions(+), 50 deletions(-) diff --git a/src/commands/keyset/cmd.rs b/src/commands/keyset/cmd.rs index 8667cf5..ab97432 100644 --- a/src/commands/keyset/cmd.rs +++ b/src/commands/keyset/cmd.rs @@ -551,19 +551,20 @@ enum SetCommands { enum RollCommands { /// Start a key roll. StartRoll, - /// Report that the first propagation step has completed. - Propagation1Complete { + + /// Report the before-ttl for the propagation1 and propagation2 steps. + BeforeTtl { /// The TTL that is required to be reported by the Report actions. ttl: u32, }, + + /// Report that the first propagation step has completed. + Propagation1Complete, /// Cached information from before Propagation1Complete should have /// expired by now. CacheExpired1, /// Report that the second propagation step has completed. - Propagation2Complete { - /// The TTL that is required to be reported by the Report actions. - ttl: u32, - }, + Propagation2Complete, /// Cached information from before Propagation2Complete should have /// expired by now. CacheExpired2, @@ -908,18 +909,16 @@ impl Keyset { let (new_stored, _) = ws.new_csk_or_ksk_zsk(env)?; let new: Vec<_> = new_stored.iter().map(|v| v.as_ref()).collect(); - let actions = ws - .state + ws.state .keyset .start_roll(RollType::AlgorithmRoll, &[], &new) .expect("should not happen"); - ws.handle_actions(&actions, env, true)?; ws.state .internal .insert(RollType::AlgorithmRoll, Default::default()); - print_actions(&actions); + ws.init_before_ttl(RollType::AlgorithmRoll); } ws.state_changed = true; } @@ -2197,6 +2196,8 @@ impl Clone for RollStateReports { /// State for the report progration checks. #[derive(Clone, Debug, Default, Deserialize, Serialize)] struct ReportState { + /// State for before-ttl checks. + before_ttl: BeforeTtl, /// State for DNSKEY propagation checks. dnskey: Option, /// State for DS propagation checks. @@ -2205,6 +2206,70 @@ struct ReportState { rrsig: Option, } +/// State of before-ttl checks. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +struct BeforeTtl { + /// Current state of the checks. + state: BeforeTtlState, + /// State of DNSKEY checks. + dnskey: Option, + /// State of DS checks. + ds: Option, + /// State of RRSIG checks. + rrsigs: Option, +} + +/// Current state of before-ttl checks. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +enum BeforeTtlState { + /// No check needed. + #[default] + Idle, + /// Wait for check to be completed. + Wait, + /// Result of the check (with the before-ttl). + Report(Ttl), +} + +/// State to capture the before-ttl of DS or DNSKEY RRsets. +#[derive(Clone, Debug, Deserialize, Serialize)] +enum BeforeTtlDsDnskey { + /// Before-ttl to report. + Report(Ttl), + /// State to capture the before-ttl of all nameservers. + Wait { + /// Wait until this time before trying again. + until: UnixTime, + /// Before-ttl of nameservers that responded. + servers: HashMap, + /// Notice why we have to wait. + notice: String, + }, +} + +/// State to capture the before-ttl of RRSIG records. +#[derive(Clone, Debug, Deserialize, Serialize)] +enum BeforeTtlRrsigs { + /// Before-ttl to report. + Report(Ttl), + /// State to capture the before-ttl. + Wait { + /// Wait until this time before trying again. + until: UnixTime, + /// Notice why we have to wait. + notice: String, + }, +} + +/// Whether to record the before TTL for the DS RRset or the DNSKEY RRset. +#[derive(Clone, Copy)] +enum DsOrDnskey { + /// Get before-ttl for the DNSKEY RRset. + Dnskey, + /// Get before-ttl for the DS RRset. + Ds, +} + // Put functions here that take WorkSpace as a parameter. impl WorkSpace { /// Implement the get subcommand. @@ -2435,37 +2500,113 @@ impl WorkSpace { ) -> Result<(), Error> { let actions = match cmd { RollCommands::StartRoll => { - let actions = match roll_variant { - RollVariant::Ksk => self.start_ksk_roll(env, true)?, - RollVariant::Zsk => self.start_zsk_roll(env, true)?, - RollVariant::Csk => self.start_csk_roll(env, true)?, - RollVariant::Algorithm => self.start_algorithm_roll(env, true)?, + match roll_variant { + RollVariant::Ksk => self.start_ksk_roll(env)?, + RollVariant::Zsk => self.start_zsk_roll(env)?, + RollVariant::Csk => self.start_csk_roll(env)?, + RollVariant::Algorithm => self.start_algorithm_roll(env)?, }; - print_actions(&actions); self.state_changed = true; return Ok(()); } - RollCommands::Propagation1Complete { ttl } => { + RollCommands::BeforeTtl { ttl } => { let roll = roll_variant.roll_variant_to_roll(&self.config); - self.state + let Some(roll_state) = self.state.keyset.rollstates().get(&roll) else { + return Err(format!("roll {roll:?} not in progress at the moment").into()); + }; + + let state_lock = match roll_state { + RollState::Propagation1 => { + &self + .state + .internal + .get_mut(&roll) + .expect("should exist") + .propagation1 + } + + RollState::Propagation2 => { + &self + .state + .internal + .get_mut(&roll) + .expect("should exist") + .propagation2 + } + RollState::CacheExpire1(_) | RollState::CacheExpire2(_) | RollState::Done => { + return Err("before-ttl is not needed in this roll state".into()) + } + }; + let mut state = state_lock.lock().expect("should not fail"); + let max_ttl = if let BeforeTtlState::Report(old_ttl) = state.before_ttl.state { + max(Ttl::from_secs(ttl), old_ttl) + } else { + Ttl::from_secs(ttl) + }; + state.before_ttl.state = BeforeTtlState::Report(max_ttl); + state.before_ttl.dnskey = None; + state.before_ttl.ds = None; + state.before_ttl.rrsigs = None; + self.state_changed = true; + self.state.keyset.actions(roll) + } + RollCommands::Propagation1Complete => { + let roll = roll_variant.roll_variant_to_roll(&self.config); + let roll_state = self + .state + .internal + .get(&roll) + .expect("should exist") + .propagation1 + .lock() + .expect("should not fail"); + if matches!(roll_state.before_ttl.state, BeforeTtlState::Wait) { + return Err("report before-ttl first".into()); + } + let BeforeTtlState::Report(ttl) = roll_state.before_ttl.state else { + panic!("before-ttl should be set"); + }; + let actions = self + .state .keyset - .propagation1_complete(roll, ttl) - .map_err(|err| format!("Error reporting propagation complete: {err}\n"))? + .propagation1_complete(roll, ttl.as_secs()) + .map_err(|err| format!("Error reporting propagation complete: {err}\n"))?; + actions } RollCommands::CacheExpired1 => { let roll = roll_variant.roll_variant_to_roll(&self.config); self.state .keyset .cache_expired1(roll) - .map_err(|err| format!("Error reporting cache expired: {err}\n"))? + .map_err(|err| format!("Error reporting cache expired: {err}\n"))?; + self.init_before_ttl(roll); + + // Handle actions when the before-ttl has been recorded. + Vec::new() } - RollCommands::Propagation2Complete { ttl } => { + RollCommands::Propagation2Complete => { let roll = roll_variant.roll_variant_to_roll(&self.config); - self.state + let roll_state = self + .state + .internal + .get(&roll) + .expect("should exist") + .propagation2 + .lock() + .expect("should not fail"); + if matches!(roll_state.before_ttl.state, BeforeTtlState::Wait) { + return Err("report before-ttl first".into()); + } + let BeforeTtlState::Report(ttl) = roll_state.before_ttl.state else { + panic!("before-ttl should be set"); + }; + let actions = self + .state .keyset - .propagation2_complete(roll, ttl) - .map_err(|err| format!("Error reporting propagation complete: {err}\n"))? + .propagation2_complete(roll, ttl.as_secs()) + .map_err(|err| format!("Error reporting propagation complete: {err}\n"))?; + actions } RollCommands::CacheExpired2 => { let roll = roll_variant.roll_variant_to_roll(&self.config); @@ -3651,6 +3792,51 @@ impl WorkSpace { Ok(()) } + /// Initialize before-ttl state when needed. + fn init_before_ttl(&mut self, roll: RollType) { + // We need to capture the before TTL for report actions. Assume that + // only the propagation steps have report actions. + + let roll_state = self + .state + .keyset + .rollstates() + .get(&roll) + .expect("roll should exist"); + + let internal_state = self.state.internal.get_mut(&roll).expect("should be there"); + let internal_roll_state = if matches!(roll_state, RollState::Propagation1) { + &internal_state.propagation1 + } else if matches!(roll_state, RollState::Propagation2) { + &internal_state.propagation2 + } else { + // Nothing to do. + return; + }; + + // Check if there are Report actions. + if self.state.keyset.actions(roll).iter().any(|a| match a { + Action::UpdateDnskeyRrset + | Action::CreateCdsRrset + | Action::RemoveCdsRrset + | Action::UpdateDsRrset + | Action::UpdateRrsig + | Action::WaitDnskeyPropagated + | Action::WaitDsPropagated + | Action::WaitRrsigPropagated => false, + Action::ReportDnskeyPropagated + | Action::ReportDsPropagated + | Action::ReportRrsigPropagated => true, + }) { + // First capture before TTL. + internal_roll_state + .lock() + .expect("should not fail") + .before_ttl + .state = BeforeTtlState::Wait; + } + } + /// Handle the actions that result from key roll steps that always need to /// be handled independent of automation. /// @@ -3708,7 +3894,7 @@ impl WorkSpace { } /// Start a KSK roll. - fn start_ksk_roll(&mut self, env: &impl Env, verbose: bool) -> Result, Error> { + fn start_ksk_roll(&mut self, env: &impl Env) -> Result<(), Error> { let roll_type = match self.config.ksk_roll_type { KskRollType::DoubleSignatureKskRoll => RollType::KskRoll, KskRollType::DoubleDsKskRoll => RollType::KskDoubleDsRoll, @@ -3767,7 +3953,7 @@ impl WorkSpace { let new = [ksk_pub_url.as_ref()]; // Start the key roll - let actions = match self + match self .state .keyset .start_roll(roll_type, &old, &new) @@ -3781,13 +3967,17 @@ impl WorkSpace { return Err(e.into()); } }; - self.handle_actions(&actions, env, verbose)?; + self.state.internal.insert(roll_type, Default::default()); - Ok(actions) + self.init_before_ttl(roll_type); + + // Assume there are never any actions to execute because we need to + // capture the before TTLs first. + Ok(()) } /// Start a ZSK roll. - fn start_zsk_roll(&mut self, env: &impl Env, verbose: bool) -> Result, Error> { + fn start_zsk_roll(&mut self, env: &impl Env) -> Result<(), Error> { let roll_type = match self.config.zsk_roll_type { ZskRollType::PrePublishZskRoll => RollType::ZskRoll, ZskRollType::DoubleSignatureZskRoll => RollType::ZskDoubleSignatureRoll, @@ -3846,7 +4036,7 @@ impl WorkSpace { let new = [zsk_pub_url.as_ref()]; // Start the key roll - let actions = match self + match self .state .keyset .start_roll(roll_type, &old, &new) @@ -3861,13 +4051,16 @@ impl WorkSpace { } }; - self.handle_actions(&actions, env, verbose)?; self.state.internal.insert(roll_type, Default::default()); - Ok(actions) + self.init_before_ttl(roll_type); + + // Assume there are never any actions to execute because we need to + // capture the before TTLs first. + Ok(()) } /// Start a CSK roll. - fn start_csk_roll(&mut self, env: &impl Env, verbose: bool) -> Result, Error> { + fn start_csk_roll(&mut self, env: &impl Env) -> Result<(), Error> { let roll_type = RollType::CskRoll; assert!(!self.state.keyset.keys().is_empty()); @@ -3899,7 +4092,7 @@ impl WorkSpace { let new: Vec<_> = new_stored.iter().map(|v| v.as_ref()).collect(); // Start the key roll - let actions = match self + match self .state .keyset .start_roll(roll_type, &old, &new) @@ -3915,17 +4108,16 @@ impl WorkSpace { } }; - self.handle_actions(&actions, env, verbose)?; self.state.internal.insert(roll_type, Default::default()); - Ok(actions) + self.init_before_ttl(roll_type); + + // Assume there are never any actions to execute because we need to + // capture the before TTLs first. + Ok(()) } /// Start an algorithm roll. - fn start_algorithm_roll( - &mut self, - env: &impl Env, - verbose: bool, - ) -> Result, Error> { + fn start_algorithm_roll(&mut self, env: &impl Env) -> Result<(), Error> { let roll_type = RollType::AlgorithmRoll; assert!(!self.state.keyset.keys().is_empty()); @@ -3954,7 +4146,7 @@ impl WorkSpace { let new: Vec<_> = new_stored.iter().map(|v| v.as_ref()).collect(); // Start the key roll - let actions = match self + match self .state .keyset .start_roll(roll_type, &old, &new) @@ -3970,9 +4162,12 @@ impl WorkSpace { } }; - self.handle_actions(&actions, env, verbose)?; self.state.internal.insert(roll_type, Default::default()); - Ok(actions) + self.init_before_ttl(roll_type); + + // Assume there are never any actions to execute because we need to + // capture the before TTLs first. + Ok(()) } /// This function automatically starts a key roll when the conditions are right. @@ -3990,7 +4185,7 @@ impl WorkSpace { env: Env, conficting_roll: impl Fn(RollType) -> bool, match_keytype: impl Fn(KeyType) -> Option, - start_roll: impl Fn(&mut WorkSpace, Env, bool) -> Result, Error>, + start_roll: impl Fn(&mut WorkSpace, Env) -> Result<(), Error>, ) -> Result<(), Error> { let now = self.faketime_or_now(); if let Some(validity) = validity { @@ -4026,7 +4221,7 @@ impl WorkSpace { .min(); if let Some(next) = next { if next < now { - start_roll(self, env, false)?; + start_roll(self, env)?; self.state_changed = true; } } @@ -4036,12 +4231,403 @@ impl WorkSpace { Ok(()) } + /// Try to capture the before-ttl for a key roll. + async fn capture_before_ttl( + &mut self, + roll: RollType, + state: &RollState, + now: UnixTime, + env: &impl Env, + ) -> Result<(), Error> { + let report_state = self.state.internal.get(&roll).expect("should not fail"); + let internal_roll_state_lock = match state { + RollState::Propagation1 => &report_state.propagation1, + RollState::Propagation2 => &report_state.propagation2, + _ => unreachable!(), + }; + + { + let internal_roll_state = internal_roll_state_lock.lock().expect("should not fail"); + if !matches!(&internal_roll_state.before_ttl.state, BeforeTtlState::Wait) { + // nothing to do + return Ok(()); + } + } + + let mut max_ttl = Ttl::ZERO; + for action in self.state.keyset.actions(roll) { + match action { + Action::UpdateDnskeyRrset + | Action::CreateCdsRrset + | Action::RemoveCdsRrset + | Action::UpdateDsRrset + | Action::UpdateRrsig + | Action::WaitDnskeyPropagated + | Action::WaitDsPropagated + | Action::WaitRrsigPropagated => (), + Action::ReportDnskeyPropagated => { + let res = Self::before_ttl_ds_dnskey( + DsOrDnskey::Dnskey, + internal_roll_state_lock, + self.state.keyset.name(), + now.clone(), + ) + .await; + internal_roll_state_lock + .lock() + .expect("should not fail") + .before_ttl + .dnskey = Some(res.clone()); + self.state_changed = true; + match res { + BeforeTtlDsDnskey::Wait { .. } => return Ok(()), + BeforeTtlDsDnskey::Report(ttl) => { + max_ttl = max(max_ttl, ttl); + } + } + } + Action::ReportDsPropagated => { + let res = Self::before_ttl_ds_dnskey( + DsOrDnskey::Ds, + internal_roll_state_lock, + self.state.keyset.name(), + now.clone(), + ) + .await; + (internal_roll_state_lock + .lock() + .expect("should not fail") + .before_ttl + .ds) = Some(res.clone()); + self.state_changed = true; + match res { + BeforeTtlDsDnskey::Wait { .. } => return Ok(()), + BeforeTtlDsDnskey::Report(ttl) => { + max_ttl = max(max_ttl, ttl); + } + } + } + Action::ReportRrsigPropagated => { + let res = Self::before_ttl_rrsigs( + self.state.keyset.name(), + &self.config.nameservers, + &self.tsig_store, + ) + .await; + + let res = match res { + Ok(res) => res, + Err(e) => BeforeTtlRrsigs::Wait { + until: now.clone() + DEFAULT_WAIT, + notice: e.to_string(), + }, + }; + + (internal_roll_state_lock + .lock() + .expect("should not fail") + .before_ttl + .rrsigs) = Some(res.clone()); + self.state_changed = true; + + match res { + BeforeTtlRrsigs::Wait { .. } => return Ok(()), + BeforeTtlRrsigs::Report(ttl) => { + max_ttl = max(max_ttl, ttl); + } + } + } + } + } + + { + let mut internal_roll_state = internal_roll_state_lock.lock().expect("should not fail"); + internal_roll_state.before_ttl.state = BeforeTtlState::Report(max_ttl); + self.state_changed = true; + } + let actions = self.state.keyset.actions(roll); + self.handle_actions(&actions, env, true)?; + Ok(()) + } + + /// Try to capture the before-ttl for the DNSKEY RRset or for the DS RRset. + async fn before_ttl_ds_dnskey( + mode: DsOrDnskey, + internal_roll_state_lock: &Mutex, + zone_name: &Name>, + now: UnixTime, + ) -> BeforeTtlDsDnskey { + let old_roll_state = { + let mut internal_roll_state = internal_roll_state_lock.lock().expect("should not fail"); + let opt_roll_state = match mode { + DsOrDnskey::Ds => &mut internal_roll_state.before_ttl.ds, + DsOrDnskey::Dnskey => &mut internal_roll_state.before_ttl.dnskey, + }; + if let Some(roll_state) = opt_roll_state { + match roll_state { + BeforeTtlDsDnskey::Report(_) => return roll_state.clone(), + BeforeTtlDsDnskey::Wait { .. } => roll_state.clone(), + } + } else { + BeforeTtlDsDnskey::Wait { + until: now.clone(), + servers: HashMap::new(), + notice: "".to_string(), + } + } + }; + let query_zone = match mode { + DsOrDnskey::Ds => match parent_zone(zone_name).await { + Ok(name) => name, + Err(e) => { + warn!("Getting parent zone name of {zone_name} failed: {e}"); + match old_roll_state { + BeforeTtlDsDnskey::Report(_) => unreachable!(), + BeforeTtlDsDnskey::Wait { servers, .. } => { + return BeforeTtlDsDnskey::Wait { + until: now + DEFAULT_WAIT, + servers, + notice: format!( + "Getting parent zone name of {zone_name} failed: {e}" + ), + } + } + } + } + }, + DsOrDnskey::Dnskey => zone_name.clone(), + }; + let addresses = match addresses_for_zone(&query_zone).await { + Ok(a) => a, + Err(e) => { + warn!("Getting nameserver addresses for {query_zone} failed: {e}"); + match old_roll_state { + BeforeTtlDsDnskey::Report(_) => unreachable!(), + BeforeTtlDsDnskey::Wait { servers, .. } => { + return BeforeTtlDsDnskey::Wait { + until: now + DEFAULT_WAIT, + servers, + notice: format!( + "Getting nameserver addresses for {query_zone} failed: {e}" + ), + } + } + } + } + }; + + // addresses_for_zone returns at least one address. + assert!(!addresses.is_empty()); + + let futures: Vec<_> = addresses + .iter() + .map(|a| Self::before_ttl_ds_dnskey_for_address(mode, &old_roll_state, zone_name, a)) + .collect(); + let res: Vec<_> = join_all(futures).await; + + let mut servers = match old_roll_state { + BeforeTtlDsDnskey::Report(_) => unreachable!(), + BeforeTtlDsDnskey::Wait { servers, .. } => servers, + }; + + // Be paranoid. The variable max_ttl is set to None initially to make + // sure that we only return a value if something has been assigned + // during the loop. + let mut max_ttl = None; + for r in res { + let (address, ttl) = match r { + Ok((address, ttl)) => (address, ttl), + Err(e) => { + warn!("before-ttl check failed: {e}"); + return BeforeTtlDsDnskey::Wait { + until: now + DEFAULT_WAIT, + servers, + notice: format!("before-ttl check failed: {e}"), + }; + } + }; + + servers.insert(address, ttl); + max_ttl = Some(max(max_ttl.unwrap_or(Ttl::from_secs(0)), ttl)); + } + + // We can only get here with Some(Ttl) because there is at least one + // address. + let max_ttl = max_ttl.expect("cannot be None"); + BeforeTtlDsDnskey::Report(max_ttl) + } + + /// Try to capture the before-ttl for the DNSKEY RRset or for the DS RRset + /// from one nameserver address. + async fn before_ttl_ds_dnskey_for_address( + mode: DsOrDnskey, + old_roll_state: &BeforeTtlDsDnskey, + zone: &Name>, + address: &IpAddr, + ) -> Result<(IpAddr, Ttl), Error> { + // Check if we already got something. + match old_roll_state { + BeforeTtlDsDnskey::Report(_) => panic!("should not be here"), + BeforeTtlDsDnskey::Wait { servers, .. } => { + if let Some(ttl) = servers.get(address) { + return Ok((*address, *ttl)); + } + } + } + + let ttl = match mode { + DsOrDnskey::Ds => lookup_name_rtype_ttl::>(zone, Rtype::DS, address).await?, + DsOrDnskey::Dnskey => { + lookup_name_rtype_ttl::>(zone, Rtype::DNSKEY, address).await? + } + }; + + Ok((*address, ttl)) + } + + /// Try to capture the before-ttl of the RRSIG records in the zone. + async fn before_ttl_rrsigs( + zone_name: &Name>, + nameservers: &HashSet, + tsig_store: &TsigKeyStore, + ) -> Result { + let resolver = StubResolver::new(); + let answer = resolver + .query((zone_name, Rtype::SOA)) + .await + .map_err(|e| format!("lookup of {zone_name}/SOA failed: {e}"))?; + let Some(Ok((mname, _))) = answer + .answer()? + .limit_to_in::>() + .map(|r| r.map(|r| (r.data().mname().clone(), r.data().serial()))) + .next() + else { + let rcode = answer.opt_rcode(); + return if rcode != OptRcode::NOERROR { + Err(format!("Unable to resolve {zone_name}/SOA: {rcode}").into()) + } else { + Err(format!("No result for {zone_name}/SOA").into()) + }; + }; + + // Use provided addresses and TSIG key names if available, fallback to + // resolving from the SOA MNAME. + let mname_nameservers: HashSet; + let nameservers = if !nameservers.is_empty() { + nameservers + } else { + mname_nameservers = addresses_for_name(&resolver, mname) + .await? + .iter() + .map(Into::into) + .collect(); + &mname_nameservers + }; + + 'addr: for ns in nameservers { + let tcp_conn = match TcpStream::connect(ns.addr).await { + Ok(conn) => conn, + Err(e) => { + warn!("DNS TCP connection to {} failed: {e}", ns.addr); + continue; + } + }; + + // Prepare the named TSIG key for use, if any. + let tsig_key = if let Some(name) = ns.tsig_key_name.as_ref() { + match tsig_store.get(name) { + Some(key) => Some(key), + None => { + warn!("Unknown TSIG key name '{name}'"); + continue; + } + } + } else { + None + }; + + // If we have a TSIG key, setup a TSIG capable transport, otherwise + // use a normal transport. Use Multi types because only those support + // the multiple possible responses that can occur when sending an + // XFR request. + let tcp: Box>>> = + if let Some(tsig_key) = tsig_key { + let (conn, transport) = stream::Connection::< + TsigRequestMessage>, Arc>, + _, + >::new(tcp_conn); + tokio::spawn(transport.run()); + Box::new(TsigConnection::new(tsig_key, conn)) + } else { + let (conn, transport) = + stream::Connection::>, _>::new(tcp_conn); + tokio::spawn(transport.run()); + Box::new(conn) + }; + + let msg = MessageBuilder::new_vec(); + let mut msg = msg.question(); + msg.push((zone_name, Rtype::AXFR)).expect("should not fail"); + let req = RequestMessageMulti::new(msg).expect("should not fail"); + + // Send a request message. + let mut request = tcp.send_request(req.clone()); + + let mut first_soa = false; + let mut max_ttl = Ttl::from_secs(0); + loop { + // Get the reply + let reply = match request.get_response().await { + Ok(reply) => reply, + Err(e) => { + warn!("reading AXFR response from {} failed: {e}", ns.addr); + continue 'addr; + } + }; + let Some(reply) = reply else { + return Err(format!("Unexpected end of AXFR for {zone_name}").into()); + }; + let rcode = reply.opt_rcode(); + if rcode != OptRcode::NOERROR { + warn!("AXFR for {zone_name} from {} failed: {rcode}", ns.addr); + continue 'addr; + } + + let answer = reply.answer()?; + for r in answer { + let r = r?; + if !first_soa { + let Some(_) = r.to_record::>()? else { + // Bad start of zone transfer. + return Err(format!( + "Wrong start of AXFR for {zone_name}, expected SOA found {}", + r.rtype() + ) + .into()); + }; + + first_soa = true; + } else if r.rtype() == Rtype::SOA { + // The end. + return Ok(BeforeTtlRrsigs::Report(max_ttl)); + } + + if r.rtype() == Rtype::RRSIG { + max_ttl = max(max_ttl, r.ttl()); + } + } + } + } + + Err(format!("AXFR for {zone_name} failed for all nameservers {nameservers:?}").into()) + } + /// Handle automation for the report, expire and done steps. /// - /// The auto parameter has the flags that control whether automation is + /// The auto parameter has the flags that controls whether automation is /// enabled or disabled for a step. The roll_list parameters are the /// roll types that are covered by the auto parameter. - /// This function calls two function (auto_report_actions and + /// This function calls two functions (auto_report_actions and /// auto_wait_actions) to handle, repectively, the Report and Wait actions. async fn auto_report_expire_done( &mut self, @@ -4063,6 +4649,18 @@ impl WorkSpace { RollState::Propagation2 => &report_state.propagation2, _ => continue, }; + + if matches!( + report_state + .lock() + .expect("should not fail") + .before_ttl + .state, + BeforeTtlState::Wait + ) { + return self.capture_before_ttl(*r, state, now, env).await; + } + let actions = self.state.keyset.actions(*r); match auto_report_actions( &actions, @@ -4111,10 +4709,21 @@ impl WorkSpace { for r in roll_list { if let Some(state) = self.state.keyset.rollstates().get(r) { let actions = match state { - RollState::CacheExpire1(_) => self.state.keyset.cache_expired1(*r), + RollState::CacheExpire1(_) => { + let res = self.state.keyset.cache_expired1(*r); + if res.is_ok() { + self.init_before_ttl(*r); + + // Handle actions when the before-ttl has been recorded. + Ok(Vec::new()) + } else { + res + } + } RollState::CacheExpire2(_) => self.state.keyset.cache_expired2(*r), _ => continue, }; + if let Err(keyset::Error::Wait(_)) = actions { // To early. continue; @@ -5941,6 +6550,61 @@ where Ok(res) } +/// Return the TTL of a name and rtype. +async fn lookup_name_rtype_ttl( + name: &Name>, + rtype: Rtype, + address: &IpAddr, +) -> Result +where + for<'a> T: ParseRecordData<'a, Bytes>, +{ + let server_addr = SocketAddr::new(*address, 53); + let udp_connect = UdpConnect::new(server_addr); + let tcp_connect = TcpConnect::new(server_addr); + let (udptcp_conn, transport) = dgram_stream::Connection::new(udp_connect, tcp_connect); + tokio::spawn(transport.run()); + + let mut msg = MessageBuilder::new_vec(); + msg.header_mut().set_rd(true); + let mut msg = msg.question(); + msg.push((name, rtype)).expect("should not fail"); + let mut req = RequestMessage::new(msg).expect("should not fail"); + req.set_dnssec_ok(true); + let mut request = udptcp_conn.send_request(req.clone()); + let response = request + .get_response() + .await + .map_err(|e| format!("{name}/{rtype} request to {address} failed: {e}"))?; + + if let Some(r) = response.answer()?.limit_to_in::().next() { + let r = r?; + return Ok(r.ttl()); + } + + // Nothing in the answer section. Se if we got a SOA record. + if let Some(r) = response + .authority()? + .limit_to_in::>>() + .next() + { + let r = r?; + return Ok(r.ttl()); + } + + let opt_rcode = response.opt_rcode(); + if opt_rcode != OptRcode::NOERROR { + return Err(format!( + "query to nameserver {address} for {name}/{rtype} failed with error {opt_rcode}" + ) + .into()); + } + Err(format!( + "query to nameserver {address} for {name}/{rtype} failed to give a sensible response" + ) + .into()) +} + /// Return the name of the parent zone. async fn parent_zone(name: &Name>) -> Result>, Error> { let parent = name