1
//! Code to represent its single guard node and track its status.
2

            
3
use tor_basic_utils::retry::RetryDelay;
4

            
5
use itertools::Itertools;
6
use serde::{Deserialize, Serialize};
7
use std::collections::HashMap;
8
use std::net::SocketAddr;
9
use tracing::{info, trace, warn};
10
use web_time_compat::{Duration, Instant, InstantExt, SystemTime};
11

            
12
use crate::dirstatus::DirStatus;
13
use crate::sample::Candidate;
14
use crate::skew::SkewObservation;
15
use crate::util::randomize_time;
16
use crate::{ExternalActivity, GuardSetSelector, GuardUsageKind, sample};
17
use crate::{GuardParams, GuardRestriction, GuardUsage, ids::GuardId};
18

            
19
#[cfg(feature = "bridge-client")]
20
use safelog::Redactable as _;
21

            
22
use tor_basic_utils::onionperf_types::{OnionperfEvent, OnionperfGuardStatus};
23
use tor_linkspec::{
24
    ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
25
};
26
use 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)]
31
pub(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)]
54
struct 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

            
62
impl CrateId {
63
    /// Return a new CrateId representing this crate.
64
117142
    fn this_crate() -> Option<Self> {
65
117142
        let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
66
117142
        let version = option_env!("CARGO_PKG_VERSION")?.to_string();
67
117142
        Some(CrateId {
68
117142
            crate_name,
69
117142
            version,
70
117142
        })
71
117142
    }
72
}
73

            
74
/// What rule do we use when we're displaying information about a guard?
75
#[derive(Clone, Default, Debug)]
76
pub(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)]
116
pub(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.
245
const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
246

            
247
/// Return a DirStatus entry for a guard.
248
117171
fn guard_dirstatus() -> DirStatus {
249
117171
    DirStatus::new(GUARD_DIR_RETRY_FLOOR)
250
117171
}
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)]
255
pub(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

            
262
impl Guard {
263
    /// Create a new unused [`Guard`] from a [`Candidate`].
264
117124
    pub(crate) fn from_candidate(
265
117124
        candidate: Candidate,
266
117124
        now: SystemTime,
267
117124
        params: &GuardParams,
268
117124
    ) -> Self {
269
        let Candidate {
270
117124
            is_dir_cache,
271
117124
            full_dir_info,
272
117124
            owned_target,
273
            ..
274
117124
        } = candidate;
275

            
276
117124
        Guard {
277
117124
            is_dir_cache,
278
117124
            dir_info_missing: !full_dir_info,
279
117124
            ..Self::from_chan_target(&owned_target, now, params)
280
117124
        }
281
117124
    }
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
117125
    fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
288
117125
    where
289
117125
        T: ChanTarget,
290
    {
291
117125
        let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
292

            
293
117125
        let pt_target = match relay.chan_method() {
294
            #[cfg(feature = "pt-client")]
295
            ChannelMethod::Pluggable(pt) => Some(pt),
296
117125
            _ => None,
297
        };
298

            
299
117125
        Self::new(
300
117125
            GuardId::from_relay_ids(relay),
301
117125
            relay.addrs().collect_vec(),
302
117125
            pt_target,
303
117125
            added_at,
304
        )
305
117125
    }
306

            
307
    /// Return a new, manually constructed [`Guard`].
308
117141
    fn new(
309
117141
        id: GuardId,
310
117141
        orports: Vec<SocketAddr>,
311
117141
        pt_target: Option<PtTarget>,
312
117141
        added_at: SystemTime,
313
117141
    ) -> Self {
314
117141
        Guard {
315
117141
            id,
316
117141
            orports,
317
117141
            pt_targets: pt_target.into_iter().collect(),
318
117141
            added_at,
319
117141
            added_by: CrateId::this_crate(),
320
117141
            disabled: None,
321
117141
            confirmed_at: None,
322
117141
            unlisted_since: None,
323
117141
            dir_info_missing: false,
324
117141
            last_tried_to_connect_at: None,
325
117141
            reachable: Reachable::Untried,
326
117141
            retry_at: None,
327
117141
            dir_status: guard_dirstatus(),
328
117141
            retry_schedule: None,
329
117141
            is_dir_cache: true,
330
117141
            exploratory_circ_pending: false,
331
117141
            circ_history: CircHistory::default(),
332
117141
            suspicious_behavior_warned: false,
333
117141
            clock_skew: None,
334
117141
            unknown_fields: Default::default(),
335
117141
            sensitivity: DisplayRule::Sensitive,
336
117141
        }
337
117141
    }
338

            
339
    /// Return the identity of this Guard.
340
10452457
    pub(crate) fn guard_id(&self) -> &GuardId {
341
10452457
        &self.id
342
10452457
    }
343

            
344
    /// Return the reachability status for this guard.
345
730554
    pub(crate) fn reachable(&self) -> Reachable {
346
730554
        self.reachable
347
730554
    }
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
13
    pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
354
13
        match &usage.kind {
355
9
            GuardUsageKind::Data => self.retry_at,
356
4
            GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
357
4
                .iter()
358
4
                .flatten()
359
4
                .max()
360
4
                .copied(),
361
        }
362
13
    }
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
1822016
    pub(crate) fn usable(&self) -> bool {
368
1822016
        self.unlisted_since.is_none() && self.disabled.is_none()
369
1822016
    }
370

            
371
    /// Return true if this guard is ready (with respect to any timeouts) for
372
    /// the given `usage` at `now`.
373
578105
    pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
374
578105
        if let Some(retry_at) = self.retry_at {
375
4
            if retry_at > now {
376
4
                return false;
377
            }
378
578101
        }
379

            
380
578101
        match usage.kind {
381
545415
            GuardUsageKind::Data => true,
382
32686
            GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
383
        }
384
578105
    }
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
8
    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
