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_linkspec::{
23 ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
24};
25use tor_persist::{Futureproof, JsonValue};
26
27#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
29#[allow(clippy::enum_variant_names)]
30pub(crate) enum Reachable {
31 Reachable,
34 Unreachable,
37 #[default]
40 Untried,
41 Retriable,
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize)]
53struct CrateId {
54 #[serde(rename = "crate")]
56 crate_name: String,
57 version: String,
59}
60
61impl CrateId {
62 fn this_crate() -> Option<Self> {
64 let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
65 let version = option_env!("CARGO_PKG_VERSION")?.to_string();
66 Some(CrateId {
67 crate_name,
68 version,
69 })
70 }
71}
72
73#[derive(Clone, Default, Debug)]
75pub(crate) enum DisplayRule {
76 #[default]
85 Sensitive,
86 #[cfg(feature = "bridge-client")]
91 Redacted,
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
115pub(crate) struct Guard {
116 id: GuardId,
118
119 orports: Vec<SocketAddr>,
125
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
140 pt_targets: Vec<PtTarget>,
141
142 #[serde(with = "humantime_serde")]
144 added_at: SystemTime,
145
146 added_by: Option<CrateId>,
148
149 #[serde(default)]
152 disabled: Option<Futureproof<GuardDisabled>>,
153
154 #[serde(with = "humantime_serde")]
159 confirmed_at: Option<SystemTime>,
160
161 #[serde(with = "humantime_serde")]
167 unlisted_since: Option<SystemTime>,
168
169 #[serde(skip)]
172 dir_info_missing: bool,
173
174 #[serde(skip)]
176 last_tried_to_connect_at: Option<Instant>,
177
178 #[serde(skip)]
184 retry_at: Option<Instant>, #[serde(skip)]
189 retry_schedule: Option<RetryDelay>,
190
191 #[serde(skip)]
193 reachable: Reachable,
194
195 #[serde(skip)]
198 is_dir_cache: bool,
199
200 #[serde(skip, default = "guard_dirstatus")]
207 dir_status: DirStatus,
208
209 #[serde(skip)]
216 exploratory_circ_pending: bool,
217
218 #[serde(skip)]
222 circ_history: CircHistory,
223
224 #[serde(skip)]
226 suspicious_behavior_warned: bool,
227
228 #[serde(skip)]
230 clock_skew: Option<SkewObservation>,
231
232 #[serde(skip)]
234 sensitivity: DisplayRule,
235
236 #[serde(flatten)]
239 unknown_fields: HashMap<String, JsonValue>,
240}
241
242const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
245
246fn guard_dirstatus() -> DirStatus {
248 DirStatus::new(GUARD_DIR_RETRY_FLOOR)
249}
250
251#[derive(Debug, Clone, Copy, Eq, PartialEq)]
254pub(crate) enum NewlyConfirmed {
255 Yes,
257 No,
259}
260
261impl Guard {
262 pub(crate) fn from_candidate(
264 candidate: Candidate,
265 now: SystemTime,
266 params: &GuardParams,
267 ) -> Self {
268 let Candidate {
269 is_dir_cache,
270 full_dir_info,
271 owned_target,
272 ..
273 } = candidate;
274
275 Guard {
276 is_dir_cache,
277 dir_info_missing: !full_dir_info,
278 ..Self::from_chan_target(&owned_target, now, params)
279 }
280 }
281
282 fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
287 where
288 T: ChanTarget,
289 {
290 let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
291
292 let pt_target = match relay.chan_method() {
293 #[cfg(feature = "pt-client")]
294 ChannelMethod::Pluggable(pt) => Some(pt),
295 _ => None,
296 };
297
298 Self::new(
299 GuardId::from_relay_ids(relay),
300 relay.addrs().collect_vec(),
301 pt_target,
302 added_at,
303 )
304 }
305
306 fn new(
308 id: GuardId,
309 orports: Vec<SocketAddr>,
310 pt_target: Option<PtTarget>,
311 added_at: SystemTime,
312 ) -> Self {
313 Guard {
314 id,
315 orports,
316 pt_targets: pt_target.into_iter().collect(),
317 added_at,
318 added_by: CrateId::this_crate(),
319 disabled: None,
320 confirmed_at: None,
321 unlisted_since: None,
322 dir_info_missing: false,
323 last_tried_to_connect_at: None,
324 reachable: Reachable::Untried,
325 retry_at: None,
326 dir_status: guard_dirstatus(),
327 retry_schedule: None,
328 is_dir_cache: true,
329 exploratory_circ_pending: false,
330 circ_history: CircHistory::default(),
331 suspicious_behavior_warned: false,
332 clock_skew: None,
333 unknown_fields: Default::default(),
334 sensitivity: DisplayRule::Sensitive,
335 }
336 }
337
338 pub(crate) fn guard_id(&self) -> &GuardId {
340 &self.id
341 }
342
343 pub(crate) fn reachable(&self) -> Reachable {
345 self.reachable
346 }
347
348 pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
353 match &usage.kind {
354 GuardUsageKind::Data => self.retry_at,
355 GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
356 .iter()
357 .flatten()
358 .max()
359 .copied(),
360 }
361 }
362
363 pub(crate) fn usable(&self) -> bool {
367 self.unlisted_since.is_none() && self.disabled.is_none()
368 }
369
370 pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
373 if let Some(retry_at) = self.retry_at {
374 if retry_at > now {
375 return false;
376 }
377 }
378
379 match usage.kind {
380 GuardUsageKind::Data => true,
381 GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
382 }
383 }
384
385 pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
399 assert!(self.same_relay_ids(&other));
405
406 Guard {
407 id: self.id,
409 pt_targets: self.pt_targets,
410 orports: self.orports,
411 added_at: self.added_at,
412 added_by: self.added_by,
413 disabled: self.disabled,
414 confirmed_at: self.confirmed_at,
415 unlisted_since: self.unlisted_since,
416 unknown_fields: self.unknown_fields,
417
418 last_tried_to_connect_at: other.last_tried_to_connect_at,
420 retry_at: other.retry_at,
421 retry_schedule: other.retry_schedule,
422 reachable: other.reachable,
423 is_dir_cache: other.is_dir_cache,
424 exploratory_circ_pending: other.exploratory_circ_pending,
425 dir_info_missing: other.dir_info_missing,
426 circ_history: other.circ_history,
427 suspicious_behavior_warned: other.suspicious_behavior_warned,
428 dir_status: other.dir_status,
429 clock_skew: other.clock_skew,
430 sensitivity: other.sensitivity,
431 }
436 }
437
438 #[allow(clippy::cognitive_complexity)]
440 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 NewlyConfirmed::Yes
734 } else {
735 NewlyConfirmed::No
736 }
737 }
738
739 pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
741 match how {
742 ExternalActivity::DirCache => {
743 self.dir_status.note_success();
744 }
745 }
746 }
747
748 pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
750 match how {
751 ExternalActivity::DirCache => {
752 self.dir_status.note_failure(now);
753 }
754 }
755 }
756
757 pub(crate) fn record_indeterminate_result(&mut self) {
760 self.circ_history.n_indeterminate += 1;
761
762 if let Some(ratio) = self.circ_history.indeterminate_ratio() {
763 const DISABLE_THRESHOLD: f64 = 0.7;
768 const WARN_THRESHOLD: f64 = 0.5;
771
772 if ratio > DISABLE_THRESHOLD {
773 let reason = GuardDisabled::TooManyIndeterminateFailures {
774 history: self.circ_history.clone(),
775 failure_ratio: ratio,
776 threshold_ratio: DISABLE_THRESHOLD,
777 };
778 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 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 }
785 }
786
787 pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
789 crate::FirstHop {
790 sample: Some(selection),
791 inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
792 self,
793 )),
794 }
795 }
796
797 pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
799 self.clock_skew = Some(observation);
800 }
801
802 pub(crate) fn skew(&self) -> Option<&SkewObservation> {
805 self.clock_skew.as_ref()
806 }
807
808 #[cfg(test)]
810 pub(crate) fn confirmed(&self) -> bool {
811 self.confirmed_at.is_some()
812 }
813}
814
815impl tor_linkspec::HasAddrs for Guard {
816 fn addrs(&self) -> impl Iterator<Item = SocketAddr> {
817 self.orports.iter().copied()
818 }
819}
820
821impl tor_linkspec::HasRelayIds for Guard {
822 fn identity(
823 &self,
824 key_type: tor_linkspec::RelayIdType,
825 ) -> Option<tor_linkspec::RelayIdRef<'_>> {
826 self.id.0.identity(key_type)
827 }
828}
829
830impl tor_linkspec::HasChanMethod for Guard {
831 fn chan_method(&self) -> ChannelMethod {
832 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![]), [] => ChannelMethod::Direct(self.orports.clone()),
838 }
839 }
840}
841
842impl tor_linkspec::ChanTarget for Guard {}
843
844impl 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#[derive(Clone, Debug, Serialize, Deserialize)]
856#[serde(tag = "type")]
857enum GuardDisabled {
858 TooManyIndeterminateFailures {
860 history: CircHistory,
862 failure_ratio: f64,
864 threshold_ratio: f64,
866 },
867}
868
869fn retry_schedule(is_primary: bool) -> RetryDelay {
873 let minimum = if is_primary {
874 Duration::from_secs(30)
875 } else {
876 Duration::from_secs(150)
877 };
878
879 RetryDelay::from_duration(minimum)
880}
881
882#[derive(Debug, Clone, Default, Serialize, Deserialize)]
907pub(crate) struct CircHistory {
908 n_successes: u32,
910 #[allow(dead_code)] n_failures: u32,
913 n_indeterminate: u32,
915}
916
917impl CircHistory {
918 fn indeterminate_ratio(&self) -> Option<f64> {
921 const MIN_OBSERVATIONS: u32 = 15;
925
926 let total = self.n_successes + self.n_indeterminate;
927 if total < MIN_OBSERVATIONS {
928 return None;
929 }
930
931 Some(f64::from(self.n_indeterminate) / f64::from(total))
932 }
933}
934
935#[cfg(test)]
936mod test {
937 #![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 #![allow(clippy::string_slice)] use super::*;
952 use crate::ids::FirstHopId;
953 use tor_linkspec::{HasRelayIds, RelayId};
954 use tor_llcrypto::pk::ed25519::Ed25519Identity;
955 use web_time_compat::SystemTimeExt;
956
957 #[test]
958 fn crate_id() {
959 let id = CrateId::this_crate().unwrap();
960 assert_eq!(&id.crate_name, "tor-guardmgr");
961 assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
962 }
963
964 fn basic_id() -> GuardId {
965 GuardId::new([13; 32].into(), [37; 20].into())
966 }
967 fn basic_guard() -> Guard {
968 let id = basic_id();
969 let ports = vec!["127.0.0.7:7777".parse().unwrap()];
970 let added = SystemTime::get();
971 Guard::new(id, ports, None, added)
972 }
973
974 #[test]
975 fn simple_accessors() {
976 fn ed(id: [u8; 32]) -> RelayId {
977 RelayId::Ed25519(id.into())
978 }
979 let id = basic_id();
980 let g = basic_guard();
981
982 assert_eq!(g.guard_id(), &id);
983 assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
984 assert_eq!(
985 g.addrs().collect_vec(),
986 &["127.0.0.7:7777".parse().unwrap()]
987 );
988 assert_eq!(g.reachable(), Reachable::Untried);
989 assert_eq!(g.reachable(), Reachable::default());
990
991 use crate::GuardUsageBuilder;
992 let mut usage1 = GuardUsageBuilder::new();
993
994 usage1
995 .restrictions()
996 .push(GuardRestriction::AvoidId(ed([22; 32])));
997 let usage1 = usage1.build().unwrap();
998 let mut usage2 = GuardUsageBuilder::new();
999 usage2
1000 .restrictions()
1001 .push(GuardRestriction::AvoidId(ed([13; 32])));
1002 let usage2 = usage2.build().unwrap();
1003 let usage3 = GuardUsage::default();
1004 let mut usage4 = GuardUsageBuilder::new();
1005 usage4
1006 .restrictions()
1007 .push(GuardRestriction::AvoidId(ed([22; 32])));
1008 usage4
1009 .restrictions()
1010 .push(GuardRestriction::AvoidId(ed([13; 32])));
1011 let usage4 = usage4.build().unwrap();
1012 let mut usage5 = GuardUsageBuilder::new();
1013 usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1014 vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1015 ));
1016 let usage5 = usage5.build().unwrap();
1017 let mut usage6 = GuardUsageBuilder::new();
1018 usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1019 vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1020 ));
1021 let usage6 = usage6.build().unwrap();
1022
1023 assert!(g.conforms_to_usage(&usage1));
1024 assert!(!g.conforms_to_usage(&usage2));
1025 assert!(g.conforms_to_usage(&usage3));
1026 assert!(!g.conforms_to_usage(&usage4));
1027 assert!(!g.conforms_to_usage(&usage5));
1028 assert!(g.conforms_to_usage(&usage6));
1029 }
1030
1031 #[allow(clippy::redundant_clone)]
1032 #[test]
1033 fn trickier_usages() {
1034 let g = basic_guard();
1035 use crate::{GuardUsageBuilder, GuardUsageKind};
1036 let data_usage = GuardUsageBuilder::new()
1037 .kind(GuardUsageKind::Data)
1038 .build()
1039 .unwrap();
1040 let dir_usage = GuardUsageBuilder::new()
1041 .kind(GuardUsageKind::OneHopDirectory)
1042 .build()
1043 .unwrap();
1044 assert!(g.conforms_to_usage(&data_usage));
1045 assert!(g.conforms_to_usage(&dir_usage));
1046
1047 let mut g2 = g.clone();
1048 g2.dir_info_missing = true;
1049 assert!(!g2.conforms_to_usage(&data_usage));
1050 assert!(g2.conforms_to_usage(&dir_usage));
1051
1052 let mut g3 = g.clone();
1053 g3.is_dir_cache = false;
1054 assert!(g3.conforms_to_usage(&data_usage));
1055 assert!(!g3.conforms_to_usage(&dir_usage));
1056 }
1057
1058 #[test]
1059 fn record_attempt() {
1060 let t1 = Instant::get() - Duration::from_secs(10);
1061 let t2 = Instant::get() - Duration::from_secs(5);
1062 let t3 = Instant::get();
1063
1064 let mut g = basic_guard();
1065
1066 assert!(g.last_tried_to_connect_at.is_none());
1067 g.record_attempt(t1);
1068 assert_eq!(g.last_tried_to_connect_at, Some(t1));
1069 g.record_attempt(t3);
1070 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1071 g.record_attempt(t2);
1072 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1073 }
1074
1075 #[test]
1076 fn record_failure() {
1077 let t1 = Instant::get() - Duration::from_secs(10);
1078 let t2 = Instant::get();
1079
1080 let mut g = basic_guard();
1081 g.record_failure(t1, true);
1082 assert!(g.retry_schedule.is_some());
1083 assert_eq!(g.reachable(), Reachable::Unreachable);
1084 let retry1 = g.retry_at.unwrap();
1085 assert_eq!(retry1, t1 + Duration::from_secs(30));
1086
1087 g.record_failure(t2, true);
1088 let retry2 = g.retry_at.unwrap();
1089 assert!(retry2 >= t2 + Duration::from_secs(30));
1090 assert!(retry2 <= t2 + Duration::from_secs(200));
1091 }
1092
1093 #[test]
1094 fn record_success() {
1095 let t1 = Instant::get() - Duration::from_secs(10);
1096 let now = SystemTime::get();
1098 let t2 = now + Duration::from_secs(300 * 86400);
1099 let t3 = Instant::get() + Duration::from_secs(310 * 86400);
1100 let t4 = now + Duration::from_secs(320 * 86400);
1101
1102 let mut g = basic_guard();
1103 g.record_failure(t1, true);
1104 assert_eq!(g.reachable(), Reachable::Unreachable);
1105
1106 let conf = g.record_success(t2, &GuardParams::default());
1107 assert_eq!(g.reachable(), Reachable::Reachable);
1108 assert_eq!(conf, NewlyConfirmed::Yes);
1109 assert!(g.retry_at.is_none());
1110 assert!(g.confirmed_at.unwrap() <= t2);
1111 assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1112 let confirmed_at_orig = g.confirmed_at;
1113
1114 g.record_failure(t3, true);
1115 assert_eq!(g.reachable(), Reachable::Unreachable);
1116
1117 let conf = g.record_success(t4, &GuardParams::default());
1118 assert_eq!(conf, NewlyConfirmed::No);
1119 assert_eq!(g.reachable(), Reachable::Reachable);
1120 assert!(g.retry_at.is_none());
1121 assert_eq!(g.confirmed_at, confirmed_at_orig);
1122 }
1123
1124 #[test]
1125 fn retry() {
1126 let t1 = Instant::get();
1127 let mut g = basic_guard();
1128
1129 g.record_failure(t1, true);
1130 assert!(g.retry_at.is_some());
1131 assert_eq!(g.reachable(), Reachable::Unreachable);
1132
1133 g.consider_retry(t1);
1135 assert!(g.retry_at.is_some());
1136 assert_eq!(g.reachable(), Reachable::Unreachable);
1137
1138 g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1140 assert!(g.retry_at.is_some());
1141 assert_eq!(g.reachable(), Reachable::Unreachable);
1142
1143 g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1145 assert!(g.retry_at.is_none());
1146 assert_eq!(g.reachable(), Reachable::Retriable);
1147 }
1148
1149 #[test]
1150 fn expiration() {
1151 const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1152 let params = GuardParams::default();
1153 let now = SystemTime::get();
1154
1155 let g = basic_guard();
1156 assert!(!g.is_expired(¶ms, now));
1157 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1158 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1159 assert!(!g.is_expired(¶ms, now + 70 * DAY));
1160 assert!(g.is_expired(¶ms, now + 200 * DAY)); let mut g = basic_guard();
1163 let _ = g.record_success(now, ¶ms);
1164 assert!(!g.is_expired(¶ms, now));
1165 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1166 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1167 assert!(g.is_expired(¶ms, now + 70 * DAY)); let mut g = basic_guard();
1170 g.mark_unlisted(now);
1171 assert!(!g.is_expired(¶ms, now));
1172 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1173 assert!(g.is_expired(¶ms, now + 25 * DAY)); }
1175
1176 #[test]
1177 fn netdir_integration() {
1178 use tor_netdir::testnet;
1179 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1180 let params = GuardParams::default();
1181 let now = SystemTime::get();
1182
1183 let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1185 let guard22 = Guard::from_chan_target(&relay22, now, ¶ms);
1186 assert!(guard22.same_relay_ids(&relay22));
1187 assert!(Some(guard22.added_at) <= Some(now));
1188
1189 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1191 let r = id.get_relay(&netdir).unwrap();
1192 assert!(r.same_relay_ids(&relay22));
1193
1194 let guard255 = Guard::new(
1196 GuardId::new([255; 32].into(), [255; 20].into()),
1197 vec![],
1198 None,
1199 now,
1200 );
1201 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1202 assert!(id.get_relay(&netdir).is_none());
1203 }
1204
1205 #[test]
1206 fn update_from_netdir() {
1207 use tor_netdir::testnet;
1208 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1209 let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1211 if idx == 22 {
1212 node.omit_rs = true;
1213 }
1214 })
1215 .unwrap()
1216 .unwrap_if_sufficient()
1217 .unwrap();
1218 let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1220 if idx == 22 {
1221 node.omit_rs = true;
1222 } else if idx == 23 {
1223 node.omit_md = true;
1224 }
1225 })
1226 .unwrap()
1227 .unwrap_if_sufficient()
1228 .unwrap();
1229
1230 let now = SystemTime::get();
1232
1233 let mut guard255 = Guard::new(
1235 GuardId::new([255; 32].into(), [255; 20].into()),
1236 vec!["8.8.8.8:53".parse().unwrap()],
1237 None,
1238 now,
1239 );
1240 assert_eq!(guard255.unlisted_since, None);
1241 assert_eq!(guard255.listed_in(&netdir), Some(false));
1242 guard255.update_from_universe(&netdir);
1243 assert_eq!(
1244 guard255.unlisted_since,
1245 Some(netdir.lifetime().valid_after())
1246 );
1247 assert!(!guard255.orports.is_empty());
1248
1249 let mut guard22 = Guard::new(
1251 GuardId::new([22; 32].into(), [22; 20].into()),
1252 vec![],
1253 None,
1254 now,
1255 );
1256 let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1257 let relay22 = id22.get_relay(&netdir).unwrap();
1258 assert_eq!(guard22.listed_in(&netdir), Some(true));
1259 guard22.update_from_universe(&netdir);
1260 assert_eq!(guard22.unlisted_since, None); assert_eq!(guard22.orports, relay22.addrs().collect_vec()); assert_eq!(guard22.listed_in(&netdir2), Some(false));
1263 guard22.update_from_universe(&netdir2);
1264 assert_eq!(
1265 guard22.unlisted_since,
1266 Some(netdir2.lifetime().valid_after())
1267 );
1268 assert_eq!(guard22.orports, relay22.addrs().collect_vec()); assert!(!guard22.dir_info_missing);
1270
1271 let mut guard23 = Guard::new(
1273 GuardId::new([23; 32].into(), [23; 20].into()),
1274 vec![],
1275 None,
1276 now,
1277 );
1278 assert_eq!(guard23.listed_in(&netdir2), Some(true));
1279 assert_eq!(guard23.listed_in(&netdir3), None);
1280 guard23.update_from_universe(&netdir3);
1281 assert!(guard23.dir_info_missing);
1282 assert!(guard23.is_dir_cache);
1283 }
1284
1285 #[test]
1286 fn pending() {
1287 let mut g = basic_guard();
1288 let t1 = Instant::get();
1289 let t2 = t1 + Duration::from_secs(100);
1290 let t3 = t1 + Duration::from_secs(200);
1291
1292 assert!(!g.exploratory_attempt_after(t1));
1293 assert!(!g.exploratory_circ_pending());
1294
1295 g.note_exploratory_circ(true);
1296 g.record_attempt(t2);
1297 assert!(g.exploratory_circ_pending());
1298 assert!(g.exploratory_attempt_after(t1));
1299 assert!(!g.exploratory_attempt_after(t3));
1300
1301 g.note_exploratory_circ(false);
1302 assert!(!g.exploratory_circ_pending());
1303 assert!(!g.exploratory_attempt_after(t1));
1304 assert!(!g.exploratory_attempt_after(t3));
1305 }
1306
1307 #[test]
1308 fn circ_history() {
1309 let mut h = CircHistory {
1310 n_successes: 3,
1311 n_failures: 4,
1312 n_indeterminate: 3,
1313 };
1314 assert!(h.indeterminate_ratio().is_none());
1315
1316 h.n_successes = 20;
1317 assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
1318 }
1319
1320 #[test]
1321 fn disable_on_failure() {
1322 let mut g = basic_guard();
1323 let params = GuardParams::default();
1324
1325 let now = SystemTime::get();
1326
1327 let _ignore = g.record_success(now, ¶ms);
1328 for _ in 0..13 {
1329 g.record_indeterminate_result();
1330 }
1331 assert!(g.disabled.is_none());
1333
1334 g.record_indeterminate_result();
1336 assert!(g.disabled.is_some());
1337
1338 #[allow(unreachable_patterns)]
1339 match g.disabled.unwrap().into_option().unwrap() {
1340 GuardDisabled::TooManyIndeterminateFailures {
1341 history: _,
1342 failure_ratio,
1343 threshold_ratio,
1344 } => {
1345 assert!((failure_ratio - 0.933).abs() < 0.01);
1346 assert!((threshold_ratio - 0.7).abs() < 0.01);
1347 }
1348 other => {
1349 panic!("Wrong variant: {:?}", other);
1350 }
1351 }
1352 }
1353
1354 #[test]
1355 fn mark_retriable() {
1356 let mut g = basic_guard();
1357 use super::Reachable::*;
1358
1359 assert_eq!(g.reachable(), Untried);
1360
1361 for (pre, post) in &[
1362 (Untried, Untried),
1363 (Unreachable, Retriable),
1364 (Reachable, Reachable),
1365 ] {
1366 g.reachable = *pre;
1367 g.mark_retriable();
1368 assert_eq!(g.reachable(), *post);
1369 }
1370 }
1371
1372 #[test]
1373 fn dir_status() {
1374 use crate::GuardUsageBuilder;
1378 let mut g = basic_guard();
1379 let inst = Instant::get();
1380 let st = SystemTime::get();
1381 let sec = Duration::from_secs(1);
1382 let params = GuardParams::default();
1383 let dir_usage = GuardUsageBuilder::new()
1384 .kind(GuardUsageKind::OneHopDirectory)
1385 .build()
1386 .unwrap();
1387 let data_usage = GuardUsage::default();
1388
1389 let _ = g.record_success(st, ¶ms);
1391 assert_eq!(g.next_retry(&dir_usage), None);
1392 assert!(g.ready_for_usage(&dir_usage, inst));
1393 assert_eq!(g.next_retry(&data_usage), None);
1394 assert!(g.ready_for_usage(&data_usage, inst));
1395
1396 g.record_external_failure(ExternalActivity::DirCache, inst);
1398 assert_eq!(g.next_retry(&data_usage), None);
1399 assert!(g.ready_for_usage(&data_usage, inst));
1400 let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1401 assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1402 assert!(!g.ready_for_usage(&dir_usage, inst));
1403 assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1404
1405 let _ = g.record_success(st, ¶ms);
1408 assert!(g.ready_for_usage(&data_usage, inst));
1409 assert!(!g.ready_for_usage(&dir_usage, inst));
1410
1411 g.record_failure(inst + sec * 10, true);
1413 let next_circ_retry = g.next_retry(&data_usage).unwrap();
1414 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1415 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1416 assert_eq!(
1417 g.next_retry(&dir_usage).unwrap(),
1418 std::cmp::max(next_circ_retry, next_dir_retry)
1419 );
1420
1421 g.record_external_success(ExternalActivity::DirCache);
1424 assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1425 assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1426 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1427 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1428 }
1429}