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 std::time::{Duration, Instant, SystemTime};
10
use tracing::{info, trace, warn};
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_linkspec::{
23
    ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
24
};
25
use tor_persist::{Futureproof, JsonValue};
26

            
27
/// Tri-state to represent whether a guard is believed to be reachable or not.
28
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
29
#[allow(clippy::enum_variant_names)]
30
pub(crate) enum Reachable {
31
    /// A guard is believed to be reachable, since we have successfully
32
    /// used it more recently than we've failed.
33
    Reachable,
34
    /// A guard is believed to be unreachable, since recent attempts
35
    /// to use it have failed, and not enough time has elapsed since then.
36
    Unreachable,
37
    /// We have never (during the lifetime of the current guard manager)
38
    /// tried to connect to this guard.
39
    #[default]
40
    Untried,
41
    /// The last time that we tried to connect to this guard, it failed,
42
    /// but enough time has elapsed that we think it is worth trying again.
43
    Retriable,
44
}
45

            
46
/// The name and version of the crate that first picked a potential
47
/// guard.
48
///
49
/// The C Tor implementation has found it useful to keep this information
50
/// about guards, to better work around any bugs discovered in the guard
51
/// implementation.
52
#[derive(Clone, Debug, Serialize, Deserialize)]
53
struct CrateId {
54
    /// The name of the crate that added this guard.
55
    #[serde(rename = "crate")]
56
    crate_name: String,
57
    /// The version of the crate that added this guard.
58
    version: String,
59
}
60

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

            
73
/// What rule do we use when we're displaying information about a guard?
74
#[derive(Clone, Default, Debug)]
75
pub(crate) enum DisplayRule {
76
    /// The guard is Sensitive; we should display it as "\[scrubbed\]".
77
    ///
78
    /// We use this for public relays on the network, since displaying even the
79
    /// redacted info about them can enough to identify them uniquely within the
80
    /// NetDir.
81
    ///
82
    /// This should not be too much of a hit for UX (we hope), since the user is
83
    /// not typically expected to work around issues with these guards themself.
84
    #[default]
85
    Sensitive,
86
    /// The guard should be Redacted; we display it as something like "192.x.x.x
87
    /// $ab...".
88
    ///
89
    /// We use this for bridges.
90
    #[cfg(feature = "bridge-client")]
91
    Redacted,
92
}
93

            
94
/// A single guard node, as held by the guard manager.
95
///
96
/// A Guard is a Tor relay that clients use for the first hop of their circuits.
97
/// It doesn't need to be a relay that's currently on the network (that is, one
98
/// that we could represent as a [`Relay`](tor_netdir::Relay)): guards might be
99
/// temporarily unlisted.
100
///
101
/// Some fields in guards are persistent; others are reset with every process.
102
///
103
/// # Identity
104
///
105
/// Every guard has at least one `RelayId`.  A guard may _gain_ identities over
106
/// time, as we learn more about it, but it should never _lose_ or _change_ its
107
/// identities of a given type.
108
///
109
/// # TODO
110
///
111
/// This structure uses [`Instant`] to represent non-persistent points in time,
112
/// and [`SystemTime`] to represent points in time that need to be persistent.
113
/// That's possibly undesirable; maybe we should come up with a better solution.
114
#[derive(Clone, Debug, Serialize, Deserialize)]
115
pub(crate) struct Guard {
116
    /// The identity keys for this guard.
117
    id: GuardId,
118

            
119
    /// The most recently seen addresses for this guard.  If `pt_targets` is
120
    /// empty, these are the addresses we use for making OR connections to this
121
    /// guard directly.  If `pt_targets` is nonempty, these are addresses at
122
    /// which the server is "located" (q.v. [`HasAddrs`]), but not ways to
123
    /// connect to it.
124
    orports: Vec<SocketAddr>,
125

            
126
    /// Any `PtTarget` instances that we know about for connecting to this guard
127
    /// over a pluggable transport.
128
    ///
129
    /// If this is empty, then this guard only supports direct connections, at
130
    /// the locations in `orports`.
131
    ///
132
    /// (Currently, this is always empty, or a singleton.  If we find more than
133
    /// one, we only look at the first. It is a vector only for forward
134
    /// compatibility.)
135
    //
136
    // TODO: We may want to replace pt_targets and orports with a new structure;
137
    // maybe a PtAddress and a list of SocketAddr.  But we'll keep them like
138
    // this for now to keep backward compatibility.
139
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
140
    pt_targets: Vec<PtTarget>,
141

            
142
    /// When, approximately, did we first add this guard to our sample?
143
    #[serde(with = "humantime_serde")]
144
    added_at: SystemTime,
145

            
146
    /// What version of this crate added this guard to our sample?
147
    added_by: Option<CrateId>,
148

            
149
    /// If present, this guard is permanently disabled, and this
150
    /// object tells us why.
151
    #[serde(default)]
152
    disabled: Option<Futureproof<GuardDisabled>>,
153

            
154
    /// When, approximately, did we first successfully use this guard?
155
    ///
156
    /// (We call a guard "confirmed" if we have successfully used it at
157
    /// least once.)
158
    #[serde(with = "humantime_serde")]
159
    confirmed_at: Option<SystemTime>,
160

            
161
    /// If this guard is not listed in the current-consensus, this is the
162
    /// `valid_after` date of the oldest consensus in which it was not listed.
163
    ///
164
    /// A guard counts as "unlisted" if it is absent, unusable, or
165
    /// doesn't have the Guard flag.
166
    #[serde(with = "humantime_serde")]
167
    unlisted_since: Option<SystemTime>,
168

            
169
    /// True if this guard is listed in the latest consensus, but we don't
170
    /// have a microdescriptor for it.
171
    #[serde(skip)]
172
    dir_info_missing: bool,
173

            
174
    /// When did we last give out this guard in response to a request?
175
    #[serde(skip)]
176
    last_tried_to_connect_at: Option<Instant>,
177

            
178
    /// If this guard is currently Unreachable, when should we next
179
    /// retry it?
180
    ///
181
    /// (Retrying a guard involves clearing this field, and setting
182
    /// `reachable`)
183
    #[serde(skip)]
184
    retry_at: Option<Instant>, // derived from retry_schedule.
185

            
186
    /// Schedule use to determine when we can next attempt to connect to this
187
    /// guard.
188
    #[serde(skip)]
189
    retry_schedule: Option<RetryDelay>,
190

            
191
    /// Current reachability status for this guard.
192
    #[serde(skip)]
193
    reachable: Reachable,
194

            
195
    /// If true, then the last time we saw a relay entry for this
196
    /// guard, it seemed like a valid directory cache.
197
    #[serde(skip)]
198
    is_dir_cache: bool,
199

            
200
    /// Status for this guard, when used as a directory cache.
201
    ///
202
    /// (This is separate from `Reachable` and `retry_schedule`, since being
203
    /// usable for circuit construction does not necessarily mean that the guard
204
    /// will have good, timely cache information.  If it were not separate, then
205
    /// circuit success would clear directory failures.)
206
    #[serde(skip, default = "guard_dirstatus")]
207
    dir_status: DirStatus,
208

            
209
    /// If true, we have given this guard out for an exploratory circuit,
210
    /// and that exploratory circuit is still pending.
211
    ///
212
    /// A circuit is "exploratory" if we launched it on a non-primary guard.
213
    // TODO: Maybe this should be an integer that counts a number of such
214
    // circuits?
215
    #[serde(skip)]
216
    exploratory_circ_pending: bool,
217

            
218
    /// A count of all the circuit statuses we've seen on this guard.
219
    ///
220
    /// Used to implement a lightweight version of path-bias detection.
221
    #[serde(skip)]
222
    circ_history: CircHistory,
223

            
224
    /// True if we have warned about this guard behaving suspiciously.
225
    #[serde(skip)]
226
    suspicious_behavior_warned: bool,
227

            
228
    /// Latest clock skew (if any) we have observed from this guard.
229
    #[serde(skip)]
230
    clock_skew: Option<SkewObservation>,
231

            
232
    /// How should we display information about this guard?
233
    #[serde(skip)]
234
    sensitivity: DisplayRule,
235

            
236
    /// Fields from the state file that was used to make this `Guard` that
237
    /// this version of Arti doesn't understand.
238
    #[serde(flatten)]
239
    unknown_fields: HashMap<String, JsonValue>,
240
}
241

            
242
/// Lower bound for delay after get a failure using a guard as a directory
243
/// cache.
244
const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
245

            
246
/// Return a DirStatus entry for a guard.
247
117392
fn guard_dirstatus() -> DirStatus {
248
117392
    DirStatus::new(GUARD_DIR_RETRY_FLOOR)
249
117392
}
250

            
251
/// Wrapper to declare whether a given successful use of a guard is the
252
/// _first_ successful use of the guard.
253
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
254
pub(crate) enum NewlyConfirmed {
255
    /// This was the first successful use of a guard.
256
    Yes,
257
    /// This guard has been used successfully before.
258
    No,
259
}
260

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

            
275
117296
        Guard {
276
117296
            is_dir_cache,
277
117296
            dir_info_missing: !full_dir_info,
278
117296
            ..Self::from_chan_target(&owned_target, now, params)
279
117296
        }