8
        assert!(self.same_relay_ids(&other));
406

            
407
8
        Guard {
408
8
            // All other persistent fields are taken from `self`.
409
8
            id: self.id,
410
8
            pt_targets: self.pt_targets,
411
8
            orports: self.orports,
412
8
            added_at: self.added_at,
413
8
            added_by: self.added_by,
414
8
            disabled: self.disabled,
415
8
            confirmed_at: self.confirmed_at,
416
8
            unlisted_since: self.unlisted_since,
417
8
            unknown_fields: self.unknown_fields,
418
8

            
419
8
            // All non-persistent fields get taken from `other`.
420
8
            last_tried_to_connect_at: other.last_tried_to_connect_at,
421
8
            retry_at: other.retry_at,
422
8
            retry_schedule: other.retry_schedule,
423
8
            reachable: other.reachable,
424
8
            is_dir_cache: other.is_dir_cache,
425
8
            exploratory_circ_pending: other.exploratory_circ_pending,
426
8
            dir_info_missing: other.dir_info_missing,
427
8
            circ_history: other.circ_history,
428
8
            suspicious_behavior_warned: other.suspicious_behavior_warned,
429
8
            dir_status: other.dir_status,
430
8
            clock_skew: other.clock_skew,
431
8
            sensitivity: other.sensitivity,
432
8
            // Note that we _could_ remove either of the above blocks and add
433
8
            // `..self` or `..other`, but that would be risky: it would increase
434
8
            // the odds that we would forget to add some persistent or
435
8
            // non-persistent field to the right group in the future.
436
8
        }
437
8
    }
438

            
439
    /// Change the reachability status for this guard.
