//! Implements a usable view of Tor network parameters. //! //! The Tor consensus document contains a number of 'network //! parameters', which are integer-valued items voted on by the //! directory authorities. They are used to tune the behavior of //! numerous aspects of the network. //! A set of Tor network parameters //! //! The Tor consensus document contains a number of 'network //! parameters', which are integer-valued items voted on by the //! directory authorities. These parameters are used to tune the //! behavior of numerous aspects of the network. //! //! This type differs from //! [`NetParams`](tor_netdoc::doc::netstatus::NetParams) in that it //! only exposes a set of parameters recognized by arti. In return //! for this restriction, it makes sure that the values it gives are //! in range, and provides default values for any parameters that are //! missing. use std::convert::TryInto; use tor_units::{ BoundedInt32, IntegerDays, IntegerMilliseconds, IntegerSeconds, Percentage, SendMeVersion, }; /// An object that can be constructed from an i32, with saturating semantics. pub trait FromInt32Saturating { /// Construct an instance of this object from `val`. /// /// If `val` is too low, treat it as the lowest value that would be /// valid. If `val` is too high, treat it as the highest value that /// would be valid. fn from_saturating(val: i32) -> Self; } impl FromInt32Saturating for i32 { fn from_saturating(val: i32) -> Self { val } } impl FromInt32Saturating for BoundedInt32 { fn from_saturating(val: i32) -> Self { Self::saturating_new(val) } } impl + FromInt32Saturating> FromInt32Saturating for Percentage { fn from_saturating(val: i32) -> Self { Self::new(T::from_saturating(val)) } } impl> FromInt32Saturating for IntegerMilliseconds { fn from_saturating(val: i32) -> Self { Self::new(T::from_saturating(val)) } } impl> FromInt32Saturating for IntegerSeconds { fn from_saturating(val: i32) -> Self { Self::new(T::from_saturating(val)) } } impl> FromInt32Saturating for IntegerDays { fn from_saturating(val: i32) -> Self { Self::new(T::from_saturating(val)) } } impl FromInt32Saturating for SendMeVersion { fn from_saturating(val: i32) -> Self { Self::new(val.clamp(0, 255) as u8) } } /// A macro to help us declare the net parameters object. It lets us /// put the information about each parameter in just one place, even /// though it will later get split between the struct declaration, the /// Default implementation, and the implementation of /// `saturating_update_override`. macro_rules! declare_net_parameters { { $(#[$s_meta:meta])* $s_v:vis struct $s_name:ident { $( $(#[$p_meta:meta])* $p_v:vis $p_name:ident : $p_type:ty = ($p_dflt:expr) from $p_string:literal ),* $( , )? } } => { $(#[$s_meta])* $s_v struct $s_name { $( $(#[$p_meta])* $p_v $p_name : $p_type ),* } impl $s_name { /// Try to construct an instance of with its default values. /// /// (This should always succeed, unless one of the default values /// is out-of-bounds for the type.) fn default_values() -> Result { Ok(Self { $( $p_name : $p_dflt.try_into()? ),* }) } /// Replace the current value for the parameter identified in the /// consensus with `key` with a new value `val`. /// /// Uses saturating semantics if the new value is out-of-range. /// /// Returns true if the key was recognized, and false otherwise. fn set_saturating(&mut self, key: &str, val: i32) -> bool { match key { $( $p_string => self.$p_name = { type T = $p_type; T::from_saturating(val) }, )* _ => return false, } true } } } } declare_net_parameters! { /// This structure holds recognized configuration parameters. All values are type-safe, /// and where applicable clamped to be within range. #[derive(Clone, Debug)] #[non_exhaustive] pub struct NetParameters { /// A weighting factor for bandwidth calculations pub bw_weight_scale: BoundedInt32<0, { i32::MAX }> = (10_000) from "bwweightscale", /// If true, do not attempt to learn circuit-build timeouts at all. pub cbt_learning_disabled: BoundedInt32<0, 1> = (0) from "cbtdisabled", /// Number of histograms bins to consider when estimating Xm for a /// Pareto-based circuit timeout estimator. pub cbt_num_xm_modes: BoundedInt32<1, 20> = (10) from "cbtnummodes", /// How many recent circuit success/timeout statuses do we remember /// when trying to tell if our circuit timeouts are too low? pub cbt_success_count: BoundedInt32<3, 1_000> = (20) from "cbtrecentcount", /// How many timeouts (in the last `cbt_success_count` observations) /// indicates that our circuit timeouts are too low? // XXXX-SPEC 10000 is greater than 1000 for cbt_success_count. pub cbt_max_timeouts: BoundedInt32<3, 10_000> = (18) from "cbtmaxtimeouts", /// Smallest number of circuit build times we have to view in order to use /// our Pareto-based circuit timeout estimator. // XXXX-SPEC 10000 disables this. pub cbt_min_circs_for_estimate: BoundedInt32<1, 10_000> = (100) from "cbtmincircs", /// Quantile to use when determining the correct circuit timeout value /// with our Pareto estimator. /// /// (We continue building circuits after this timeout, but only /// for build-time measurement purposes.) pub cbt_timeout_quantile: Percentage> = (80) from "cbtquantile", /// Quantile to use when determining when to abandon circuits completely /// with our Pareto estimator. pub cbt_abandon_quantile: Percentage> = (99) from "cbtclosequantile", /// Lowest permissible timeout value for Pareto timeout estimator. pub cbt_min_timeout: IntegerMilliseconds> = (10) from "cbtmintimeout", /// Timeout value to use for our Pareto timeout estimator when we have /// no initial estimate. pub cbt_initial_timeout: IntegerMilliseconds> = (60_000) from "cbtinitialtimeout", /// When we don't have a good build-time estimate yet, how long /// (in seconds) do we wait between trying to launch build-time /// testing circuits through the network? pub cbt_testing_delay: IntegerSeconds> = (10) from "cbttestfreq", /// How many circuits can be open before we will no longer /// consider launching testing circuits to learn average build /// times? pub cbt_max_open_circuits_for_testing: BoundedInt32<0, 14> = (10) from "cbtmaxopencircs", /// The maximum cell window size? pub circuit_window: BoundedInt32<100, 1000> = (1_000) from "circwindow", /// The decay parameter for circuit priority pub circuit_priority_half_life: IntegerMilliseconds> = (30_000) from "CircuitPriorityHalflifeMsec", /// Whether to perform circuit extensions by Ed25519 ID pub extend_by_ed25519_id: BoundedInt32<0, 1> = (0) from "ExtendByEd25519ID", /// If we have excluded so many possible guards that the /// available fraction is below this threshold, we should use a different /// guard sample. (TODO: not actually implemented) pub guard_meaningful_restriction: Percentage> = (20) from "guard-meaningful-restriction-percent", /// We should warn the user if they have excluded so many guards /// that the available fraction is below this threshold. pub guard_extreme_restriction: Percentage> = (1) from "guard-extreme-restriction-percent", /// How long should we keep an unconfirmed guard (one we have not /// contacted) before removing it from the guard sample? pub guard_lifetime_unconfirmed: IntegerDays> = (120) from "guard-lifetime-days", /// How long should we keep a _confirmed_ guard (one we have contacted) /// before removing it from the guard sample? pub guard_lifetime_confirmed: IntegerDays> = (60) from "guard-confirmed-min-lifetime-days", /// If all circuits have failed for this interval, then treat the internet /// as "probably down", and treat any guard failures in that interval /// as unproven. pub guard_internet_likely_down: IntegerSeconds> = (600) from "guard-internet-likely-down-interval", /// Largest number of guards that a client should try to maintain in /// a sample of possible guards. pub guard_max_sample_size: BoundedInt32<1, {i32::MAX}> = (60) from "guard-max-sample-size", /// Largest fraction of guard bandwidth on the network that a client /// should try to remain in a sample of possible guards. pub guard_max_sample_threshold: Percentage> = (20) from "guard-max-sample-threshold", /// If the client ever has fewer than this many guards in their sample, /// after filtering out unusable guards, they should try to add more guards /// to the sample (if allowed). pub guard_filtered_min_sample_size: BoundedInt32<1,{i32::MAX}> = (20) from "guard-min-filtered-sample-size", /// The number of confirmed guards that the client should treat as /// "primary guards". pub guard_n_primary: BoundedInt32<1,{i32::MAX}> = (3) from "guard-n-primary-guards", /// The number of primary guards that the client should use in parallel. /// Other primary guards won't get used unless earlier ones are down. pub guard_use_parallelism: BoundedInt32<1, {i32::MAX}> = (1) from "guard-n-primary-guards-to-use", /// The number of primary guards that the client should use in /// parallel. Other primary directory guards won't get used /// unless earlier ones are down. pub guard_dir_use_parallelism: BoundedInt32<1, {i32::MAX}> = (3) from "guard-n-primary-dir-guards-to-use", /// When trying to confirm nonprimary guards, if a guard doesn't /// answer for more than this long in seconds, treat any lower- /// priority guards as possibly usable. pub guard_nonprimary_connect_timeout: IntegerSeconds> = (15) from "guard-nonprimary-guard-connect-timeout", /// When trying to confirm nonprimary guards, if a guard doesn't /// answer for more than _this_ long in seconds, treat it as down. pub guard_nonprimary_idle_timeout: IntegerSeconds> = (600) from "guard-nonprimary-guard-idle-timeout", /// If a guard has been unlisted in the consensus for at least this /// long, remove it from the consensus. pub guard_remove_unlisted_after: IntegerDays> = (20) from "guard-remove-unlisted-guards-after-days", /// The minimum threshold for circuit patch construction pub min_circuit_path_threshold: Percentage> = (60) from "min_paths_for_circs_pct", /// The minimum sendme version to accept. pub sendme_accept_min_version: SendMeVersion = (0) from "sendme_accept_min_version", /// The minimum sendme version to transmit. pub sendme_emit_min_version: SendMeVersion = (0) from "sendme_emit_min_version", /// How long should never-used client circuits stay available, /// in the steady state? pub unused_client_circ_timeout: IntegerSeconds> = (30*60) from "nf_conntimeout_clients", /// When we're learning circuit timeouts, how long should never-used client /// circuits stay available? pub unused_client_circ_timeout_while_learning_cbt: IntegerSeconds> = (3*60) from "cbtlearntimeout", } } impl Default for NetParameters { fn default() -> Self { NetParameters::default_values().expect("Default parameters were out-of-bounds") } } impl NetParameters { /// Construct a new NetParameters from a given list of key=value parameters. /// /// Unrecognized parameters are ignored. pub fn from_map(p: &tor_netdoc::doc::netstatus::NetParams) -> Self { let mut params = NetParameters::default(); let _ = params.saturating_update(p.iter()); params } /// Replace a list of parameters, using the logic of /// `set_saturating`. /// /// Return a vector of the parameter names we didn't recognize. pub(crate) fn saturating_update<'a, S>( &mut self, iter: impl Iterator, ) -> Vec where S: AsRef, { let mut unrecognized = Vec::new(); for (k, v) in iter { if !self.set_saturating(k.as_ref(), *v) { unrecognized.push(k); } } unrecognized } } #[cfg(test)] #[allow(clippy::many_single_char_names)] #[allow(clippy::unwrap_used)] #[allow(clippy::cognitive_complexity)] mod test { use super::*; use std::string::String; #[test] fn empty_list() { let mut x = NetParameters::default(); let y = Vec::<(&String, &i32)>::new(); let u = x.saturating_update(y.into_iter()); assert!(u.is_empty()); } #[test] fn unknown_parameter() { let mut x = NetParameters::default(); let mut y = Vec::<(&String, &i32)>::new(); let k = &String::from("This_is_not_a_real_key"); let v = &456; y.push((k, v)); let u = x.saturating_update(y.into_iter()); assert_eq!(u, vec![&String::from("This_is_not_a_real_key")]); } // #[test] // fn duplicate_parameter() {} #[test] fn single_good_parameter() { let mut x = NetParameters::default(); let mut y = Vec::<(&String, &i32)>::new(); let k = &String::from("min_paths_for_circs_pct"); let v = &54; y.push((k, v)); let z = x.saturating_update(y.into_iter()); assert!(z.is_empty()); assert_eq!(x.min_circuit_path_threshold.as_percent().get(), 54); } #[test] fn multiple_good_parameters() { let mut x = NetParameters::default(); let mut y = Vec::<(&String, &i32)>::new(); let k = &String::from("min_paths_for_circs_pct"); let v = &54; y.push((k, v)); let k = &String::from("circwindow"); let v = &900; y.push((k, v)); let z = x.saturating_update(y.into_iter()); assert!(z.is_empty()); assert_eq!(x.min_circuit_path_threshold.as_percent().get(), 54); assert_eq!(x.circuit_window.get(), 900); } #[test] fn good_out_of_range() { let mut x = NetParameters::default(); let mut y = Vec::<(&String, &i32)>::new(); let k = &String::from("sendme_accept_min_version"); let v = &30; y.push((k, v)); let k = &String::from("min_paths_for_circs_pct"); let v = &255; y.push((k, v)); let z = x.saturating_update(y.into_iter()); assert!(z.is_empty()); assert_eq!(x.sendme_accept_min_version.get(), 30); assert_eq!(x.min_circuit_path_threshold.as_percent().get(), 95); } #[test] fn good_invalid_rep() { let mut x = NetParameters::default(); let mut y = Vec::<(&String, &i32)>::new(); let k = &String::from("sendme_accept_min_version"); let v = &30; y.push((k, v)); let k = &String::from("min_paths_for_circs_pct"); let v = &9000; y.push((k, v)); let z = x.saturating_update(y.into_iter()); assert!(z.is_empty()); assert_eq!(x.sendme_accept_min_version.get(), 30); assert_eq!(x.min_circuit_path_threshold.as_percent().get(), 95); } // #[test] // fn good_duplicate() {} #[test] fn good_unknown() { let mut x = NetParameters::default(); let mut y = Vec::<(&String, &i32)>::new(); let k = &String::from("sendme_accept_min_version"); let v = &30; y.push((k, v)); let k = &String::from("not_a_real_parameter"); let v = &9000; y.push((k, v)); let z = x.saturating_update(y.into_iter()); assert_eq!(z, vec![&String::from("not_a_real_parameter")]); assert_eq!(x.sendme_accept_min_version.get(), 30); } #[test] fn from_consensus() { let mut p = NetParameters::default(); let mut mp: std::collections::HashMap = std::collections::HashMap::new(); mp.insert("bwweightscale".to_string(), 70); mp.insert("min_paths_for_circs_pct".to_string(), 45); mp.insert("im_a_little_teapot".to_string(), 1); mp.insert("circwindow".to_string(), 99999); mp.insert("ExtendByEd25519ID".to_string(), 1); let z = p.saturating_update(mp.iter()); assert_eq!(z, vec![&String::from("im_a_little_teapot")]); assert_eq!(p.bw_weight_scale.get(), 70); assert_eq!(p.min_circuit_path_threshold.as_percent().get(), 45); let b_val: bool = p.extend_by_ed25519_id.into(); assert!(b_val); } #[test] fn all_parameters() { use std::convert::TryFrom; use std::time::Duration; let mut p = NetParameters::default(); let mp = [ ("bwweightscale", 10), ("cbtdisabled", 1), ("cbtnummodes", 11), ("cbtrecentcount", 12), ("cbtmaxtimeouts", 13), ("cbtmincircs", 5), ("cbtquantile", 61), ("cbtclosequantile", 15), ("cbtlearntimeout", 1900), ("cbtmintimeout", 2020), ("cbtinitialtimeout", 2050), ("cbttestfreq", 110), ("cbtmaxopencircs", 14), ("circwindow", 999), ("CircuitPriorityHalflifeMsec", 222), ("guard-lifetime-days", 36), ("guard-confirmed-min-lifetime-days", 37), ("guard-internet-likely-down-interval", 38), ("guard-max-sample-size", 39), ("guard-max-sample-threshold", 40), ("guard-min-filtered-sample-size", 41), ("guard-n-primary-guards", 42), ("guard-n-primary-guards-to-use", 43), ("guard-n-primary-dir-guards-to-use", 44), ("guard-nonprimary-guard-connect-timeout", 45), ("guard-nonprimary-guard-idle-timeout", 46), ("guard-remove-unlisted-guards-after-days", 47), ("guard-meaningful-restriction-percent", 12), ("guard-extreme-restriction-percent", 3), ("ExtendByEd25519ID", 0), ("min_paths_for_circs_pct", 51), ("nf_conntimeout_clients", 606), ("sendme_accept_min_version", 31), ("sendme_emit_min_version", 32), ]; let ignored = p.saturating_update(mp.iter().map(|(a, b)| (a, b))); assert!(ignored.is_empty()); assert_eq!(p.bw_weight_scale.get(), 10); assert!(bool::from(p.cbt_learning_disabled)); assert_eq!(p.cbt_num_xm_modes.get(), 11); assert_eq!(p.cbt_success_count.get(), 12); assert_eq!(p.cbt_max_timeouts.get(), 13); assert_eq!(p.cbt_min_circs_for_estimate.get(), 5); assert_eq!(p.cbt_timeout_quantile.as_percent().get(), 61); assert_eq!(p.cbt_abandon_quantile.as_percent().get(), 15); assert_eq!( Duration::try_from(p.unused_client_circ_timeout_while_learning_cbt).unwrap(), Duration::from_secs(1900) ); assert_eq!( Duration::try_from(p.cbt_min_timeout).unwrap(), Duration::from_millis(2020) ); assert_eq!( Duration::try_from(p.cbt_initial_timeout).unwrap(), Duration::from_millis(2050) ); assert_eq!( Duration::try_from(p.cbt_testing_delay).unwrap(), Duration::from_secs(110) ); assert_eq!(p.cbt_max_open_circuits_for_testing.get(), 14); assert_eq!(p.circuit_window.get(), 999); assert_eq!( Duration::try_from(p.circuit_priority_half_life).unwrap(), Duration::from_millis(222) ); assert!(!bool::from(p.extend_by_ed25519_id)); assert_eq!(p.min_circuit_path_threshold.as_percent().get(), 51); assert_eq!( Duration::try_from(p.unused_client_circ_timeout).unwrap(), Duration::from_secs(606) ); assert_eq!(p.sendme_accept_min_version.get(), 31); assert_eq!(p.sendme_emit_min_version.get(), 32); assert_eq!( Duration::try_from(p.guard_lifetime_unconfirmed).unwrap(), Duration::from_secs(86400 * 36) ); assert_eq!( Duration::try_from(p.guard_lifetime_confirmed).unwrap(), Duration::from_secs(86400 * 37) ); assert_eq!( Duration::try_from(p.guard_internet_likely_down).unwrap(), Duration::from_secs(38) ); assert_eq!(p.guard_max_sample_size.get(), 39); assert_eq!(p.guard_max_sample_threshold.as_percent().get(), 40); assert_eq!(p.guard_filtered_min_sample_size.get(), 41); assert_eq!(p.guard_n_primary.get(), 42); assert_eq!(p.guard_use_parallelism.get(), 43); assert_eq!(p.guard_dir_use_parallelism.get(), 44); assert_eq!( Duration::try_from(p.guard_nonprimary_connect_timeout).unwrap(), Duration::from_secs(45) ); assert_eq!( Duration::try_from(p.guard_nonprimary_idle_timeout).unwrap(), Duration::from_secs(46) ); assert_eq!( Duration::try_from(p.guard_remove_unlisted_after).unwrap(), Duration::from_secs(86400 * 47) ); assert_eq!(p.guard_meaningful_restriction.as_percent().get(), 12); assert_eq!(p.guard_extreme_restriction.as_percent().get(), 3); } }