280
117296
    }
281

            
282
    /// Create a new unused [`Guard`] from a [`ChanTarget`].
283
    ///
284
    /// This function doesn't check whether the provided relay is a
285
    /// suitable guard node or not: that's up to the caller to decide.
286
117298
    fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
287
117298
    where
288
117298
        T: ChanTarget,
289
    {
290
117298
        let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
291

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

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

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

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

            
343
    /// Return the reachability status for this guard.
344
731456
    pub(crate) fn reachable(&self) -> Reachable {
345
731456
        self.reachable
346
731456
    }
347

            
348
    /// Return the next time at which this guard will be retriable for a given
349
    /// usage.
350
    ///
351
    /// (Return None if we think this guard might be reachable right now.)
352
26
    pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
353
26
        match &usage.kind {
354
18
            GuardUsageKind::Data => self.retry_at,
355
8
            GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
356
8
                .iter()
357
8
                .flatten()
358
8
                .max()
359
8
                .copied(),
360
        }
361
26
    }
362

            
363
    /// Return true if this guard is usable and working according to our latest
364
    /// configuration and directory information, and hasn't been turned off for
365
    /// some other reason.
366
1825128
    pub(crate) fn usable(&self) -> bool {
367
1825128
        self.unlisted_since.is_none() && self.disabled.is_none()
368
1825128
    }