440
10284
    fn set_reachable(&mut self, r: Reachable) {
441
        use Reachable as R;
442

            
443
10284
        if self.reachable != r {
444
            // High-level logs, if change is interesting to user.
445
774
            match (self.reachable, r) {
446
737
                (_, R::Reachable) => info!("We have found that guard {} is usable.", self),
447
33
                (R::Untried | R::Reachable, R::Unreachable) => match self.retry_at {
448
33
                    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
4
                (_, _) => {} // not interesting.
459
            }
460
            //
461
774
            trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed.");
462
774
            self.reachable = r;
463
9510
        }
464
10284
    }
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
578091
    pub(crate) fn exploratory_circ_pending(&self) -> bool {
476
578091
        self.exploratory_circ_pending
477
578091
    }
478

            
479
    /// Note that an exploratory circuit is pending (if `pending` is true),
480
    /// or not pending (if `pending` is false.
481
1479509
    pub(crate) fn note_exploratory_circ(&mut self, pending: bool) {
482
1479509
        self.exploratory_circ_pending = pending;
483
1479509
    }
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
5457195
    pub(crate) fn consider_retry(&mut self, now: Instant) {
492
5457195
        if let Some(retry_at) = self.retry_at {
493
33
            debug_assert!(self.reachable == Reachable::Unreachable);
494
33
            if retry_at <= now {
495
1
                self.mark_retriable();
496
32
            }
497
5457162
        }
498
5457195
    }
499

            
500
    /// If this guard is marked Unreachable, clear its unreachability status
501
    /// and mark it as Retriable.
502
6
    pub(crate) fn mark_retriable(&mut self) {
503
6
        if self.reachable == Reachable::Unreachable {
504
4
            self.set_reachable(Reachable::Retriable);
505
4
            self.retry_at = None;
506
4
            self.retry_schedule = None;
507
4
        }
508
6
    }
509

            
510
    /// Return true if this guard obeys all of the given restrictions.
511
578110
    fn obeys_restrictions(&self, restrictions: &[GuardRestriction]) -> bool {
512
578110
        restrictions.iter().all(|r| self.obeys_restriction(r))
513
578110
    }
514

            
515
    /// Return true if this guard obeys a single restriction.
516
4802
    fn obeys_restriction(&self, r: &GuardRestriction) -> bool {
517
4802
        match r {
518
4
            GuardRestriction::AvoidId(avoid_id) => !self.id.0.has_identity(avoid_id.as_ref()),
519
4798
            GuardRestriction::AvoidAllIds(avoid_ids) => {
520
9595
                self.id.0.identities().all(|id| !avoid_ids.contains(id))
521
            }
522
        }
523
4802
    }
524

            
525
    /// Return true if this guard is suitable to use for the provided `usage`.
526
578112
    pub(crate) fn conforms_to_usage(&self, usage: &GuardUsage) -> bool {
527
578112
        match usage.kind {
528
            GuardUsageKind::OneHopDirectory => {
529
32681
                if !self.is_dir_cache {
530
1
                    return false;
531
32680
                }
532
            }
533
            GuardUsageKind::Data => {
534
                // We need a "definitely listed" guard to build a multihop
535
                // circuit.
536
545431
                if self.dir_info_missing {
537
1
                    return false;
538
545430
                }
539
            }
540
        }
541
578110
        self.obeys_restrictions(&usage.restrictions[..])
542
578112
    }
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
21
    pub(crate) fn listed_in<U: sample::Universe>(&self, universe: &U) -> Option<bool> {
551
21
        universe.contains(self)
552
21
    }
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
34
    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
34
        let listed_as_guard = match universe.status(self) {
570
            Present(Candidate {
571
31
                listed_as_guard,
572
31
                is_dir_cache,
573
31
                full_dir_info,
574
31
                owned_target,
575
31
                sensitivity,
576
            }) => {
577
                // Update address information.
578
31
                self.orports = owned_target.addrs().collect_vec();
579
                // Update Pt information.
580
31
                self.pt_targets = match owned_target.chan_method() {
581
                    #[cfg(feature = "pt-client")]
582
                    ChannelMethod::Pluggable(pt) => vec![pt],
583
31
                    _ => Vec::new(),
584
                };
585
                // Check whether we can currently use it as a directory cache.
586
31
                self.is_dir_cache = is_dir_cache;
587
                // Update our IDs: the Relay will have strictly more.
588
31
                assert!(owned_target.has_all_relay_ids_from(self));
589
31
                self.id = GuardId(RelayIds::from_relay_ids(&owned_target));
590
31
                self.dir_info_missing = !full_dir_info;
591
31
                self.sensitivity = sensitivity;
592

            
593
31
                listed_as_guard
594
            }
595
2
            Absent => false, // Definitely not listed.
596
            Uncertain => {
597
                // We can't tell if this is listed without more directory information.
598
1
                self.dir_info_missing = true;
599
1
                return;
600
            }
601
        };
602

            
603
33
        if listed_as_guard {
604
31
            // Definitely listed, so clear unlisted_since.
605
31
            self.mark_listed();
606
31
        } else {
607
2
            // Unlisted or not a guard; mark it unlisted.
608
2
            self.mark_unlisted(universe.timestamp());
609
2
        }
610
34
    }
611

            
612
    /// Mark this guard as currently listed in the directory.
613
31
    fn mark_listed(&mut self) {
614
31
        if self.unlisted_since.is_some() {
615
            trace!(guard_id = ?self.id, "Guard is now listed again.");
616
            self.unlisted_since = None;
617
31
        }
618
31
    }
619

            
620
    /// Mark this guard as having been unlisted since `now`, if it is not
