Skip to main content

tor_guardmgr/
guard.rs

1//! Code to represent its single guard node and track its status.
2
3use tor_basic_utils::retry::RetryDelay;
4
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use tracing::{info, trace, warn};
10use web_time_compat::{Duration, Instant, InstantExt, SystemTime};
11
12use crate::dirstatus::DirStatus;
13use crate::sample::Candidate;
14use crate::skew::SkewObservation;
15use crate::util::randomize_time;
16use crate::{ExternalActivity, GuardSetSelector, GuardUsageKind, sample};
17use crate::{GuardParams, GuardRestriction, GuardUsage, ids::GuardId};
18
19#[cfg(feature = "bridge-client")]
20use safelog::Redactable as _;
21
22use tor_linkspec::{
23    ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
24};
25use tor_persist::{Futureproof, JsonValue};
26
27/// Tri-state to represent whether a guard is believed to be reachable or not.
28#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
29#[allow(clippy::enum_variant_names)]
30pub(crate) enum Reachable {
31    /// A guard is believed to be reachable, since we have successfully
32    /// used it more recently than we've failed.
33    Reachable,
34    /// A guard is believed to be unreachable, since recent attempts
35    /// to use it have failed, and not enough time has elapsed since then.
36    Unreachable,
37    /// We have never (during the lifetime of the current guard manager)
38    /// tried to connect to this guard.
39    #[default]
40    Untried,
41    /// The last time that we tried to connect to this guard, it failed,
42    /// but enough time has elapsed that we think it is worth trying again.
43    Retriable,
44}
45
46/// The name and version of the crate that first picked a potential
47/// guard.
48///
49/// The C Tor implementation has found it useful to keep this information
50/// about guards, to better work around any bugs discovered in the guard
51/// implementation.
52#[derive(Clone, Debug, Serialize, Deserialize)]
53struct CrateId {
54    /// The name of the crate that added this guard.
55    #[serde(rename = "crate")]
56    crate_name: String,
57    /// The version of the crate that added this guard.
58    version: String,
59}
60
61impl CrateId {
62    /// Return a new CrateId representing this crate.
63    fn this_crate() -> Option<Self> {
64        let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
65        let version = option_env!("CARGO_PKG_VERSION")?.to_string();
66        Some(CrateId {
67            crate_name,
68            version,
69        })
70    }
71}
72
73/// What rule do we use when we're displaying information about a guard?
74#[derive(Clone, Default, Debug)]
75pub(crate) enum DisplayRule {
76    /// The guard is Sensitive; we should display it as "\[scrubbed\]".
77    ///
78    /// We use this for public relays on the network, since displaying even the
79    /// redacted info about them can enough to identify them uniquely within the
80    /// NetDir.
81    ///
82    /// This should not be too much of a hit for UX (we hope), since the user is
83    /// not typically expected to work around issues with these guards themself.
84    #[default]
85    Sensitive,
86    /// The guard should be Redacted; we display it as something like "192.x.x.x
87    /// $ab...".
88    ///
89    /// We use this for bridges.
90    #[cfg(feature = "bridge-client")]
91    Redacted,
92}
93
94/// A single guard node, as held by the guard manager.
95///
96/// A Guard is a Tor relay that clients use for the first hop of their circuits.
97/// It doesn't need to be a relay that's currently on the network (that is, one
98/// that we could represent as a [`Relay`](tor_netdir::Relay)): guards might be
99/// temporarily unlisted.
100///
101/// Some fields in guards are persistent; others are reset with every process.
102///
103/// # Identity
104///
105/// Every guard has at least one `RelayId`.  A guard may _gain_ identities over
106/// time, as we learn more about it, but it should never _lose_ or _change_ its
107/// identities of a given type.
108///
109/// # TODO
110///
111/// This structure uses [`Instant`] to represent non-persistent points in time,
112/// and [`SystemTime`] to represent points in time that need to be persistent.
113/// That's possibly undesirable; maybe we should come up with a better solution.
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub(crate) struct Guard {
116    /// The identity keys for this guard.
117    id: GuardId,
118
119    /// The most recently seen addresses for this guard.  If `pt_targets` is
120    /// empty, these are the addresses we use for making OR connections to this
121    /// guard directly.  If `pt_targets` is nonempty, these are addresses at
122    /// which the server is "located" (q.v. [`HasAddrs`]), but not ways to
123    /// connect to it.
124    orports: Vec<SocketAddr>,
125
126    /// Any `PtTarget` instances that we know about for connecting to this guard
127    /// over a pluggable transport.
128    ///
129    /// If this is empty, then this guard only supports direct connections, at
130    /// the locations in `orports`.
131    ///
132    /// (Currently, this is always empty, or a singleton.  If we find more than
133    /// one, we only look at the first. It is a vector only for forward
134    /// compatibility.)
135    //
136    // TODO: We may want to replace pt_targets and orports with a new structure;
137    // maybe a PtAddress and a list of SocketAddr.  But we'll keep them like
138    // this for now to keep backward compatibility.
139    #[serde(default, skip_serializing_if = "Vec::is_empty")]
140    pt_targets: Vec<PtTarget>,
141
142    /// When, approximately, did we first add this guard to our sample?
143    #[serde(with = "humantime_serde")]
144    added_at: SystemTime,
145
146    /// What version of this crate added this guard to our sample?
147    added_by: Option<CrateId>,
148
149    /// If present, this guard is permanently disabled, and this
150    /// object tells us why.
151    #[serde(default)]
152    disabled: Option<Futureproof<GuardDisabled>>,
153
154    /// When, approximately, did we first successfully use this guard?
155    ///
156    /// (We call a guard "confirmed" if we have successfully used it at
157    /// least once.)
158    #[serde(with = "humantime_serde")]
159    confirmed_at: Option<SystemTime>,
160
161    /// If this guard is not listed in the current-consensus, this is the
162    /// `valid_after` date of the oldest consensus in which it was not listed.
163    ///
164    /// A guard counts as "unlisted" if it is absent, unusable, or
165    /// doesn't have the Guard flag.
166    #[serde(with = "humantime_serde")]
167    unlisted_since: Option<SystemTime>,
168
169    /// True if this guard is listed in the latest consensus, but we don't
170    /// have a microdescriptor for it.
171    #[serde(skip)]
172    dir_info_missing: bool,
173
174    /// When did we last give out this guard in response to a request?
175    #[serde(skip)]
176    last_tried_to_connect_at: Option<Instant>,
177
178    /// If this guard is currently Unreachable, when should we next
179    /// retry it?
180    ///
181    /// (Retrying a guard involves clearing this field, and setting
182    /// `reachable`)
183    #[serde(skip)]
184    retry_at: Option<Instant>, // derived from retry_schedule.
185
186    /// Schedule use to determine when we can next attempt to connect to this
187    /// guard.
188    #[serde(skip)]
189    retry_schedule: Option<RetryDelay>,
190
191    /// Current reachability status for this guard.
192    #[serde(skip)]
193    reachable: Reachable,
194
195    /// If true, then the last time we saw a relay entry for this
196    /// guard, it seemed like a valid directory cache.
197    #[serde(skip)]
198    is_dir_cache: bool,
199
200    /// Status for this guard, when used as a directory cache.
201    ///
202    /// (This is separate from `Reachable` and `retry_schedule`, since being
203    /// usable for circuit construction does not necessarily mean that the guard
204    /// will have good, timely cache information.  If it were not separate, then
205    /// circuit success would clear directory failures.)
206    #[serde(skip, default = "guard_dirstatus")]
207    dir_status: DirStatus,
208
209    /// If true, we have given this guard out for an exploratory circuit,
210    /// and that exploratory circuit is still pending.
211    ///
212    /// A circuit is "exploratory" if we launched it on a non-primary guard.
213    // TODO: Maybe this should be an integer that counts a number of such
214    // circuits?
215    #[serde(skip)]
216    exploratory_circ_pending: bool,
217
218    /// A count of all the circuit statuses we've seen on this guard.
219    ///
220    /// Used to implement a lightweight version of path-bias detection.
221    #[serde(skip)]
222    circ_history: CircHistory,
223
224    /// True if we have warned about this guard behaving suspiciously.
225    #[serde(skip)]
226    suspicious_behavior_warned: bool,
227
228    /// Latest clock skew (if any) we have observed from this guard.
229    #[serde(skip)]
230    clock_skew: Option<SkewObservation>,
231
232    /// How should we display information about this guard?
233    #[serde(skip)]
234    sensitivity: DisplayRule,
235
236    /// Fields from the state file that was used to make this `Guard` that
237    /// this version of Arti doesn't understand.
238    #[serde(flatten)]
239    unknown_fields: HashMap<String, JsonValue>,
240}
241
242/// Lower bound for delay after get a failure using a guard as a directory
243/// cache.
244const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
245
246/// Return a DirStatus entry for a guard.
247fn guard_dirstatus() -> DirStatus {
248    DirStatus::new(GUARD_DIR_RETRY_FLOOR)
249}
250
251/// Wrapper to declare whether a given successful use of a guard is the
252/// _first_ successful use of the guard.
253#[derive(Debug, Clone, Copy, Eq, PartialEq)]
254pub(crate) enum NewlyConfirmed {
255    /// This was the first successful use of a guard.
256    Yes,
257    /// This guard has been used successfully before.
258    No,
259}
260
261impl Guard {
262    /// Create a new unused [`Guard`] from a [`Candidate`].
263    pub(crate) fn from_candidate(
264        candidate: Candidate,
265        now: SystemTime,
266        params: &GuardParams,
267    ) -> Self {
268        let Candidate {
269            is_dir_cache,
270            full_dir_info,
271            owned_target,
272            ..
273        } = candidate;
274
275        Guard {
276            is_dir_cache,
277            dir_info_missing: !full_dir_info,
278            ..Self::from_chan_target(&owned_target, now, params)
279        }
280    }
281
282    /// Create a new unused [`Guard`] from a [`ChanTarget`].
283    ///
284    /// This function doesn't check whether the provided relay is a
285    /// suitable guard node or not: that's up to the caller to decide.
286    fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
287    where
288        T: ChanTarget,
289    {
290        let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
291
292        let pt_target = match relay.chan_method() {
293            #[cfg(feature = "pt-client")]
294            ChannelMethod::Pluggable(pt) => Some(pt),
295            _ => None,
296        };
297
298        Self::new(
299            GuardId::from_relay_ids(relay),
300            relay.addrs().collect_vec(),
301            pt_target,
302            added_at,
303        )
304    }
305
306    /// Return a new, manually constructed [`Guard`].
307    fn new(
308        id: GuardId,
309        orports: Vec<SocketAddr>,
310        pt_target: Option<PtTarget>,
311        added_at: SystemTime,
312    ) -> Self {
313        Guard {
314            id,
315            orports,
316            pt_targets: pt_target.into_iter().collect(),
317            added_at,
318            added_by: CrateId::this_crate(),
319            disabled: None,
320            confirmed_at: None,
321            unlisted_since: None,
322            dir_info_missing: false,
323            last_tried_to_connect_at: None,
324            reachable: Reachable::Untried,
325            retry_at: None,
326            dir_status: guard_dirstatus(),
327            retry_schedule: None,
328            is_dir_cache: true,
329            exploratory_circ_pending: false,
330            circ_history: CircHistory::default(),
331            suspicious_behavior_warned: false,
332            clock_skew: None,
333            unknown_fields: Default::default(),
334            sensitivity: DisplayRule::Sensitive,
335        }
336    }
337
338    /// Return the identity of this Guard.
339    pub(crate) fn guard_id(&self) -> &GuardId {
340        &self.id
341    }
342
343    /// Return the reachability status for this guard.
344    pub(crate) fn reachable(&self) -> Reachable {
345        self.reachable
346    }
347
348    /// Return the next time at which this guard will be retriable for a given
349    /// usage.
350    ///
351    /// (Return None if we think this guard might be reachable right now.)
352    pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
353        match &usage.kind {
354            GuardUsageKind::Data => self.retry_at,
355            GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
356                .iter()
357                .flatten()
358                .max()
359                .copied(),
360        }
361    }
362
363    /// Return true if this guard is usable and working according to our latest
364    /// configuration and directory information, and hasn't been turned off for
365    /// some other reason.
366    pub(crate) fn usable(&self) -> bool {
367        self.unlisted_since.is_none() && self.disabled.is_none()
368    }
369
370    /// Return true if this guard is ready (with respect to any timeouts) for
371    /// the given `usage` at `now`.
372    pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
373        if let Some(retry_at) = self.retry_at {
374            if retry_at > now {
375                return false;
376            }
377        }
378
379        match usage.kind {
380            GuardUsageKind::Data => true,
381            GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
382        }
383    }
384
385    /// Copy all _non-persistent_ status from `other` to self.
386    ///
387    /// We do this when we were not the owner of our persistent state, and we
388    /// have just reloaded it (as `self`), but we have some ephemeral knowledge
389    /// about this guard (as `other`).
390    ///
391    /// You should not invent new uses for this function; instead we should come
392    /// up with alternatives.
393    ///
394    /// # Panics
395    ///
396    /// Panics if the identities in `self` are not exactly the same as the
397    /// identities in `other`.
398    pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
399        // It is not safe to copy failure information unless these identities
400        // are a superset of those in `other`; but it is not safe to copy success
401        // information unless these identities are a subset of those in `other`.
402        //
403        // To simplify matters, we just insist that the identities have to be the same.
404        assert!(self.same_relay_ids(&other));
405
406        Guard {
407            // All other persistent fields are taken from `self`.
408            id: self.id,
409            pt_targets: self.pt_targets,
410            orports: self.orports,
411            added_at: self.added_at,
412            added_by: self.added_by,
413            disabled: self.disabled,
414            confirmed_at: self.confirmed_at,
415            unlisted_since: self.unlisted_since,
416            unknown_fields: self.unknown_fields,
417
418            // All non-persistent fields get taken from `other`.
419            last_tried_to_connect_at: other.last_tried_to_connect_at,
420            retry_at: other.retry_at,
421            retry_schedule: other.retry_schedule,
422            reachable: other.reachable,
423            is_dir_cache: other.is_dir_cache,
424            exploratory_circ_pending: other.exploratory_circ_pending,
425            dir_info_missing: other.dir_info_missing,
426            circ_history: other.circ_history,
427            suspicious_behavior_warned: other.suspicious_behavior_warned,
428            dir_status: other.dir_status,
429            clock_skew: other.clock_skew,
430            sensitivity: other.sensitivity,
431            // Note that we _could_ remove either of the above blocks and add
432            // `..self` or `..other`, but that would be risky: it would increase
433            // the odds that we would forget to add some persistent or
434            // non-persistent field to the right group in the future.
435        }
436    }
437
438    /// Change the reachability status for this guard.
439    #[allow(clippy::cognitive_complexity)]
440    fn set_reachable(&mut self, r: Reachable) {
441        use Reachable as R;
442
443        if self.reachable != r {
444            // High-level logs, if change is interesting to user.
445            match (self.reachable, r) {
446                (_, R::Reachable) => info!("We have found that guard {} is usable.", self),
447                (R::Untried | R::Reachable, R::Unreachable) => match self.retry_at {
448                    Some(retry_at) => warn!(
449                        "Could not connect to guard {}. Retrying in {}.",
450                        self,
451                        humantime::format_duration(retry_at - Instant::get()),
452                    ),
453                    None => warn!(
454                        "Could not connect to guard {}. Next retry time unknown.",
455                        self
456                    ),
457                },
458                (_, _) => {} // not interesting.
459            }
460            //
461            trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed.");
462            self.reachable = r;
463        }
464    }
465
466    /// Return true if at least one exploratory circuit is pending to this
467    /// guard.
468    ///
469    /// A circuit is "exploratory" if launched on a non-primary guard.
470    ///
471    /// # TODO
472    ///
473    /// The "exploratory" definition doesn't quite match up with the behavior
474    /// in the spec, but it is what Tor does.
475    pub(crate) fn exploratory_circ_pending(&self) -> bool {
476        self.exploratory_circ_pending
477    }
478
479    /// Note that an exploratory circuit is pending (if `pending` is true),
480    /// or not pending (if `pending` is false.
481    pub(crate) fn note_exploratory_circ(&mut self, pending: bool) {
482        self.exploratory_circ_pending = pending;
483    }
484
485    /// Possibly mark this guard as retriable, if it has been down for
486    /// long enough.
487    ///
488    /// Specifically, if the guard is to be Unreachable, and our last attempt
489    /// to connect to it is far enough in the past from `now`, we change its
490    /// status to Unknown.
491    pub(crate) fn consider_retry(&mut self, now: Instant) {
492        if let Some(retry_at) = self.retry_at {
493            debug_assert!(self.reachable == Reachable::Unreachable);
494            if retry_at <= now {
495                self.mark_retriable();
496            }
497        }
498    }
499
500    /// If this guard is marked Unreachable, clear its unreachability status
501    /// and mark it as Retriable.
502    pub(crate) fn mark_retriable(&mut self) {
503        if self.reachable == Reachable::Unreachable {
504            self.set_reachable(Reachable::Retriable);
505            self.retry_at = None;
506            self.retry_schedule = None;
507        }
508    }
509
510    /// Return true if this guard obeys all of the given restrictions.
511    fn obeys_restrictions(&self, restrictions: &[GuardRestriction]) -> bool {
512        restrictions.iter().all(|r| self.obeys_restriction(r))
513    }
514
515    /// Return true if this guard obeys a single restriction.
516    fn obeys_restriction(&self, r: &GuardRestriction) -> bool {
517        match r {
518            GuardRestriction::AvoidId(avoid_id) => !self.id.0.has_identity(avoid_id.as_ref()),
519            GuardRestriction::AvoidAllIds(avoid_ids) => {
520                self.id.0.identities().all(|id| !avoid_ids.contains(id))
521            }
522        }
523    }
524
525    /// Return true if this guard is suitable to use for the provided `usage`.
526    pub(crate) fn conforms_to_usage(&self, usage: &GuardUsage) -> bool {
527        match usage.kind {
528            GuardUsageKind::OneHopDirectory => {
529                if !self.is_dir_cache {
530                    return false;
531                }
532            }
533            GuardUsageKind::Data => {
534                // We need a "definitely listed" guard to build a multihop
535                // circuit.
536                if self.dir_info_missing {
537                    return false;
538                }
539            }
540        }
541        self.obeys_restrictions(&usage.restrictions[..])
542    }
543
544    /// Check whether this guard is listed in the provided [`sample::Universe`].
545    ///
546    /// Returns `Some(true)` if it is definitely listed, and `Some(false)` if it
547    /// is definitely not listed.  A `None` return indicates that we need to
548    /// download more directory information about this guard before we can be
549    /// certain whether this guard is listed or not.
550    pub(crate) fn listed_in<U: sample::Universe>(&self, universe: &U) -> Option<bool> {
551        universe.contains(self)
552    }
553
554    /// Change this guard's status based on a newly received or newly updated
555    /// [`sample::Universe`].
556    ///
557    /// A guard may become "listed" or "unlisted": a listed guard is one that
558    /// appears in the consensus with the Guard flag.
559    ///
560    /// A guard may acquire additional identities if we learned them from the
561    /// guard, either directly or via an authenticated directory document.
562    ///
563    /// Additionally, a guard's `orports` or `pt_targets` may change, if the
564    /// `universe` lists a new address for the relay.
565    pub(crate) fn update_from_universe<U: sample::Universe>(&mut self, universe: &U) {
566        // This is a tricky check, since if we're missing directory information
567        // for the guard, we won't know its full set of identities.
568        use sample::CandidateStatus::*;
569        let listed_as_guard = match universe.status(self) {
570            Present(Candidate {
571                listed_as_guard,
572                is_dir_cache,
573                full_dir_info,
574                owned_target,
575                sensitivity,
576            }) => {
577                // Update address information.
578                self.orports = owned_target.addrs().collect_vec();
579                // Update Pt information.
580                self.pt_targets = match owned_target.chan_method() {
581                    #[cfg(feature = "pt-client")]
582                    ChannelMethod::Pluggable(pt) => vec![pt],
583                    _ => Vec::new(),
584                };
585                // Check whether we can currently use it as a directory cache.
586                self.is_dir_cache = is_dir_cache;
587                // Update our IDs: the Relay will have strictly more.
588                assert!(owned_target.has_all_relay_ids_from(self));
589                self.id = GuardId(RelayIds::from_relay_ids(&owned_target));
590                self.dir_info_missing = !full_dir_info;
591                self.sensitivity = sensitivity;
592
593                listed_as_guard
594            }
595            Absent => false, // Definitely not listed.
596            Uncertain => {
597                // We can't tell if this is listed without more directory information.
598                self.dir_info_missing = true;
599                return;
600            }
601        };
602
603        if listed_as_guard {
604            // Definitely listed, so clear unlisted_since.
605            self.mark_listed();
606        } else {
607            // Unlisted or not a guard; mark it unlisted.
608            self.mark_unlisted(universe.timestamp());
609        }
610    }
611
612    /// Mark this guard as currently listed in the directory.
613    fn mark_listed(&mut self) {
614        if self.unlisted_since.is_some() {
615            trace!(guard_id = ?self.id, "Guard is now listed again.");
616            self.unlisted_since = None;
617        }
618    }
619
620    /// Mark this guard as having been unlisted since `now`, if it is not
621    /// already so marked.
622    fn mark_unlisted(&mut self, now: SystemTime) {
623        if self.unlisted_since.is_none() {
624            trace!(guard_id = ?self.id, "Guard is now unlisted.");
625            self.unlisted_since = Some(now);
626        }
627    }
628
629    /// Return true if we should remove this guard from the current guard
630    /// sample.
631    ///
632    /// Guards may be ready for removal because they have been
633    /// confirmed too long ago, if they have been sampled too long ago
634    /// (if they are not confirmed), or if they have been unlisted for
635    /// too long.
636    pub(crate) fn is_expired(&self, params: &GuardParams, now: SystemTime) -> bool {
637        /// Helper: Return true if `t2` is after `t1` by at least `d`.
638        fn expired_by(t1: SystemTime, d: Duration, t2: SystemTime) -> bool {
639            if let Ok(elapsed) = t2.duration_since(t1) {
640                elapsed > d
641            } else {
642                false
643            }
644        }
645        if self.disabled.is_some() {
646            // We never forget a guard that we've disabled: we've disabled
647            // it for a reason.
648            return false;
649        }
650        if let Some(confirmed_at) = self.confirmed_at {
651            if expired_by(confirmed_at, params.lifetime_confirmed, now) {
652                return true;
653            }
654        } else if expired_by(self.added_at, params.lifetime_unconfirmed, now) {
655            return true;
656        }
657
658        if let Some(unlisted_since) = self.unlisted_since {
659            if expired_by(unlisted_since, params.lifetime_unlisted, now) {
660                return true;
661            }
662        }
663
664        false
665    }
666
667    /// Record that a failure has happened for this guard.
668    ///
669    /// If `is_primary` is true, this is a primary guard (q.v.).
670    pub(crate) fn record_failure(&mut self, now: Instant, is_primary: bool) {
671        let mut rng = rand::rng();
672        let retry_interval = self
673            .retry_schedule
674            .get_or_insert_with(|| retry_schedule(is_primary))
675            .next_delay(&mut rng);
676
677        // TODO-SPEC: Document this behavior in guard-spec.
678        self.retry_at = Some(now + retry_interval);
679
680        self.set_reachable(Reachable::Unreachable);
681        self.exploratory_circ_pending = false;
682
683        self.circ_history.n_failures += 1;
684    }
685
686    /// Note that we have launch an attempted use of this guard.
687    ///
688    /// We use this time to decide when to retry failing guards, and
689    /// to see if the guard has been "pending" for a long time.
690    pub(crate) fn record_attempt(&mut self, connect_attempt: Instant) {
691        self.last_tried_to_connect_at = self
692            .last_tried_to_connect_at
693            .map(|last| last.max(connect_attempt))
694            .or(Some(connect_attempt));
695    }
696
697    /// Return true if this guard has an exploratory circuit pending and
698    /// if the most recent attempt to connect to it is after `when`.
699    ///
700    /// See [`Self::exploratory_circ_pending`].
701    pub(crate) fn exploratory_attempt_after(&self, when: Instant) -> bool {
702        self.exploratory_circ_pending
703            && self.last_tried_to_connect_at.map(|t| t > when) == Some(true)
704    }
705
706    /// Note that a guard has been used successfully.
707    ///
708    /// Updates that guard's status to reachable, clears any failing status
709    /// information for it, and decides whether the guard is newly confirmed.
710    ///
711    /// If the guard is newly confirmed, the caller must add it to the
712    /// list of confirmed guards.
713    #[must_use = "You need to check whether a succeeding guard is confirmed."]
714    pub(crate) fn record_success(
715        &mut self,
716        now: SystemTime,
717        params: &GuardParams,
718    ) -> NewlyConfirmed {
719        self.retry_at = None;
720        self.retry_schedule = None;
721        self.set_reachable(Reachable::Reachable);
722        self.exploratory_circ_pending = false;
723        self.circ_history.n_successes += 1;
724
725        if self.confirmed_at.is_none() {
726            self.confirmed_at = Some(
727                randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10)
728                    .max(self.added_at),
729            );
730            // TODO-SPEC: The "max" above isn't specified by guard-spec,
731            // but I think it's wise.
732            trace!(guard_id = ?self.id, "Newly confirmed");
733            NewlyConfirmed::Yes
734        } else {
735            NewlyConfirmed::No
736        }
737    }
738
739    /// Record that an external operation has succeeded on this guard.
740    pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
741        match how {
742            ExternalActivity::DirCache => {
743                self.dir_status.note_success();
744            }
745        }
746    }
747
748    /// Record that an external operation has failed on this guard.
749    pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
750        match how {
751            ExternalActivity::DirCache => {
752                self.dir_status.note_failure(now);
753            }
754        }
755    }
756
757    /// Note that a circuit through this guard died in a way that we couldn't
758    /// necessarily attribute to the guard.
759    pub(crate) fn record_indeterminate_result(&mut self) {
760        self.circ_history.n_indeterminate += 1;
761
762        if let Some(ratio) = self.circ_history.indeterminate_ratio() {
763            // TODO: These should not be hardwired, and they may be set
764            // too high.
765            /// If this fraction of circs are suspicious, we should disable
766            /// the guard.
767            const DISABLE_THRESHOLD: f64 = 0.7;
768            /// If this fraction of circuits are suspicious, we should
769            /// warn.
770            const WARN_THRESHOLD: f64 = 0.5;
771
772            if ratio > DISABLE_THRESHOLD {
773                let reason = GuardDisabled::TooManyIndeterminateFailures {
774                    history: self.circ_history.clone(),
775                    failure_ratio: ratio,
776                    threshold_ratio: DISABLE_THRESHOLD,
777                };
778                warn!(guard=?self.id, "Disabling guard: {:.1}% of circuits died under mysterious circumstances, exceeding threshold of {:.1}%", ratio*100.0, (DISABLE_THRESHOLD*100.0));
779                self.disabled = Some(reason.into());
780            } else if ratio > WARN_THRESHOLD && !self.suspicious_behavior_warned {
781                warn!(guard=?self.id, "Questionable guard: {:.1}% of circuits died under mysterious circumstances.", ratio*100.0);
782                self.suspicious_behavior_warned = true;
783            }
784        }
785    }
786
787    /// Return a [`FirstHop`](crate::FirstHop) object to represent this guard.
788    pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
789        crate::FirstHop {
790            sample: Some(selection),
791            inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
792                self,
793            )),
794        }
795    }
796
797    /// Record that a given fallback has told us about clock skew.
798    pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
799        self.clock_skew = Some(observation);
800    }
801
802    /// Return the most recent clock skew observation for this guard, if we have
803    /// made one.
804    pub(crate) fn skew(&self) -> Option<&SkewObservation> {
805        self.clock_skew.as_ref()
806    }
807
808    /// Testing only: Return true if this guard was ever contacted successfully.
809    #[cfg(test)]
810    pub(crate) fn confirmed(&self) -> bool {
811        self.confirmed_at.is_some()
812    }
813}
814
815impl tor_linkspec::HasAddrs for Guard {
816    fn addrs(&self) -> impl Iterator<Item = SocketAddr> {
817        self.orports.iter().copied()
818    }
819}
820
821impl tor_linkspec::HasRelayIds for Guard {
822    fn identity(
823        &self,
824        key_type: tor_linkspec::RelayIdType,
825    ) -> Option<tor_linkspec::RelayIdRef<'_>> {
826        self.id.0.identity(key_type)
827    }
828}
829
830impl tor_linkspec::HasChanMethod for Guard {
831    fn chan_method(&self) -> ChannelMethod {
832        match &self.pt_targets[..] {
833            #[cfg(feature = "pt-client")]
834            [first, ..] => ChannelMethod::Pluggable(first.clone()),
835            #[cfg(not(feature = "pt-client"))]
836            [_first, ..] => ChannelMethod::Direct(vec![]), // can't connect to this; no pt support.
837            [] => ChannelMethod::Direct(self.orports.clone()),
838        }
839    }
840}
841
842impl tor_linkspec::ChanTarget for Guard {}
843
844impl std::fmt::Display for Guard {
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        match self.sensitivity {
847            DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f),
848            #[cfg(feature = "bridge-client")]
849            DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f),
850        }
851    }
852}
853
854/// A reason for permanently disabling a guard.
855#[derive(Clone, Debug, Serialize, Deserialize)]
856#[serde(tag = "type")]
857enum GuardDisabled {
858    /// Too many attempts to use this guard failed for indeterminate reasons.
859    TooManyIndeterminateFailures {
860        /// Observed count of status reports about this guard.
861        history: CircHistory,
862        /// Observed fraction of indeterminate status reports.
863        failure_ratio: f64,
864        /// Threshold that was exceeded.
865        threshold_ratio: f64,
866    },
867}
868
869/// Return a new RetryDelay tracker for a guard.
870///
871/// `is_primary should be true if the guard is primary.
872fn retry_schedule(is_primary: bool) -> RetryDelay {
873    let minimum = if is_primary {
874        Duration::from_secs(30)
875    } else {
876        Duration::from_secs(150)
877    };
878
879    RetryDelay::from_duration(minimum)
880}
881
882/// The recent history of circuit activity on this guard.
883///
884/// We keep this information so that we can tell if too many circuits are
885/// winding up in "indeterminate" status.
886///
887/// # What's this for?
888///
889/// Recall that an "indeterminate" circuit failure is one that might
890/// or might not be the guard's fault.  For example, if the second hop
891/// of the circuit fails, we can't tell whether to blame the guard,
892/// the second hop, or the internet between them.
893///
894/// But we don't want to allow an unbounded number of indeterminate
895/// failures: if we did, it would allow a malicious guard to simply
896/// reject any circuit whose second hop it didn't like, and thereby
897/// filter the client's paths down to a hostile subset.
898///
899/// So as a workaround, and to discourage this kind of behavior, we
900/// track the fraction of indeterminate circuits, and disable any guard
901/// where the fraction is too high.
902//
903// TODO: We may eventually want to make this structure persistent.  If we
904// do, however, we'll need a way to make ancient history expire.  We might
905// want that anyway, to make attacks harder.
906#[derive(Debug, Clone, Default, Serialize, Deserialize)]
907pub(crate) struct CircHistory {
908    /// How many times have we seen this guard succeed?
909    n_successes: u32,
910    /// How many times have we seen this guard fail?
911    #[allow(dead_code)] // not actually used yet.
912    n_failures: u32,
913    /// How many times has this guard given us indeterminate results?
914    n_indeterminate: u32,
915}
916
917impl CircHistory {
918    /// If we have seen enough, return the fraction of circuits that have
919    /// "died under mysterious circumstances".
920    fn indeterminate_ratio(&self) -> Option<f64> {
921        // TODO: This should probably not be hardwired
922
923        /// Don't try to give a ratio unless we've seen this many observations.
924        const MIN_OBSERVATIONS: u32 = 15;
925
926        let total = self.n_successes + self.n_indeterminate;
927        if total < MIN_OBSERVATIONS {
928            return None;
929        }
930
931        Some(f64::from(self.n_indeterminate) / f64::from(total))
932    }
933}
934
935#[cfg(test)]
936mod test {
937    // @@ begin test lint list maintained by maint/add_warning @@
938    #![allow(clippy::bool_assert_comparison)]
939    #![allow(clippy::clone_on_copy)]
940    #![allow(clippy::dbg_macro)]
941    #![allow(clippy::mixed_attributes_style)]
942    #![allow(clippy::print_stderr)]
943    #![allow(clippy::print_stdout)]
944    #![allow(clippy::single_char_pattern)]
945    #![allow(clippy::unwrap_used)]
946    #![allow(clippy::unchecked_time_subtraction)]
947    #![allow(clippy::useless_vec)]
948    #![allow(clippy::needless_pass_by_value)]
949    #![allow(clippy::string_slice)] // See arti#2571
950    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
951    use super::*;
952    use crate::ids::FirstHopId;
953    use tor_linkspec::{HasRelayIds, RelayId};
954    use tor_llcrypto::pk::ed25519::Ed25519Identity;
955    use web_time_compat::SystemTimeExt;
956
957    #[test]
958    fn crate_id() {
959        let id = CrateId::this_crate().unwrap();
960        assert_eq!(&id.crate_name, "tor-guardmgr");
961        assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
962    }
963
964    fn basic_id() -> GuardId {
965        GuardId::new([13; 32].into(), [37; 20].into())
966    }
967    fn basic_guard() -> Guard {
968        let id = basic_id();
969        let ports = vec!["127.0.0.7:7777".parse().unwrap()];
970        let added = SystemTime::get();
971        Guard::new(id, ports, None, added)
972    }
973
974    #[test]
975    fn simple_accessors() {
976        fn ed(id: [u8; 32]) -> RelayId {
977            RelayId::Ed25519(id.into())
978        }
979        let id = basic_id();
980        let g = basic_guard();
981
982        assert_eq!(g.guard_id(), &id);
983        assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
984        assert_eq!(
985            g.addrs().collect_vec(),
986            &["127.0.0.7:7777".parse().unwrap()]
987        );
988        assert_eq!(g.reachable(), Reachable::Untried);
989        assert_eq!(g.reachable(), Reachable::default());
990
991        use crate::GuardUsageBuilder;
992        let mut usage1 = GuardUsageBuilder::new();
993
994        usage1
995            .restrictions()
996            .push(GuardRestriction::AvoidId(ed([22; 32])));
997        let usage1 = usage1.build().unwrap();
998        let mut usage2 = GuardUsageBuilder::new();
999        usage2
1000            .restrictions()
1001            .push(GuardRestriction::AvoidId(ed([13; 32])));
1002        let usage2 = usage2.build().unwrap();
1003        let usage3 = GuardUsage::default();
1004        let mut usage4 = GuardUsageBuilder::new();
1005        usage4
1006            .restrictions()
1007            .push(GuardRestriction::AvoidId(ed([22; 32])));
1008        usage4
1009            .restrictions()
1010            .push(GuardRestriction::AvoidId(ed([13; 32])));
1011        let usage4 = usage4.build().unwrap();
1012        let mut usage5 = GuardUsageBuilder::new();
1013        usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1014            vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1015        ));
1016        let usage5 = usage5.build().unwrap();
1017        let mut usage6 = GuardUsageBuilder::new();
1018        usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1019            vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1020        ));
1021        let usage6 = usage6.build().unwrap();
1022
1023        assert!(g.conforms_to_usage(&usage1));
1024        assert!(!g.conforms_to_usage(&usage2));
1025        assert!(g.conforms_to_usage(&usage3));
1026        assert!(!g.conforms_to_usage(&usage4));
1027        assert!(!g.conforms_to_usage(&usage5));
1028        assert!(g.conforms_to_usage(&usage6));
1029    }
1030
1031    #[allow(clippy::redundant_clone)]
1032    #[test]
1033    fn trickier_usages() {
1034        let g = basic_guard();
1035        use crate::{GuardUsageBuilder, GuardUsageKind};
1036        let data_usage = GuardUsageBuilder::new()
1037            .kind(GuardUsageKind::Data)
1038            .build()
1039            .unwrap();
1040        let dir_usage = GuardUsageBuilder::new()
1041            .kind(GuardUsageKind::OneHopDirectory)
1042            .build()
1043            .unwrap();
1044        assert!(g.conforms_to_usage(&data_usage));
1045        assert!(g.conforms_to_usage(&dir_usage));
1046
1047        let mut g2 = g.clone();
1048        g2.dir_info_missing = true;
1049        assert!(!g2.conforms_to_usage(&data_usage));
1050        assert!(g2.conforms_to_usage(&dir_usage));
1051
1052        let mut g3 = g.clone();
1053        g3.is_dir_cache = false;
1054        assert!(g3.conforms_to_usage(&data_usage));
1055        assert!(!g3.conforms_to_usage(&dir_usage));
1056    }
1057
1058    #[test]
1059    fn record_attempt() {
1060        let t1 = Instant::get() - Duration::from_secs(10);
1061        let t2 = Instant::get() - Duration::from_secs(5);
1062        let t3 = Instant::get();
1063
1064        let mut g = basic_guard();
1065
1066        assert!(g.last_tried_to_connect_at.is_none());
1067        g.record_attempt(t1);
1068        assert_eq!(g.last_tried_to_connect_at, Some(t1));
1069        g.record_attempt(t3);
1070        assert_eq!(g.last_tried_to_connect_at, Some(t3));
1071        g.record_attempt(t2);
1072        assert_eq!(g.last_tried_to_connect_at, Some(t3));
1073    }
1074
1075    #[test]
1076    fn record_failure() {
1077        let t1 = Instant::get() - Duration::from_secs(10);
1078        let t2 = Instant::get();
1079
1080        let mut g = basic_guard();
1081        g.record_failure(t1, true);
1082        assert!(g.retry_schedule.is_some());
1083        assert_eq!(g.reachable(), Reachable::Unreachable);
1084        let retry1 = g.retry_at.unwrap();
1085        assert_eq!(retry1, t1 + Duration::from_secs(30));
1086
1087        g.record_failure(t2, true);
1088        let retry2 = g.retry_at.unwrap();
1089        assert!(retry2 >= t2 + Duration::from_secs(30));
1090        assert!(retry2 <= t2 + Duration::from_secs(200));
1091    }
1092
1093    #[test]
1094    fn record_success() {
1095        let t1 = Instant::get() - Duration::from_secs(10);
1096        // has to be in the future, since the guard's "added_at" time is based on now.
1097        let now = SystemTime::get();
1098        let t2 = now + Duration::from_secs(300 * 86400);
1099        let t3 = Instant::get() + Duration::from_secs(310 * 86400);
1100        let t4 = now + Duration::from_secs(320 * 86400);
1101
1102        let mut g = basic_guard();
1103        g.record_failure(t1, true);
1104        assert_eq!(g.reachable(), Reachable::Unreachable);
1105
1106        let conf = g.record_success(t2, &GuardParams::default());
1107        assert_eq!(g.reachable(), Reachable::Reachable);
1108        assert_eq!(conf, NewlyConfirmed::Yes);
1109        assert!(g.retry_at.is_none());
1110        assert!(g.confirmed_at.unwrap() <= t2);
1111        assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1112        let confirmed_at_orig = g.confirmed_at;
1113
1114        g.record_failure(t3, true);
1115        assert_eq!(g.reachable(), Reachable::Unreachable);
1116
1117        let conf = g.record_success(t4, &GuardParams::default());
1118        assert_eq!(conf, NewlyConfirmed::No);
1119        assert_eq!(g.reachable(), Reachable::Reachable);
1120        assert!(g.retry_at.is_none());
1121        assert_eq!(g.confirmed_at, confirmed_at_orig);
1122    }
1123
1124    #[test]
1125    fn retry() {
1126        let t1 = Instant::get();
1127        let mut g = basic_guard();
1128
1129        g.record_failure(t1, true);
1130        assert!(g.retry_at.is_some());
1131        assert_eq!(g.reachable(), Reachable::Unreachable);
1132
1133        // Not yet retriable.
1134        g.consider_retry(t1);
1135        assert!(g.retry_at.is_some());
1136        assert_eq!(g.reachable(), Reachable::Unreachable);
1137
1138        // Not retriable right before the retry time.
1139        g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1140        assert!(g.retry_at.is_some());
1141        assert_eq!(g.reachable(), Reachable::Unreachable);
1142
1143        // Retriable right after the retry time.
1144        g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1145        assert!(g.retry_at.is_none());
1146        assert_eq!(g.reachable(), Reachable::Retriable);
1147    }
1148
1149    #[test]
1150    fn expiration() {
1151        const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1152        let params = GuardParams::default();
1153        let now = SystemTime::get();
1154
1155        let g = basic_guard();
1156        assert!(!g.is_expired(&params, now));
1157        assert!(!g.is_expired(&params, now + 10 * DAY));
1158        assert!(!g.is_expired(&params, now + 25 * DAY));
1159        assert!(!g.is_expired(&params, now + 70 * DAY));
1160        assert!(g.is_expired(&params, now + 200 * DAY)); // lifetime_unconfirmed.
1161
1162        let mut g = basic_guard();
1163        let _ = g.record_success(now, &params);
1164        assert!(!g.is_expired(&params, now));
1165        assert!(!g.is_expired(&params, now + 10 * DAY));
1166        assert!(!g.is_expired(&params, now + 25 * DAY));
1167        assert!(g.is_expired(&params, now + 70 * DAY)); // lifetime_confirmed.
1168
1169        let mut g = basic_guard();
1170        g.mark_unlisted(now);
1171        assert!(!g.is_expired(&params, now));
1172        assert!(!g.is_expired(&params, now + 10 * DAY));
1173        assert!(g.is_expired(&params, now + 25 * DAY)); // lifetime_unlisted
1174    }
1175
1176    #[test]
1177    fn netdir_integration() {
1178        use tor_netdir::testnet;
1179        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1180        let params = GuardParams::default();
1181        let now = SystemTime::get();
1182
1183        // Construct a guard from a relay from the netdir.
1184        let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1185        let guard22 = Guard::from_chan_target(&relay22, now, &params);
1186        assert!(guard22.same_relay_ids(&relay22));
1187        assert!(Some(guard22.added_at) <= Some(now));
1188
1189        // Can we still get the relay back?
1190        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1191        let r = id.get_relay(&netdir).unwrap();
1192        assert!(r.same_relay_ids(&relay22));
1193
1194        // Now try a guard that isn't in the netdir.
1195        let guard255 = Guard::new(
1196            GuardId::new([255; 32].into(), [255; 20].into()),
1197            vec![],
1198            None,
1199            now,
1200        );
1201        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1202        assert!(id.get_relay(&netdir).is_none());
1203    }
1204
1205    #[test]
1206    fn update_from_netdir() {
1207        use tor_netdir::testnet;
1208        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1209        // Same as above but omit [22]
1210        let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1211            if idx == 22 {
1212                node.omit_rs = true;
1213            }
1214        })
1215        .unwrap()
1216        .unwrap_if_sufficient()
1217        .unwrap();
1218        // Same as above but omit [22] as well as MD for [23].
1219        let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1220            if idx == 22 {
1221                node.omit_rs = true;
1222            } else if idx == 23 {
1223                node.omit_md = true;
1224            }
1225        })
1226        .unwrap()
1227        .unwrap_if_sufficient()
1228        .unwrap();
1229
1230        //let params = GuardParams::default();
1231        let now = SystemTime::get();
1232
1233        // Try a guard that isn't in the netdir at all.
1234        let mut guard255 = Guard::new(
1235            GuardId::new([255; 32].into(), [255; 20].into()),
1236            vec!["8.8.8.8:53".parse().unwrap()],
1237            None,
1238            now,
1239        );
1240        assert_eq!(guard255.unlisted_since, None);
1241        assert_eq!(guard255.listed_in(&netdir), Some(false));
1242        guard255.update_from_universe(&netdir);
1243        assert_eq!(
1244            guard255.unlisted_since,
1245            Some(netdir.lifetime().valid_after())
1246        );
1247        assert!(!guard255.orports.is_empty());
1248
1249        // Try a guard that is in netdir, but not netdir2.
1250        let mut guard22 = Guard::new(
1251            GuardId::new([22; 32].into(), [22; 20].into()),
1252            vec![],
1253            None,
1254            now,
1255        );
1256        let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1257        let relay22 = id22.get_relay(&netdir).unwrap();
1258        assert_eq!(guard22.listed_in(&netdir), Some(true));
1259        guard22.update_from_universe(&netdir);
1260        assert_eq!(guard22.unlisted_since, None); // It's listed.
1261        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs are set.
1262        assert_eq!(guard22.listed_in(&netdir2), Some(false));
1263        guard22.update_from_universe(&netdir2);
1264        assert_eq!(
1265            guard22.unlisted_since,
1266            Some(netdir2.lifetime().valid_after())
1267        );
1268        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs still set.
1269        assert!(!guard22.dir_info_missing);
1270
1271        // Now see what happens for a guard that's in the consensus, but missing an MD.
1272        let mut guard23 = Guard::new(
1273            GuardId::new([23; 32].into(), [23; 20].into()),
1274            vec![],
1275            None,
1276            now,
1277        );
1278        assert_eq!(guard23.listed_in(&netdir2), Some(true));
1279        assert_eq!(guard23.listed_in(&netdir3), None);
1280        guard23.update_from_universe(&netdir3);
1281        assert!(guard23.dir_info_missing);
1282        assert!(guard23.is_dir_cache);
1283    }
1284
1285    #[test]
1286    fn pending() {
1287        let mut g = basic_guard();
1288        let t1 = Instant::get();
1289        let t2 = t1 + Duration::from_secs(100);
1290        let t3 = t1 + Duration::from_secs(200);
1291
1292        assert!(!g.exploratory_attempt_after(t1));
1293        assert!(!g.exploratory_circ_pending());
1294
1295        g.note_exploratory_circ(true);
1296        g.record_attempt(t2);
1297        assert!(g.exploratory_circ_pending());
1298        assert!(g.exploratory_attempt_after(t1));
1299        assert!(!g.exploratory_attempt_after(t3));
1300
1301        g.note_exploratory_circ(false);
1302        assert!(!g.exploratory_circ_pending());
1303        assert!(!g.exploratory_attempt_after(t1));
1304        assert!(!g.exploratory_attempt_after(t3));
1305    }
1306
1307    #[test]
1308    fn circ_history() {
1309        let mut h = CircHistory {
1310            n_successes: 3,
1311            n_failures: 4,
1312            n_indeterminate: 3,
1313        };
1314        assert!(h.indeterminate_ratio().is_none());
1315
1316        h.n_successes = 20;
1317        assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
1318    }
1319
1320    #[test]
1321    fn disable_on_failure() {
1322        let mut g = basic_guard();
1323        let params = GuardParams::default();
1324
1325        let now = SystemTime::get();
1326
1327        let _ignore = g.record_success(now, &params);
1328        for _ in 0..13 {
1329            g.record_indeterminate_result();
1330        }
1331        // We're still under the observation threshold.
1332        assert!(g.disabled.is_none());
1333
1334        // This crosses the threshold.
1335        g.record_indeterminate_result();
1336        assert!(g.disabled.is_some());
1337
1338        #[allow(unreachable_patterns)]
1339        match g.disabled.unwrap().into_option().unwrap() {
1340            GuardDisabled::TooManyIndeterminateFailures {
1341                history: _,
1342                failure_ratio,
1343                threshold_ratio,
1344            } => {
1345                assert!((failure_ratio - 0.933).abs() < 0.01);
1346                assert!((threshold_ratio - 0.7).abs() < 0.01);
1347            }
1348            other => {
1349                panic!("Wrong variant: {:?}", other);
1350            }
1351        }
1352    }
1353
1354    #[test]
1355    fn mark_retriable() {
1356        let mut g = basic_guard();
1357        use super::Reachable::*;
1358
1359        assert_eq!(g.reachable(), Untried);
1360
1361        for (pre, post) in &[
1362            (Untried, Untried),
1363            (Unreachable, Retriable),
1364            (Reachable, Reachable),
1365        ] {
1366            g.reachable = *pre;
1367            g.mark_retriable();
1368            assert_eq!(g.reachable(), *post);
1369        }
1370    }
1371
1372    #[test]
1373    fn dir_status() {
1374        // We're going to see how directory failures interact with circuit
1375        // failures.
1376
1377        use crate::GuardUsageBuilder;
1378        let mut g = basic_guard();
1379        let inst = Instant::get();
1380        let st = SystemTime::get();
1381        let sec = Duration::from_secs(1);
1382        let params = GuardParams::default();
1383        let dir_usage = GuardUsageBuilder::new()
1384            .kind(GuardUsageKind::OneHopDirectory)
1385            .build()
1386            .unwrap();
1387        let data_usage = GuardUsage::default();
1388
1389        // Record a circuit success.
1390        let _ = g.record_success(st, &params);
1391        assert_eq!(g.next_retry(&dir_usage), None);
1392        assert!(g.ready_for_usage(&dir_usage, inst));
1393        assert_eq!(g.next_retry(&data_usage), None);
1394        assert!(g.ready_for_usage(&data_usage, inst));
1395
1396        // Record a dircache failure.  This does not influence data usage.
1397        g.record_external_failure(ExternalActivity::DirCache, inst);
1398        assert_eq!(g.next_retry(&data_usage), None);
1399        assert!(g.ready_for_usage(&data_usage, inst));
1400        let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1401        assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1402        assert!(!g.ready_for_usage(&dir_usage, inst));
1403        assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1404
1405        // Record a circuit success again.  This does not make the guard usable
1406        // as a directory cache.
1407        let _ = g.record_success(st, &params);
1408        assert!(g.ready_for_usage(&data_usage, inst));
1409        assert!(!g.ready_for_usage(&dir_usage, inst));
1410
1411        // Record a circuit failure.
1412        g.record_failure(inst + sec * 10, true);
1413        let next_circ_retry = g.next_retry(&data_usage).unwrap();
1414        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1415        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1416        assert_eq!(
1417            g.next_retry(&dir_usage).unwrap(),
1418            std::cmp::max(next_circ_retry, next_dir_retry)
1419        );
1420
1421        // Record a directory success.  This won't supersede the circuit
1422        // failure.
1423        g.record_external_success(ExternalActivity::DirCache);
1424        assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1425        assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1426        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1427        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1428    }
1429}