369

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

            
379
578482
        match usage.kind {
380
545582
            GuardUsageKind::Data => true,
381
32900
            GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
382
        }
383
578490
    }
384

            
385
    /// Copy all _non-persistent_ status from `other` to self.
386
    ///
387
    /// We do this when we were not the owner of our persistent state, and we
388
    /// have just reloaded it (as `self`), but we have some ephemeral knowledge
389
    /// about this guard (as `other`).
390
    ///
391
    /// You should not invent new uses for this function; instead we should come
392
    /// up with alternatives.
393
    ///
394
    /// # Panics
395
    ///
396
    /// Panics if the identities in `self` are not exactly the same as the
397
    /// identities in `other`.
398
16
    pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
399
        // It is not safe to copy failure information unless these identities
400
        // are a superset of those in `other`; but it is not safe to copy success
401
        // information unless these identities are a subset of those in `other`.
402
        //
403
        // To simplify matters, we just insist that the identities have to be the same.
404
16
        assert!(self.same_relay_ids(&other));
405

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

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

            
438
    /// Change the reachability status for this guard.
439
    #[allow(clippy::cognitive_complexity)]
440
10366
    fn set_reachable(&mut self, r: Reachable) {
441
        use Reachable as R;
442

            
443
10366
        if self.reachable != r {
444
            // High-level logs, if change is interesting to user.
445
848
            match (self.reachable, r) {
446
774
                (_, R::Reachable) => info!("We have found that guard {} is usable.", self),
447
66
                (R::Untried | R::Reachable, R::Unreachable) => match self.retry_at {
448
66
                    Some(retry_at) => warn!(
449
                        "Could not connect to guard {}. Retrying in {:?}.",
450
                        self,
451
                        humantime::format_duration(retry_at - Instant::now()),
452
                    ),
453
                    None => warn!(
454
                        "Could not connect to guard {}. Next retry time unknown.",
455
                        self
456
                    ),
457
                },
458
8
                (_, _) => {} // not interesting.
459
            }
460
            //
461
848
            trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed.");
462
848
            self.reachable = r;
463
9518
        }
464
10366
    }
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
578462
    pub(crate) fn exploratory_circ_pending(&self) -> bool {
476
578462
        self.exploratory_circ_pending
477
578462
    }
