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_basic_utils::onionperf_types::{OnionperfEvent, OnionperfGuardStatus};
23use tor_linkspec::{
24    ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
25};
26use tor_persist::{Futureproof, JsonValue};
27
28/// Tri-state to represent whether a guard is believed to be reachable or not.
29#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
30#[allow(clippy::enum_variant_names)]
31pub(crate) enum Reachable {
32    /// A guard is believed to be reachable, since we have successfully
33    /// used it more recently than we've failed.
34    Reachable,
35    /// A guard is believed to be unreachable, since recent attempts
36    /// to use it have failed, and not enough time has elapsed since then.
37    Unreachable,
38    /// We have never (during the lifetime of the current guard manager)
39    /// tried to connect to this guard.
40    #[default]
41    Untried,
42    /// The last time that we tried to connect to this guard, it failed,
43    /// but enough time has elapsed that we think it is worth trying again.
44    Retriable,
45}
46
47/// The name and version of the crate that first picked a potential
48/// guard.
49///
50/// The C Tor implementation has found it useful to keep this information
51/// about guards, to better work around any bugs discovered in the guard
52/// implementation.
53#[derive(Clone, Debug, Serialize, Deserialize)]
54struct CrateId {
55    /// The name of the crate that added this guard.
56    #[serde(rename = "crate")]
57    crate_name: String,
58    /// The version of the crate that added this guard.
59    version: String,
60}
61
62impl CrateId {
63    /// Return a new CrateId representing this crate.
64    fn this_crate() -> Option<Self> {
65        let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
66        let version = option_env!("CARGO_PKG_VERSION")?.to_string();
67        Some(CrateId {
68            crate_name,
69            version,
70        })
71    }
72}
73
74/// What rule do we use when we're displaying information about a guard?
75#[derive(Clone, Default, Debug)]
76pub(crate) enum DisplayRule {
77    /// The guard is Sensitive; we should display it as "\[scrubbed\]".
78    ///
79    /// We use this for public relays on the network, since displaying even the
80    /// redacted info about them can enough to identify them uniquely within the
81    /// NetDir.
82    ///
83    /// This should not be too much of a hit for UX (we hope), since the user is
84    /// not typically expected to work around issues with these guards themself.
85    #[default]
86    Sensitive,
87    /// The guard should be Redacted; we display it as something like "192.x.x.x
88    /// $ab...".
89    ///
90    /// We use this for bridges.
91    #[cfg(feature = "bridge-client")]
92    Redacted,
93}
94
95/// A single guard node, as held by the guard manager.
96///
97/// A Guard is a Tor relay that clients use for the first hop of their circuits.
98/// It doesn't need to be a relay that's currently on the network (that is, one
99/// that we could represent as a [`Relay`](tor_netdir::Relay)): guards might be
100/// temporarily unlisted.
101///
102/// Some fields in guards are persistent; others are reset with every process.
103///
104/// # Identity
105///
106/// Every guard has at least one `RelayId`.  A guard may _gain_ identities over
107/// time, as we learn more about it, but it should never _lose_ or _change_ its
108/// identities of a given type.
109///
110/// # TODO
111///
112/// This structure uses [`Instant`] to represent non-persistent points in time,
113/// and [`SystemTime`] to represent points in time that need to be persistent.
114/// That's possibly undesirable; maybe we should come up with a better solution.
115#[derive(Clone, Debug, Serialize, Deserialize)]
116pub(crate) struct Guard {
117    /// The identity keys for this guard.
118    id: GuardId,
119
120    /// The most recently seen addresses for this guard.  If `pt_targets` is
121    /// empty, these are the addresses we use for making OR connections to this
122    /// guard directly.  If `pt_targets` is nonempty, these are addresses at
123    /// which the server is "located" (q.v. [`HasAddrs`]), but not ways to
124    /// connect to it.
125    orports: Vec<SocketAddr>,
126
127    /// Any `PtTarget` instances that we know about for connecting to this guard
128    /// over a pluggable transport.
129    ///
130    /// If this is empty, then this guard only supports direct connections, at
131    /// the locations in `orports`.
132    ///
133    /// (Currently, this is always empty, or a singleton.  If we find more than
134    /// one, we only look at the first. It is a vector only for forward
135    /// compatibility.)
136    //
137    // TODO: We may want to replace pt_targets and orports with a new structure;
138    // maybe a PtAddress and a list of SocketAddr.  But we'll keep them like
139    // this for now to keep backward compatibility.
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pt_targets: Vec<PtTarget>,
142
143    /// When, approximately, did we first add this guard to our sample?
144    #[serde(with = "humantime_serde")]
145    added_at: SystemTime,
146
147    /// What version of this crate added this guard to our sample?
148    added_by: Option<CrateId>,
149
150    /// If present, this guard is permanently disabled, and this
151    /// object tells us why.
152    #[serde(default)]
153    disabled: Option<Futureproof<GuardDisabled>>,
154
155    /// When, approximately, did we first successfully use this guard?
156    ///
157    /// (We call a guard "confirmed" if we have successfully used it at
158    /// least once.)
159    #[serde(with = "humantime_serde")]
160    confirmed_at: Option<SystemTime>,
161
162    /// If this guard is not listed in the current-consensus, this is the
163    /// `valid_after` date of the oldest consensus in which it was not listed.
164    ///
165    /// A guard counts as "unlisted" if it is absent, unusable, or
166    /// doesn't have the Guard flag.
167    #[serde(with = "humantime_serde")]
168    unlisted_since: Option<SystemTime>,
169
170    /// True if this guard is listed in the latest consensus, but we don't
171    /// have a microdescriptor for it.
172    #[serde(skip)]
173    dir_info_missing: bool,
174
175    /// When did we last give out this guard in response to a request?
176    #[serde(skip)]
177    last_tried_to_connect_at: Option<Instant>,
178
179    /// If this guard is currently Unreachable, when should we next
180    /// retry it?
181    ///
182    /// (Retrying a guard involves clearing this field, and setting
183    /// `reachable`)
184    #[serde(skip)]
185    retry_at: Option<Instant>, // derived from retry_schedule.
186
187    /// Schedule use to determine when we can next attempt to connect to this
188    /// guard.
189    #[serde(skip)]
190    retry_schedule: Option<RetryDelay>,
191
192    /// Current reachability status for this guard.
193    #[serde(skip)]
194    reachable: Reachable,
195
196    /// If true, then the last time we saw a relay entry for this
197    /// guard, it seemed like a valid directory cache.
198    #[serde(skip)]
199    is_dir_cache: bool,
200
201    /// Status for this guard, when used as a directory cache.
202    ///
203    /// (This is separate from `Reachable` and `retry_schedule`, since being
204    /// usable for circuit construction does not necessarily mean that the guard
205    /// will have good, timely cache information.  If it were not separate, then
206    /// circuit success would clear directory failures.)
207    #[serde(skip, default = "guard_dirstatus")]
208    dir_status: DirStatus,
209
210    /// If true, we have given this guard out for an exploratory circuit,
211    /// and that exploratory circuit is still pending.
212    ///
213    /// A circuit is "exploratory" if we launched it on a non-primary guard.
214    // TODO: Maybe this should be an integer that counts a number of such
215    // circuits?
216    #[serde(skip)]
217    exploratory_circ_pending: bool,
218
219    /// A count of all the circuit statuses we've seen on this guard.
220    ///
221    /// Used to implement a lightweight version of path-bias detection.
222    #[serde(skip)]
223    circ_history: CircHistory,
224
225    /// True if we have warned about this guard behaving suspiciously.
226    #[serde(skip)]
227    suspicious_behavior_warned: bool,
228
229    /// Latest clock skew (if any) we have observed from this guard.
230    #[serde(skip)]
231    clock_skew: Option<SkewObservation>,
232
233    /// How should we display information about this guard?
234    #[serde(skip)]
235    sensitivity: DisplayRule,
236
237    /// Fields from the state file that was used to make this `Guard` that
238    /// this version of Arti doesn't understand.
239    #[serde(flatten)]
240    unknown_fields: HashMap<String, JsonValue>,
241}
242
243/// Lower bound for delay after get a failure using a guard as a directory
244/// cache.
245const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
246
247/// Return a DirStatus entry for a guard.
248fn guard_dirstatus() -> DirStatus {
249    DirStatus::new(GUARD_DIR_RETRY_FLOOR)
250}
251
252/// Wrapper to declare whether a given successful use of a guard is the
253/// _first_ successful use of the guard.
254#[derive(Debug, Clone, Copy, Eq, PartialEq)]
255pub(crate) enum NewlyConfirmed {
256    /// This was the first successful use of a guard.
257    Yes,
258    /// This guard has been used successfully before.
259    No,
260}
261
262impl Guard {
263    /// Create a new unused [`Guard`] from a [`Candidate`].
264    pub(crate) fn from_candidate(
265        candidate: Candidate,
266        now: SystemTime,
267        params: &GuardParams,
268    ) -> Self {
269        let Candidate {
270            is_dir_cache,
271            full_dir_info,
272            owned_target,
273            ..
274        } = candidate;
275
276        Guard {
277            is_dir_cache,
278            dir_info_missing: !full_dir_info,
279            ..Self::from_chan_target(&owned_target, now, params)
280        }
281    }
282
283    /// Create a new unused [`Guard`] from a [`ChanTarget`].
284    ///
285    /// This function doesn't check whether the provided relay is a
286    /// suitable guard node or not: that's up to the caller to decide.
287    fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
288    where
289        T: ChanTarget,
290    {
291        let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
292
293        let pt_target = match relay.chan_method() {
294            #[cfg(feature = "pt-client")]
295            ChannelMethod::Pluggable(pt) => Some(pt),
296            _ => None,
297        };
298
299        Self::new(
300            GuardId::from_relay_ids(relay),
301            relay.addrs().collect_vec(),
302            pt_target,
303            added_at,
304        )
305    }
306
307    /// Return a new, manually constructed [`Guard`].
308    fn new(
309        id: GuardId,
310        orports: Vec<SocketAddr>,
311        pt_target: Option<PtTarget>,
312        added_at: SystemTime,
313    ) -> Self {
314        Guard {
315            id,
316            orports,
317            pt_targets: pt_target.into_iter().collect(),
318            added_at,
319            added_by: CrateId::this_crate(),
320            disabled: None,
321            confirmed_at: None,
322            unlisted_since: None,
323            dir_info_missing: false,
324            last_tried_to_connect_at: None,
325            reachable: Reachable::Untried,
326            retry_at: None,
327            dir_status: guard_dirstatus(),
328            retry_schedule: None,
329            is_dir_cache: true,
330            exploratory_circ_pending: false,
331            circ_history: CircHistory::default(),
332            suspicious_behavior_warned: false,
333            clock_skew: None,
334            unknown_fields: Default::default(),
335            sensitivity: DisplayRule::Sensitive,
336        }
337    }
338
339    /// Return the identity of this Guard.
340    pub(crate) fn guard_id(&self) -> &GuardId {
341        &self.id
342    }
343
344    /// Return the reachability status for this guard.
345    pub(crate) fn reachable(&self) -> Reachable {
346        self.reachable
347    }
348
349    /// Return the next time at which this guard will be retriable for a given
350    /// usage.
351    ///
352    /// (Return None if we think this guard might be reachable right now.)
353    pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
354        match &usage.kind {
355            GuardUsageKind::Data => self.retry_at,
356            GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
357                .iter()
358                .flatten()
359                .max()
360                .copied(),
361        }
362    }
363
364    /// Return true if this guard is usable and working according to our latest
365    /// configuration and directory information, and hasn't been turned off for
366    /// some other reason.
367    pub(crate) fn usable(&self) -> bool {
368        self.unlisted_since.is_none() && self.disabled.is_none()
369    }
370
371    /// Return true if this guard is ready (with respect to any timeouts) for
372    /// the given `usage` at `now`.
373    pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
374        if let Some(retry_at) = self.retry_at {
375            if retry_at > now {
376                return false;
377            }
378        }
379
380        match usage.kind {
381            GuardUsageKind::Data => true,
382            GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
383        }
384    }
385
386    /// Copy all _non-persistent_ status from `other` to self.
387    ///
388    /// We do this when we were not the owner of our persistent state, and we
389    /// have just reloaded it (as `self`), but we have some ephemeral knowledge
390    /// about this guard (as `other`).
391    ///
392    /// You should not invent new uses for this function; instead we should come
393    /// up with alternatives.
394    ///
395    /// # Panics
396    ///
397    /// Panics if the identities in `self` are not exactly the same as the
398    /// identities in `other`.
399    pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
400        // It is not safe to copy failure information unless these identities
401        // are a superset of those in `other`; but it is not safe to copy success
402        // information unless these identities are a subset of those in `other`.
403        //
404        // To simplify matters, we just insist that the identities have to be the same.
405        assert!(self.same_relay_ids(&other));
406
407        Guard {
408            // All other persistent fields are taken from `self`.
409            id: self.id,
410            pt_targets: self.pt_targets,
411            orports: self.orports,
412            added_at: self.added_at,
413            added_by: self.added_by,
414            disabled: self.disabled,
415            confirmed_at: self.confirmed_at,
416            unlisted_since: self.unlisted_since,
417            unknown_fields: self.unknown_fields,
418
419            // All non-persistent fields get taken from `other`.
420            last_tried_to_connect_at: other.last_tried_to_connect_at,
421            retry_at: other.retry_at,
422            retry_schedule: other.retry_schedule,
423            reachable: other.reachable,
424            is_dir_cache: other.is_dir_cache,
425            exploratory_circ_pending: other.exploratory_circ_pending,
426            dir_info_missing: other.dir_info_missing,
427            circ_history: other.circ_history,
428            suspicious_behavior_warned: other.suspicious_behavior_warned,
429            dir_status: other.dir_status,
430            clock_skew: other.clock_skew,
431            sensitivity: other.sensitivity,
432            // Note that we _could_ remove either of the above blocks and add
433            // `..self` or `..other`, but that would be risky: it would increase
434            // the odds that we would forget to add some persistent or
435            // non-persistent field to the right group in the future.
436        }
437    }
438
439    /// Change the reachability status for this guard.
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            trace!(onionperf = true, event = ?OnionperfEvent::Guard(OnionperfGuardStatus::Up));
734            NewlyConfirmed::Yes
735        } else {
736            NewlyConfirmed::No
737        }
738    }
739
740    /// Record that an external operation has succeeded on this guard.
741    pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
742        match how {
743            ExternalActivity::DirCache => {
744                self.dir_status.note_success();
745            }
746        }
747    }
748
749    /// Record that an external operation has failed on this guard.
750    pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
751        match how {
752            ExternalActivity::DirCache => {
753                self.dir_status.note_failure(now);
754            }
755        }
756    }
757
758    /// Note that a circuit through this guard died in a way that we couldn't
759    /// necessarily attribute to the guard.
760    pub(crate) fn record_indeterminate_result(&mut self) {
761        self.circ_history.n_indeterminate += 1;
762
763        if let Some(ratio) = self.circ_history.indeterminate_ratio() {
764            // TODO: These should not be hardwired.
765
766            /// If this fraction of circs are suspicious, we should disable
767            /// the guard.
768            ///
769            /// (We may lower this in the future, but see discussion in arti#2752
770            /// and analysis in proposal 344.  It's currently set to 2.0 while we
771            /// wait to get more network simulation and testing; 2.0 is impossible,
772            /// since the ratio will be in range `0.0..=1.0`.)
773            ///
774            /// (Choosing `DISABLE_THRESHOLD = 1.0` would have the same effect,
775            /// since we compare `ratio > DISABLE_THRESHOLD`, but using 2.0 makes it more
776            /// clear that we will never disable a guard.)
777            const DISABLE_THRESHOLD: f64 = 2.0;
778
779            /// If this fraction of circuits are suspicious, we should
780            /// warn.
781            ///
782            /// (This value is deliberately _lower_ than recommended on arti#2752,
783            /// under the theory that warnings don't hurt anybody.  If we start to see
784            /// excessive warnings, we should investigate.)
785            ///
786            /// Generally speaking, we would expect an adversary who controls fraction
787            /// X of the network to succeed in a path bias attack with probability X^2.
788            /// Therefore, if we set this value to 1.0-X^2, we should catch about half
789            /// of the attempts by such an attacker to mount a path-bias attack.)
790            const WARN_THRESHOLD: f64 = 0.91;
791
792            if ratio > DISABLE_THRESHOLD {
793                let reason = GuardDisabled::TooManyIndeterminateFailures {
794                    history: self.circ_history.clone(),
795                    failure_ratio: ratio,
796                    threshold_ratio: DISABLE_THRESHOLD,
797                };
798                warn!(guard=?self.id, "Disabling guard: {:.1}% of circuits died under mysterious circumstances, exceeding threshold of {:.1}%", ratio*100.0, (DISABLE_THRESHOLD*100.0));
799                self.disabled = Some(reason.into());
800            } else if ratio > WARN_THRESHOLD && !self.suspicious_behavior_warned {
801                warn!(guard=?self.id, "Questionable guard: {:.1}% of circuits died under mysterious circumstances.", ratio*100.0);
802                self.suspicious_behavior_warned = true;
803            }
804        }
805    }
806
807    /// Return a [`FirstHop`](crate::FirstHop) object to represent this guard.
808    pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
809        crate::FirstHop {
810            sample: Some(selection),
811            inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
812                self,
813            )),
814        }
815    }
816
817    /// Record that a given fallback has told us about clock skew.
818    pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
819        self.clock_skew = Some(observation);
820    }
821
822    /// Return the most recent clock skew observation for this guard, if we have
823    /// made one.
824    pub(crate) fn skew(&self) -> Option<&SkewObservation> {
825        self.clock_skew.as_ref()
826    }
827
828    /// Testing only: Return true if this guard was ever contacted successfully.
829    #[cfg(test)]
830    pub(crate) fn confirmed(&self) -> bool {
831        self.confirmed_at.is_some()
832    }
833}
834
835impl tor_linkspec::HasAddrs for Guard {
836    fn addrs(&self) -> impl Iterator<Item = SocketAddr> {
837        self.orports.iter().copied()
838    }
839}
840
841impl tor_linkspec::HasRelayIds for Guard {
842    fn identity(
843        &self,
844        key_type: tor_linkspec::RelayIdType,
845    ) -> Option<tor_linkspec::RelayIdRef<'_>> {
846        self.id.0.identity(key_type)
847    }
848}
849
850impl tor_linkspec::HasChanMethod for Guard {
851    fn chan_method(&self) -> ChannelMethod {
852        match &self.pt_targets[..] {
853            #[cfg(feature = "pt-client")]
854            [first, ..] => ChannelMethod::Pluggable(first.clone()),
855            #[cfg(not(feature = "pt-client"))]
856            [_first, ..] => ChannelMethod::Direct(vec![]), // can't connect to this; no pt support.
857            [] => ChannelMethod::Direct(self.orports.clone()),
858        }
859    }
860}
861
862impl tor_linkspec::ChanTarget for Guard {}
863
864impl std::fmt::Display for Guard {
865    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
866        match self.sensitivity {
867            DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f),
868            #[cfg(feature = "bridge-client")]
869            DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f),
870        }
871    }
872}
873
874/// A reason for permanently disabling a guard.
875#[derive(Clone, Debug, Serialize, Deserialize)]
876#[serde(tag = "type")]
877enum GuardDisabled {
878    /// Too many attempts to use this guard failed for indeterminate reasons.
879    TooManyIndeterminateFailures {
880        /// Observed count of status reports about this guard.
881        history: CircHistory,
882        /// Observed fraction of indeterminate status reports.
883        failure_ratio: f64,
884        /// Threshold that was exceeded.
885        threshold_ratio: f64,
886    },
887}
888
889/// Return a new RetryDelay tracker for a guard.
890///
891/// `is_primary should be true if the guard is primary.
892fn retry_schedule(is_primary: bool) -> RetryDelay {
893    let minimum = if is_primary {
894        Duration::from_secs(30)
895    } else {
896        Duration::from_secs(150)
897    };
898
899    RetryDelay::from_duration(minimum)
900}
901
902/// The recent history of circuit activity on this guard.
903///
904/// We keep this information so that we can tell if too many circuits are
905/// winding up in "indeterminate" status.
906///
907/// # What's this for?
908///
909/// Recall that an "indeterminate" circuit failure is one that might
910/// or might not be the guard's fault.  For example, if the second hop
911/// of the circuit fails, we can't tell whether to blame the guard,
912/// the second hop, or the internet between them.
913///
914/// But we don't want to allow an unbounded number of indeterminate
915/// failures: if we did, it would allow a malicious guard to simply
916/// reject any circuit whose second hop it didn't like, and thereby
917/// filter the client's paths down to a hostile subset.
918///
919/// So as a workaround, and to discourage this kind of behavior, we
920/// track the fraction of indeterminate circuits, and disable any guard
921/// where the fraction is too high.
922//
923// TODO: We may eventually want to make this structure persistent.  If we
924// do, however, we'll need a way to make ancient history expire.  We might
925// want that anyway, to make attacks harder.
926#[derive(Debug, Clone, Default, Serialize, Deserialize)]
927pub(crate) struct CircHistory {
928    /// How many times have we seen this guard succeed?
929    n_successes: u32,
930    /// How many times have we seen this guard fail?
931    #[allow(dead_code)] // not actually used yet.
932    n_failures: u32,
933    /// How many times has this guard given us indeterminate results?
934    n_indeterminate: u32,
935}
936
937impl CircHistory {
938    /// If we have seen enough, return the fraction of circuits that have
939    /// "died under mysterious circumstances".
940    fn indeterminate_ratio(&self) -> Option<f64> {
941        // TODO: This should probably not be hardwired
942
943        /// Don't try to give a ratio unless we've seen this many observations.
944        const MIN_OBSERVATIONS: u32 = 100;
945
946        let total = self.n_successes + self.n_indeterminate;
947        if total < MIN_OBSERVATIONS {
948            return None;
949        }
950
951        Some(f64::from(self.n_indeterminate) / f64::from(total))
952    }
953}
954
955#[cfg(test)]
956mod test {
957    // @@ begin test lint list maintained by maint/add_warning @@
958    #![allow(clippy::bool_assert_comparison)]
959    #![allow(clippy::clone_on_copy)]
960    #![allow(clippy::dbg_macro)]
961    #![allow(clippy::mixed_attributes_style)]
962    #![allow(clippy::print_stderr)]
963    #![allow(clippy::print_stdout)]
964    #![allow(clippy::single_char_pattern)]
965    #![allow(clippy::unwrap_used)]
966    #![allow(clippy::unchecked_time_subtraction)]
967    #![allow(clippy::useless_vec)]
968    #![allow(clippy::needless_pass_by_value)]
969    #![allow(clippy::string_slice)] // See arti#2571
970    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
971    use super::*;
972    use crate::ids::FirstHopId;
973    use tor_linkspec::{HasRelayIds, RelayId};
974    use tor_llcrypto::pk::ed25519::Ed25519Identity;
975    use web_time_compat::SystemTimeExt;
976
977    #[test]
978    fn crate_id() {
979        let id = CrateId::this_crate().unwrap();
980        assert_eq!(&id.crate_name, "tor-guardmgr");
981        assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
982    }
983
984    fn basic_id() -> GuardId {
985        GuardId::new([13; 32].into(), [37; 20].into())
986    }
987    fn basic_guard() -> Guard {
988        let id = basic_id();
989        let ports = vec!["127.0.0.7:7777".parse().unwrap()];
990        let added = SystemTime::get();
991        Guard::new(id, ports, None, added)
992    }
993
994    #[test]
995    fn simple_accessors() {
996        fn ed(id: [u8; 32]) -> RelayId {
997            RelayId::Ed25519(id.into())
998        }
999        let id = basic_id();
1000        let g = basic_guard();
1001
1002        assert_eq!(g.guard_id(), &id);
1003        assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
1004        assert_eq!(
1005            g.addrs().collect_vec(),
1006            &["127.0.0.7:7777".parse().unwrap()]
1007        );
1008        assert_eq!(g.reachable(), Reachable::Untried);
1009        assert_eq!(g.reachable(), Reachable::default());
1010
1011        use crate::GuardUsageBuilder;
1012        let mut usage1 = GuardUsageBuilder::new();
1013
1014        usage1
1015            .restrictions()
1016            .push(GuardRestriction::AvoidId(ed([22; 32])));
1017        let usage1 = usage1.build().unwrap();
1018        let mut usage2 = GuardUsageBuilder::new();
1019        usage2
1020            .restrictions()
1021            .push(GuardRestriction::AvoidId(ed([13; 32])));
1022        let usage2 = usage2.build().unwrap();
1023        let usage3 = GuardUsage::default();
1024        let mut usage4 = GuardUsageBuilder::new();
1025        usage4
1026            .restrictions()
1027            .push(GuardRestriction::AvoidId(ed([22; 32])));
1028        usage4
1029            .restrictions()
1030            .push(GuardRestriction::AvoidId(ed([13; 32])));
1031        let usage4 = usage4.build().unwrap();
1032        let mut usage5 = GuardUsageBuilder::new();
1033        usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1034            vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1035        ));
1036        let usage5 = usage5.build().unwrap();
1037        let mut usage6 = GuardUsageBuilder::new();
1038        usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1039            vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1040        ));
1041        let usage6 = usage6.build().unwrap();
1042
1043        assert!(g.conforms_to_usage(&usage1));
1044        assert!(!g.conforms_to_usage(&usage2));
1045        assert!(g.conforms_to_usage(&usage3));
1046        assert!(!g.conforms_to_usage(&usage4));
1047        assert!(!g.conforms_to_usage(&usage5));
1048        assert!(g.conforms_to_usage(&usage6));
1049    }
1050
1051    #[allow(clippy::redundant_clone)]
1052    #[test]
1053    fn trickier_usages() {
1054        let g = basic_guard();
1055        use crate::{GuardUsageBuilder, GuardUsageKind};
1056        let data_usage = GuardUsageBuilder::new()
1057            .kind(GuardUsageKind::Data)
1058            .build()
1059            .unwrap();
1060        let dir_usage = GuardUsageBuilder::new()
1061            .kind(GuardUsageKind::OneHopDirectory)
1062            .build()
1063            .unwrap();
1064        assert!(g.conforms_to_usage(&data_usage));
1065        assert!(g.conforms_to_usage(&dir_usage));
1066
1067        let mut g2 = g.clone();
1068        g2.dir_info_missing = true;
1069        assert!(!g2.conforms_to_usage(&data_usage));
1070        assert!(g2.conforms_to_usage(&dir_usage));
1071
1072        let mut g3 = g.clone();
1073        g3.is_dir_cache = false;
1074        assert!(g3.conforms_to_usage(&data_usage));
1075        assert!(!g3.conforms_to_usage(&dir_usage));
1076    }
1077
1078    #[test]
1079    fn record_attempt() {
1080        let t1 = Instant::get() - Duration::from_secs(10);
1081        let t2 = Instant::get() - Duration::from_secs(5);
1082        let t3 = Instant::get();
1083
1084        let mut g = basic_guard();
1085
1086        assert!(g.last_tried_to_connect_at.is_none());
1087        g.record_attempt(t1);
1088        assert_eq!(g.last_tried_to_connect_at, Some(t1));
1089        g.record_attempt(t3);
1090        assert_eq!(g.last_tried_to_connect_at, Some(t3));
1091        g.record_attempt(t2);
1092        assert_eq!(g.last_tried_to_connect_at, Some(t3));
1093    }
1094
1095    #[test]
1096    fn record_failure() {
1097        let t1 = Instant::get() - Duration::from_secs(10);
1098        let t2 = Instant::get();
1099
1100        let mut g = basic_guard();
1101        g.record_failure(t1, true);
1102        assert!(g.retry_schedule.is_some());
1103        assert_eq!(g.reachable(), Reachable::Unreachable);
1104        let retry1 = g.retry_at.unwrap();
1105        assert_eq!(retry1, t1 + Duration::from_secs(30));
1106
1107        g.record_failure(t2, true);
1108        let retry2 = g.retry_at.unwrap();
1109        assert!(retry2 >= t2 + Duration::from_secs(30));
1110        assert!(retry2 <= t2 + Duration::from_secs(200));
1111    }
1112
1113    #[test]
1114    fn record_success() {
1115        let t1 = Instant::get() - Duration::from_secs(10);
1116        // has to be in the future, since the guard's "added_at" time is based on now.
1117        let now = SystemTime::get();
1118        let t2 = now + Duration::from_secs(300 * 86400);
1119        let t3 = Instant::get() + Duration::from_secs(310 * 86400);
1120        let t4 = now + Duration::from_secs(320 * 86400);
1121
1122        let mut g = basic_guard();
1123        g.record_failure(t1, true);
1124        assert_eq!(g.reachable(), Reachable::Unreachable);
1125
1126        let conf = g.record_success(t2, &GuardParams::default());
1127        assert_eq!(g.reachable(), Reachable::Reachable);
1128        assert_eq!(conf, NewlyConfirmed::Yes);
1129        assert!(g.retry_at.is_none());
1130        assert!(g.confirmed_at.unwrap() <= t2);
1131        assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1132        let confirmed_at_orig = g.confirmed_at;
1133
1134        g.record_failure(t3, true);
1135        assert_eq!(g.reachable(), Reachable::Unreachable);
1136
1137        let conf = g.record_success(t4, &GuardParams::default());
1138        assert_eq!(conf, NewlyConfirmed::No);
1139        assert_eq!(g.reachable(), Reachable::Reachable);
1140        assert!(g.retry_at.is_none());
1141        assert_eq!(g.confirmed_at, confirmed_at_orig);
1142    }
1143
1144    #[test]
1145    fn retry() {
1146        let t1 = Instant::get();
1147        let mut g = basic_guard();
1148
1149        g.record_failure(t1, true);
1150        assert!(g.retry_at.is_some());
1151        assert_eq!(g.reachable(), Reachable::Unreachable);
1152
1153        // Not yet retriable.
1154        g.consider_retry(t1);
1155        assert!(g.retry_at.is_some());
1156        assert_eq!(g.reachable(), Reachable::Unreachable);
1157
1158        // Not retriable right before the retry time.
1159        g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1160        assert!(g.retry_at.is_some());
1161        assert_eq!(g.reachable(), Reachable::Unreachable);
1162
1163        // Retriable right after the retry time.
1164        g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1165        assert!(g.retry_at.is_none());
1166        assert_eq!(g.reachable(), Reachable::Retriable);
1167    }
1168
1169    #[test]
1170    fn expiration() {
1171        const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1172        let params = GuardParams::default();
1173        let now = SystemTime::get();
1174
1175        let g = basic_guard();
1176        assert!(!g.is_expired(&params, now));
1177        assert!(!g.is_expired(&params, now + 10 * DAY));
1178        assert!(!g.is_expired(&params, now + 25 * DAY));
1179        assert!(!g.is_expired(&params, now + 70 * DAY));
1180        assert!(g.is_expired(&params, now + 200 * DAY)); // lifetime_unconfirmed.
1181
1182        let mut g = basic_guard();
1183        let _ = g.record_success(now, &params);
1184        assert!(!g.is_expired(&params, now));
1185        assert!(!g.is_expired(&params, now + 10 * DAY));
1186        assert!(!g.is_expired(&params, now + 25 * DAY));
1187        assert!(g.is_expired(&params, now + 70 * DAY)); // lifetime_confirmed.
1188
1189        let mut g = basic_guard();
1190        g.mark_unlisted(now);
1191        assert!(!g.is_expired(&params, now));
1192        assert!(!g.is_expired(&params, now + 10 * DAY));
1193        assert!(g.is_expired(&params, now + 25 * DAY)); // lifetime_unlisted
1194    }
1195
1196    #[test]
1197    fn netdir_integration() {
1198        use tor_netdir::testnet;
1199        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1200        let params = GuardParams::default();
1201        let now = SystemTime::get();
1202
1203        // Construct a guard from a relay from the netdir.
1204        let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1205        let guard22 = Guard::from_chan_target(&relay22, now, &params);
1206        assert!(guard22.same_relay_ids(&relay22));
1207        assert!(Some(guard22.added_at) <= Some(now));
1208
1209        // Can we still get the relay back?
1210        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1211        let r = id.get_relay(&netdir).unwrap();
1212        assert!(r.same_relay_ids(&relay22));
1213
1214        // Now try a guard that isn't in the netdir.
1215        let guard255 = Guard::new(
1216            GuardId::new([255; 32].into(), [255; 20].into()),
1217            vec![],
1218            None,
1219            now,
1220        );
1221        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1222        assert!(id.get_relay(&netdir).is_none());
1223    }
1224
1225    #[test]
1226    fn update_from_netdir() {
1227        use tor_netdir::testnet;
1228        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1229        // Same as above but omit [22]
1230        let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1231            if idx == 22 {
1232                node.omit_rs = true;
1233            }
1234        })
1235        .unwrap()
1236        .unwrap_if_sufficient()
1237        .unwrap();
1238        // Same as above but omit [22] as well as MD for [23].
1239        let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1240            if idx == 22 {
1241                node.omit_rs = true;
1242            } else if idx == 23 {
1243                node.omit_md = true;
1244            }
1245        })
1246        .unwrap()
1247        .unwrap_if_sufficient()
1248        .unwrap();
1249
1250        //let params = GuardParams::default();
1251        let now = SystemTime::get();
1252
1253        // Try a guard that isn't in the netdir at all.
1254        let mut guard255 = Guard::new(
1255            GuardId::new([255; 32].into(), [255; 20].into()),
1256            vec!["8.8.8.8:53".parse().unwrap()],
1257            None,
1258            now,
1259        );
1260        assert_eq!(guard255.unlisted_since, None);
1261        assert_eq!(guard255.listed_in(&netdir), Some(false));
1262        guard255.update_from_universe(&netdir);
1263        assert_eq!(
1264            guard255.unlisted_since,
1265            Some(netdir.lifetime().valid_after())
1266        );
1267        assert!(!guard255.orports.is_empty());
1268
1269        // Try a guard that is in netdir, but not netdir2.
1270        let mut guard22 = Guard::new(
1271            GuardId::new([22; 32].into(), [22; 20].into()),
1272            vec![],
1273            None,
1274            now,
1275        );
1276        let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1277        let relay22 = id22.get_relay(&netdir).unwrap();
1278        assert_eq!(guard22.listed_in(&netdir), Some(true));
1279        guard22.update_from_universe(&netdir);
1280        assert_eq!(guard22.unlisted_since, None); // It's listed.
1281        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs are set.
1282        assert_eq!(guard22.listed_in(&netdir2), Some(false));
1283        guard22.update_from_universe(&netdir2);
1284        assert_eq!(
1285            guard22.unlisted_since,
1286            Some(netdir2.lifetime().valid_after())
1287        );
1288        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs still set.
1289        assert!(!guard22.dir_info_missing);
1290
1291        // Now see what happens for a guard that's in the consensus, but missing an MD.
1292        let mut guard23 = Guard::new(
1293            GuardId::new([23; 32].into(), [23; 20].into()),
1294            vec![],
1295            None,
1296            now,
1297        );
1298        assert_eq!(guard23.listed_in(&netdir2), Some(true));
1299        assert_eq!(guard23.listed_in(&netdir3), None);
1300        guard23.update_from_universe(&netdir3);
1301        assert!(guard23.dir_info_missing);
1302        assert!(guard23.is_dir_cache);
1303    }
1304
1305    #[test]
1306    fn pending() {
1307        let mut g = basic_guard();
1308        let t1 = Instant::get();
1309        let t2 = t1 + Duration::from_secs(100);
1310        let t3 = t1 + Duration::from_secs(200);
1311
1312        assert!(!g.exploratory_attempt_after(t1));
1313        assert!(!g.exploratory_circ_pending());
1314
1315        g.note_exploratory_circ(true);
1316        g.record_attempt(t2);
1317        assert!(g.exploratory_circ_pending());
1318        assert!(g.exploratory_attempt_after(t1));
1319        assert!(!g.exploratory_attempt_after(t3));
1320
1321        g.note_exploratory_circ(false);
1322        assert!(!g.exploratory_circ_pending());
1323        assert!(!g.exploratory_attempt_after(t1));
1324        assert!(!g.exploratory_attempt_after(t3));
1325    }
1326
1327    #[test]
1328    fn circ_history() {
1329        let mut h = CircHistory {
1330            n_successes: 3,
1331            n_failures: 4,
1332            n_indeterminate: 3,
1333        };
1334        assert!(h.indeterminate_ratio().is_none());
1335
1336        h.n_successes = 100;
1337        assert!((h.indeterminate_ratio().unwrap() - 3.0 / 103.0).abs() < 0.0001);
1338    }
1339
1340    // This test is skipped for now, since DISABLE_THRESHOLD is deliberately set
1341    // to be >= 1.0
1342    #[test]
1343    #[ignore]
1344    fn disable_on_failure() {
1345        let mut g = basic_guard();
1346
1347        // Disabled for now, since DISABLE_THRESHOLD is 2.0
1348        //let params = GuardParams::default();
1349        //let now = SystemTime::get();
1350        //let _ignore = g.record_success(now, &params);
1351
1352        for _ in 0..99 {
1353            g.record_indeterminate_result();
1354        }
1355        // We're still under the observation threshold.
1356        assert!(g.disabled.is_none());
1357
1358        // This crosses the threshold.
1359        g.record_indeterminate_result();
1360        assert!(g.disabled.is_some());
1361
1362        #[allow(unreachable_patterns)]
1363        match g.disabled.unwrap().into_option().unwrap() {
1364            GuardDisabled::TooManyIndeterminateFailures {
1365                history: _,
1366                failure_ratio,
1367                threshold_ratio,
1368            } => {
1369                assert!((failure_ratio - 1.0).abs() < 0.01);
1370                assert!((threshold_ratio - 1.0).abs() < 0.01);
1371            }
1372            other => {
1373                panic!("Wrong variant: {:?}", other);
1374            }
1375        }
1376    }
1377
1378    #[test]
1379    fn mark_retriable() {
1380        let mut g = basic_guard();
1381        use super::Reachable::*;
1382
1383        assert_eq!(g.reachable(), Untried);
1384
1385        for (pre, post) in &[
1386            (Untried, Untried),
1387            (Unreachable, Retriable),
1388            (Reachable, Reachable),
1389        ] {
1390            g.reachable = *pre;
1391            g.mark_retriable();
1392            assert_eq!(g.reachable(), *post);
1393        }
1394    }
1395
1396    #[test]
1397    fn dir_status() {
1398        // We're going to see how directory failures interact with circuit
1399        // failures.
1400
1401        use crate::GuardUsageBuilder;
1402        let mut g = basic_guard();
1403        let inst = Instant::get();
1404        let st = SystemTime::get();
1405        let sec = Duration::from_secs(1);
1406        let params = GuardParams::default();
1407        let dir_usage = GuardUsageBuilder::new()
1408            .kind(GuardUsageKind::OneHopDirectory)
1409            .build()
1410            .unwrap();
1411        let data_usage = GuardUsage::default();
1412
1413        // Record a circuit success.
1414        let _ = g.record_success(st, &params);
1415        assert_eq!(g.next_retry(&dir_usage), None);
1416        assert!(g.ready_for_usage(&dir_usage, inst));
1417        assert_eq!(g.next_retry(&data_usage), None);
1418        assert!(g.ready_for_usage(&data_usage, inst));
1419
1420        // Record a dircache failure.  This does not influence data usage.
1421        g.record_external_failure(ExternalActivity::DirCache, inst);
1422        assert_eq!(g.next_retry(&data_usage), None);
1423        assert!(g.ready_for_usage(&data_usage, inst));
1424        let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1425        assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1426        assert!(!g.ready_for_usage(&dir_usage, inst));
1427        assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1428
1429        // Record a circuit success again.  This does not make the guard usable
1430        // as a directory cache.
1431        let _ = g.record_success(st, &params);
1432        assert!(g.ready_for_usage(&data_usage, inst));
1433        assert!(!g.ready_for_usage(&dir_usage, inst));
1434
1435        // Record a circuit failure.
1436        g.record_failure(inst + sec * 10, true);
1437        let next_circ_retry = g.next_retry(&data_usage).unwrap();
1438        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1439        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1440        assert_eq!(
1441            g.next_retry(&dir_usage).unwrap(),
1442            std::cmp::max(next_circ_retry, next_dir_retry)
1443        );
1444
1445        // Record a directory success.  This won't supersede the circuit
1446        // failure.
1447        g.record_external_success(ExternalActivity::DirCache);
1448        assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1449        assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1450        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1451        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1452    }
1453}