//! Code to represent its single guard node and track its status. use tor_basic_utils::retry::RetryDelay; use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; use tracing::{info, trace, warn}; use web_time_compat::{Duration, Instant, InstantExt, SystemTime}; use crate::dirstatus::DirStatus; use crate::sample::Candidate; use crate::skew::SkewObservation; use crate::util::randomize_time; use crate::{ExternalActivity, GuardSetSelector, GuardUsageKind, sample}; use crate::{GuardParams, GuardRestriction, GuardUsage, ids::GuardId}; #[cfg(feature = "bridge-client")] use safelog::Redactable as _; use tor_linkspec::{ ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds, }; use tor_persist::{Futureproof, JsonValue}; /// Tri-state to represent whether a guard is believed to be reachable or not. #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] #[allow(clippy::enum_variant_names)] pub(crate) enum Reachable { /// A guard is believed to be reachable, since we have successfully /// used it more recently than we've failed. Reachable, /// A guard is believed to be unreachable, since recent attempts /// to use it have failed, and not enough time has elapsed since then. Unreachable, /// We have never (during the lifetime of the current guard manager) /// tried to connect to this guard. #[default] Untried, /// The last time that we tried to connect to this guard, it failed, /// but enough time has elapsed that we think it is worth trying again. Retriable, } /// The name and version of the crate that first picked a potential /// guard. /// /// The C Tor implementation has found it useful to keep this information /// about guards, to better work around any bugs discovered in the guard /// implementation. #[derive(Clone, Debug, Serialize, Deserialize)] struct CrateId { /// The name of the crate that added this guard. #[serde(rename = "crate")] crate_name: String, /// The version of the crate that added this guard. version: String, } impl CrateId { /// Return a new CrateId representing this crate. fn this_crate() -> Option { let crate_name = option_env!("CARGO_PKG_NAME")?.to_string(); let version = option_env!("CARGO_PKG_VERSION")?.to_string(); Some(CrateId { crate_name, version, }) } } /// What rule do we use when we're displaying information about a guard? #[derive(Clone, Default, Debug)] pub(crate) enum DisplayRule { /// The guard is Sensitive; we should display it as "\[scrubbed\]". /// /// We use this for public relays on the network, since displaying even the /// redacted info about them can enough to identify them uniquely within the /// NetDir. /// /// This should not be too much of a hit for UX (we hope), since the user is /// not typically expected to work around issues with these guards themself. #[default] Sensitive, /// The guard should be Redacted; we display it as something like "192.x.x.x /// $ab...". /// /// We use this for bridges. #[cfg(feature = "bridge-client")] Redacted, } /// A single guard node, as held by the guard manager. /// /// A Guard is a Tor relay that clients use for the first hop of their circuits. /// It doesn't need to be a relay that's currently on the network (that is, one /// that we could represent as a [`Relay`](tor_netdir::Relay)): guards might be /// temporarily unlisted. /// /// Some fields in guards are persistent; others are reset with every process. /// /// # Identity /// /// Every guard has at least one `RelayId`. A guard may _gain_ identities over /// time, as we learn more about it, but it should never _lose_ or _change_ its /// identities of a given type. /// /// # TODO /// /// This structure uses [`Instant`] to represent non-persistent points in time, /// and [`SystemTime`] to represent points in time that need to be persistent. /// That's possibly undesirable; maybe we should come up with a better solution. #[derive(Clone, Debug, Serialize, Deserialize)] pub(crate) struct Guard { /// The identity keys for this guard. id: GuardId, /// The most recently seen addresses for this guard. If `pt_targets` is /// empty, these are the addresses we use for making OR connections to this /// guard directly. If `pt_targets` is nonempty, these are addresses at /// which the server is "located" (q.v. [`HasAddrs`]), but not ways to /// connect to it. orports: Vec, /// Any `PtTarget` instances that we know about for connecting to this guard /// over a pluggable transport. /// /// If this is empty, then this guard only supports direct connections, at /// the locations in `orports`. /// /// (Currently, this is always empty, or a singleton. If we find more than /// one, we only look at the first. It is a vector only for forward /// compatibility.) // // TODO: We may want to replace pt_targets and orports with a new structure; // maybe a PtAddress and a list of SocketAddr. But we'll keep them like // this for now to keep backward compatibility. #[serde(default, skip_serializing_if = "Vec::is_empty")] pt_targets: Vec, /// When, approximately, did we first add this guard to our sample? #[serde(with = "humantime_serde")] added_at: SystemTime, /// What version of this crate added this guard to our sample? added_by: Option, /// If present, this guard is permanently disabled, and this /// object tells us why. #[serde(default)] disabled: Option>, /// When, approximately, did we first successfully use this guard? /// /// (We call a guard "confirmed" if we have successfully used it at /// least once.) #[serde(with = "humantime_serde")] confirmed_at: Option, /// If this guard is not listed in the current-consensus, this is the /// `valid_after` date of the oldest consensus in which it was not listed. /// /// A guard counts as "unlisted" if it is absent, unusable, or /// doesn't have the Guard flag. #[serde(with = "humantime_serde")] unlisted_since: Option, /// True if this guard is listed in the latest consensus, but we don't /// have a microdescriptor for it. #[serde(skip)] dir_info_missing: bool, /// When did we last give out this guard in response to a request? #[serde(skip)] last_tried_to_connect_at: Option, /// If this guard is currently Unreachable, when should we next /// retry it? /// /// (Retrying a guard involves clearing this field, and setting /// `reachable`) #[serde(skip)] retry_at: Option, // derived from retry_schedule. /// Schedule use to determine when we can next attempt to connect to this /// guard. #[serde(skip)] retry_schedule: Option, /// Current reachability status for this guard. #[serde(skip)] reachable: Reachable, /// If true, then the last time we saw a relay entry for this /// guard, it seemed like a valid directory cache. #[serde(skip)] is_dir_cache: bool, /// Status for this guard, when used as a directory cache. /// /// (This is separate from `Reachable` and `retry_schedule`, since being /// usable for circuit construction does not necessarily mean that the guard /// will have good, timely cache information. If it were not separate, then /// circuit success would clear directory failures.) #[serde(skip, default = "guard_dirstatus")] dir_status: DirStatus, /// If true, we have given this guard out for an exploratory circuit, /// and that exploratory circuit is still pending. /// /// A circuit is "exploratory" if we launched it on a non-primary guard. // TODO: Maybe this should be an integer that counts a number of such // circuits? #[serde(skip)] exploratory_circ_pending: bool, /// A count of all the circuit statuses we've seen on this guard. /// /// Used to implement a lightweight version of path-bias detection. #[serde(skip)] circ_history: CircHistory, /// True if we have warned about this guard behaving suspiciously. #[serde(skip)] suspicious_behavior_warned: bool, /// Latest clock skew (if any) we have observed from this guard. #[serde(skip)] clock_skew: Option, /// How should we display information about this guard? #[serde(skip)] sensitivity: DisplayRule, /// Fields from the state file that was used to make this `Guard` that /// this version of Arti doesn't understand. #[serde(flatten)] unknown_fields: HashMap, } /// Lower bound for delay after get a failure using a guard as a directory /// cache. const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60); /// Return a DirStatus entry for a guard. fn guard_dirstatus() -> DirStatus { DirStatus::new(GUARD_DIR_RETRY_FLOOR) } /// Wrapper to declare whether a given successful use of a guard is the /// _first_ successful use of the guard. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub(crate) enum NewlyConfirmed { /// This was the first successful use of a guard. Yes, /// This guard has been used successfully before. No, } impl Guard { /// Create a new unused [`Guard`] from a [`Candidate`]. pub(crate) fn from_candidate( candidate: Candidate, now: SystemTime, params: &GuardParams, ) -> Self { let Candidate { is_dir_cache, full_dir_info, owned_target, .. } = candidate; Guard { is_dir_cache, dir_info_missing: !full_dir_info, ..Self::from_chan_target(&owned_target, now, params) } } /// Create a new unused [`Guard`] from a [`ChanTarget`]. /// /// This function doesn't check whether the provided relay is a /// suitable guard node or not: that's up to the caller to decide. fn from_chan_target(relay: &T, now: SystemTime, params: &GuardParams) -> Self where T: ChanTarget, { let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10); let pt_target = match relay.chan_method() { #[cfg(feature = "pt-client")] ChannelMethod::Pluggable(pt) => Some(pt), _ => None, }; Self::new( GuardId::from_relay_ids(relay), relay.addrs().collect_vec(), pt_target, added_at, ) } /// Return a new, manually constructed [`Guard`]. fn new( id: GuardId, orports: Vec, pt_target: Option, added_at: SystemTime, ) -> Self { Guard { id, orports, pt_targets: pt_target.into_iter().collect(), added_at, added_by: CrateId::this_crate(), disabled: None, confirmed_at: None, unlisted_since: None, dir_info_missing: false, last_tried_to_connect_at: None, reachable: Reachable::Untried, retry_at: None, dir_status: guard_dirstatus(), retry_schedule: None, is_dir_cache: true, exploratory_circ_pending: false, circ_history: CircHistory::default(), suspicious_behavior_warned: false, clock_skew: None, unknown_fields: Default::default(), sensitivity: DisplayRule::Sensitive, } } /// Return the identity of this Guard. pub(crate) fn guard_id(&self) -> &GuardId { &self.id } /// Return the reachability status for this guard. pub(crate) fn reachable(&self) -> Reachable { self.reachable } /// Return the next time at which this guard will be retriable for a given /// usage. /// /// (Return None if we think this guard might be reachable right now.) pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option { match &usage.kind { GuardUsageKind::Data => self.retry_at, GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()] .iter() .flatten() .max() .copied(), } } /// Return true if this guard is usable and working according to our latest /// configuration and directory information, and hasn't been turned off for /// some other reason. pub(crate) fn usable(&self) -> bool { self.unlisted_since.is_none() && self.disabled.is_none() } /// Return true if this guard is ready (with respect to any timeouts) for /// the given `usage` at `now`. pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool { if let Some(retry_at) = self.retry_at { if retry_at > now { return false; } } match usage.kind { GuardUsageKind::Data => true, GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now), } } /// Copy all _non-persistent_ status from `other` to self. /// /// We do this when we were not the owner of our persistent state, and we /// have just reloaded it (as `self`), but we have some ephemeral knowledge /// about this guard (as `other`). /// /// You should not invent new uses for this function; instead we should come /// up with alternatives. /// /// # Panics /// /// Panics if the identities in `self` are not exactly the same as the /// identities in `other`. pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard { // It is not safe to copy failure information unless these identities // are a superset of those in `other`; but it is not safe to copy success // information unless these identities are a subset of those in `other`. // // To simplify matters, we just insist that the identities have to be the same. assert!(self.same_relay_ids(&other)); Guard { // All other persistent fields are taken from `self`. id: self.id, pt_targets: self.pt_targets, orports: self.orports, added_at: self.added_at, added_by: self.added_by, disabled: self.disabled, confirmed_at: self.confirmed_at, unlisted_since: self.unlisted_since, unknown_fields: self.unknown_fields, // All non-persistent fields get taken from `other`. last_tried_to_connect_at: other.last_tried_to_connect_at, retry_at: other.retry_at, retry_schedule: other.retry_schedule, reachable: other.reachable, is_dir_cache: other.is_dir_cache, exploratory_circ_pending: other.exploratory_circ_pending, dir_info_missing: other.dir_info_missing, circ_history: other.circ_history, suspicious_behavior_warned: other.suspicious_behavior_warned, dir_status: other.dir_status, clock_skew: other.clock_skew, sensitivity: other.sensitivity, // Note that we _could_ remove either of the above blocks and add // `..self` or `..other`, but that would be risky: it would increase // the odds that we would forget to add some persistent or // non-persistent field to the right group in the future. } } /// Change the reachability status for this guard. fn set_reachable(&mut self, r: Reachable) { use Reachable as R; if self.reachable != r { // High-level logs, if change is interesting to user. match (self.reachable, r) { (_, R::Reachable) => info!("We have found that guard {} is usable.", self), (R::Untried | R::Reachable, R::Unreachable) => match self.retry_at { Some(retry_at) => warn!( "Could not connect to guard {}. Retrying in {}.", self, humantime::format_duration(retry_at - Instant::get()), ), None => warn!( "Could not connect to guard {}. Next retry time unknown.", self ), }, (_, _) => {} // not interesting. } // trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed."); self.reachable = r; } } /// Return true if at least one exploratory circuit is pending to this /// guard. /// /// A circuit is "exploratory" if launched on a non-primary guard. /// /// # TODO /// /// The "exploratory" definition doesn't quite match up with the behavior /// in the spec, but it is what Tor does. pub(crate) fn exploratory_circ_pending(&self) -> bool { self.exploratory_circ_pending } /// Note that an exploratory circuit is pending (if `pending` is true), /// or not pending (if `pending` is false. pub(crate) fn note_exploratory_circ(&mut self, pending: bool) { self.exploratory_circ_pending = pending; } /// Possibly mark this guard as retriable, if it has been down for /// long enough. /// /// Specifically, if the guard is to be Unreachable, and our last attempt /// to connect to it is far enough in the past from `now`, we change its /// status to Unknown. pub(crate) fn consider_retry(&mut self, now: Instant) { if let Some(retry_at) = self.retry_at { debug_assert!(self.reachable == Reachable::Unreachable); if retry_at <= now { self.mark_retriable(); } } } /// If this guard is marked Unreachable, clear its unreachability status /// and mark it as Retriable. pub(crate) fn mark_retriable(&mut self) { if self.reachable == Reachable::Unreachable { self.set_reachable(Reachable::Retriable); self.retry_at = None; self.retry_schedule = None; } } /// Return true if this guard obeys all of the given restrictions. fn obeys_restrictions(&self, restrictions: &[GuardRestriction]) -> bool { restrictions.iter().all(|r| self.obeys_restriction(r)) } /// Return true if this guard obeys a single restriction. fn obeys_restriction(&self, r: &GuardRestriction) -> bool { match r { GuardRestriction::AvoidId(avoid_id) => !self.id.0.has_identity(avoid_id.as_ref()), GuardRestriction::AvoidAllIds(avoid_ids) => { self.id.0.identities().all(|id| !avoid_ids.contains(id)) } } } /// Return true if this guard is suitable to use for the provided `usage`. pub(crate) fn conforms_to_usage(&self, usage: &GuardUsage) -> bool { match usage.kind { GuardUsageKind::OneHopDirectory => { if !self.is_dir_cache { return false; } } GuardUsageKind::Data => { // We need a "definitely listed" guard to build a multihop // circuit. if self.dir_info_missing { return false; } } } self.obeys_restrictions(&usage.restrictions[..]) } /// Check whether this guard is listed in the provided [`sample::Universe`]. /// /// Returns `Some(true)` if it is definitely listed, and `Some(false)` if it /// is definitely not listed. A `None` return indicates that we need to /// download more directory information about this guard before we can be /// certain whether this guard is listed or not. pub(crate) fn listed_in(&self, universe: &U) -> Option { universe.contains(self) } /// Change this guard's status based on a newly received or newly updated /// [`sample::Universe`]. /// /// A guard may become "listed" or "unlisted": a listed guard is one that /// appears in the consensus with the Guard flag. /// /// A guard may acquire additional identities if we learned them from the /// guard, either directly or via an authenticated directory document. /// /// Additionally, a guard's `orports` or `pt_targets` may change, if the /// `universe` lists a new address for the relay. pub(crate) fn update_from_universe(&mut self, universe: &U) { // This is a tricky check, since if we're missing directory information // for the guard, we won't know its full set of identities. use sample::CandidateStatus::*; let listed_as_guard = match universe.status(self) { Present(Candidate { listed_as_guard, is_dir_cache, full_dir_info, owned_target, sensitivity, }) => { // Update address information. self.orports = owned_target.addrs().collect_vec(); // Update Pt information. self.pt_targets = match owned_target.chan_method() { #[cfg(feature = "pt-client")] ChannelMethod::Pluggable(pt) => vec![pt], _ => Vec::new(), }; // Check whether we can currently use it as a directory cache. self.is_dir_cache = is_dir_cache; // Update our IDs: the Relay will have strictly more. assert!(owned_target.has_all_relay_ids_from(self)); self.id = GuardId(RelayIds::from_relay_ids(&owned_target)); self.dir_info_missing = !full_dir_info; self.sensitivity = sensitivity; listed_as_guard } Absent => false, // Definitely not listed. Uncertain => { // We can't tell if this is listed without more directory information. self.dir_info_missing = true; return; } }; if listed_as_guard { // Definitely listed, so clear unlisted_since. self.mark_listed(); } else { // Unlisted or not a guard; mark it unlisted. self.mark_unlisted(universe.timestamp()); } } /// Mark this guard as currently listed in the directory. fn mark_listed(&mut self) { if self.unlisted_since.is_some() { trace!(guard_id = ?self.id, "Guard is now listed again."); self.unlisted_since = None; } } /// Mark this guard as having been unlisted since `now`, if it is not /// already so marked. fn mark_unlisted(&mut self, now: SystemTime) { if self.unlisted_since.is_none() { trace!(guard_id = ?self.id, "Guard is now unlisted."); self.unlisted_since = Some(now); } } /// Return true if we should remove this guard from the current guard /// sample. /// /// Guards may be ready for removal because they have been /// confirmed too long ago, if they have been sampled too long ago /// (if they are not confirmed), or if they have been unlisted for /// too long. pub(crate) fn is_expired(&self, params: &GuardParams, now: SystemTime) -> bool { /// Helper: Return true if `t2` is after `t1` by at least `d`. fn expired_by(t1: SystemTime, d: Duration, t2: SystemTime) -> bool { if let Ok(elapsed) = t2.duration_since(t1) { elapsed > d } else { false } } if self.disabled.is_some() { // We never forget a guard that we've disabled: we've disabled // it for a reason. return false; } if let Some(confirmed_at) = self.confirmed_at { if expired_by(confirmed_at, params.lifetime_confirmed, now) { return true; } } else if expired_by(self.added_at, params.lifetime_unconfirmed, now) { return true; } if let Some(unlisted_since) = self.unlisted_since { if expired_by(unlisted_since, params.lifetime_unlisted, now) { return true; } } false } /// Record that a failure has happened for this guard. /// /// If `is_primary` is true, this is a primary guard (q.v.). pub(crate) fn record_failure(&mut self, now: Instant, is_primary: bool) { let mut rng = rand::rng(); let retry_interval = self .retry_schedule .get_or_insert_with(|| retry_schedule(is_primary)) .next_delay(&mut rng); // TODO-SPEC: Document this behavior in guard-spec. self.retry_at = Some(now + retry_interval); self.set_reachable(Reachable::Unreachable); self.exploratory_circ_pending = false; self.circ_history.n_failures += 1; } /// Note that we have launch an attempted use of this guard. /// /// We use this time to decide when to retry failing guards, and /// to see if the guard has been "pending" for a long time. pub(crate) fn record_attempt(&mut self, connect_attempt: Instant) { self.last_tried_to_connect_at = self .last_tried_to_connect_at .map(|last| last.max(connect_attempt)) .or(Some(connect_attempt)); } /// Return true if this guard has an exploratory circuit pending and /// if the most recent attempt to connect to it is after `when`. /// /// See [`Self::exploratory_circ_pending`]. pub(crate) fn exploratory_attempt_after(&self, when: Instant) -> bool { self.exploratory_circ_pending && self.last_tried_to_connect_at.map(|t| t > when) == Some(true) } /// Note that a guard has been used successfully. /// /// Updates that guard's status to reachable, clears any failing status /// information for it, and decides whether the guard is newly confirmed. /// /// If the guard is newly confirmed, the caller must add it to the /// list of confirmed guards. #[must_use = "You need to check whether a succeeding guard is confirmed."] pub(crate) fn record_success( &mut self, now: SystemTime, params: &GuardParams, ) -> NewlyConfirmed { self.retry_at = None; self.retry_schedule = None; self.set_reachable(Reachable::Reachable); self.exploratory_circ_pending = false; self.circ_history.n_successes += 1; if self.confirmed_at.is_none() { self.confirmed_at = Some( randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10) .max(self.added_at), ); // TODO-SPEC: The "max" above isn't specified by guard-spec, // but I think it's wise. trace!(guard_id = ?self.id, "Newly confirmed"); NewlyConfirmed::Yes } else { NewlyConfirmed::No } } /// Record that an external operation has succeeded on this guard. pub(crate) fn record_external_success(&mut self, how: ExternalActivity) { match how { ExternalActivity::DirCache => { self.dir_status.note_success(); } } } /// Record that an external operation has failed on this guard. pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) { match how { ExternalActivity::DirCache => { self.dir_status.note_failure(now); } } } /// Note that a circuit through this guard died in a way that we couldn't /// necessarily attribute to the guard. pub(crate) fn record_indeterminate_result(&mut self) { self.circ_history.n_indeterminate += 1; if let Some(ratio) = self.circ_history.indeterminate_ratio() { // TODO: These should not be hardwired, and they may be set // too high. /// If this fraction of circs are suspicious, we should disable /// the guard. const DISABLE_THRESHOLD: f64 = 0.7; /// If this fraction of circuits are suspicious, we should /// warn. const WARN_THRESHOLD: f64 = 0.5; if ratio > DISABLE_THRESHOLD { let reason = GuardDisabled::TooManyIndeterminateFailures { history: self.circ_history.clone(), failure_ratio: ratio, threshold_ratio: DISABLE_THRESHOLD, }; warn!(guard=?self.id, "Disabling guard: {:.1}% of circuits died under mysterious circumstances, exceeding threshold of {:.1}%", ratio*100.0, (DISABLE_THRESHOLD*100.0)); self.disabled = Some(reason.into()); } else if ratio > WARN_THRESHOLD && !self.suspicious_behavior_warned { warn!(guard=?self.id, "Questionable guard: {:.1}% of circuits died under mysterious circumstances.", ratio*100.0); self.suspicious_behavior_warned = true; } } } /// Return a [`FirstHop`](crate::FirstHop) object to represent this guard. pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop { crate::FirstHop { sample: Some(selection), inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target( self, )), } } /// Record that a given fallback has told us about clock skew. pub(crate) fn note_skew(&mut self, observation: SkewObservation) { self.clock_skew = Some(observation); } /// Return the most recent clock skew observation for this guard, if we have /// made one. pub(crate) fn skew(&self) -> Option<&SkewObservation> { self.clock_skew.as_ref() } /// Testing only: Return true if this guard was ever contacted successfully. #[cfg(test)] pub(crate) fn confirmed(&self) -> bool { self.confirmed_at.is_some() } } impl tor_linkspec::HasAddrs for Guard { fn addrs(&self) -> impl Iterator { self.orports.iter().copied() } } impl tor_linkspec::HasRelayIds for Guard { fn identity( &self, key_type: tor_linkspec::RelayIdType, ) -> Option> { self.id.0.identity(key_type) } } impl tor_linkspec::HasChanMethod for Guard { fn chan_method(&self) -> ChannelMethod { match &self.pt_targets[..] { #[cfg(feature = "pt-client")] [first, ..] => ChannelMethod::Pluggable(first.clone()), #[cfg(not(feature = "pt-client"))] [_first, ..] => ChannelMethod::Direct(vec![]), // can't connect to this; no pt support. [] => ChannelMethod::Direct(self.orports.clone()), } } } impl tor_linkspec::ChanTarget for Guard {} impl std::fmt::Display for Guard { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.sensitivity { DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f), #[cfg(feature = "bridge-client")] DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f), } } } /// A reason for permanently disabling a guard. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type")] enum GuardDisabled { /// Too many attempts to use this guard failed for indeterminate reasons. TooManyIndeterminateFailures { /// Observed count of status reports about this guard. history: CircHistory, /// Observed fraction of indeterminate status reports. failure_ratio: f64, /// Threshold that was exceeded. threshold_ratio: f64, }, } /// Return a new RetryDelay tracker for a guard. /// /// `is_primary should be true if the guard is primary. fn retry_schedule(is_primary: bool) -> RetryDelay { let minimum = if is_primary { Duration::from_secs(30) } else { Duration::from_secs(150) }; RetryDelay::from_duration(minimum) } /// The recent history of circuit activity on this guard. /// /// We keep this information so that we can tell if too many circuits are /// winding up in "indeterminate" status. /// /// # What's this for? /// /// Recall that an "indeterminate" circuit failure is one that might /// or might not be the guard's fault. For example, if the second hop /// of the circuit fails, we can't tell whether to blame the guard, /// the second hop, or the internet between them. /// /// But we don't want to allow an unbounded number of indeterminate /// failures: if we did, it would allow a malicious guard to simply /// reject any circuit whose second hop it didn't like, and thereby /// filter the client's paths down to a hostile subset. /// /// So as a workaround, and to discourage this kind of behavior, we /// track the fraction of indeterminate circuits, and disable any guard /// where the fraction is too high. // // TODO: We may eventually want to make this structure persistent. If we // do, however, we'll need a way to make ancient history expire. We might // want that anyway, to make attacks harder. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub(crate) struct CircHistory { /// How many times have we seen this guard succeed? n_successes: u32, /// How many times have we seen this guard fail? #[allow(dead_code)] // not actually used yet. n_failures: u32, /// How many times has this guard given us indeterminate results? n_indeterminate: u32, } impl CircHistory { /// If we have seen enough, return the fraction of circuits that have /// "died under mysterious circumstances". fn indeterminate_ratio(&self) -> Option { // TODO: This should probably not be hardwired /// Don't try to give a ratio unless we've seen this many observations. const MIN_OBSERVATIONS: u32 = 15; let total = self.n_successes + self.n_indeterminate; if total < MIN_OBSERVATIONS { return None; } Some(f64::from(self.n_indeterminate) / f64::from(total)) } } #[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)] #![allow(clippy::string_slice)] // See arti#2571 //! use super::*; use crate::ids::FirstHopId; use tor_linkspec::{HasRelayIds, RelayId}; use tor_llcrypto::pk::ed25519::Ed25519Identity; use web_time_compat::SystemTimeExt; #[test] fn crate_id() { let id = CrateId::this_crate().unwrap(); assert_eq!(&id.crate_name, "tor-guardmgr"); assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION")); } fn basic_id() -> GuardId { GuardId::new([13; 32].into(), [37; 20].into()) } fn basic_guard() -> Guard { let id = basic_id(); let ports = vec!["127.0.0.7:7777".parse().unwrap()]; let added = SystemTime::get(); Guard::new(id, ports, None, added) } #[test] fn simple_accessors() { fn ed(id: [u8; 32]) -> RelayId { RelayId::Ed25519(id.into()) } let id = basic_id(); let g = basic_guard(); assert_eq!(g.guard_id(), &id); assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id))); assert_eq!( g.addrs().collect_vec(), &["127.0.0.7:7777".parse().unwrap()] ); assert_eq!(g.reachable(), Reachable::Untried); assert_eq!(g.reachable(), Reachable::default()); use crate::GuardUsageBuilder; let mut usage1 = GuardUsageBuilder::new(); usage1 .restrictions() .push(GuardRestriction::AvoidId(ed([22; 32]))); let usage1 = usage1.build().unwrap(); let mut usage2 = GuardUsageBuilder::new(); usage2 .restrictions() .push(GuardRestriction::AvoidId(ed([13; 32]))); let usage2 = usage2.build().unwrap(); let usage3 = GuardUsage::default(); let mut usage4 = GuardUsageBuilder::new(); usage4 .restrictions() .push(GuardRestriction::AvoidId(ed([22; 32]))); usage4 .restrictions() .push(GuardRestriction::AvoidId(ed([13; 32]))); let usage4 = usage4.build().unwrap(); let mut usage5 = GuardUsageBuilder::new(); usage5.restrictions().push(GuardRestriction::AvoidAllIds( vec![ed([22; 32]), ed([13; 32])].into_iter().collect(), )); let usage5 = usage5.build().unwrap(); let mut usage6 = GuardUsageBuilder::new(); usage6.restrictions().push(GuardRestriction::AvoidAllIds( vec![ed([99; 32]), ed([100; 32])].into_iter().collect(), )); let usage6 = usage6.build().unwrap(); assert!(g.conforms_to_usage(&usage1)); assert!(!g.conforms_to_usage(&usage2)); assert!(g.conforms_to_usage(&usage3)); assert!(!g.conforms_to_usage(&usage4)); assert!(!g.conforms_to_usage(&usage5)); assert!(g.conforms_to_usage(&usage6)); } #[allow(clippy::redundant_clone)] #[test] fn trickier_usages() { let g = basic_guard(); use crate::{GuardUsageBuilder, GuardUsageKind}; let data_usage = GuardUsageBuilder::new() .kind(GuardUsageKind::Data) .build() .unwrap(); let dir_usage = GuardUsageBuilder::new() .kind(GuardUsageKind::OneHopDirectory) .build() .unwrap(); assert!(g.conforms_to_usage(&data_usage)); assert!(g.conforms_to_usage(&dir_usage)); let mut g2 = g.clone(); g2.dir_info_missing = true; assert!(!g2.conforms_to_usage(&data_usage)); assert!(g2.conforms_to_usage(&dir_usage)); let mut g3 = g.clone(); g3.is_dir_cache = false; assert!(g3.conforms_to_usage(&data_usage)); assert!(!g3.conforms_to_usage(&dir_usage)); } #[test] fn record_attempt() { let t1 = Instant::get() - Duration::from_secs(10); let t2 = Instant::get() - Duration::from_secs(5); let t3 = Instant::get(); let mut g = basic_guard(); assert!(g.last_tried_to_connect_at.is_none()); g.record_attempt(t1); assert_eq!(g.last_tried_to_connect_at, Some(t1)); g.record_attempt(t3); assert_eq!(g.last_tried_to_connect_at, Some(t3)); g.record_attempt(t2); assert_eq!(g.last_tried_to_connect_at, Some(t3)); } #[test] fn record_failure() { let t1 = Instant::get() - Duration::from_secs(10); let t2 = Instant::get(); let mut g = basic_guard(); g.record_failure(t1, true); assert!(g.retry_schedule.is_some()); assert_eq!(g.reachable(), Reachable::Unreachable); let retry1 = g.retry_at.unwrap(); assert_eq!(retry1, t1 + Duration::from_secs(30)); g.record_failure(t2, true); let retry2 = g.retry_at.unwrap(); assert!(retry2 >= t2 + Duration::from_secs(30)); assert!(retry2 <= t2 + Duration::from_secs(200)); } #[test] fn record_success() { let t1 = Instant::get() - Duration::from_secs(10); // has to be in the future, since the guard's "added_at" time is based on now. let now = SystemTime::get(); let t2 = now + Duration::from_secs(300 * 86400); let t3 = Instant::get() + Duration::from_secs(310 * 86400); let t4 = now + Duration::from_secs(320 * 86400); let mut g = basic_guard(); g.record_failure(t1, true); assert_eq!(g.reachable(), Reachable::Unreachable); let conf = g.record_success(t2, &GuardParams::default()); assert_eq!(g.reachable(), Reachable::Reachable); assert_eq!(conf, NewlyConfirmed::Yes); assert!(g.retry_at.is_none()); assert!(g.confirmed_at.unwrap() <= t2); assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400)); let confirmed_at_orig = g.confirmed_at; g.record_failure(t3, true); assert_eq!(g.reachable(), Reachable::Unreachable); let conf = g.record_success(t4, &GuardParams::default()); assert_eq!(conf, NewlyConfirmed::No); assert_eq!(g.reachable(), Reachable::Reachable); assert!(g.retry_at.is_none()); assert_eq!(g.confirmed_at, confirmed_at_orig); } #[test] fn retry() { let t1 = Instant::get(); let mut g = basic_guard(); g.record_failure(t1, true); assert!(g.retry_at.is_some()); assert_eq!(g.reachable(), Reachable::Unreachable); // Not yet retriable. g.consider_retry(t1); assert!(g.retry_at.is_some()); assert_eq!(g.reachable(), Reachable::Unreachable); // Not retriable right before the retry time. g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1)); assert!(g.retry_at.is_some()); assert_eq!(g.reachable(), Reachable::Unreachable); // Retriable right after the retry time. g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1)); assert!(g.retry_at.is_none()); assert_eq!(g.reachable(), Reachable::Retriable); } #[test] fn expiration() { const DAY: Duration = Duration::from_secs(24 * 60 * 60); let params = GuardParams::default(); let now = SystemTime::get(); let g = basic_guard(); assert!(!g.is_expired(¶ms, now)); assert!(!g.is_expired(¶ms, now + 10 * DAY)); assert!(!g.is_expired(¶ms, now + 25 * DAY)); assert!(!g.is_expired(¶ms, now + 70 * DAY)); assert!(g.is_expired(¶ms, now + 200 * DAY)); // lifetime_unconfirmed. let mut g = basic_guard(); let _ = g.record_success(now, ¶ms); assert!(!g.is_expired(¶ms, now)); assert!(!g.is_expired(¶ms, now + 10 * DAY)); assert!(!g.is_expired(¶ms, now + 25 * DAY)); assert!(g.is_expired(¶ms, now + 70 * DAY)); // lifetime_confirmed. let mut g = basic_guard(); g.mark_unlisted(now); assert!(!g.is_expired(¶ms, now)); assert!(!g.is_expired(¶ms, now + 10 * DAY)); assert!(g.is_expired(¶ms, now + 25 * DAY)); // lifetime_unlisted } #[test] fn netdir_integration() { use tor_netdir::testnet; let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap(); let params = GuardParams::default(); let now = SystemTime::get(); // Construct a guard from a relay from the netdir. let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap(); let guard22 = Guard::from_chan_target(&relay22, now, ¶ms); assert!(guard22.same_relay_ids(&relay22)); assert!(Some(guard22.added_at) <= Some(now)); // Can we still get the relay back? let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id); let r = id.get_relay(&netdir).unwrap(); assert!(r.same_relay_ids(&relay22)); // Now try a guard that isn't in the netdir. let guard255 = Guard::new( GuardId::new([255; 32].into(), [255; 20].into()), vec![], None, now, ); let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id); assert!(id.get_relay(&netdir).is_none()); } #[test] fn update_from_netdir() { use tor_netdir::testnet; let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap(); // Same as above but omit [22] let netdir2 = testnet::construct_custom_netdir(|idx, node, _| { if idx == 22 { node.omit_rs = true; } }) .unwrap() .unwrap_if_sufficient() .unwrap(); // Same as above but omit [22] as well as MD for [23]. let netdir3 = testnet::construct_custom_netdir(|idx, node, _| { if idx == 22 { node.omit_rs = true; } else if idx == 23 { node.omit_md = true; } }) .unwrap() .unwrap_if_sufficient() .unwrap(); //let params = GuardParams::default(); let now = SystemTime::get(); // Try a guard that isn't in the netdir at all. let mut guard255 = Guard::new( GuardId::new([255; 32].into(), [255; 20].into()), vec!["8.8.8.8:53".parse().unwrap()], None, now, ); assert_eq!(guard255.unlisted_since, None); assert_eq!(guard255.listed_in(&netdir), Some(false)); guard255.update_from_universe(&netdir); assert_eq!( guard255.unlisted_since, Some(netdir.lifetime().valid_after()) ); assert!(!guard255.orports.is_empty()); // Try a guard that is in netdir, but not netdir2. let mut guard22 = Guard::new( GuardId::new([22; 32].into(), [22; 20].into()), vec![], None, now, ); let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone()); let relay22 = id22.get_relay(&netdir).unwrap(); assert_eq!(guard22.listed_in(&netdir), Some(true)); guard22.update_from_universe(&netdir); assert_eq!(guard22.unlisted_since, None); // It's listed. assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs are set. assert_eq!(guard22.listed_in(&netdir2), Some(false)); guard22.update_from_universe(&netdir2); assert_eq!( guard22.unlisted_since, Some(netdir2.lifetime().valid_after()) ); assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs still set. assert!(!guard22.dir_info_missing); // Now see what happens for a guard that's in the consensus, but missing an MD. let mut guard23 = Guard::new( GuardId::new([23; 32].into(), [23; 20].into()), vec![], None, now, ); assert_eq!(guard23.listed_in(&netdir2), Some(true)); assert_eq!(guard23.listed_in(&netdir3), None); guard23.update_from_universe(&netdir3); assert!(guard23.dir_info_missing); assert!(guard23.is_dir_cache); } #[test] fn pending() { let mut g = basic_guard(); let t1 = Instant::get(); let t2 = t1 + Duration::from_secs(100); let t3 = t1 + Duration::from_secs(200); assert!(!g.exploratory_attempt_after(t1)); assert!(!g.exploratory_circ_pending()); g.note_exploratory_circ(true); g.record_attempt(t2); assert!(g.exploratory_circ_pending()); assert!(g.exploratory_attempt_after(t1)); assert!(!g.exploratory_attempt_after(t3)); g.note_exploratory_circ(false); assert!(!g.exploratory_circ_pending()); assert!(!g.exploratory_attempt_after(t1)); assert!(!g.exploratory_attempt_after(t3)); } #[test] fn circ_history() { let mut h = CircHistory { n_successes: 3, n_failures: 4, n_indeterminate: 3, }; assert!(h.indeterminate_ratio().is_none()); h.n_successes = 20; assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001); } #[test] fn disable_on_failure() { let mut g = basic_guard(); let params = GuardParams::default(); let now = SystemTime::get(); let _ignore = g.record_success(now, ¶ms); for _ in 0..13 { g.record_indeterminate_result(); } // We're still under the observation threshold. assert!(g.disabled.is_none()); // This crosses the threshold. g.record_indeterminate_result(); assert!(g.disabled.is_some()); #[allow(unreachable_patterns)] match g.disabled.unwrap().into_option().unwrap() { GuardDisabled::TooManyIndeterminateFailures { history: _, failure_ratio, threshold_ratio, } => { assert!((failure_ratio - 0.933).abs() < 0.01); assert!((threshold_ratio - 0.7).abs() < 0.01); } other => { panic!("Wrong variant: {:?}", other); } } } #[test] fn mark_retriable() { let mut g = basic_guard(); use super::Reachable::*; assert_eq!(g.reachable(), Untried); for (pre, post) in &[ (Untried, Untried), (Unreachable, Retriable), (Reachable, Reachable), ] { g.reachable = *pre; g.mark_retriable(); assert_eq!(g.reachable(), *post); } } #[test] fn dir_status() { // We're going to see how directory failures interact with circuit // failures. use crate::GuardUsageBuilder; let mut g = basic_guard(); let inst = Instant::get(); let st = SystemTime::get(); let sec = Duration::from_secs(1); let params = GuardParams::default(); let dir_usage = GuardUsageBuilder::new() .kind(GuardUsageKind::OneHopDirectory) .build() .unwrap(); let data_usage = GuardUsage::default(); // Record a circuit success. let _ = g.record_success(st, ¶ms); assert_eq!(g.next_retry(&dir_usage), None); assert!(g.ready_for_usage(&dir_usage, inst)); assert_eq!(g.next_retry(&data_usage), None); assert!(g.ready_for_usage(&data_usage, inst)); // Record a dircache failure. This does not influence data usage. g.record_external_failure(ExternalActivity::DirCache, inst); assert_eq!(g.next_retry(&data_usage), None); assert!(g.ready_for_usage(&data_usage, inst)); let next_dir_retry = g.next_retry(&dir_usage).unwrap(); assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR); assert!(!g.ready_for_usage(&dir_usage, inst)); assert!(g.ready_for_usage(&dir_usage, next_dir_retry)); // Record a circuit success again. This does not make the guard usable // as a directory cache. let _ = g.record_success(st, ¶ms); assert!(g.ready_for_usage(&data_usage, inst)); assert!(!g.ready_for_usage(&dir_usage, inst)); // Record a circuit failure. g.record_failure(inst + sec * 10, true); let next_circ_retry = g.next_retry(&data_usage).unwrap(); assert!(!g.ready_for_usage(&data_usage, inst + sec * 10)); assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10)); assert_eq!( g.next_retry(&dir_usage).unwrap(), std::cmp::max(next_circ_retry, next_dir_retry) ); // Record a directory success. This won't supersede the circuit // failure. g.record_external_success(ExternalActivity::DirCache); assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry); assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry); assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10)); assert!(!g.ready_for_usage(&data_usage, inst + sec * 10)); } }