478

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

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

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

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

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

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

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

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

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

            
658
14308
        if let Some(unlisted_since) = self.unlisted_since {
659
6
            if expired_by(unlisted_since, params.lifetime_unlisted, now) {
660
2
                return true;
661
4
            }
662
14302
        }
663

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

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

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

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

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

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

            
748
    /// Record that an external operation has failed on this guard.
749
14
    pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
750
14
        match how {
751
14
            ExternalActivity::DirCache => {
752
14
                self.dir_status.note_failure(now);
753
14
            }
754
        }
755
14
    }
756

            
757
    /// Note that a circuit through this guard died in a way that we couldn't
758
    /// necessarily attribute to the guard.
759
28
    pub(crate) fn record_indeterminate_result(&mut self) {
760
28
        self.circ_history.n_indeterminate += 1;
761

            
762
28
        if let Some(ratio) = self.circ_history.indeterminate_ratio() {
763
            // TODO: These should not be hardwired, and they may be set
764
            // too high.
765
            /// If this fraction of circs are suspicious, we should disable
766
            /// the guard.
767
            const DISABLE_THRESHOLD: f64 = 0.7;
768
            /// If this fraction of circuits are suspicious, we should
769
            /// warn.
770
            const WARN_THRESHOLD: f64 = 0.5;
771

            
772
2
            if ratio > DISABLE_THRESHOLD {
773
2
                let reason = GuardDisabled::TooManyIndeterminateFailures {
774
2
                    history: self.circ_history.clone(),
775
2
                    failure_ratio: ratio,
776
2
                    threshold_ratio: DISABLE_THRESHOLD,
777
2
                };
778
2
                warn!(guard=?self.id, "Disabling guard: {:.1}% of circuits died under mysterious circumstances, exceeding threshold of {:.1}%", ratio*100.0, (DISABLE_THRESHOLD*100.0));
779
2
                self.disabled = Some(reason.into());
780
            } else if ratio > WARN_THRESHOLD && !self.suspicious_behavior_warned {
781
                warn!(guard=?self.id, "Questionable guard: {:.1}% of circuits died under mysterious circumstances.", ratio*100.0);
782
                self.suspicious_behavior_warned = true;
783
            }
784
26
        }
785
28
    }
786

            
787
    /// Return a [`FirstHop`](crate::FirstHop) object to represent this guard.
788
555940
    pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
789
555940
        crate::FirstHop {
790
555940
            sample: Some(selection),
791
555940
            inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
792
555940
                self,
793
555940
            )),
794
555940
        }
795
555940
    }
796

            
797
    /// Record that a given fallback has told us about clock skew.
798
    pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
799
        self.clock_skew = Some(observation);
800
    }
801

            
802
    /// Return the most recent clock skew observation for this guard, if we have
803
    /// made one.
804
    pub(crate) fn skew(&self) -> Option<&SkewObservation> {
805
        self.clock_skew.as_ref()
806
    }
807

            
808
    /// Testing only: Return true if this guard was ever contacted successfully.
809
    #[cfg(test)]
810
4
    pub(crate) fn confirmed(&self) -> bool {
811
4
        self.confirmed_at.is_some()
812
4
    }
