//! An internal pool object that we use to implement HsCircPool. use std::time::{Duration, Instant}; use crate::{ AbstractTunnel, hspool::{HsCircStem, HsCircStemKind}, }; use rand::Rng; use tor_basic_utils::RngExt as _; /// A collection of circuits used to fulfil onion-service-related requests. pub(super) struct Pool { /// The collection of circuits themselves, in no particular order. circuits: Vec>, /// The number of NAIVE elements that we would like to have in our pool. stem_target: usize, /// The number of GUARDED elements that we would like to have in our pool. guarded_stem_target: usize, /// True if we have exhausted our pool since the last time we decided /// whether to change our target level. have_been_exhausted: bool, /// True if we have been under 4/5 of our target since the last time we /// decided whether to change it. have_been_under_highwater: bool, /// Last time when we changed our target size. last_changed_target: Option, } /// Our default (and minimum) target NAIVE pool size. const DEFAULT_NAIVE_STEM_TARGET: usize = 3; /// Our default (and minimum) target GUARDED pool size. const DEFAULT_GUARDED_STEM_TARGET: usize = 1; /// Our maximum target NAIVE pool size. We will never let our NAIVE target grow above this /// value. const MAX_NAIVE_STEM_TARGET: usize = 384; /// Our maximum target GUARDED pool size. We will never let our GUARDED target grow above this /// value. const MAX_GUARDED_STEM_TARGET: usize = 128; /// A type of circuit we would like to launch. /// /// [`ForLaunch::note_circ_launched`] should be called whenever a circuit /// of this [`HsCircStemKind`] is launched, to decrement the internal target `count`. pub(super) struct ForLaunch<'a> { /// The kind of circuit we want to launch. kind: HsCircStemKind, /// How many circuits of this kind do we need? /// /// This is a mutable reference to one of the target values from [`CircsToLaunch`]; /// we decrement it when we have launched a circuit of this type. count: &'a mut usize, } impl<'a> ForLaunch<'a> { /// A circuit was launched, decrement the current target for its kind. pub(super) fn note_circ_launched(self) { *self.count -= 1; } /// The kind of circuit we want to launch. pub(super) fn kind(&self) -> HsCircStemKind { self.kind } } /// The circuits we need to launch. pub(super) struct CircsToLaunch { /// The number of NAIVE circuits we want to launch. stem_target: usize, /// The number of GUARDED circuits we want to launch. guarded_stem_target: usize, } impl CircsToLaunch { /// Return a [`ForLaunch`] representing a circuit we would like to launch. pub(super) fn for_launch(&mut self) -> ForLaunch { // We start by launching NAIVE circuits. if self.stem_target > 0 { ForLaunch { kind: HsCircStemKind::Naive, count: &mut self.stem_target, } } else { // If we have enough NAIVE circuits, we can start launching GUARDED ones too. ForLaunch { kind: HsCircStemKind::Guarded, count: &mut self.guarded_stem_target, } } } /// Return the number of NAIVE circuits we would like to launch. pub(super) fn stem(&self) -> usize { self.stem_target } /// Return the number of GUARDED circuits we would like to launch. pub(super) fn guarded_stem(&self) -> usize { self.guarded_stem_target } /// Return the total number of circuits we would currently like to launch. pub(super) fn n_to_launch(&self) -> usize { self.stem_target + self.guarded_stem_target } } impl Default for Pool { fn default() -> Self { Self { circuits: Vec::new(), stem_target: DEFAULT_NAIVE_STEM_TARGET, guarded_stem_target: DEFAULT_GUARDED_STEM_TARGET, have_been_exhausted: false, have_been_under_highwater: false, last_changed_target: None, } } } impl Pool { /// Add `circ` to this pool pub(super) fn insert(&mut self, circ: HsCircStem) { self.circuits.push(circ); } /// Remove every circuit from this pool for which `f` returns false. pub(super) fn retain(&mut self, f: F) where F: FnMut(&HsCircStem) -> bool, { self.circuits.retain(f); } /// Return true if we are very low on circuits and should build more immediately. pub(super) fn very_low(&self) -> bool { self.circuits.len() <= self.target() / 3 } /// Return a [`CircsToLaunch`] describing the circuits we would currently like to launch. pub(super) fn circs_to_launch(&self) -> CircsToLaunch { CircsToLaunch { stem_target: self.stems_to_launch(), guarded_stem_target: self.guarded_stems_to_launch(), } } /// Return the number of NAIVE circuits we would currently like to launch. fn stems_to_launch(&self) -> usize { let circ_count = self .circuits .iter() .filter(|c| c.kind == HsCircStemKind::Naive) .count(); self.stem_target.saturating_sub(circ_count) } /// Return the number of GUARDED circuits we would currently like to launch. fn guarded_stems_to_launch(&self) -> usize { let circ_count = self .circuits .iter() .filter(|c| c.kind == HsCircStemKind::Guarded) .count(); self.guarded_stem_target.saturating_sub(circ_count) } /// Return the total number of circuits we would like to launch. /// /// We do not discard when we are _above_ this threshold, but we do /// try to build when we are low. fn target(&self) -> usize { self.stem_target + self.guarded_stem_target } /// If there is any circuit in this pool for which `f` returns true and that satisfies /// all of the specified [`HsCircPrefs`], return one such circuit at random, and remove /// it from the pool. /// /// If none of the circuits satisfy `prefs`, return a randomly selected circuit for which `f` /// returns true, and remove it from the pool. pub(super) fn take_one_where( &mut self, rng: &mut R, f: F, prefs: &HsCircPrefs, ) -> Option> where R: Rng, F: Fn(&HsCircStem) -> bool, { let rv = match random_idx_where(rng, &mut self.circuits[..], |circ_stem| { // First, check if any circuit matches _all_ the prefs circ_stem.satisfies_prefs(prefs) && f(circ_stem) }) .or_else(|| { // Select a circuit satisfying `f` at random. random_idx_where(rng, &mut self.circuits[..], f) }) { Some(idx) => Some(self.circuits.swap_remove(idx)), None => None, }; if self.circuits.is_empty() { self.have_been_exhausted = true; self.have_been_under_highwater = true; } else if self.circuits.len() < self.target() * 4 / 5 { self.have_been_under_highwater = true; } rv } /// Update the target sizes for our pool. /// /// This updates our target numbers of NAIVE and GUARDED circuits. pub(super) fn update_target_size(&mut self, now: Instant) { /// Minimum amount of time that must elapse between a change and a /// decision to grow our pool. We use this to control the rate of /// growth and make sure that we are allowing enough time for circuits /// to complete. const MIN_TIME_TO_GROW: Duration = Duration::from_secs(120); /// Minimum amount of time that must elapse between a target change and /// a decisions to shrink our target. We use this to make sure that we /// aren't shrinking too rapidly, and that we are allowing enough time /// for the pool to actually get used. const MIN_TIME_TO_SHRINK: Duration = Duration::from_secs(600); let last_changed = self.last_changed_target.get_or_insert(now); let time_since_last_change = now.saturating_duration_since(*last_changed); // TODO: we may want to have separate have_been_exhausted/have_been_under_highwater // flags for NAIVE and GUARDED circuits. // // TODO: stem_target and guarded_stem_target currently grow/shrink at the same rate, // which is not ideal. // // Instead, we should switch to an adaptive strategy, where the two targets are updated // based on how many NAIVE/GUARDED circuit requests we got. if self.have_been_exhausted { if time_since_last_change < MIN_TIME_TO_GROW { return; } self.stem_target *= 2; self.guarded_stem_target *= 2; } else if !self.have_been_under_highwater { if time_since_last_change < MIN_TIME_TO_SHRINK { return; } self.stem_target /= 2; self.guarded_stem_target /= 2; } self.last_changed_target = Some(now); self.stem_target = self .stem_target .clamp(DEFAULT_NAIVE_STEM_TARGET, MAX_NAIVE_STEM_TARGET); self.guarded_stem_target = self .guarded_stem_target .clamp(DEFAULT_GUARDED_STEM_TARGET, MAX_GUARDED_STEM_TARGET); self.have_been_exhausted = false; self.have_been_under_highwater = false; } /// Purge all the circuits from the pool. #[allow(clippy::unnecessary_wraps)] // for consistency and future-proofing pub(super) fn retire_all_circuits(&mut self) -> Result<(), tor_config::ReconfigureError> { self.have_been_exhausted = true; // Purge all circuits from this pool self.circuits.clear(); Ok(()) } } /// Preferences for what kind of circuit to select from the pool. #[derive(Default, Debug, Clone)] pub(super) struct HsCircPrefs { /// If `Some`, specifies the [`HsCircStemKind`] we would like. pub(super) kind_prefs: Option, } impl HsCircPrefs { /// Set the preferred [`HsCircStemKind`]. pub(super) fn preferred_stem_kind(&mut self, kind: HsCircStemKind) -> &mut Self { self.kind_prefs = Some(kind); self } } /// Helper: find a random item `elt` in `slice` such that `predicate(elt)` is /// true. Return the index of that item. /// /// Can arbitrarily reorder `slice`. This allows us to visit the indices in uniform-at-random /// order, without having to do any O(N) operations or allocations. fn random_idx_where(rng: &mut R, mut slice: &mut [T], predicate: P) -> Option where R: Rng, P: Fn(&T) -> bool, { while !slice.is_empty() { let idx = rng .gen_range_checked(0..slice.len()) .expect("slice was not empty but is now empty"); if predicate(&slice[idx]) { return Some(idx); } let last_idx = slice.len() - 1; // Move the one we just tried to the end, // and eliminate it from consideration. slice.swap(idx, last_idx); slice = &mut slice[..last_idx]; } // We didn't find any. None } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_time_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! use super::*; use tor_basic_utils::test_rng::testing_rng; #[test] fn random_idx() { let mut rng = testing_rng(); let mut orig_numbers: Vec = vec![1, 3, 4, 8, 11, 19, 12, 6, 27]; let mut numbers = orig_numbers.clone(); let mut found: std::collections::HashMap = numbers.iter().map(|n| (*n, false)).collect(); for _ in 0..1000 { let idx = random_idx_where(&mut rng, &mut numbers[..], |n| n & 1 == 1).unwrap(); assert!(numbers[idx] & 1 == 1); found.insert(numbers[idx], true); } for num in numbers.iter() { assert!(found[num] == (num & 1 == 1)); } // Number may be reordered, but should still have the same elements. numbers.sort(); orig_numbers.sort(); assert_eq!(numbers, orig_numbers); } #[test] fn random_idx_empty() { let mut rng = testing_rng(); let idx = random_idx_where(&mut rng, &mut [], |_: &i32| panic!()); assert_eq!(idx, None); } #[test] fn random_idx_none() { let mut rng = testing_rng(); let mut numbers: Vec = vec![1, 3, 4, 8, 11, 19, 12, 6, 27]; assert_eq!( random_idx_where(&mut rng, &mut numbers[..], |_: &i32| false), None ); } }