1use tor_basic_utils::retry::RetryDelay;
4
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use tracing::{info, trace, warn};
10use web_time_compat::{Duration, Instant, InstantExt, SystemTime};
11
12use crate::dirstatus::DirStatus;
13use crate::sample::Candidate;
14use crate::skew::SkewObservation;
15use crate::util::randomize_time;
16use crate::{ExternalActivity, GuardSetSelector, GuardUsageKind, sample};
17use crate::{GuardParams, GuardRestriction, GuardUsage, ids::GuardId};
18
19#[cfg(feature = "bridge-client")]
20use safelog::Redactable as _;
21
22use tor_basic_utils::onionperf_types::{OnionperfEvent, OnionperfGuardStatus};
23use tor_linkspec::{
24 ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
25};
26use tor_persist::{Futureproof, JsonValue};
27
28#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
30#[allow(clippy::enum_variant_names)]
31pub(crate) enum Reachable {
32 Reachable,
35 Unreachable,
38 #[default]
41 Untried,
42 Retriable,
45}
46
47#[derive(Clone, Debug, Serialize, Deserialize)]
54struct CrateId {
55 #[serde(rename = "crate")]
57 crate_name: String,
58 version: String,
60}
61
62impl CrateId {
63 fn this_crate() -> Option<Self> {
65 let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
66 let version = option_env!("CARGO_PKG_VERSION")?.to_string();
67 Some(CrateId {
68 crate_name,
69 version,
70 })
71 }
72}
73
74#[derive(Clone, Default, Debug)]
76pub(crate) enum DisplayRule {
77 #[default]
86 Sensitive,
87 #[cfg(feature = "bridge-client")]
92 Redacted,
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize)]
116pub(crate) struct Guard {
117 id: GuardId,
119
120 orports: Vec<SocketAddr>,
126
127 #[serde(default, skip_serializing_if = "Vec::is_empty")]
141 pt_targets: Vec<PtTarget>,
142
143 #[serde(with = "humantime_serde")]
145 added_at: SystemTime,
146
147 added_by: Option<CrateId>,
149
150 #[serde(default)]
153 disabled: Option<Futureproof<GuardDisabled>>,
154
155 #[serde(with = "humantime_serde")]
160 confirmed_at: Option<SystemTime>,
161
162 #[serde(with = "humantime_serde")]
168 unlisted_since: Option<SystemTime>,
169
170 #[serde(skip)]
173 dir_info_missing: bool,
174
175 #[serde(skip)]
177 last_tried_to_connect_at: Option<Instant>,
178
179 #[serde(skip)]
185 retry_at: Option<Instant>, #[serde(skip)]
190 retry_schedule: Option<RetryDelay>,
191
192 #[serde(skip)]
194 reachable: Reachable,
195
196 #[serde(skip)]
199 is_dir_cache: bool,
200
201 #[serde(skip, default = "guard_dirstatus")]
208 dir_status: DirStatus,
209
210 #[serde(skip)]
217 exploratory_circ_pending: bool,
218
219 #[serde(skip)]
223 circ_history: CircHistory,
224
225 #[serde(skip)]
227 suspicious_behavior_warned: bool,
228
229 #[serde(skip)]
231 clock_skew: Option<SkewObservation>,
232
233 #[serde(skip)]
235 sensitivity: DisplayRule,
236
237 #[serde(flatten)]
240 unknown_fields: HashMap<String, JsonValue>,
241}
242
243const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
246
247fn guard_dirstatus() -> DirStatus {
249 DirStatus::new(GUARD_DIR_RETRY_FLOOR)
250}
251
252#[derive(Debug, Clone, Copy, Eq, PartialEq)]
255pub(crate) enum NewlyConfirmed {
256 Yes,
258 No,
260}
261
262impl Guard {
263 pub(crate) fn from_candidate(
265 candidate: Candidate,
266 now: SystemTime,
267 params: &GuardParams,
268 ) -> Self {
269 let Candidate {
270 is_dir_cache,
271 full_dir_info,
272 owned_target,
273 ..
274 } = candidate;
275
276 Guard {
277 is_dir_cache,
278 dir_info_missing: !full_dir_info,
279 ..Self::from_chan_target(&owned_target, now, params)
280 }
281 }
282
283 fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
288 where
289 T: ChanTarget,
290 {
291 let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
292
293 let pt_target = match relay.chan_method() {
294 #[cfg(feature = "pt-client")]
295 ChannelMethod::Pluggable(pt) => Some(pt),
296 _ => None,
297 };
298
299 Self::new(
300 GuardId::from_relay_ids(relay),
301 relay.addrs().collect_vec(),
302 pt_target,
303 added_at,
304 )
305 }
306
307 fn new(
309 id: GuardId,
310 orports: Vec<SocketAddr>,
311 pt_target: Option<PtTarget>,
312 added_at: SystemTime,
313 ) -> Self {
314 Guard {
315 id,
316 orports,
317 pt_targets: pt_target.into_iter().collect(),
318 added_at,
319 added_by: CrateId::this_crate(),
320 disabled: None,
321 confirmed_at: None,
322 unlisted_since: None,
323 dir_info_missing: false,
324 last_tried_to_connect_at: None,
325 reachable: Reachable::Untried,
326 retry_at: None,
327 dir_status: guard_dirstatus(),
328 retry_schedule: None,
329 is_dir_cache: true,
330 exploratory_circ_pending: false,
331 circ_history: CircHistory::default(),
332 suspicious_behavior_warned: false,
333 clock_skew: None,
334 unknown_fields: Default::default(),
335 sensitivity: DisplayRule::Sensitive,
336 }
337 }
338
339 pub(crate) fn guard_id(&self) -> &GuardId {
341 &self.id
342 }
343
344 pub(crate) fn reachable(&self) -> Reachable {
346 self.reachable
347 }
348
349 pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
354 match &usage.kind {
355 GuardUsageKind::Data => self.retry_at,
356 GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
357 .iter()
358 .flatten()
359 .max()
360 .copied(),
361 }
362 }
363
364 pub(crate) fn usable(&self) -> bool {
368 self.unlisted_since.is_none() && self.disabled.is_none()
369 }
370
371 pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
374 if let Some(retry_at) = self.retry_at {
375 if retry_at > now {
376 return false;
377 }
378 }
379
380 match usage.kind {
381 GuardUsageKind::Data => true,
382 GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
383 }
384 }
385
386 pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
400 assert!(self.same_relay_ids(&other));
406
407 Guard {
408 id: self.id,
410 pt_targets: self.pt_targets,
411 orports: self.orports,
412 added_at: self.added_at,
413 added_by: self.added_by,
414 disabled: self.disabled,
415 confirmed_at: self.confirmed_at,
416 unlisted_since: self.unlisted_since,
417 unknown_fields: self.unknown_fields,
418
419 last_tried_to_connect_at: other.last_tried_to_connect_at,
421 retry_at: other.retry_at,
422 retry_schedule: other.retry_schedule,
423 reachable: other.reachable,
424 is_dir_cache: other.is_dir_cache,
425 exploratory_circ_pending: other.exploratory_circ_pending,
426 dir_info_missing: other.dir_info_missing,
427 circ_history: other.circ_history,
428 suspicious_behavior_warned: other.suspicious_behavior_warned,
429 dir_status: other.dir_status,
430 clock_skew: other.clock_skew,
431 sensitivity: other.sensitivity,
432 }
437 }
438
439 fn set_reachable(&mut self, r: Reachable) {
441 use Reachable as R;
442
443 if self.reachable != r {
444 match (self.reachable, r) {
446 (_, R::Reachable) => info!("We have found that guard {} is usable.", self),
447 (R::Untried | R::Reachable, R::Unreachable) => match self.retry_at {
448 Some(retry_at) => warn!(
449 "Could not connect to guard {}. Retrying in {}.",
450 self,
451 humantime::format_duration(retry_at - Instant::get()),
452 ),
453 None => warn!(
454 "Could not connect to guard {}. Next retry time unknown.",
455 self
456 ),
457 },
458 (_, _) => {} }
460 trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed.");
462 self.reachable = r;
463 }
464 }
465
466 pub(crate) fn exploratory_circ_pending(&self) -> bool {
476 self.exploratory_circ_pending
477 }
478
479 pub(crate) fn note_exploratory_circ(&mut self, pending: bool) {
482 self.exploratory_circ_pending = pending;
483 }
484
485 pub(crate) fn consider_retry(&mut self, now: Instant) {
492 if let Some(retry_at) = self.retry_at {
493 debug_assert!(self.reachable == Reachable::Unreachable);
494 if retry_at <= now {
495 self.mark_retriable();
496 }
497 }
498 }
499
500 pub(crate) fn mark_retriable(&mut self) {
503 if self.reachable == Reachable::Unreachable {
504 self.set_reachable(Reachable::Retriable);
505 self.retry_at = None;
506 self.retry_schedule = None;
507 }
508 }
509
510 fn obeys_restrictions(&self, restrictions: &[GuardRestriction]) -> bool {
512 restrictions.iter().all(|r| self.obeys_restriction(r))
513 }
514
515 fn obeys_restriction(&self, r: &GuardRestriction) -> bool {
517 match r {
518 GuardRestriction::AvoidId(avoid_id) => !self.id.0.has_identity(avoid_id.as_ref()),
519 GuardRestriction::AvoidAllIds(avoid_ids) => {
520 self.id.0.identities().all(|id| !avoid_ids.contains(id))
521 }
522 }
523 }
524
525 pub(crate) fn conforms_to_usage(&self, usage: &GuardUsage) -> bool {
527 match usage.kind {
528 GuardUsageKind::OneHopDirectory => {
529 if !self.is_dir_cache {
530 return false;
531 }
532 }
533 GuardUsageKind::Data => {
534 if self.dir_info_missing {
537 return false;
538 }
539 }
540 }
541 self.obeys_restrictions(&usage.restrictions[..])
542 }
543
544 pub(crate) fn listed_in<U: sample::Universe>(&self, universe: &U) -> Option<bool> {
551 universe.contains(self)
552 }
553
554 pub(crate) fn update_from_universe<U: sample::Universe>(&mut self, universe: &U) {
566 use sample::CandidateStatus::*;
569 let listed_as_guard = match universe.status(self) {
570 Present(Candidate {
571 listed_as_guard,
572 is_dir_cache,
573 full_dir_info,
574 owned_target,
575 sensitivity,
576 }) => {
577 self.orports = owned_target.addrs().collect_vec();
579 self.pt_targets = match owned_target.chan_method() {
581 #[cfg(feature = "pt-client")]
582 ChannelMethod::Pluggable(pt) => vec![pt],
583 _ => Vec::new(),
584 };
585 self.is_dir_cache = is_dir_cache;
587 assert!(owned_target.has_all_relay_ids_from(self));
589 self.id = GuardId(RelayIds::from_relay_ids(&owned_target));
590 self.dir_info_missing = !full_dir_info;
591 self.sensitivity = sensitivity;
592
593 listed_as_guard
594 }
595 Absent => false, Uncertain => {
597 self.dir_info_missing = true;
599 return;
600 }
601 };
602
603 if listed_as_guard {
604 self.mark_listed();
606 } else {
607 self.mark_unlisted(universe.timestamp());
609 }
610 }
611
612 fn mark_listed(&mut self) {
614 if self.unlisted_since.is_some() {
615 trace!(guard_id = ?self.id, "Guard is now listed again.");
616 self.unlisted_since = None;
617 }
618 }
619
620 fn mark_unlisted(&mut self, now: SystemTime) {
623 if self.unlisted_since.is_none() {
624 trace!(guard_id = ?self.id, "Guard is now unlisted.");
625 self.unlisted_since = Some(now);
626 }
627 }
628
629 pub(crate) fn is_expired(&self, params: &GuardParams, now: SystemTime) -> bool {
637 fn expired_by(t1: SystemTime, d: Duration, t2: SystemTime) -> bool {
639 if let Ok(elapsed) = t2.duration_since(t1) {
640 elapsed > d
641 } else {
642 false
643 }
644 }
645 if self.disabled.is_some() {
646 return false;
649 }
650 if let Some(confirmed_at) = self.confirmed_at {
651 if expired_by(confirmed_at, params.lifetime_confirmed, now) {
652 return true;
653 }
654 } else if expired_by(self.added_at, params.lifetime_unconfirmed, now) {
655 return true;
656 }
657
658 if let Some(unlisted_since) = self.unlisted_since {
659 if expired_by(unlisted_since, params.lifetime_unlisted, now) {
660 return true;
661 }
662 }
663
664 false
665 }
666
667 pub(crate) fn record_failure(&mut self, now: Instant, is_primary: bool) {
671 let mut rng = rand::rng();
672 let retry_interval = self
673 .retry_schedule
674 .get_or_insert_with(|| retry_schedule(is_primary))
675 .next_delay(&mut rng);
676
677 self.retry_at = Some(now + retry_interval);
679
680 self.set_reachable(Reachable::Unreachable);
681 self.exploratory_circ_pending = false;
682
683 self.circ_history.n_failures += 1;
684 }
685
686 pub(crate) fn record_attempt(&mut self, connect_attempt: Instant) {
691 self.last_tried_to_connect_at = self
692 .last_tried_to_connect_at
693 .map(|last| last.max(connect_attempt))
694 .or(Some(connect_attempt));
695 }
696
697 pub(crate) fn exploratory_attempt_after(&self, when: Instant) -> bool {
702 self.exploratory_circ_pending
703 && self.last_tried_to_connect_at.map(|t| t > when) == Some(true)
704 }
705
706 #[must_use = "You need to check whether a succeeding guard is confirmed."]
714 pub(crate) fn record_success(
715 &mut self,
716 now: SystemTime,
717 params: &GuardParams,
718 ) -> NewlyConfirmed {
719 self.retry_at = None;
720 self.retry_schedule = None;
721 self.set_reachable(Reachable::Reachable);
722 self.exploratory_circ_pending = false;
723 self.circ_history.n_successes += 1;
724
725 if self.confirmed_at.is_none() {
726 self.confirmed_at = Some(
727 randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10)
728 .max(self.added_at),
729 );
730 trace!(guard_id = ?self.id, "Newly confirmed");
733 trace!(onionperf = true, event = ?OnionperfEvent::Guard(OnionperfGuardStatus::Up));
734 NewlyConfirmed::Yes
735 } else {
736 NewlyConfirmed::No
737 }
738 }
739
740 pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
742 match how {
743 ExternalActivity::DirCache => {
744 self.dir_status.note_success();
745 }
746 }
747 }
748
749 pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
751 match how {
752 ExternalActivity::DirCache => {
753 self.dir_status.note_failure(now);
754 }
755 }
756 }
757
758 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 const DISABLE_THRESHOLD: f64 = 2.0;
778
779 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 pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
809 crate::FirstHop {
810 sample: Some(selection),
811 inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
812 self,
813 )),
814 }
815 }
816
817 pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
819 self.clock_skew = Some(observation);
820 }
821
822 pub(crate) fn skew(&self) -> Option<&SkewObservation> {
825 self.clock_skew.as_ref()
826 }
827
828 #[cfg(test)]
830 pub(crate) fn confirmed(&self) -> bool {
831 self.confirmed_at.is_some()
832 }
833}
834
835impl tor_linkspec::HasAddrs for Guard {
836 fn addrs(&self) -> impl Iterator<Item = SocketAddr> {
837 self.orports.iter().copied()
838 }
839}
840
841impl tor_linkspec::HasRelayIds for Guard {
842 fn identity(
843 &self,
844 key_type: tor_linkspec::RelayIdType,
845 ) -> Option<tor_linkspec::RelayIdRef<'_>> {
846 self.id.0.identity(key_type)
847 }
848}
849
850impl tor_linkspec::HasChanMethod for Guard {
851 fn chan_method(&self) -> ChannelMethod {
852 match &self.pt_targets[..] {
853 #[cfg(feature = "pt-client")]
854 [first, ..] => ChannelMethod::Pluggable(first.clone()),
855 #[cfg(not(feature = "pt-client"))]
856 [_first, ..] => ChannelMethod::Direct(vec![]), [] => ChannelMethod::Direct(self.orports.clone()),
858 }
859 }
860}
861
862impl tor_linkspec::ChanTarget for Guard {}
863
864impl std::fmt::Display for Guard {
865 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
866 match self.sensitivity {
867 DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f),
868 #[cfg(feature = "bridge-client")]
869 DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f),
870 }
871 }
872}
873
874#[derive(Clone, Debug, Serialize, Deserialize)]
876#[serde(tag = "type")]
877enum GuardDisabled {
878 TooManyIndeterminateFailures {
880 history: CircHistory,
882 failure_ratio: f64,
884 threshold_ratio: f64,
886 },
887}
888
889fn retry_schedule(is_primary: bool) -> RetryDelay {
893 let minimum = if is_primary {
894 Duration::from_secs(30)
895 } else {
896 Duration::from_secs(150)
897 };
898
899 RetryDelay::from_duration(minimum)
900}
901
902#[derive(Debug, Clone, Default, Serialize, Deserialize)]
927pub(crate) struct CircHistory {
928 n_successes: u32,
930 #[allow(dead_code)] n_failures: u32,
933 n_indeterminate: u32,
935}
936
937impl CircHistory {
938 fn indeterminate_ratio(&self) -> Option<f64> {
941 const MIN_OBSERVATIONS: u32 = 100;
945
946 let total = self.n_successes + self.n_indeterminate;
947 if total < MIN_OBSERVATIONS {
948 return None;
949 }
950
951 Some(f64::from(self.n_indeterminate) / f64::from(total))
952 }
953}
954
955#[cfg(test)]
956mod test {
957 #![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)] use super::*;
972 use crate::ids::FirstHopId;
973 use tor_linkspec::{HasRelayIds, RelayId};
974 use tor_llcrypto::pk::ed25519::Ed25519Identity;
975 use web_time_compat::SystemTimeExt;
976
977 #[test]
978 fn crate_id() {
979 let id = CrateId::this_crate().unwrap();
980 assert_eq!(&id.crate_name, "tor-guardmgr");
981 assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
982 }
983
984 fn basic_id() -> GuardId {
985 GuardId::new([13; 32].into(), [37; 20].into())
986 }
987 fn basic_guard() -> Guard {
988 let id = basic_id();
989 let ports = vec!["127.0.0.7:7777".parse().unwrap()];
990 let added = SystemTime::get();
991 Guard::new(id, ports, None, added)
992 }
993
994 #[test]
995 fn simple_accessors() {
996 fn ed(id: [u8; 32]) -> RelayId {
997 RelayId::Ed25519(id.into())
998 }
999 let id = basic_id();
1000 let g = basic_guard();
1001
1002 assert_eq!(g.guard_id(), &id);
1003 assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
1004 assert_eq!(
1005 g.addrs().collect_vec(),
1006 &["127.0.0.7:7777".parse().unwrap()]
1007 );
1008 assert_eq!(g.reachable(), Reachable::Untried);
1009 assert_eq!(g.reachable(), Reachable::default());
1010
1011 use crate::GuardUsageBuilder;
1012 let mut usage1 = GuardUsageBuilder::new();
1013
1014 usage1
1015 .restrictions()
1016 .push(GuardRestriction::AvoidId(ed([22; 32])));
1017 let usage1 = usage1.build().unwrap();
1018 let mut usage2 = GuardUsageBuilder::new();
1019 usage2
1020 .restrictions()
1021 .push(GuardRestriction::AvoidId(ed([13; 32])));
1022 let usage2 = usage2.build().unwrap();
1023 let usage3 = GuardUsage::default();
1024 let mut usage4 = GuardUsageBuilder::new();
1025 usage4
1026 .restrictions()
1027 .push(GuardRestriction::AvoidId(ed([22; 32])));
1028 usage4
1029 .restrictions()
1030 .push(GuardRestriction::AvoidId(ed([13; 32])));
1031 let usage4 = usage4.build().unwrap();
1032 let mut usage5 = GuardUsageBuilder::new();
1033 usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1034 vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1035 ));
1036 let usage5 = usage5.build().unwrap();
1037 let mut usage6 = GuardUsageBuilder::new();
1038 usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1039 vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1040 ));
1041 let usage6 = usage6.build().unwrap();
1042
1043 assert!(g.conforms_to_usage(&usage1));
1044 assert!(!g.conforms_to_usage(&usage2));
1045 assert!(g.conforms_to_usage(&usage3));
1046 assert!(!g.conforms_to_usage(&usage4));
1047 assert!(!g.conforms_to_usage(&usage5));
1048 assert!(g.conforms_to_usage(&usage6));
1049 }
1050
1051 #[allow(clippy::redundant_clone)]
1052 #[test]
1053 fn trickier_usages() {
1054 let g = basic_guard();
1055 use crate::{GuardUsageBuilder, GuardUsageKind};
1056 let data_usage = GuardUsageBuilder::new()
1057 .kind(GuardUsageKind::Data)
1058 .build()
1059 .unwrap();
1060 let dir_usage = GuardUsageBuilder::new()
1061 .kind(GuardUsageKind::OneHopDirectory)
1062 .build()
1063 .unwrap();
1064 assert!(g.conforms_to_usage(&data_usage));
1065 assert!(g.conforms_to_usage(&dir_usage));
1066
1067 let mut g2 = g.clone();
1068 g2.dir_info_missing = true;
1069 assert!(!g2.conforms_to_usage(&data_usage));
1070 assert!(g2.conforms_to_usage(&dir_usage));
1071
1072 let mut g3 = g.clone();
1073 g3.is_dir_cache = false;
1074 assert!(g3.conforms_to_usage(&data_usage));
1075 assert!(!g3.conforms_to_usage(&dir_usage));
1076 }
1077
1078 #[test]
1079 fn record_attempt() {
1080 let t1 = Instant::get() - Duration::from_secs(10);
1081 let t2 = Instant::get() - Duration::from_secs(5);
1082 let t3 = Instant::get();
1083
1084 let mut g = basic_guard();
1085
1086 assert!(g.last_tried_to_connect_at.is_none());
1087 g.record_attempt(t1);
1088 assert_eq!(g.last_tried_to_connect_at, Some(t1));
1089 g.record_attempt(t3);
1090 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1091 g.record_attempt(t2);
1092 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1093 }
1094
1095 #[test]
1096 fn record_failure() {
1097 let t1 = Instant::get() - Duration::from_secs(10);
1098 let t2 = Instant::get();
1099
1100 let mut g = basic_guard();
1101 g.record_failure(t1, true);
1102 assert!(g.retry_schedule.is_some());
1103 assert_eq!(g.reachable(), Reachable::Unreachable);
1104 let retry1 = g.retry_at.unwrap();
1105 assert_eq!(retry1, t1 + Duration::from_secs(30));
1106
1107 g.record_failure(t2, true);
1108 let retry2 = g.retry_at.unwrap();
1109 assert!(retry2 >= t2 + Duration::from_secs(30));
1110 assert!(retry2 <= t2 + Duration::from_secs(200));
1111 }
1112
1113 #[test]
1114 fn record_success() {
1115 let t1 = Instant::get() - Duration::from_secs(10);
1116 let now = SystemTime::get();
1118 let t2 = now + Duration::from_secs(300 * 86400);
1119 let t3 = Instant::get() + Duration::from_secs(310 * 86400);
1120 let t4 = now + Duration::from_secs(320 * 86400);
1121
1122 let mut g = basic_guard();
1123 g.record_failure(t1, true);
1124 assert_eq!(g.reachable(), Reachable::Unreachable);
1125
1126 let conf = g.record_success(t2, &GuardParams::default());
1127 assert_eq!(g.reachable(), Reachable::Reachable);
1128 assert_eq!(conf, NewlyConfirmed::Yes);
1129 assert!(g.retry_at.is_none());
1130 assert!(g.confirmed_at.unwrap() <= t2);
1131 assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1132 let confirmed_at_orig = g.confirmed_at;
1133
1134 g.record_failure(t3, true);
1135 assert_eq!(g.reachable(), Reachable::Unreachable);
1136
1137 let conf = g.record_success(t4, &GuardParams::default());
1138 assert_eq!(conf, NewlyConfirmed::No);
1139 assert_eq!(g.reachable(), Reachable::Reachable);
1140 assert!(g.retry_at.is_none());
1141 assert_eq!(g.confirmed_at, confirmed_at_orig);
1142 }
1143
1144 #[test]
1145 fn retry() {
1146 let t1 = Instant::get();
1147 let mut g = basic_guard();
1148
1149 g.record_failure(t1, true);
1150 assert!(g.retry_at.is_some());
1151 assert_eq!(g.reachable(), Reachable::Unreachable);
1152
1153 g.consider_retry(t1);
1155 assert!(g.retry_at.is_some());
1156 assert_eq!(g.reachable(), Reachable::Unreachable);
1157
1158 g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1160 assert!(g.retry_at.is_some());
1161 assert_eq!(g.reachable(), Reachable::Unreachable);
1162
1163 g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1165 assert!(g.retry_at.is_none());
1166 assert_eq!(g.reachable(), Reachable::Retriable);
1167 }
1168
1169 #[test]
1170 fn expiration() {
1171 const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1172 let params = GuardParams::default();
1173 let now = SystemTime::get();
1174
1175 let g = basic_guard();
1176 assert!(!g.is_expired(¶ms, now));
1177 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1178 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1179 assert!(!g.is_expired(¶ms, now + 70 * DAY));
1180 assert!(g.is_expired(¶ms, now + 200 * DAY)); let mut g = basic_guard();
1183 let _ = g.record_success(now, ¶ms);
1184 assert!(!g.is_expired(¶ms, now));
1185 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1186 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1187 assert!(g.is_expired(¶ms, now + 70 * DAY)); let mut g = basic_guard();
1190 g.mark_unlisted(now);
1191 assert!(!g.is_expired(¶ms, now));
1192 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1193 assert!(g.is_expired(¶ms, now + 25 * DAY)); }
1195
1196 #[test]
1197 fn netdir_integration() {
1198 use tor_netdir::testnet;
1199 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1200 let params = GuardParams::default();
1201 let now = SystemTime::get();
1202
1203 let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1205 let guard22 = Guard::from_chan_target(&relay22, now, ¶ms);
1206 assert!(guard22.same_relay_ids(&relay22));
1207 assert!(Some(guard22.added_at) <= Some(now));
1208
1209 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1211 let r = id.get_relay(&netdir).unwrap();
1212 assert!(r.same_relay_ids(&relay22));
1213
1214 let guard255 = Guard::new(
1216 GuardId::new([255; 32].into(), [255; 20].into()),
1217 vec![],
1218 None,
1219 now,
1220 );
1221 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1222 assert!(id.get_relay(&netdir).is_none());
1223 }
1224
1225 #[test]
1226 fn update_from_netdir() {
1227 use tor_netdir::testnet;
1228 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1229 let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1231 if idx == 22 {
1232 node.omit_rs = true;
1233 }
1234 })
1235 .unwrap()
1236 .unwrap_if_sufficient()
1237 .unwrap();
1238 let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1240 if idx == 22 {
1241 node.omit_rs = true;
1242 } else if idx == 23 {
1243 node.omit_md = true;
1244 }
1245 })
1246 .unwrap()
1247 .unwrap_if_sufficient()
1248 .unwrap();
1249
1250 let now = SystemTime::get();
1252
1253 let mut guard255 = Guard::new(
1255 GuardId::new([255; 32].into(), [255; 20].into()),
1256 vec!["8.8.8.8:53".parse().unwrap()],
1257 None,
1258 now,
1259 );
1260 assert_eq!(guard255.unlisted_since, None);
1261 assert_eq!(guard255.listed_in(&netdir), Some(false));
1262 guard255.update_from_universe(&netdir);
1263 assert_eq!(
1264 guard255.unlisted_since,
1265 Some(netdir.lifetime().valid_after())
1266 );
1267 assert!(!guard255.orports.is_empty());
1268
1269 let mut guard22 = Guard::new(
1271 GuardId::new([22; 32].into(), [22; 20].into()),
1272 vec![],
1273 None,
1274 now,
1275 );
1276 let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1277 let relay22 = id22.get_relay(&netdir).unwrap();
1278 assert_eq!(guard22.listed_in(&netdir), Some(true));
1279 guard22.update_from_universe(&netdir);
1280 assert_eq!(guard22.unlisted_since, None); assert_eq!(guard22.orports, relay22.addrs().collect_vec()); assert_eq!(guard22.listed_in(&netdir2), Some(false));
1283 guard22.update_from_universe(&netdir2);
1284 assert_eq!(
1285 guard22.unlisted_since,
1286 Some(netdir2.lifetime().valid_after())
1287 );
1288 assert_eq!(guard22.orports, relay22.addrs().collect_vec()); assert!(!guard22.dir_info_missing);
1290
1291 let mut guard23 = Guard::new(
1293 GuardId::new([23; 32].into(), [23; 20].into()),
1294 vec![],
1295 None,
1296 now,
1297 );
1298 assert_eq!(guard23.listed_in(&netdir2), Some(true));
1299 assert_eq!(guard23.listed_in(&netdir3), None);
1300 guard23.update_from_universe(&netdir3);
1301 assert!(guard23.dir_info_missing);
1302 assert!(guard23.is_dir_cache);
1303 }
1304
1305 #[test]
1306 fn pending() {
1307 let mut g = basic_guard();
1308 let t1 = Instant::get();
1309 let t2 = t1 + Duration::from_secs(100);
1310 let t3 = t1 + Duration::from_secs(200);
1311
1312 assert!(!g.exploratory_attempt_after(t1));
1313 assert!(!g.exploratory_circ_pending());
1314
1315 g.note_exploratory_circ(true);
1316 g.record_attempt(t2);
1317 assert!(g.exploratory_circ_pending());
1318 assert!(g.exploratory_attempt_after(t1));
1319 assert!(!g.exploratory_attempt_after(t3));
1320
1321 g.note_exploratory_circ(false);
1322 assert!(!g.exploratory_circ_pending());
1323 assert!(!g.exploratory_attempt_after(t1));
1324 assert!(!g.exploratory_attempt_after(t3));
1325 }
1326
1327 #[test]
1328 fn circ_history() {
1329 let mut h = CircHistory {
1330 n_successes: 3,
1331 n_failures: 4,
1332 n_indeterminate: 3,
1333 };
1334 assert!(h.indeterminate_ratio().is_none());
1335
1336 h.n_successes = 100;
1337 assert!((h.indeterminate_ratio().unwrap() - 3.0 / 103.0).abs() < 0.0001);
1338 }
1339
1340 #[test]
1343 #[ignore]
1344 fn disable_on_failure() {
1345 let mut g = basic_guard();
1346
1347 for _ in 0..99 {
1353 g.record_indeterminate_result();
1354 }
1355 assert!(g.disabled.is_none());
1357
1358 g.record_indeterminate_result();
1360 assert!(g.disabled.is_some());
1361
1362 #[allow(unreachable_patterns)]
1363 match g.disabled.unwrap().into_option().unwrap() {
1364 GuardDisabled::TooManyIndeterminateFailures {
1365 history: _,
1366 failure_ratio,
1367 threshold_ratio,
1368 } => {
1369 assert!((failure_ratio - 1.0).abs() < 0.01);
1370 assert!((threshold_ratio - 1.0).abs() < 0.01);
1371 }
1372 other => {
1373 panic!("Wrong variant: {:?}", other);
1374 }
1375 }
1376 }
1377
1378 #[test]
1379 fn mark_retriable() {
1380 let mut g = basic_guard();
1381 use super::Reachable::*;
1382
1383 assert_eq!(g.reachable(), Untried);
1384
1385 for (pre, post) in &[
1386 (Untried, Untried),
1387 (Unreachable, Retriable),
1388 (Reachable, Reachable),
1389 ] {
1390 g.reachable = *pre;
1391 g.mark_retriable();
1392 assert_eq!(g.reachable(), *post);
1393 }
1394 }
1395
1396 #[test]
1397 fn dir_status() {
1398 use crate::GuardUsageBuilder;
1402 let mut g = basic_guard();
1403 let inst = Instant::get();
1404 let st = SystemTime::get();
1405 let sec = Duration::from_secs(1);
1406 let params = GuardParams::default();
1407 let dir_usage = GuardUsageBuilder::new()
1408 .kind(GuardUsageKind::OneHopDirectory)
1409 .build()
1410 .unwrap();
1411 let data_usage = GuardUsage::default();
1412
1413 let _ = g.record_success(st, ¶ms);
1415 assert_eq!(g.next_retry(&dir_usage), None);
1416 assert!(g.ready_for_usage(&dir_usage, inst));
1417 assert_eq!(g.next_retry(&data_usage), None);
1418 assert!(g.ready_for_usage(&data_usage, inst));
1419
1420 g.record_external_failure(ExternalActivity::DirCache, inst);
1422 assert_eq!(g.next_retry(&data_usage), None);
1423 assert!(g.ready_for_usage(&data_usage, inst));
1424 let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1425 assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1426 assert!(!g.ready_for_usage(&dir_usage, inst));
1427 assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1428
1429 let _ = g.record_success(st, ¶ms);
1432 assert!(g.ready_for_usage(&data_usage, inst));
1433 assert!(!g.ready_for_usage(&dir_usage, inst));
1434
1435 g.record_failure(inst + sec * 10, true);
1437 let next_circ_retry = g.next_retry(&data_usage).unwrap();
1438 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1439 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1440 assert_eq!(
1441 g.next_retry(&dir_usage).unwrap(),
1442 std::cmp::max(next_circ_retry, next_dir_retry)
1443 );
1444
1445 g.record_external_success(ExternalActivity::DirCache);
1448 assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1449 assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1450 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1451 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1452 }
1453}