813
}
814

            
815
impl tor_linkspec::HasAddrs for Guard {
816
555942
    fn addrs(&self) -> impl Iterator<Item = SocketAddr> {
817
555942
        self.orports.iter().copied()
818
555942
    }
819
}
820

            
821
impl tor_linkspec::HasRelayIds for Guard {
822
51881160
    fn identity(
823
51881160
        &self,
824
51881160
        key_type: tor_linkspec::RelayIdType,
825
51881160
    ) -> Option<tor_linkspec::RelayIdRef<'_>> {
826
51881160
        self.id.0.identity(key_type)
827
51881160
    }
828
}
829

            
830
impl tor_linkspec::HasChanMethod for Guard {
831
556008
    fn chan_method(&self) -> ChannelMethod {
832
556008
        match &self.pt_targets[..] {
833
            #[cfg(feature = "pt-client")]
834
            [first, ..] => ChannelMethod::Pluggable(first.clone()),
835
            #[cfg(not(feature = "pt-client"))]
836
            [_first, ..] => ChannelMethod::Direct(vec![]), // can't connect to this; no pt support.
837
556008
            [] => ChannelMethod::Direct(self.orports.clone()),
838
        }
839
556008
    }
840
}
841

            
842
impl tor_linkspec::ChanTarget for Guard {}
843

            
844
impl std::fmt::Display for Guard {
845
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846
        match self.sensitivity {
847
            DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f),
848
            #[cfg(feature = "bridge-client")]
849
            DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f),
850
        }
851
    }
