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}