tor_hsservice/ipt_mgr/
persist.rs

1//! Persistent state for the IPT manager
2//!
3//! Records of our IPTs.
4//! Does *not* include private keys - those are in the `KeyMgr`.
5
6use super::*;
7use crate::time_store;
8
9/// Handle for a suitable persistent storage manager
10pub(crate) type IptStorageHandle = tor_persist::state_dir::StorageHandle<StateRecord>;
11
12//---------- On disk data structures, done with serde ----------
13
14/// Record of intro point establisher state, as stored on disk
15#[derive(Serialize, Deserialize, Debug)]
16pub(crate) struct StateRecord {
17    /// Relays
18    ipt_relays: Vec<RelayRecord>,
19    /// Reference time
20    stored: time_store::Reference,
21}
22
23/// Record of a selected intro point relay, as stored on disk
24#[derive(Serialize, Deserialize, Debug)]
25struct RelayRecord {
26    /// Which relay?
27    relay: RelayIds,
28    /// When do we plan to retire it?
29    planned_retirement: time_store::FutureTimestamp,
30    /// The IPTs, including the current one and any still-wanted old ones
31    ipts: Vec<IptRecord>,
32}
33
34/// Record of a single intro point, as stored on disk
35#[derive(Serialize, Deserialize, Debug)]
36struct IptRecord {
37    /// Used to find the cryptographic keys, amongst other things
38    lid: IptLocalId,
39    /// Is this IPT current, or are we just keeping it because of old descriptors
40    #[serde(default, skip_serializing_if = "<&bool as std::ops::Not>::not")]
41    is_current: bool,
42}
43
44//---------- Storing ----------
45
46/// Store the IPTs in the persistent state
47pub(super) fn store<R: Runtime, M: Mockable<R>>(
48    imm: &Immutable<R>,
49    state: &mut State<R, M>,
50) -> Result<(), IptStoreError> {
51    let tstoring = time_store::Storing::start(&imm.runtime);
52
53    // Convert the IPT relays (to the on-disk format)
54    let ipt_relays = state
55        .irelays
56        .iter()
57        .map(|irelay| {
58            // Convert one IPT relay, with its IPTs, to the on-disk format
59            let relay = irelay.relay.clone();
60            let planned_retirement = tstoring.store_future(irelay.planned_retirement);
61            let ipts = irelay
62                .ipts
63                .iter()
64                .map(|ipt| {
65                    // Convert one IPT - at least, the parts we store here
66                    IptRecord {
67                        lid: ipt.lid,
68                        is_current: ipt.is_current.is_some(),
69                    }
70                })
71                .collect_vec();
72            RelayRecord {
73                relay,
74                planned_retirement,
75                ipts,
76            }
77        })
78        .collect_vec();
79
80    let on_disk = StateRecord {
81        ipt_relays,
82        stored: tstoring.store_ref(),
83    };
84    state.storage.store(&on_disk)?;
85    Ok(())
86}
87
88//---------- Loading ----------
89
90/// Load the IPTs from the persistent state
91///
92/// `publish_set` should already have been loaded from its persistent state.
93pub(super) fn load<R: Runtime, M: Mockable<R>>(
94    imm: &Immutable<R>,
95    storage: &IptStorageHandle,
96    config: &watch::Receiver<Arc<OnionServiceConfig>>,
97    mockable: &mut M,
98    publish_set: &PublishIptSet,
99) -> Result<Vec<IptRelay>, StartupError> {
100    let on_disk = storage.load().map_err(StartupError::LoadState)?;
101
102    let Some(on_disk) = on_disk else {
103        return Ok(vec![]);
104    };
105
106    // Throughout, we use exhaustive struct patterns on the data we got from disk,
107    // so we avoid missing any of the data.
108    let StateRecord { ipt_relays, stored } = on_disk;
109
110    let tloading = time_store::Loading::start(&imm.runtime, stored);
111
112    // Load the IPT relays (from the on-disk to the in-memory format)
113    let mut ipt_relays: Vec<_> = ipt_relays
114        .into_iter()
115        .map(|rrelay| {
116            // Load one IPT relay
117            let RelayRecord {
118                relay,
119                planned_retirement,
120                ipts,
121            } = rrelay;
122            let planned_retirement = tloading.load_future(planned_retirement);
123            // Load the IPTs at this relay, restarting their establishers, etc.
124            let ipts = ipts
125                .into_iter()
126                .map(|ipt| ipt.load_restart(imm, config, mockable, &relay))
127                .try_collect()?;
128            Ok::<_, StartupError>(IptRelay {
129                relay,
130                planned_retirement,
131                ipts,
132            })
133        })
134        .try_collect()?;
135
136    IptManager::<R, M>::import_new_expiry_times(&mut ipt_relays, publish_set);
137
138    Ok(ipt_relays)
139}
140
141impl IptRecord {
142    /// Recreate (load) one IPT
143    fn load_restart<R: Runtime, M: Mockable<R>>(
144        self,
145        imm: &Immutable<R>,
146        new_configs: &watch::Receiver<Arc<OnionServiceConfig>>,
147        mockable: &mut M,
148        relay: &RelayIds,
149    ) -> Result<Ipt, StartupError> {
150        let IptRecord { lid, is_current } = self;
151
152        let ipt = Ipt::start_establisher(
153            imm,
154            new_configs,
155            mockable,
156            relay,
157            lid,
158            is_current.then_some(IsCurrent),
159            Some(IptExpectExistingKeys),
160            // last_descriptor_expiry_including_slop
161            // is restored by the `import_new_expiry_times` call in `load`
162            PromiseLastDescriptorExpiryNoneIsGood {},
163        )
164        .map_err(|e| match e {
165            CreateIptError::Fatal(e) => e.into(),
166            // During startup we're trying to *read* the keystore;
167            // if it goes wrong, we bail rather than continuing the startup attempt.
168            CreateIptError::Keystore(cause) => StartupError::Keystore {
169                action: "load IPT key(s)",
170                cause,
171            },
172            CreateIptError::OpenReplayLog { file, error } => {
173                StartupError::StateDirectoryInaccessibleIo {
174                    source: error,
175                    action: "opening",
176                    path: file,
177                }
178            }
179        })?;
180
181        // We don't record whether this IPT was published, so we should assume it was.
182        mockable.start_accepting(&*ipt.establisher);
183
184        Ok(ipt)
185    }
186}