852
}
853

            
854
/// A reason for permanently disabling a guard.
855
#[derive(Clone, Debug, Serialize, Deserialize)]
856
#[serde(tag = "type")]
857
enum GuardDisabled {
858
    /// Too many attempts to use this guard failed for indeterminate reasons.
859
    TooManyIndeterminateFailures {
860
        /// Observed count of status reports about this guard.
861
        history: CircHistory,
862
        /// Observed fraction of indeterminate status reports.
863
        failure_ratio: f64,
864
        /// Threshold that was exceeded.
865
        threshold_ratio: f64,
866
    },
867
}
868

            
869
/// Return a new RetryDelay tracker for a guard.
870
///
871
/// `is_primary should be true if the guard is primary.
872
66
fn retry_schedule(is_primary: bool) -> RetryDelay {
873
66
    let minimum = if is_primary {
874
48
        Duration::from_secs(30)
875
    } else {
876
18
        Duration::from_secs(150)
877
    };
878

            
879
66
    RetryDelay::from_duration(minimum)
880
66
}
881

            
882
/// The recent history of circuit activity on this guard.
883
///
884
/// We keep this information so that we can tell if too many circuits are
885
/// winding up in "indeterminate" status.
886
///
887
/// # What's this for?
888
///
889
/// Recall that an "indeterminate" circuit failure is one that might
890
/// or might not be the guard's fault.  For example, if the second hop
891
/// of the circuit fails, we can't tell whether to blame the guard,
892
/// the second hop, or the internet between them.
893
///
894
/// But we don't want to allow an unbounded number of indeterminate
895
/// failures: if we did, it would allow a malicious guard to simply
896
/// reject any circuit whose second hop it didn't like, and thereby
897
/// filter the client's paths down to a hostile subset.
898
///
899
/// So as a workaround, and to discourage this kind of behavior, we
900
/// track the fraction of indeterminate circuits, and disable any guard
901
/// where the fraction is too high.
902
//
903
// TODO: We may eventually want to make this structure persistent.  If we
904
// do, however, we'll need a way to make ancient history expire.  We might
905
// want that anyway, to make attacks harder.
906
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
907
pub(crate) struct CircHistory {
908
    /// How many times have we seen this guard succeed?
909
    n_successes: u32,
910
    /// How many times have we seen this guard fail?
911
    #[allow(dead_code)] // not actually used yet.
912
    n_failures: u32,
913
    /// How many times has this guard given us indeterminate results?
914
    n_indeterminate: u32,
915
}
916

            
917
impl CircHistory {
918
    /// If we have seen enough, return the fraction of circuits that have
919
    /// "died under mysterious circumstances".
920
32
    fn indeterminate_ratio(&self) -> Option<f64> {
921
        // TODO: This should probably not be hardwired
922

            
923
        /// Don't try to give a ratio unless we've seen this many observations.
924
        const MIN_OBSERVATIONS: u32 = 15;
925

            
926
32
        let total = self.n_successes + self.n_indeterminate;
927
32
        if total < MIN_OBSERVATIONS {
928
28
            return None;
929
4
        }
930

            
931
4
        Some(f64::from(self.n_indeterminate) / f64::from(total))
932
32
    }
933
}
934

            
935
#[cfg(test)]
936
mod test {
937
    // @@ begin test lint list maintained by maint/add_warning @@
938
    #![allow(clippy::bool_assert_comparison)]
939
    #![allow(clippy::clone_on_copy)]
940
    #![allow(clippy::dbg_macro)]
941
    #![allow(clippy::mixed_attributes_style)]
942
    #![allow(clippy::print_stderr)]
943
    #![allow(clippy::print_stdout)]
944
    #![allow(clippy::single_char_pattern)]
945
    #![allow(clippy::unwrap_used)]
946
    #![allow(clippy::unchecked_time_subtraction)]
947
    #![allow(clippy::useless_vec)]
948
    #![allow(clippy::needless_pass_by_value)]
949
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
950
    use super::*;
951
    use crate::ids::FirstHopId;
952
    use tor_linkspec::{HasRelayIds, RelayId};
953
    use tor_llcrypto::pk::ed25519::Ed25519Identity;
954

            
955
    #[test]
956
    fn crate_id() {
957
        let id = CrateId::this_crate().unwrap();
958
        assert_eq!(&id.crate_name, "tor-guardmgr");
959
        assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
960
    }
961

            
962
    fn basic_id() -> GuardId {
963
        GuardId::new([13; 32].into(), [37; 20].into())
964
    }
965
    fn basic_guard() -> Guard {
966
        let id = basic_id();
967
        let ports = vec!["127.0.0.7:7777".parse().unwrap()];
968
        let added = SystemTime::now();
969
        Guard::new(id, ports, None, added)
970
    }
971

            
972
    #[test]
973
    fn simple_accessors() {
974
        fn ed(id: [u8; 32]) -> RelayId {
975
            RelayId::Ed25519(id.into())
976
        }
977
        let id = basic_id();
978
        let g = basic_guard();
979

            
980
        assert_eq!(g.guard_id(), &id);
981
        assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
982
        assert_eq!(
983
            g.addrs().collect_vec(),
984
            &["127.0.0.7:7777".parse().unwrap()]
985
        );
986
        assert_eq!(g.reachable(), Reachable::Untried);
987
        assert_eq!(g.reachable(), Reachable::default());
988

            
989
        use crate::GuardUsageBuilder;
990
        let mut usage1 = GuardUsageBuilder::new();
991

            
992
        usage1
993
            .restrictions()
994
            .push(GuardRestriction::AvoidId(ed([22; 32])));
995
        let usage1 = usage1.build().unwrap();
996
        let mut usage2 = GuardUsageBuilder::new();
997
        usage2
998
            .restrictions()
999
            .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::now() - Duration::from_secs(10);
        let t2 = Instant::now() - Duration::from_secs(5);
        let t3 = Instant::now();
        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::now() - Duration::from_secs(10);
        let t2 = Instant::now();
        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::now() - Duration::from_secs(10);
        // has to be in the future, since the guard's "added_at" time is based on now.
        let now = SystemTime::now();
        let t2 = now + Duration::from_secs(300 * 86400);
        let t3 = Instant::now() + 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::now();
        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::now();
        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::now();
        // 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::now();
        // 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::now();
        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 = 20;
        assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
    }
    #[test]
    fn disable_on_failure() {
        let mut g = basic_guard();
        let params = GuardParams::default();
        let now = SystemTime::now();
        let _ignore = g.record_success(now, &params);
        for _ in 0..13 {
            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 - 0.933).abs() < 0.01);
                assert!((threshold_ratio - 0.7).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::now();
        let st = SystemTime::now();
        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));
    }
}