tor_dircommon/config.rs
1//! Types for managing directory configuration.
2//!
3//! Directory configuration tells us where to load and store directory
4//! information, where to fetch it from, and how to validate it.
5//!
6//! # Semver note
7//!
8//! The types in this module are re-exported from `arti-client`: any changes
9//! here must be reflected in the version of `arti-client`.
10
11use std::{fmt::Formatter, time::Duration};
12
13use derive_builder::Builder;
14use getset::{CopyGetters, Getters};
15use serde::{
16 Deserialize, Deserializer, Serialize,
17 de::{MapAccess, SeqAccess, Visitor, value::MapAccessDeserializer},
18};
19use tor_checkable::timed::TimerangeBound;
20use tor_config::{ConfigBuildError, define_list_builder_accessors, impl_standard_builder};
21use tor_netdoc::doc::netstatus::Lifetime;
22use tracing::warn;
23
24use crate::{
25 authority::{AuthorityContacts, AuthorityContactsBuilder, LegacyAuthority},
26 fallback::{FallbackDirBuilder, FallbackList, FallbackListBuilder},
27 retry::{DownloadSchedule, DownloadScheduleBuilder},
28};
29
30/// Configuration information about the Tor network itself; used as
31/// part of Arti's configuration.
32///
33/// This type is immutable once constructed. To make one, use
34/// [`NetworkConfigBuilder`], or deserialize it from a string.
35//
36// TODO: We should move this type around, since the fallbacks part will no longer be used in
37// dirmgr, but only in guardmgr. Probably this type belongs in `arti-client`.
38#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters)]
39#[builder(build_fn(validate = "Self::validate", error = "ConfigBuildError"))]
40#[builder(derive(Debug, Serialize, Deserialize))]
41#[non_exhaustive]
42pub struct NetworkConfig {
43 /// List of locations to look in when downloading directory information, if
44 /// we don't actually have a directory yet.
45 ///
46 /// (If we do have a cached directory, we use directory caches listed there
47 /// instead.)
48 ///
49 /// This section can be changed in a running Arti client. Doing so will
50 /// affect future download attempts only.
51 ///
52 /// The default is to use a set of compiled-in fallback directories,
53 /// whose addresses and public keys are shipped as part of the Arti source code.
54 #[builder(sub_builder, setter(custom))]
55 #[getset(get = "pub")]
56 fallback_caches: FallbackList,
57
58 /// List of directory authorities which we expect to perform various operations
59 /// affecting the overall Tor network.
60 ///
61 /// (If none are specified, we use a default list of authorities shipped
62 /// with Arti.)
63 ///
64 /// This section cannot be changed in a running Arti client.
65 ///
66 /// The default is to use a set of compiled-in authorities,
67 /// whose identities and public keys are shipped as part of the Arti source code.
68 #[builder(sub_builder)]
69 #[builder_field_attr(serde(default, deserialize_with = "authority_compat"))]
70 #[getset(get = "pub")]
71 authorities: AuthorityContacts,
72}
73
74impl_standard_builder! { NetworkConfig }
75
76define_list_builder_accessors! {
77 struct NetworkConfigBuilder {
78 pub fallback_caches: [FallbackDirBuilder],
79 }
80}
81
82/// Compatibility function for legacy configuration syntaxes.
83///
84/// Before Arti 1.6.0, we used the following syntax for defining custom authorities:
85/// ```toml
86/// [tor_network]
87/// authorities = [
88/// { name = "test000a", v3ident = "1811E131971D37C118E3D3842A53400D5F5DFFA6" },
89/// { name = "test001a", v3ident = "5F2AB6BAB847F18CBFCDD9425EAB4761473632A4" },
90/// { name = "test002a", v3ident = "F92C5F21BF17035E03CD4B73262F1B7F10FAFE98" },
91/// { name = "test003a", v3ident = "997E81DA5052D5172073E6FAB22A97165EDA8912" },
92/// ]
93/// ```
94///
95/// Starting with Arti 1.6.0 and the implementation of prop330, we now use a
96/// different syntax, which is without doubt way more cumbersome to define.
97/// However, this option is rarely set by hand and it allows greater flexibility.
98/// ```toml
99/// [tor_network.authorities]
100/// v3idents = [
101/// "000D252DCFA8FC91143A4DC5A3EDE0ECF29919AE",
102/// "754169383C399466CA2531D0B3B71AA06DDFF853",
103/// "1DB224D49199FAF22327031888EAE56AE4D3E99C",
104/// "F216A4D49B51A3F460350410AE666594E87624D5",
105/// ]
106/// uploads = [
107/// [
108/// "127.0.0.1:7100",
109/// ],
110/// [
111/// "127.0.0.1:7101",
112/// ],
113/// [
114/// "127.0.0.1:7102",
115/// ],
116/// [
117/// "127.0.0.1:7103",
118/// ],
119/// ]
120/// downloads = [
121/// [
122/// "127.0.0.1:7100",
123/// ],
124/// [
125/// "127.0.0.1:7101",
126/// ],
127/// [
128/// "127.0.0.1:7102",
129/// ],
130/// [
131/// "127.0.0.1:7103",
132/// ],
133/// ]
134/// votes = [
135/// [
136/// "127.0.0.1:7100",
137/// ],
138/// [
139/// "127.0.0.1:7101",
140/// ],
141/// [
142/// "127.0.0.1:7102",
143/// ],
144/// [
145/// "127.0.0.1:7103",
146/// ],
147/// ]
148/// ```
149///
150/// This code is largely inspired by the following serde document:
151/// <https://serde.rs/string-or-struct.html>
152fn authority_compat<'de, D>(deserializer: D) -> Result<AuthorityContactsBuilder, D::Error>
153where
154 D: Deserializer<'de>,
155{
156 struct LegacyOrProp330;
157
158 impl<'de> Visitor<'de> for LegacyOrProp330 {
159 type Value = AuthorityContactsBuilder;
160
161 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
162 formatter.write_str("legacy or prop330")
163 }
164
165 /// A sequence (aka list) means that we are using the legacy syntax.
166 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
167 where
168 A: SeqAccess<'de>,
169 {
170 warn!("using deprecated (before arti 1.6.0) authority configuration syntax");
171 let mut builder = AuthorityContacts::builder();
172 while let Some(legacy_authority) = seq.next_element::<LegacyAuthority>()? {
173 builder.v3idents().push(legacy_authority.v3ident);
174 }
175
176 Ok(builder)
177 }
178
179 /// A map means it is the new syntax; pass responsibility to
180 /// [`AuthorityContactsBuilder`] using [`MapAccessDeserializer`].
181 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
182 where
183 A: MapAccess<'de>,
184 {
185 Deserialize::deserialize(MapAccessDeserializer::new(map))
186 }
187 }
188
189 deserializer.deserialize_any(LegacyOrProp330)
190}
191
192impl NetworkConfigBuilder {
193 /// Check that this builder will give a reasonable network.
194 fn validate(&self) -> std::result::Result<(), ConfigBuildError> {
195 if self.authorities.opt_v3idents().is_some() && self.opt_fallback_caches().is_none() {
196 return Err(ConfigBuildError::Inconsistent {
197 fields: vec!["authorities".to_owned(), "fallbacks".to_owned()],
198 problem: "Non-default authorities are use, but the fallback list is not overridden"
199 .to_owned(),
200 });
201 }
202
203 Ok(())
204 }
205}
206
207/// Configuration information for how exactly we download documents from the
208/// Tor directory caches.
209///
210/// This type is immutable once constructed. To make one, use
211/// [`DownloadScheduleConfigBuilder`], or deserialize it from a string.
212#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters, CopyGetters)]
213#[builder(build_fn(error = "ConfigBuildError"))]
214#[builder(derive(Debug, Serialize, Deserialize))]
215#[non_exhaustive]
216pub struct DownloadScheduleConfig {
217 /// Top-level configuration for how to retry our initial bootstrap attempt.
218 #[builder(
219 sub_builder,
220 field(build = "self.retry_bootstrap.build_retry_bootstrap()?")
221 )]
222 #[builder_field_attr(serde(default))]
223 #[getset(get_copy = "pub")]
224 retry_bootstrap: DownloadSchedule,
225
226 /// Configuration for how to retry a consensus download.
227 #[builder(sub_builder)]
228 #[builder_field_attr(serde(default))]
229 #[getset(get_copy = "pub")]
230 retry_consensus: DownloadSchedule,
231
232 /// Configuration for how to retry an authority cert download.
233 #[builder(sub_builder)]
234 #[builder_field_attr(serde(default))]
235 #[getset(get_copy = "pub")]
236 retry_certs: DownloadSchedule,
237
238 /// Configuration for how to retry a microdescriptor download.
239 #[builder(
240 sub_builder,
241 field(build = "self.retry_microdescs.build_retry_microdescs()?")
242 )]
243 #[builder_field_attr(serde(default))]
244 #[getset(get_copy = "pub")]
245 retry_microdescs: DownloadSchedule,
246}
247
248impl_standard_builder! { DownloadScheduleConfig }
249
250/// Configuration for how much much to extend the official tolerances of our
251/// directory information.
252///
253/// Because of possible clock skew, and because we want to tolerate possible
254/// failures of the directory authorities to reach a consensus, we want to
255/// consider a directory to be valid for a while before and after its official
256/// range of validity.
257///
258/// TODO: Remove the [`Default`] because it is too tightly bound to a client.
259#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters, CopyGetters)]
260#[builder(derive(Debug, Serialize, Deserialize))]
261#[builder(build_fn(error = "ConfigBuildError"))]
262#[non_exhaustive]
263pub struct DirTolerance {
264 /// For how long before a directory document is valid should we accept it?
265 ///
266 /// Having a nonzero value here allows us to tolerate a little clock skew.
267 ///
268 /// Defaults to 1 day.
269 #[builder(default = "Duration::from_secs(24 * 60 * 60)")]
270 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
271 #[getset(get_copy = "pub")]
272 pre_valid_tolerance: Duration,
273
274 /// For how long after a directory document is valid should we consider it
275 /// usable?
276 ///
277 /// Having a nonzero value here allows us to tolerate a little clock skew,
278 /// and makes us more robust to temporary failures for the directory
279 /// authorities to reach consensus.
280 ///
281 /// Defaults to 3 days (per [prop212]).
282 ///
283 /// [prop212]:
284 /// https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/proposals/212-using-old-consensus.txt
285 #[builder(default = "Duration::from_secs(3 * 24 * 60 * 60)")]
286 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
287 #[getset(get_copy = "pub")]
288 post_valid_tolerance: Duration,
289}
290
291impl_standard_builder! { DirTolerance }
292
293impl DirTolerance {
294 /// Return a new [`TimerangeBound`] that extends the validity interval of
295 /// `timebound` according to this configuration.
296 pub fn extend_tolerance<B>(&self, timebound: TimerangeBound<B>) -> TimerangeBound<B> {
297 timebound
298 .extend_tolerance(self.post_valid_tolerance)
299 .extend_pre_tolerance(self.pre_valid_tolerance)
300 }
301
302 /// Return a new consensus [`Lifetime`] that extends the validity intervals
303 /// of `lifetime` according to this configuration.
304 pub fn extend_lifetime(&self, lifetime: &Lifetime) -> Lifetime {
305 Lifetime::new(
306 lifetime.valid_after() - self.pre_valid_tolerance,
307 lifetime.fresh_until(),
308 lifetime.valid_until() + self.post_valid_tolerance,
309 )
310 .expect("Logic error when constructing lifetime")
311 }
312}
313
314#[cfg(test)]
315mod test {
316 // @@ begin test lint list maintained by maint/add_warning @@
317 #![allow(clippy::bool_assert_comparison)]
318 #![allow(clippy::clone_on_copy)]
319 #![allow(clippy::dbg_macro)]
320 #![allow(clippy::mixed_attributes_style)]
321 #![allow(clippy::print_stderr)]
322 #![allow(clippy::print_stdout)]
323 #![allow(clippy::single_char_pattern)]
324 #![allow(clippy::unwrap_used)]
325 #![allow(clippy::unchecked_time_subtraction)]
326 #![allow(clippy::useless_vec)]
327 #![allow(clippy::needless_pass_by_value)]
328 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
329 #![allow(clippy::unnecessary_wraps)]
330
331 use crate::fallback::FallbackDir;
332
333 use super::*;
334
335 #[test]
336 fn build_network() {
337 let dflt = NetworkConfig::default();
338
339 // with nothing set, we get the default.
340 let mut bld = NetworkConfig::builder();
341 let cfg = bld.build().unwrap();
342 assert_eq!(
343 cfg.authorities.v3idents().len(),
344 dflt.authorities.v3idents().len()
345 );
346 assert_eq!(cfg.fallback_caches.len(), dflt.fallback_caches.len());
347
348 // with any authorities set, the fallback list _must_ be set
349 // or the build fails.
350 bld.authorities
351 .set_v3idents(vec![[b'?'; 20].into(), [b'!'; 20].into()]);
352 assert!(bld.build().is_err());
353
354 bld.set_fallback_caches(vec![{
355 let mut bld = FallbackDir::builder();
356 bld.rsa_identity([b'x'; 20].into())
357 .ed_identity([b'y'; 32].into());
358 bld.orports().push("127.0.0.1:99".parse().unwrap());
359 bld.orports().push("[::]:99".parse().unwrap());
360 bld
361 }]);
362 let cfg = bld.build().unwrap();
363 assert_eq!(cfg.authorities.v3idents().len(), 2);
364 assert_eq!(cfg.fallback_caches.len(), 1);
365 }
366
367 #[test]
368 fn deserialize_compat() {
369 // Test whether we can serialize both formats.
370
371 let mut netcfg_legacy: NetworkConfigBuilder = toml::from_str(
372 "
373 authorities = [
374 { name = \"test000a\", v3ident = \"911F7C74212214823DDBDE3044B5B1AF3EFB98A0\" },
375 { name = \"test001a\", v3ident = \"46C4A4492D103A8C5CA544AC653B51C7B9AC8692\" },
376 { name = \"test002a\", v3ident = \"28D4680EA9C3660D1028FC40BACAC1319414581E\" },
377 { name = \"test003a\", v3ident = \"3817C9EB7E41C957594D0D9BCD6C7D7D718479C2\" },
378 ]",
379 )
380 .unwrap();
381
382 let mut netcfg_prop330: NetworkConfigBuilder = toml::from_str(
383 "
384 [authorities]
385 v3idents = [
386 \"911F7C74212214823DDBDE3044B5B1AF3EFB98A0\",
387 \"46C4A4492D103A8C5CA544AC653B51C7B9AC8692\",
388 \"28D4680EA9C3660D1028FC40BACAC1319414581E\",
389 \"3817C9EB7E41C957594D0D9BCD6C7D7D718479C2\",
390 ]",
391 )
392 .unwrap();
393
394 assert_eq!(netcfg_legacy.authorities.v3idents().len(), 4);
395 assert_eq!(
396 netcfg_legacy.authorities.v3idents(),
397 netcfg_prop330.authorities.v3idents(),
398 );
399 }
400}