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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
950    use super::*;
951    use crate::ids::FirstHopId;
952    use tor_linkspec::{HasRelayIds, RelayId};
953    use tor_llcrypto::pk::ed25519::Ed25519Identity;
954    use web_time_compat::SystemTimeExt;
955
956    #[test]
957    fn crate_id() {
958        let id = CrateId::this_crate().unwrap();
959        assert_eq!(&id.crate_name, "tor-guardmgr");
960        assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
961    }
962
963    fn basic_id() -> GuardId {
964        GuardId::new([13; 32].into(), [37; 20].into())
965    }
966    fn basic_guard() -> Guard {
967        let id = basic_id();
968        let ports = vec!["127.0.0.7:7777".parse().unwrap()];
969        let added = SystemTime::get();
970        Guard::new(id, ports, None, added)
971    }
972
973    #[test]
974    fn simple_accessors() {
975        fn ed(id: [u8; 32]) -> RelayId {
976            RelayId::Ed25519(id.into())
977        }
978        let id = basic_id();
979        let g = basic_guard();
980
981        assert_eq!(g.guard_id(), &id);
982        assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
983        assert_eq!(
984            g.addrs().collect_vec(),
985            &["127.0.0.7:7777".parse().unwrap()]
986        );
987        assert_eq!(g.reachable(), Reachable::Untried);
988        assert_eq!(g.reachable(), Reachable::default());
989
990        use crate::GuardUsageBuilder;
991        let mut usage1 = GuardUsageBuilder::new();
992
993        usage1
994            .restrictions()
995            .push(GuardRestriction::AvoidId(ed([22; 32])));
996        let usage1 = usage1.build().unwrap();
997        let mut usage2 = GuardUsageBuilder::new();
998        usage2
999            .restrictions()
1000            .push(GuardRestriction::AvoidId(ed([13; 32])));
1001        let usage2 = usage2.build().unwrap();
1002        let usage3 = GuardUsage::default();
1003        let mut usage4 = GuardUsageBuilder::new();
1004        usage4
1005            .restrictions()
1006            .push(GuardRestriction::AvoidId(ed([22; 32])));
1007        usage4
1008            .restrictions()
1009            .push(GuardRestriction::AvoidId(ed([13; 32])));
1010        let usage4 = usage4.build().unwrap();
1011        let mut usage5 = GuardUsageBuilder::new();
1012        usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1013            vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1014        ));
1015        let usage5 = usage5.build().unwrap();
1016        let mut usage6 = GuardUsageBuilder::new();
1017        usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1018            vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1019        ));
1020        let usage6 = usage6.build().unwrap();
1021
1022        assert!(g.conforms_to_usage(&usage1));
1023        assert!(!g.conforms_to_usage(&usage2));
1024        assert!(g.conforms_to_usage(&usage3));
1025        assert!(!g.conforms_to_usage(&usage4));
1026        assert!(!g.conforms_to_usage(&usage5));
1027        assert!(g.conforms_to_usage(&usage6));
1028    }
1029
1030    #[allow(clippy::redundant_clone)]
1031    #[test]
1032    fn trickier_usages() {
1033        let g = basic_guard();
1034        use crate::{GuardUsageBuilder, GuardUsageKind};
1035        let data_usage = GuardUsageBuilder::new()
1036            .kind(GuardUsageKind::Data)
1037            .build()
1038            .unwrap();
1039        let dir_usage = GuardUsageBuilder::new()
1040            .kind(GuardUsageKind::OneHopDirectory)
1041            .build()
1042            .unwrap();
1043        assert!(g.conforms_to_usage(&data_usage));
1044        assert!(g.conforms_to_usage(&dir_usage));
1045
1046        let mut g2 = g.clone();
1047        g2.dir_info_missing = true;
1048        assert!(!g2.conforms_to_usage(&data_usage));
1049        assert!(g2.conforms_to_usage(&dir_usage));
1050
1051        let mut g3 = g.clone();
1052        g3.is_dir_cache = false;
1053        assert!(g3.conforms_to_usage(&data_usage));
1054        assert!(!g3.conforms_to_usage(&dir_usage));
1055    }
1056
1057    #[test]
1058    fn record_attempt() {
1059        let t1 = Instant::get() - Duration::from_secs(10);
1060        let t2 = Instant::get() - Duration::from_secs(5);
1061        let t3 = Instant::get();
1062
1063        let mut g = basic_guard();
1064
1065        assert!(g.last_tried_to_connect_at.is_none());
1066        g.record_attempt(t1);
1067        assert_eq!(g.last_tried_to_connect_at, Some(t1));
1068        g.record_attempt(t3);
1069        assert_eq!(g.last_tried_to_connect_at, Some(t3));
1070        g.record_attempt(t2);
1071        assert_eq!(g.last_tried_to_connect_at, Some(t3));
1072    }
1073
1074    #[test]
1075    fn record_failure() {
1076        let t1 = Instant::get() - Duration::from_secs(10);
1077        let t2 = Instant::get();
1078
1079        let mut g = basic_guard();
1080        g.record_failure(t1, true);
1081        assert!(g.retry_schedule.is_some());
1082        assert_eq!(g.reachable(), Reachable::Unreachable);
1083        let retry1 = g.retry_at.unwrap();
1084        assert_eq!(retry1, t1 + Duration::from_secs(30));
1085
1086        g.record_failure(t2, true);
1087        let retry2 = g.retry_at.unwrap();
1088        assert!(retry2 >= t2 + Duration::from_secs(30));
1089        assert!(retry2 <= t2 + Duration::from_secs(200));
1090    }
1091
1092    #[test]
1093    fn record_success() {
1094        let t1 = Instant::get() - Duration::from_secs(10);
1095        // has to be in the future, since the guard's "added_at" time is based on now.
1096        let now = SystemTime::get();
1097        let t2 = now + Duration::from_secs(300 * 86400);
1098        let t3 = Instant::get() + Duration::from_secs(310 * 86400);
1099        let t4 = now + Duration::from_secs(320 * 86400);
1100
1101        let mut g = basic_guard();
1102        g.record_failure(t1, true);
1103        assert_eq!(g.reachable(), Reachable::Unreachable);
1104
1105        let conf = g.record_success(t2, &GuardParams::default());
1106        assert_eq!(g.reachable(), Reachable::Reachable);
1107        assert_eq!(conf, NewlyConfirmed::Yes);
1108        assert!(g.retry_at.is_none());
1109        assert!(g.confirmed_at.unwrap() <= t2);
1110        assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1111        let confirmed_at_orig = g.confirmed_at;
1112
1113        g.record_failure(t3, true);
1114        assert_eq!(g.reachable(), Reachable::Unreachable);
1115
1116        let conf = g.record_success(t4, &GuardParams::default());
1117        assert_eq!(conf, NewlyConfirmed::No);
1118        assert_eq!(g.reachable(), Reachable::Reachable);
1119        assert!(g.retry_at.is_none());
1120        assert_eq!(g.confirmed_at, confirmed_at_orig);
1121    }
1122
1123    #[test]
1124    fn retry() {
1125        let t1 = Instant::get();
1126        let mut g = basic_guard();
1127
1128        g.record_failure(t1, true);
1129        assert!(g.retry_at.is_some());
1130        assert_eq!(g.reachable(), Reachable::Unreachable);
1131
1132        // Not yet retriable.
1133        g.consider_retry(t1);
1134        assert!(g.retry_at.is_some());
1135        assert_eq!(g.reachable(), Reachable::Unreachable);
1136
1137        // Not retriable right before the retry time.
1138        g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1139        assert!(g.retry_at.is_some());
1140        assert_eq!(g.reachable(), Reachable::Unreachable);
1141
1142        // Retriable right after the retry time.
1143        g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1144        assert!(g.retry_at.is_none());
1145        assert_eq!(g.reachable(), Reachable::Retriable);
1146    }
1147
1148    #[test]
1149    fn expiration() {
1150        const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1151        let params = GuardParams::default();
1152        let now = SystemTime::get();
1153
1154        let g = basic_guard();
1155        assert!(!g.is_expired(&params, now));
1156        assert!(!g.is_expired(&params, now + 10 * DAY));
1157        assert!(!g.is_expired(&params, now + 25 * DAY));
1158        assert!(!g.is_expired(&params, now + 70 * DAY));
1159        assert!(g.is_expired(&params, now + 200 * DAY)); // lifetime_unconfirmed.
1160
1161        let mut g = basic_guard();
1162        let _ = g.record_success(now, &params);
1163        assert!(!g.is_expired(&params, now));
1164        assert!(!g.is_expired(&params, now + 10 * DAY));
1165        assert!(!g.is_expired(&params, now + 25 * DAY));
1166        assert!(g.is_expired(&params, now + 70 * DAY)); // lifetime_confirmed.
1167
1168        let mut g = basic_guard();
1169        g.mark_unlisted(now);
1170        assert!(!g.is_expired(&params, now));
1171        assert!(!g.is_expired(&params, now + 10 * DAY));
1172        assert!(g.is_expired(&params, now + 25 * DAY)); // lifetime_unlisted
1173    }
1174
1175    #[test]
1176    fn netdir_integration() {
1177        use tor_netdir::testnet;
1178        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1179        let params = GuardParams::default();
1180        let now = SystemTime::get();
1181
1182        // Construct a guard from a relay from the netdir.
1183        let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1184        let guard22 = Guard::from_chan_target(&relay22, now, &params);
1185        assert!(guard22.same_relay_ids(&relay22));
1186        assert!(Some(guard22.added_at) <= Some(now));
1187
1188        // Can we still get the relay back?
1189        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1190        let r = id.get_relay(&netdir).unwrap();
1191        assert!(r.same_relay_ids(&relay22));
1192
1193        // Now try a guard that isn't in the netdir.
1194        let guard255 = Guard::new(
1195            GuardId::new([255; 32].into(), [255; 20].into()),
1196            vec![],
1197            None,
1198            now,
1199        );
1200        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1201        assert!(id.get_relay(&netdir).is_none());
1202    }
1203
1204    #[test]
1205    fn update_from_netdir() {
1206        use tor_netdir::testnet;
1207        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1208        // Same as above but omit [22]
1209        let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1210            if idx == 22 {
1211                node.omit_rs = true;
1212            }
1213        })
1214        .unwrap()
1215        .unwrap_if_sufficient()
1216        .unwrap();
1217        // Same as above but omit [22] as well as MD for [23].
1218        let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1219            if idx == 22 {
1220                node.omit_rs = true;
1221            } else if idx == 23 {
1222                node.omit_md = true;
1223            }
1224        })
1225        .unwrap()
1226        .unwrap_if_sufficient()
1227        .unwrap();
1228
1229        //let params = GuardParams::default();
1230        let now = SystemTime::get();
1231
1232        // Try a guard that isn't in the netdir at all.
1233        let mut guard255 = Guard::new(
1234            GuardId::new([255; 32].into(), [255; 20].into()),
1235            vec!["8.8.8.8:53".parse().unwrap()],
1236            None,
1237            now,
1238        );
1239        assert_eq!(guard255.unlisted_since, None);
1240        assert_eq!(guard255.listed_in(&netdir), Some(false));
1241        guard255.update_from_universe(&netdir);
1242        assert_eq!(
1243            guard255.unlisted_since,
1244            Some(netdir.lifetime().valid_after())
1245        );
1246        assert!(!guard255.orports.is_empty());
1247
1248        // Try a guard that is in netdir, but not netdir2.
1249        let mut guard22 = Guard::new(
1250            GuardId::new([22; 32].into(), [22; 20].into()),
1251            vec![],
1252            None,
1253            now,
1254        );
1255        let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1256        let relay22 = id22.get_relay(&netdir).unwrap();
1257        assert_eq!(guard22.listed_in(&netdir), Some(true));
1258        guard22.update_from_universe(&netdir);
1259        assert_eq!(guard22.unlisted_since, None); // It's listed.
1260        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs are set.
1261        assert_eq!(guard22.listed_in(&netdir2), Some(false));
1262        guard22.update_from_universe(&netdir2);
1263        assert_eq!(
1264            guard22.unlisted_since,
1265            Some(netdir2.lifetime().valid_after())
1266        );
1267        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs still set.
1268        assert!(!guard22.dir_info_missing);
1269
1270        // Now see what happens for a guard that's in the consensus, but missing an MD.
1271        let mut guard23 = Guard::new(
1272            GuardId::new([23; 32].into(), [23; 20].into()),
1273            vec![],
1274            None,
1275            now,
1276        );
1277        assert_eq!(guard23.listed_in(&netdir2), Some(true));
1278        assert_eq!(guard23.listed_in(&netdir3), None);
1279        guard23.update_from_universe(&netdir3);
1280        assert!(guard23.dir_info_missing);
1281        assert!(guard23.is_dir_cache);
1282    }
1283
1284    #[test]
1285    fn pending() {
1286        let mut g = basic_guard();
1287        let t1 = Instant::get();
1288        let t2 = t1 + Duration::from_secs(100);
1289        let t3 = t1 + Duration::from_secs(200);
1290
1291        assert!(!g.exploratory_attempt_after(t1));
1292        assert!(!g.exploratory_circ_pending());
1293
1294        g.note_exploratory_circ(true);
1295        g.record_attempt(t2);
1296        assert!(g.exploratory_circ_pending());
1297        assert!(g.exploratory_attempt_after(t1));
1298        assert!(!g.exploratory_attempt_after(t3));
1299
1300        g.note_exploratory_circ(false);
1301        assert!(!g.exploratory_circ_pending());
1302        assert!(!g.exploratory_attempt_after(t1));
1303        assert!(!g.exploratory_attempt_after(t3));
1304    }
1305
1306    #[test]
1307    fn circ_history() {
1308        let mut h = CircHistory {
1309            n_successes: 3,
1310            n_failures: 4,
1311            n_indeterminate: 3,
1312        };
1313        assert!(h.indeterminate_ratio().is_none());
1314
1315        h.n_successes = 20;
1316        assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
1317    }
1318
1319    #[test]
1320    fn disable_on_failure() {
1321        let mut g = basic_guard();
1322        let params = GuardParams::default();
1323
1324        let now = SystemTime::get();
1325
1326        let _ignore = g.record_success(now, &params);
1327        for _ in 0..13 {
1328            g.record_indeterminate_result();
1329        }
1330        // We're still under the observation threshold.
1331        assert!(g.disabled.is_none());
1332
1333        // This crosses the threshold.
1334        g.record_indeterminate_result();
1335        assert!(g.disabled.is_some());
1336
1337        #[allow(unreachable_patterns)]
1338        match g.disabled.unwrap().into_option().unwrap() {
1339            GuardDisabled::TooManyIndeterminateFailures {
1340                history: _,
1341                failure_ratio,
1342                threshold_ratio,
1343            } => {
1344                assert!((failure_ratio - 0.933).abs() < 0.01);
1345                assert!((threshold_ratio - 0.7).abs() < 0.01);
1346            }
1347            other => {
1348                panic!("Wrong variant: {:?}", other);
1349            }
1350        }
1351    }
1352
1353    #[test]
1354    fn mark_retriable() {
1355        let mut g = basic_guard();
1356        use super::Reachable::*;
1357
1358        assert_eq!(g.reachable(), Untried);
1359
1360        for (pre, post) in &[
1361            (Untried, Untried),
1362            (Unreachable, Retriable),
1363            (Reachable, Reachable),
1364        ] {
1365            g.reachable = *pre;
1366            g.mark_retriable();
1367            assert_eq!(g.reachable(), *post);
1368        }
1369    }
1370
1371    #[test]
1372    fn dir_status() {
1373        // We're going to see how directory failures interact with circuit
1374        // failures.
1375
1376        use crate::GuardUsageBuilder;
1377        let mut g = basic_guard();
1378        let inst = Instant::get();
1379        let st = SystemTime::get();
1380        let sec = Duration::from_secs(1);
1381        let params = GuardParams::default();
1382        let dir_usage = GuardUsageBuilder::new()
1383            .kind(GuardUsageKind::OneHopDirectory)
1384            .build()
1385            .unwrap();
1386        let data_usage = GuardUsage::default();
1387
1388        // Record a circuit success.
1389        let _ = g.record_success(st, &params);
1390        assert_eq!(g.next_retry(&dir_usage), None);
1391        assert!(g.ready_for_usage(&dir_usage, inst));
1392        assert_eq!(g.next_retry(&data_usage), None);
1393        assert!(g.ready_for_usage(&data_usage, inst));
1394
1395        // Record a dircache failure.  This does not influence data usage.
1396        g.record_external_failure(ExternalActivity::DirCache, inst);
1397        assert_eq!(g.next_retry(&data_usage), None);
1398        assert!(g.ready_for_usage(&data_usage, inst));
1399        let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1400        assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1401        assert!(!g.ready_for_usage(&dir_usage, inst));
1402        assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1403
1404        // Record a circuit success again.  This does not make the guard usable
1405        // as a directory cache.
1406        let _ = g.record_success(st, &params);
1407        assert!(g.ready_for_usage(&data_usage, inst));
1408        assert!(!g.ready_for_usage(&dir_usage, inst));
1409
1410        // Record a circuit failure.
1411        g.record_failure(inst + sec * 10, true);
1412        let next_circ_retry = g.next_retry(&data_usage).unwrap();
1413        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1414        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1415        assert_eq!(
1416            g.next_retry(&dir_usage).unwrap(),
1417            std::cmp::max(next_circ_retry, next_dir_retry)
1418        );
1419
1420        // Record a directory success.  This won't supersede the circuit
1421        // failure.
1422        g.record_external_success(ExternalActivity::DirCache);
1423        assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1424        assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1425        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1426        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1427    }
1428}