621
    /// already so marked.
622
3
    fn mark_unlisted(&mut self, now: SystemTime) {
623
3
        if self.unlisted_since.is_none() {
624
3
            trace!(guard_id = ?self.id, "Guard is now unlisted.");
625
3
            self.unlisted_since = Some(now);
626
        }
627
3
    }
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
14305
    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
14308
        fn expired_by(t1: SystemTime, d: Duration, t2: SystemTime) -> bool {
639
14308
            if let Ok(elapsed) = t2.duration_since(t1) {
640
14305
                elapsed > d
641
            } else {
642
3
                false
643
            }
644
14308
        }
645
14305
        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
14305
        }
650
14305
        if let Some(confirmed_at) = self.confirmed_at {
651
32
            if expired_by(confirmed_at, params.lifetime_confirmed, now) {
652
3
                return true;
653
29
            }
654
14273
        } else if expired_by(self.added_at, params.lifetime_unconfirmed, now) {
655
19
            return true;
656
14254
        }
657

            
658
14283
        if let Some(unlisted_since) = self.unlisted_since {
659
3
            if expired_by(unlisted_since, params.lifetime_unlisted, now) {
660
1
                return true;
661
2
            }
662
14280
        }
663

            
664
14282
        false
665
14305
    }
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
35
    pub(crate) fn record_failure(&mut self, now: Instant, is_primary: bool) {
671
35
        let mut rng = rand::rng();
672
35
        let retry_interval = self
673
35
            .retry_schedule
674
35
            .get_or_insert_with(|| retry_schedule(is_primary))
675
35
            .next_delay(&mut rng);
676

            
677
        // TODO-SPEC: Document this behavior in guard-spec.
678
35
        self.retry_at = Some(now + retry_interval);
679

            
680
35
        self.set_reachable(Reachable::Unreachable);
681
35
        self.exploratory_circ_pending = false;
682

            
683
35
        self.circ_history.n_failures += 1;
684
35
    }
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
555888
    pub(crate) fn record_attempt(&mut self, connect_attempt: Instant) {
691
555888
        self.last_tried_to_connect_at = self
692
555888
            .last_tried_to_connect_at
693
555888
            .map(|last| last.max(connect_attempt))
694
555888
            .or(Some(connect_attempt));
695
555888
    }
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
6
    pub(crate) fn exploratory_attempt_after(&self, when: Instant) -> bool {
702
6
        self.exploratory_circ_pending
703
3
            && self.last_tried_to_connect_at.map(|t| t > when) == Some(true)
704
6
    }
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
10245
    pub(crate) fn record_success(
715
10245
        &mut self,
716
10245
        now: SystemTime,
717
10245
        params: &GuardParams,
718
10245
    ) -> NewlyConfirmed {
719
10245
        self.retry_at = None;
720
10245
        self.retry_schedule = None;
721
10245
        self.set_reachable(Reachable::Reachable);
722
10245
        self.exploratory_circ_pending = false;
723
10245
        self.circ_history.n_successes += 1;
724

            
725
10245
        if self.confirmed_at.is_none() {
726
736
            self.confirmed_at = Some(
727
736
                randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10)
728
736
                    .max(self.added_at),
729
736
            );
730
            // TODO-SPEC: The "max" above isn't specified by guard-spec,
731
            // but I think it's wise.
732
736
            trace!(guard_id = ?self.id, "Newly confirmed");
733
736
            trace!(onionperf = true, event = ?OnionperfEvent::Guard(OnionperfGuardStatus::Up));
734
736
            NewlyConfirmed::Yes
735
        } else {
736
9509
            NewlyConfirmed::No
737
        }
738
10245
    }
739

            
740
    /// Record that an external operation has succeeded on this guard.
741
7
    pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
742
7
        match how {
743
7
            ExternalActivity::DirCache => {
744
7
                self.dir_status.note_success();
745
7
            }
746
        }
747
7
    }
748

            
749
    /// Record that an external operation has failed on this guard.
750
7
    pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
751
7
        match how {
752
7
            ExternalActivity::DirCache => {
753
7
                self.dir_status.note_failure(now);
754
7
            }
755
        }
756
7
    }
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
555874
    pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
809
555874
        crate::FirstHop {
810
555874
            sample: Some(selection),
811
555874
            inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
812
555874
                self,
813
555874
            )),
814
555874
        }
