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