Lines
90.68 %
Functions
19.05 %
Branches
100 %
//! Persistent state for the IPT manager
//!
//! Records of our IPTs.
//! Does *not* include private keys - those are in the `KeyMgr`.
use super::*;
use crate::time_store;
/// Handle for a suitable persistent storage manager
pub(crate) type IptStorageHandle = tor_persist::state_dir::StorageHandle<StateRecord>;
//---------- On disk data structures, done with serde ----------
/// Record of intro point establisher state, as stored on disk
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct StateRecord {
/// Relays
ipt_relays: Vec<RelayRecord>,
/// Reference time
stored: time_store::Reference,
}
/// Record of a selected intro point relay, as stored on disk
struct RelayRecord {
/// Which relay?
relay: RelayIds,
/// When do we plan to retire it?
planned_retirement: time_store::FutureTimestamp,
/// The IPTs, including the current one and any still-wanted old ones
ipts: Vec<IptRecord>,
/// Record of a single intro point, as stored on disk
struct IptRecord {
/// Used to find the cryptographic keys, amongst other things
lid: IptLocalId,
/// Is this IPT current, or are we just keeping it because of old descriptors
#[serde(default, skip_serializing_if = "<&bool as std::ops::Not>::not")]
is_current: bool,
//---------- Storing ----------
/// Store the IPTs in the persistent state
pub(super) fn store<R: Runtime, M: Mockable<R>>(
imm: &Immutable<R>,
state: &mut State<R, M>,
) -> Result<(), IptStoreError> {
let tstoring = time_store::Storing::start(&imm.runtime);
// Convert the IPT relays (to the on-disk format)
let ipt_relays = state
.irelays
.iter()
.map(|irelay| {
// Convert one IPT relay, with its IPTs, to the on-disk format
let relay = irelay.relay.clone();
let planned_retirement = tstoring.store_future(irelay.planned_retirement);
let ipts = irelay
.ipts
.map(|ipt| {
// Convert one IPT - at least, the parts we store here
IptRecord {
lid: ipt.lid,
is_current: ipt.is_current.is_some(),
})
.collect_vec();
RelayRecord {
relay,
planned_retirement,
ipts,
let on_disk = StateRecord {
ipt_relays,
stored: tstoring.store_ref(),
};
state.storage.store(&on_disk)?;
Ok(())
//---------- Loading ----------
/// Load the IPTs from the persistent state
///
/// `publish_set` should already have been loaded from its persistent state.
pub(super) fn load<R: Runtime, M: Mockable<R>>(
storage: &IptStorageHandle,
config: &watch::Receiver<Arc<OnionServiceConfig>>,
mockable: &mut M,
publish_set: &PublishIptSet,
) -> Result<Vec<IptRelay>, StartupError> {
let on_disk = storage.load().map_err(StartupError::LoadState)?;
let Some(on_disk) = on_disk else {
return Ok(vec![]);
// Throughout, we use exhaustive struct patterns on the data we got from disk,
// so we avoid missing any of the data.
let StateRecord { ipt_relays, stored } = on_disk;
let tloading = time_store::Loading::start(&imm.runtime, stored);
// Load the IPT relays (from the on-disk to the in-memory format)
let mut ipt_relays: Vec<_> = ipt_relays
.into_iter()
.map(|rrelay| {
// Load one IPT relay
let RelayRecord {
} = rrelay;
let planned_retirement = tloading.load_future(planned_retirement);
// Load the IPTs at this relay, restarting their establishers, etc.
let ipts = ipts
.map(|ipt| ipt.load_restart(imm, config, mockable, &relay))
.try_collect()?;
Ok::<_, StartupError>(IptRelay {
IptManager::<R, M>::import_new_expiry_times(&mut ipt_relays, publish_set);
Ok(ipt_relays)
impl IptRecord {
/// Recreate (load) one IPT
fn load_restart<R: Runtime, M: Mockable<R>>(
self,
new_configs: &watch::Receiver<Arc<OnionServiceConfig>>,
relay: &RelayIds,
) -> Result<Ipt, StartupError> {
let IptRecord { lid, is_current } = self;
let ipt = Ipt::start_establisher(
imm,
new_configs,
mockable,
lid,
is_current.then_some(IsCurrent),
Some(IptExpectExistingKeys),
// last_descriptor_expiry_including_slop
// is restored by the `import_new_expiry_times` call in `load`
PromiseLastDescriptorExpiryNoneIsGood {},
)
.map_err(|e| match e {
CreateIptError::Fatal(e) => e.into(),
// During startup we're trying to *read* the keystore;
// if it goes wrong, we bail rather than continuing the startup attempt.
CreateIptError::Keystore(cause) => StartupError::Keystore {
action: "load IPT key(s)",
cause,
},
CreateIptError::OpenReplayLog { file, error } => {
StartupError::StateDirectoryInaccessibleIo {
source: error,
action: "opening",
path: file,
})?;
// We don't record whether this IPT was published, so we should assume it was.
mockable.start_accepting(&*ipt.establisher);
Ok(ipt)