//! Types for managing directory configuration. //! //! Directory configuration tells us where to load and store directory //! information, where to fetch it from, and how to validate it. //! //! # Semver note //! //! The types in this module are re-exported from `arti-client`: any changes //! here must be reflected in the version of `arti-client`. use crate::retry::DownloadSchedule; use crate::storage::DynStore; use crate::{Authority, Result}; use tor_config::ConfigBuildError; use tor_netdir::fallback::FallbackDir; use tor_netdoc::doc::netstatus; use derive_builder::Builder; use std::path::PathBuf; use serde::Deserialize; /// Configuration information about the Tor network itself; used as /// part of Arti's configuration. /// /// This type is immutable once constructed. To make one, use /// [`NetworkConfigBuilder`], or deserialize it from a string. #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)] #[serde(deny_unknown_fields)] #[builder(build_fn(validate = "Self::validate", error = "ConfigBuildError"))] pub struct NetworkConfig { /// List of locations to look in when downloading directory information, if /// we don't actually have a directory yet. /// /// (If we do have a cached directory, we use directory caches listed there /// instead.) /// /// This section can be changed in a running Arti client. Doing so will /// affect future download attempts only. #[serde(default = "fallbacks::default_fallbacks")] #[builder(default = "fallbacks::default_fallbacks()")] fallback_caches: Vec, /// List of directory authorities which we expect to sign consensus /// documents. /// /// (If none are specified, we use a default list of authorities shipped /// with Arti.) /// /// This section cannot be changed in a running Arti client. #[serde(default = "crate::authority::default_authorities")] #[builder(default = "crate::authority::default_authorities()")] authorities: Vec, } impl Default for NetworkConfig { fn default() -> Self { NetworkConfig { fallback_caches: fallbacks::default_fallbacks(), authorities: crate::authority::default_authorities(), } } } impl From for NetworkConfigBuilder { fn from(cfg: NetworkConfig) -> NetworkConfigBuilder { let mut builder = NetworkConfigBuilder::default(); builder .fallback_caches(cfg.fallback_caches) .authorities(cfg.authorities); builder } } impl NetworkConfig { /// Return a new builder to construct a NetworkConfig. pub fn builder() -> NetworkConfigBuilder { NetworkConfigBuilder::default() } /// Return the configured directory authorities pub(crate) fn authorities(&self) -> &[Authority] { &self.authorities[..] } /// Return the configured fallback directories pub(crate) fn fallbacks(&self) -> &[FallbackDir] { &self.fallback_caches[..] } } impl NetworkConfigBuilder { /// Check that this builder will give a reasonable network. fn validate(&self) -> std::result::Result<(), ConfigBuildError> { if self.authorities.is_some() && self.fallback_caches.is_none() { return Err(ConfigBuildError::Inconsistent { fields: vec!["authorities".to_owned(), "fallbacks".to_owned()], problem: "Non-default authorities are use, but the fallback list is not overridden" .to_owned(), }); } Ok(()) } } /// Configuration information for how exactly we download documents from the /// Tor directory caches. /// /// This type is immutable once constructed. To make one, use /// [`DownloadScheduleConfigBuilder`], or deserialize it from a string. #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)] #[serde(deny_unknown_fields)] #[builder(build_fn(error = "ConfigBuildError"))] pub struct DownloadScheduleConfig { /// Top-level configuration for how to retry our initial bootstrap attempt. #[serde(default = "default_retry_bootstrap")] #[builder(default = "default_retry_bootstrap()")] retry_bootstrap: DownloadSchedule, /// Configuration for how to retry a consensus download. #[serde(default)] #[builder(default)] retry_consensus: DownloadSchedule, /// Configuration for how to retry an authority cert download. #[serde(default)] #[builder(default)] retry_certs: DownloadSchedule, /// Configuration for how to retry a microdescriptor download. #[serde(default = "default_microdesc_schedule")] #[builder(default = "default_microdesc_schedule()")] retry_microdescs: DownloadSchedule, } /// Default value for retry_bootstrap in DownloadScheduleConfig. fn default_retry_bootstrap() -> DownloadSchedule { DownloadSchedule::new(128, std::time::Duration::new(1, 0), 1) } /// Default value for microdesc_bootstrap in DownloadScheduleConfig. fn default_microdesc_schedule() -> DownloadSchedule { DownloadSchedule::new(3, std::time::Duration::new(1, 0), 4) } impl Default for DownloadScheduleConfig { fn default() -> Self { Self::builder() .build() .expect("default builder setting didn't work") } } impl DownloadScheduleConfig { /// Return a new builder to make a [`DownloadScheduleConfig`] pub fn builder() -> DownloadScheduleConfigBuilder { DownloadScheduleConfigBuilder::default() } } impl From for DownloadScheduleConfigBuilder { fn from(cfg: DownloadScheduleConfig) -> DownloadScheduleConfigBuilder { let mut builder = DownloadScheduleConfigBuilder::default(); builder .retry_bootstrap(cfg.retry_bootstrap) .retry_consensus(cfg.retry_consensus) .retry_certs(cfg.retry_certs) .retry_microdescs(cfg.retry_microdescs); builder } } /// Configuration type for network directory operations. /// /// This type is immutable once constructed. /// /// To create an object of this type, use [`DirMgrConfigBuilder`], or /// deserialize it from a string. (Arti generally uses Toml for configuration, /// but you can use other formats if you prefer.) /// /// Many members of this type can be replaced with a new configuration on a /// running Arti client. Those that cannot are documented. #[derive(Debug, Clone, Builder, Eq, PartialEq)] #[builder(build_fn(error = "ConfigBuildError"))] pub struct DirMgrConfig { /// Location to use for storing and reading current-format /// directory information. /// /// Cannot be changed on a running Arti client. #[builder(setter(into))] cache_path: PathBuf, /// Configuration information about the network. #[builder(default)] network_config: NetworkConfig, /// Configuration information about when we download things. /// /// This can be replaced on a running Arti client. Doing so affects _future_ /// download attempts, but has no effect on attempts that are currently in /// progress or being retried. /// /// (The above is a limitation: we would like it to someday have an effect /// on in-progress attempts as well, at least at the top level. Users /// should _not_ assume that the effect of changing this option will always /// be delayed.) #[builder(default)] schedule_config: DownloadScheduleConfig, /// A map of network parameters that we're overriding from their settings in /// the consensus. /// /// This can be replaced on a running Arti client. Doing so will take /// effect the next time a consensus is downloaded. /// /// (The above is a limitation: we would like it to someday take effect /// immediately. Users should _not_ assume that the effect of changing this /// option will always be delayed.) #[builder(default)] override_net_params: netstatus::NetParams, } impl DirMgrConfigBuilder { /// Overrides the network consensus parameter named `param` with a /// new value. /// /// If the new value is out of range, it will be clamped to the /// acceptable range. /// /// If the parameter is not recognized by Arti, it will be /// ignored, and a warning will be produced when we try to apply /// it to the consensus. /// /// By default no parameters will be overridden. pub fn override_net_param(&mut self, param: String, value: i32) -> &mut Self { self.override_net_params .get_or_insert_with(netstatus::NetParams::default) .set(param, value); self } } impl DirMgrConfig { /// Return a new builder to construct a DirMgrConfig. pub fn builder() -> DirMgrConfigBuilder { DirMgrConfigBuilder::default() } /// Create a store from this configuration. /// /// Note that each time this is called, a new store object will be /// created: you probably only want to call this once. pub(crate) fn open_store(&self, readonly: bool) -> Result { Ok(Box::new(crate::storage::SqliteStore::from_path( &self.cache_path, readonly, )?)) } /// Return the configured cache path. pub(crate) fn cache_path(&self) -> &std::path::Path { self.cache_path.as_ref() } /// Return a slice of the configured authorities pub(crate) fn authorities(&self) -> &[Authority] { self.network_config.authorities() } /// Return the configured set of fallback directories pub(crate) fn fallbacks(&self) -> &[FallbackDir] { self.network_config.fallbacks() } /// Return set of configured networkstatus parameter overrides. pub(crate) fn override_net_params(&self) -> &netstatus::NetParams { &self.override_net_params } /// Return the schedule configuration we should use to decide when to /// attempt and retry downloads. pub(crate) fn schedule(&self) -> &DownloadScheduleConfig { &self.schedule_config } /// Construct a new configuration object where all replaceable fields in /// `self` are replaced with those from `new_config`. /// /// Any fields which aren't allowed to change at runtime are copied from self. pub(crate) fn update_config(&self, new_config: &DirMgrConfig) -> DirMgrConfig { DirMgrConfig { cache_path: self.cache_path.clone(), network_config: NetworkConfig { fallback_caches: new_config.network_config.fallback_caches.clone(), authorities: self.network_config.authorities.clone(), }, schedule_config: new_config.schedule_config.clone(), override_net_params: new_config.override_net_params.clone(), } } } impl DownloadScheduleConfig { /// Return configuration for retrying our entire bootstrap /// operation at startup. pub(crate) fn retry_bootstrap(&self) -> &DownloadSchedule { &self.retry_bootstrap } /// Return configuration for retrying a consensus download. pub(crate) fn retry_consensus(&self) -> &DownloadSchedule { &self.retry_consensus } /// Return configuration for retrying an authority certificate download pub(crate) fn retry_certs(&self) -> &DownloadSchedule { &self.retry_certs } /// Return configuration for retrying an authority certificate download pub(crate) fn retry_microdescs(&self) -> &DownloadSchedule { &self.retry_microdescs } } /// Helpers for initializing the fallback list. mod fallbacks { use tor_llcrypto::pk::{ed25519::Ed25519Identity, rsa::RsaIdentity}; use tor_netdir::fallback::FallbackDir; /// Return a list of the default fallback directories shipped with /// arti. pub(crate) fn default_fallbacks() -> Vec { /// Build a fallback directory; panic if input is bad. fn fallback(rsa: &str, ed: &str, ports: &[&str]) -> FallbackDir { let rsa = hex::decode(rsa).expect("Bad hex in built-in fallback list"); let rsa = RsaIdentity::from_bytes(&rsa).expect("Wrong length in built-in fallback list"); let ed = base64::decode_config(ed, base64::STANDARD_NO_PAD) .expect("Bad hex in built-in fallback list"); let ed = Ed25519Identity::from_bytes(&ed).expect("Wrong length in built-in fallback list"); let mut bld = FallbackDir::builder(); bld.rsa_identity(rsa).ed_identity(ed); ports .iter() .map(|s| s.parse().expect("Bad socket address in fallbacklist")) .for_each(|p| { bld.orport(p); }); bld.build() .expect("Unable to build default fallback directory!?") } include!("fallback_dirs.inc") } } #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] #![allow(clippy::unnecessary_wraps)] use super::*; use tempfile::tempdir; #[test] fn simplest_config() -> Result<()> { let tmp = tempdir().unwrap(); let dir = DirMgrConfigBuilder::default() .cache_path(tmp.path().to_path_buf()) .build() .unwrap(); assert!(dir.authorities().len() >= 3); assert!(dir.fallbacks().len() >= 3); // TODO: verify other defaults. Ok(()) } #[test] fn build_network() -> Result<()> { let dflt = NetworkConfig::default(); // with nothing set, we get the default. let mut bld = NetworkConfig::builder(); let cfg = bld.build().unwrap(); assert_eq!(cfg.authorities().len(), dflt.authorities.len()); assert_eq!(cfg.fallbacks().len(), dflt.fallback_caches.len()); // with any authorities set, the fallback list _must_ be set // or the build fails. bld.authorities(vec![ Authority::builder() .name("Hello") .v3ident([b'?'; 20].into()) .build() .unwrap(), Authority::builder() .name("world") .v3ident([b'!'; 20].into()) .build() .unwrap(), ]); assert!(bld.build().is_err()); bld.fallback_caches(vec![FallbackDir::builder() .rsa_identity([b'x'; 20].into()) .ed_identity([b'y'; 32].into()) .orport("127.0.0.1:99".parse().unwrap()) .orport("[::]:99".parse().unwrap()) .build() .unwrap()]); let cfg = bld.build().unwrap(); assert_eq!(cfg.authorities().len(), 2); assert_eq!(cfg.fallbacks().len(), 1); Ok(()) } #[test] fn build_schedule() -> Result<()> { use std::time::Duration; let mut bld = DownloadScheduleConfig::builder(); let cfg = bld.build().unwrap(); assert_eq!(cfg.retry_microdescs().parallelism(), 4); assert_eq!(cfg.retry_microdescs().n_attempts(), 3); assert_eq!(cfg.retry_bootstrap().n_attempts(), 128); bld.retry_consensus(DownloadSchedule::new(7, Duration::new(86400, 0), 1)) .retry_bootstrap(DownloadSchedule::new(4, Duration::new(3600, 0), 1)) .retry_certs(DownloadSchedule::new(5, Duration::new(3600, 0), 1)) .retry_microdescs(DownloadSchedule::new(6, Duration::new(3600, 0), 0)); let cfg = bld.build().unwrap(); assert_eq!(cfg.retry_microdescs().parallelism(), 1); // gets clamped assert_eq!(cfg.retry_microdescs().n_attempts(), 6); assert_eq!(cfg.retry_bootstrap().n_attempts(), 4); assert_eq!(cfg.retry_consensus().n_attempts(), 7); assert_eq!(cfg.retry_certs().n_attempts(), 5); Ok(()) } #[test] fn build_dirmgrcfg() -> Result<()> { let mut bld = DirMgrConfig::builder(); let tmp = tempdir().unwrap(); let cfg = bld .override_net_param("circwindow".into(), 999) .cache_path(tmp.path()) .network_config(NetworkConfig::default()) .schedule_config(DownloadScheduleConfig::default()) .build() .unwrap(); assert_eq!(cfg.override_net_params().get("circwindow").unwrap(), &999); Ok(()) } }