815
555874
    }
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
2
    pub(crate) fn confirmed(&self) -> bool {
831
2
        self.confirmed_at.is_some()
832
2
    }
833
}
834

            
835
impl tor_linkspec::HasAddrs for Guard {
836
555875
    fn addrs(&self) -> impl Iterator<Item = SocketAddr> {
837
555875
        self.orports.iter().copied()
838
555875
    }
839
}
840

            
841
impl tor_linkspec::HasRelayIds for Guard {
842
51778596
    fn identity(
843
51778596
        &self,
844
51778596
        key_type: tor_linkspec::RelayIdType,
845
51778596
    ) -> Option<tor_linkspec::RelayIdRef<'_>> {
846
51778596
        self.id.0.identity(key_type)
847
51778596
    }
848
}
849

            
850
impl tor_linkspec::HasChanMethod for Guard {
851
555908
    fn chan_method(&self) -> ChannelMethod {
852
555908
        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
555908
            [] => ChannelMethod::Direct(self.orports.clone()),
858
        }
859
555908
    }
860
}
861

            
862
impl tor_linkspec::ChanTarget for Guard {}
863

            
864
impl 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")]
877
enum 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.
892
33
fn retry_schedule(is_primary: bool) -> RetryDelay {
893
33
    let minimum = if is_primary {
894
24
        Duration::from_secs(30)
895
    } else {
896
9
        Duration::from_secs(150)
897
    };
898

            
899
33
    RetryDelay::from_duration(minimum)
900
33
}
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)]
927
pub(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

            
937
impl CircHistory {
938
    /// If we have seen enough, return the fraction of circuits that have
939
    /// "died under mysterious circumstances".
940
2
    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
2
        let total = self.n_successes + self.n_indeterminate;
947
2
        if total < MIN_OBSERVATIONS {
948
1
            return None;
949
1
        }
950

            
951
1
        Some(f64::from(self.n_indeterminate) / f64::from(total))
952
2
    }
953
}
954

            
955
#[cfg(test)]
956
mod 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();
        let g = basic_guard();
        assert_eq!(g.guard_id(), &id);
        assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
        assert_eq!(
            g.addrs().collect_vec(),
            &["127.0.0.7:7777".parse().unwrap()]
        );
        assert_eq!(g.reachable(), Reachable::Untried);
        assert_eq!(g.reachable(), Reachable::default());
        use crate::GuardUsageBuilder;
        let mut usage1 = GuardUsageBuilder::new();
        usage1
            .restrictions()
            .push(GuardRestriction::AvoidId(ed([22; 32])));
        let usage1 = usage1.build().unwrap();
        let mut usage2 = GuardUsageBuilder::new();
        usage2
            .restrictions()
            .push(GuardRestriction::AvoidId(ed([13; 32])));
        let usage2 = usage2.build().unwrap();
        let usage3 = GuardUsage::default();
        let mut usage4 = GuardUsageBuilder::new();
        usage4
            .restrictions()
            .push(GuardRestriction::AvoidId(ed([22; 32])));
        usage4
            .restrictions()
            .push(GuardRestriction::AvoidId(ed([13; 32])));
        let usage4 = usage4.build().unwrap();
        let mut usage5 = GuardUsageBuilder::new();
        usage5.restrictions().push(GuardRestriction::AvoidAllIds(
            vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
        ));
        let usage5 = usage5.build().unwrap();
        let mut usage6 = GuardUsageBuilder::new();
        usage6.restrictions().push(GuardRestriction::AvoidAllIds(
            vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
        ));
        let usage6 = usage6.build().unwrap();
        assert!(g.conforms_to_usage(&usage1));
        assert!(!g.conforms_to_usage(&usage2));
        assert!(g.conforms_to_usage(&usage3));
        assert!(!g.conforms_to_usage(&usage4));
        assert!(!g.conforms_to_usage(&usage5));
        assert!(g.conforms_to_usage(&usage6));
    }
    #[allow(clippy::redundant_clone)]
    #[test]
    fn trickier_usages() {
        let g = basic_guard();
        use crate::{GuardUsageBuilder, GuardUsageKind};
        let data_usage = GuardUsageBuilder::new()
            .kind(GuardUsageKind::Data)
            .build()
            .unwrap();
        let dir_usage = GuardUsageBuilder::new()
            .kind(GuardUsageKind::OneHopDirectory)
            .build()
            .unwrap();
        assert!(g.conforms_to_usage(&data_usage));
        assert!(g.conforms_to_usage(&dir_usage));
        let mut g2 = g.clone();
        g2.dir_info_missing = true;
        assert!(!g2.conforms_to_usage(&data_usage));
        assert!(g2.conforms_to_usage(&dir_usage));
        let mut g3 = g.clone();
        g3.is_dir_cache = false;
        assert!(g3.conforms_to_usage(&data_usage));
        assert!(!g3.conforms_to_usage(&dir_usage));
    }
    #[test]
    fn record_attempt() {
        let t1 = Instant::get() - Duration::from_secs(10);
        let t2 = Instant::get() - Duration::from_secs(5);
        let t3 = Instant::get();
        let mut g = basic_guard();
        assert!(g.last_tried_to_connect_at.is_none());
        g.record_attempt(t1);
        assert_eq!(g.last_tried_to_connect_at, Some(t1));
        g.record_attempt(t3);
        assert_eq!(g.last_tried_to_connect_at, Some(t3));
        g.record_attempt(t2);
        assert_eq!(g.last_tried_to_connect_at, Some(t3));
    }
    #[test]
    fn record_failure() {
        let t1 = Instant::get() - Duration::from_secs(10);
        let t2 = Instant::get();
        let mut g = basic_guard();
        g.record_failure(t1, true);
        assert!(g.retry_schedule.is_some());
        assert_eq!(g.reachable(), Reachable::Unreachable);
        let retry1 = g.retry_at.unwrap();
        assert_eq!(retry1, t1 + Duration::from_secs(30));
        g.record_failure(t2, true);
        let retry2 = g.retry_at.unwrap();
        assert!(retry2 >= t2 + Duration::from_secs(30));
        assert!(retry2 <= t2 + Duration::from_secs(200));
    }
    #[test]
    fn record_success() {
        let t1 = Instant::get() - Duration::from_secs(10);
        // has to be in the future, since the guard's "added_at" time is based on now.
        let now = SystemTime::get();
        let t2 = now + Duration::from_secs(300 * 86400);
        let t3 = Instant::get() + Duration::from_secs(310 * 86400);
        let t4 = now + Duration::from_secs(320 * 86400);
        let mut g = basic_guard();
        g.record_failure(t1, true);
        assert_eq!(g.reachable(), Reachable::Unreachable);
        let conf = g.record_success(t2, &GuardParams::default());
        assert_eq!(g.reachable(), Reachable::Reachable);
        assert_eq!(conf, NewlyConfirmed::Yes);
        assert!(g.retry_at.is_none());
        assert!(g.confirmed_at.unwrap() <= t2);
        assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
        let confirmed_at_orig = g.confirmed_at;
        g.record_failure(t3, true);
        assert_eq!(g.reachable(), Reachable::Unreachable);
        let conf = g.record_success(t4, &GuardParams::default());
        assert_eq!(conf, NewlyConfirmed::No);
        assert_eq!(g.reachable(), Reachable::Reachable);
        assert!(g.retry_at.is_none());
        assert_eq!(g.confirmed_at, confirmed_at_orig);
    }
    #[test]
    fn retry() {
        let t1 = Instant::get();
        let mut g = basic_guard();
        g.record_failure(t1, true);
        assert!(g.retry_at.is_some());
        assert_eq!(g.reachable(), Reachable::Unreachable);
        // Not yet retriable.
        g.consider_retry(t1);
        assert!(g.retry_at.is_some());
        assert_eq!(g.reachable(), Reachable::Unreachable);
        // Not retriable right before the retry time.
        g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
        assert!(g.retry_at.is_some());
        assert_eq!(g.reachable(), Reachable::Unreachable);
        // Retriable right after the retry time.
        g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
        assert!(g.retry_at.is_none());
        assert_eq!(g.reachable(), Reachable::Retriable);
    }
    #[test]
    fn expiration() {
        const DAY: Duration = Duration::from_secs(24 * 60 * 60);
        let params = GuardParams::default();
        let now = SystemTime::get();
        let g = basic_guard();
        assert!(!g.is_expired(&params, now));
        assert!(!g.is_expired(&params, now + 10 * DAY));
        assert!(!g.is_expired(&params, now + 25 * DAY));
        assert!(!g.is_expired(&params, now + 70 * DAY));
        assert!(g.is_expired(&params, now + 200 * DAY)); // lifetime_unconfirmed.
        let mut g = basic_guard();
        let _ = g.record_success(now, &params);
        assert!(!g.is_expired(&params, now));
        assert!(!g.is_expired(&params, now + 10 * DAY));
        assert!(!g.is_expired(&params, now + 25 * DAY));
        assert!(g.is_expired(&params, now + 70 * DAY)); // lifetime_confirmed.
        let mut g = basic_guard();
        g.mark_unlisted(now);
        assert!(!g.is_expired(&params, now));
        assert!(!g.is_expired(&params, now + 10 * DAY));
        assert!(g.is_expired(&params, now + 25 * DAY)); // lifetime_unlisted
    }
    #[test]
    fn netdir_integration() {
        use tor_netdir::testnet;
        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
        let params = GuardParams::default();
        let now = SystemTime::get();
        // Construct a guard from a relay from the netdir.
        let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
        let guard22 = Guard::from_chan_target(&relay22, now, &params);
        assert!(guard22.same_relay_ids(&relay22));
        assert!(Some(guard22.added_at) <= Some(now));
        // Can we still get the relay back?
        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
        let r = id.get_relay(&netdir).unwrap();
        assert!(r.same_relay_ids(&relay22));
        // Now try a guard that isn't in the netdir.
        let guard255 = Guard::new(
            GuardId::new([255; 32].into(), [255; 20].into()),
            vec![],
            None,
            now,
        );
        let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
        assert!(id.get_relay(&netdir).is_none());
    }
    #[test]
    fn update_from_netdir() {
        use tor_netdir::testnet;
        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
        // Same as above but omit [22]
        let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
            if idx == 22 {
                node.omit_rs = true;
            }
        })
        .unwrap()
        .unwrap_if_sufficient()
        .unwrap();
        // Same as above but omit [22] as well as MD for [23].
        let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
            if idx == 22 {
                node.omit_rs = true;
            } else if idx == 23 {
                node.omit_md = true;
            }
        })
        .unwrap()
        .unwrap_if_sufficient()
        .unwrap();
        //let params = GuardParams::default();
        let now = SystemTime::get();
        // Try a guard that isn't in the netdir at all.
        let mut guard255 = Guard::new(
            GuardId::new([255; 32].into(), [255; 20].into()),
            vec!["8.8.8.8:53".parse().unwrap()],
            None,
            now,
        );
        assert_eq!(guard255.unlisted_since, None);
        assert_eq!(guard255.listed_in(&netdir), Some(false));
        guard255.update_from_universe(&netdir);
        assert_eq!(
            guard255.unlisted_since,
            Some(netdir.lifetime().valid_after())
        );
        assert!(!guard255.orports.is_empty());
        // Try a guard that is in netdir, but not netdir2.
        let mut guard22 = Guard::new(
            GuardId::new([22; 32].into(), [22; 20].into()),
            vec![],
            None,
            now,
        );
        let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
        let relay22 = id22.get_relay(&netdir).unwrap();
        assert_eq!(guard22.listed_in(&netdir), Some(true));
        guard22.update_from_universe(&netdir);
        assert_eq!(guard22.unlisted_since, None); // It's listed.
        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs are set.
        assert_eq!(guard22.listed_in(&netdir2), Some(false));
        guard22.update_from_universe(&netdir2);
        assert_eq!(
            guard22.unlisted_since,
            Some(netdir2.lifetime().valid_after())
        );
        assert_eq!(guard22.orports, relay22.addrs().collect_vec()); // Addrs still set.
        assert!(!guard22.dir_info_missing);
        // Now see what happens for a guard that's in the consensus, but missing an MD.
        let mut guard23 = Guard::new(
            GuardId::new([23; 32].into(), [23; 20].into()),
            vec![],
            None,
            now,
        );
        assert_eq!(guard23.listed_in(&netdir2), Some(true));
        assert_eq!(guard23.listed_in(&netdir3), None);
        guard23.update_from_universe(&netdir3);
        assert!(guard23.dir_info_missing);
        assert!(guard23.is_dir_cache);
    }
    #[test]
    fn pending() {
        let mut g = basic_guard();
        let t1 = Instant::get();
        let t2 = t1 + Duration::from_secs(100);
        let t3 = t1 + Duration::from_secs(200);
        assert!(!g.exploratory_attempt_after(t1));
        assert!(!g.exploratory_circ_pending());
        g.note_exploratory_circ(true);
        g.record_attempt(t2);
        assert!(g.exploratory_circ_pending());
        assert!(g.exploratory_attempt_after(t1));
        assert!(!g.exploratory_attempt_after(t3));
        g.note_exploratory_circ(false);
        assert!(!g.exploratory_circ_pending());
        assert!(!g.exploratory_attempt_after(t1));
        assert!(!g.exploratory_attempt_after(t3));
    }
    #[test]
    fn circ_history() {
        let mut h = CircHistory {
            n_successes: 3,
            n_failures: 4,
            n_indeterminate: 3,
        };
        assert!(h.indeterminate_ratio().is_none());
        h.n_successes = 100;
        assert!((h.indeterminate_ratio().unwrap() - 3.0 / 103.0).abs() < 0.0001);
    }
    // This test is skipped for now, since DISABLE_THRESHOLD is deliberately set
    // to be >= 1.0
    #[test]
    #[ignore]
    fn disable_on_failure() {
        let mut g = basic_guard();
        // Disabled for now, since DISABLE_THRESHOLD is 2.0
        //let params = GuardParams::default();
        //let now = SystemTime::get();
        //let _ignore = g.record_success(now, &params);
        for _ in 0..99 {
            g.record_indeterminate_result();
        }
        // We're still under the observation threshold.
        assert!(g.disabled.is_none());
        // This crosses the threshold.
        g.record_indeterminate_result();
        assert!(g.disabled.is_some());
        #[allow(unreachable_patterns)]
        match g.disabled.unwrap().into_option().unwrap() {
            GuardDisabled::TooManyIndeterminateFailures {
                history: _,
                failure_ratio,
                threshold_ratio,
            } => {
                assert!((failure_ratio - 1.0).abs() < 0.01);
                assert!((threshold_ratio - 1.0).abs() < 0.01);
            }
            other => {
                panic!("Wrong variant: {:?}", other);
            }
        }
    }
    #[test]
    fn mark_retriable() {
        let mut g = basic_guard();
        use super::Reachable::*;
        assert_eq!(g.reachable(), Untried);
        for (pre, post) in &[
            (Untried, Untried),
            (Unreachable, Retriable),
            (Reachable, Reachable),
        ] {
            g.reachable = *pre;
            g.mark_retriable();
            assert_eq!(g.reachable(), *post);
        }
    }
    #[test]
    fn dir_status() {
        // We're going to see how directory failures interact with circuit
        // failures.
        use crate::GuardUsageBuilder;
        let mut g = basic_guard();
        let inst = Instant::get();
        let st = SystemTime::get();
        let sec = Duration::from_secs(1);
        let params = GuardParams::default();
        let dir_usage = GuardUsageBuilder::new()
            .kind(GuardUsageKind::OneHopDirectory)
            .build()
            .unwrap();
        let data_usage = GuardUsage::default();
        // Record a circuit success.
        let _ = g.record_success(st, &params);
        assert_eq!(g.next_retry(&dir_usage), None);
        assert!(g.ready_for_usage(&dir_usage, inst));
        assert_eq!(g.next_retry(&data_usage), None);
        assert!(g.ready_for_usage(&data_usage, inst));
        // Record a dircache failure.  This does not influence data usage.
        g.record_external_failure(ExternalActivity::DirCache, inst);
        assert_eq!(g.next_retry(&data_usage), None);
        assert!(g.ready_for_usage(&data_usage, inst));
        let next_dir_retry = g.next_retry(&dir_usage).unwrap();
        assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
        assert!(!g.ready_for_usage(&dir_usage, inst));
        assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
        // Record a circuit success again.  This does not make the guard usable
        // as a directory cache.
        let _ = g.record_success(st, &params);
        assert!(g.ready_for_usage(&data_usage, inst));
        assert!(!g.ready_for_usage(&dir_usage, inst));
        // Record a circuit failure.
        g.record_failure(inst + sec * 10, true);
        let next_circ_retry = g.next_retry(&data_usage).unwrap();
        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
        assert_eq!(
            g.next_retry(&dir_usage).unwrap(),
            std::cmp::max(next_circ_retry, next_dir_retry)
        );
        // Record a directory success.  This won't supersede the circuit
        // failure.
        g.record_external_success(ExternalActivity::DirCache);
        assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
        assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
        assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
        assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
    }
}