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 use super::*;
951 use crate::ids::FirstHopId;
952 use tor_linkspec::{HasRelayIds, RelayId};
953 use tor_llcrypto::pk::ed25519::Ed25519Identity;
954 use web_time_compat::SystemTimeExt;
955
956 #[test]
957 fn crate_id() {
958 let id = CrateId::this_crate().unwrap();
959 assert_eq!(&id.crate_name, "tor-guardmgr");
960 assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
961 }
962
963 fn basic_id() -> GuardId {
964 GuardId::new([13; 32].into(), [37; 20].into())
965 }
966 fn basic_guard() -> Guard {
967 let id = basic_id();
968 let ports = vec!["127.0.0.7:7777".parse().unwrap()];
969 let added = SystemTime::get();
970 Guard::new(id, ports, None, added)
971 }
972
973 #[test]
974 fn simple_accessors() {
975 fn ed(id: [u8; 32]) -> RelayId {
976 RelayId::Ed25519(id.into())
977 }
978 let id = basic_id();
979 let g = basic_guard();
980
981 assert_eq!(g.guard_id(), &id);
982 assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
983 assert_eq!(
984 g.addrs().collect_vec(),
985 &["127.0.0.7:7777".parse().unwrap()]
986 );
987 assert_eq!(g.reachable(), Reachable::Untried);
988 assert_eq!(g.reachable(), Reachable::default());
989
990 use crate::GuardUsageBuilder;
991 let mut usage1 = GuardUsageBuilder::new();
992
993 usage1
994 .restrictions()
995 .push(GuardRestriction::AvoidId(ed([22; 32])));
996 let usage1 = usage1.build().unwrap();
997 let mut usage2 = GuardUsageBuilder::new();
998 usage2
999 .restrictions()
1000 .push(GuardRestriction::AvoidId(ed([13; 32])));
1001 let usage2 = usage2.build().unwrap();
1002 let usage3 = GuardUsage::default();
1003 let mut usage4 = GuardUsageBuilder::new();
1004 usage4
1005 .restrictions()
1006 .push(GuardRestriction::AvoidId(ed([22; 32])));
1007 usage4
1008 .restrictions()
1009 .push(GuardRestriction::AvoidId(ed([13; 32])));
1010 let usage4 = usage4.build().unwrap();
1011 let mut usage5 = GuardUsageBuilder::new();
1012 usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1013 vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1014 ));
1015 let usage5 = usage5.build().unwrap();
1016 let mut usage6 = GuardUsageBuilder::new();
1017 usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1018 vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1019 ));
1020 let usage6 = usage6.build().unwrap();
1021
1022 assert!(g.conforms_to_usage(&usage1));
1023 assert!(!g.conforms_to_usage(&usage2));
1024 assert!(g.conforms_to_usage(&usage3));
1025 assert!(!g.conforms_to_usage(&usage4));
1026 assert!(!g.conforms_to_usage(&usage5));
1027 assert!(g.conforms_to_usage(&usage6));
1028 }
1029
1030 #[allow(clippy::redundant_clone)]
1031 #[test]
1032 fn trickier_usages() {
1033 let g = basic_guard();
1034 use crate::{GuardUsageBuilder, GuardUsageKind};
1035 let data_usage = GuardUsageBuilder::new()
1036 .kind(GuardUsageKind::Data)
1037 .build()
1038 .unwrap();
1039 let dir_usage = GuardUsageBuilder::new()
1040 .kind(GuardUsageKind::OneHopDirectory)
1041 .build()
1042 .unwrap();
1043 assert!(g.conforms_to_usage(&data_usage));
1044 assert!(g.conforms_to_usage(&dir_usage));
1045
1046 let mut g2 = g.clone();
1047 g2.dir_info_missing = true;
1048 assert!(!g2.conforms_to_usage(&data_usage));
1049 assert!(g2.conforms_to_usage(&dir_usage));
1050
1051 let mut g3 = g.clone();
1052 g3.is_dir_cache = false;
1053 assert!(g3.conforms_to_usage(&data_usage));
1054 assert!(!g3.conforms_to_usage(&dir_usage));
1055 }
1056
1057 #[test]
1058 fn record_attempt() {
1059 let t1 = Instant::get() - Duration::from_secs(10);
1060 let t2 = Instant::get() - Duration::from_secs(5);
1061 let t3 = Instant::get();
1062
1063 let mut g = basic_guard();
1064
1065 assert!(g.last_tried_to_connect_at.is_none());
1066 g.record_attempt(t1);
1067 assert_eq!(g.last_tried_to_connect_at, Some(t1));
1068 g.record_attempt(t3);
1069 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1070 g.record_attempt(t2);
1071 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1072 }
1073
1074 #[test]
1075 fn record_failure() {
1076 let t1 = Instant::get() - Duration::from_secs(10);
1077 let t2 = Instant::get();
1078
1079 let mut g = basic_guard();
1080 g.record_failure(t1, true);
1081 assert!(g.retry_schedule.is_some());
1082 assert_eq!(g.reachable(), Reachable::Unreachable);
1083 let retry1 = g.retry_at.unwrap();
1084 assert_eq!(retry1, t1 + Duration::from_secs(30));
1085
1086 g.record_failure(t2, true);
1087 let retry2 = g.retry_at.unwrap();
1088 assert!(retry2 >= t2 + Duration::from_secs(30));
1089 assert!(retry2 <= t2 + Duration::from_secs(200));
1090 }
1091
1092 #[test]
1093 fn record_success() {
1094 let t1 = Instant::get() - Duration::from_secs(10);
1095 let now = SystemTime::get();
1097 let t2 = now + Duration::from_secs(300 * 86400);
1098 let t3 = Instant::get() + Duration::from_secs(310 * 86400);
1099 let t4 = now + Duration::from_secs(320 * 86400);
1100
1101 let mut g = basic_guard();
1102 g.record_failure(t1, true);
1103 assert_eq!(g.reachable(), Reachable::Unreachable);
1104
1105 let conf = g.record_success(t2, &GuardParams::default());
1106 assert_eq!(g.reachable(), Reachable::Reachable);
1107 assert_eq!(conf, NewlyConfirmed::Yes);
1108 assert!(g.retry_at.is_none());
1109 assert!(g.confirmed_at.unwrap() <= t2);
1110 assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1111 let confirmed_at_orig = g.confirmed_at;
1112
1113 g.record_failure(t3, true);
1114 assert_eq!(g.reachable(), Reachable::Unreachable);
1115
1116 let conf = g.record_success(t4, &GuardParams::default());
1117 assert_eq!(conf, NewlyConfirmed::No);
1118 assert_eq!(g.reachable(), Reachable::Reachable);
1119 assert!(g.retry_at.is_none());
1120 assert_eq!(g.confirmed_at, confirmed_at_orig);
1121 }
1122
1123 #[test]
1124 fn retry() {
1125 let t1 = Instant::get();
1126 let mut g = basic_guard();
1127
1128 g.record_failure(t1, true);
1129 assert!(g.retry_at.is_some());
1130 assert_eq!(g.reachable(), Reachable::Unreachable);
1131
1132 g.consider_retry(t1);
1134 assert!(g.retry_at.is_some());
1135 assert_eq!(g.reachable(), Reachable::Unreachable);
1136
1137 g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1139 assert!(g.retry_at.is_some());
1140 assert_eq!(g.reachable(), Reachable::Unreachable);
1141
1142 g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1144 assert!(g.retry_at.is_none());
1145 assert_eq!(g.reachable(), Reachable::Retriable);
1146 }
1147
1148 #[test]
1149 fn expiration() {
1150 const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1151 let params = GuardParams::default();
1152 let now = SystemTime::get();
1153
1154 let g = basic_guard();
1155 assert!(!g.is_expired(¶ms, now));
1156 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1157 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1158 assert!(!g.is_expired(¶ms, now + 70 * DAY));
1159 assert!(g.is_expired(¶ms, now + 200 * DAY)); let mut g = basic_guard();
1162 let _ = g.record_success(now, ¶ms);
1163 assert!(!g.is_expired(¶ms, now));
1164 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1165 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1166 assert!(g.is_expired(¶ms, now + 70 * DAY)); let mut g = basic_guard();
1169 g.mark_unlisted(now);
1170 assert!(!g.is_expired(¶ms, now));
1171 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1172 assert!(g.is_expired(¶ms, now + 25 * DAY)); }
1174
1175 #[test]
1176 fn netdir_integration() {
1177 use tor_netdir::testnet;
1178 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1179 let params = GuardParams::default();
1180 let now = SystemTime::get();
1181
1182 let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1184 let guard22 = Guard::from_chan_target(&relay22, now, ¶ms);
1185 assert!(guard22.same_relay_ids(&relay22));
1186 assert!(Some(guard22.added_at) <= Some(now));
1187
1188 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1190 let r = id.get_relay(&netdir).unwrap();
1191 assert!(r.same_relay_ids(&relay22));
1192
1193 let guard255 = Guard::new(
1195 GuardId::new([255; 32].into(), [255; 20].into()),
1196 vec![],
1197 None,
1198 now,
1199 );
1200 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1201 assert!(id.get_relay(&netdir).is_none());
1202 }
1203
1204 #[test]
1205 fn update_from_netdir() {
1206 use tor_netdir::testnet;
1207 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1208 let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1210 if idx == 22 {
1211 node.omit_rs = true;
1212 }
1213 })
1214 .unwrap()
1215 .unwrap_if_sufficient()
1216 .unwrap();
1217 let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1219 if idx == 22 {
1220 node.omit_rs = true;
1221 } else if idx == 23 {
1222 node.omit_md = true;
1223 }
1224 })
1225 .unwrap()
1226 .unwrap_if_sufficient()
1227 .unwrap();
1228
1229 let now = SystemTime::get();
1231
1232 let mut guard255 = Guard::new(
1234 GuardId::new([255; 32].into(), [255; 20].into()),
1235 vec!["8.8.8.8:53".parse().unwrap()],
1236 None,
1237 now,
1238 );
1239 assert_eq!(guard255.unlisted_since, None);
1240 assert_eq!(guard255.listed_in(&netdir), Some(false));
1241 guard255.update_from_universe(&netdir);
1242 assert_eq!(
1243 guard255.unlisted_since,
1244 Some(netdir.lifetime().valid_after())
1245 );
1246 assert!(!guard255.orports.is_empty());
1247
1248 let mut guard22 = Guard::new(
1250 GuardId::new([22; 32].into(), [22; 20].into()),
1251 vec![],
1252 None,
1253 now,
1254 );
1255 let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1256 let relay22 = id22.get_relay(&netdir).unwrap();
1257 assert_eq!(guard22.listed_in(&netdir), Some(true));
1258 guard22.update_from_universe(&netdir);
1259 assert_eq!(guard22.unlisted_since, None); assert_eq!(guard22.orports, relay22.addrs().collect_vec()); assert_eq!(guard22.listed_in(&netdir2), Some(false));
1262 guard22.update_from_universe(&netdir2);
1263 assert_eq!(
1264 guard22.unlisted_since,
1265 Some(netdir2.lifetime().valid_after())
1266 );
1267 assert_eq!(guard22.orports, relay22.addrs().collect_vec()); assert!(!guard22.dir_info_missing);
1269
1270 let mut guard23 = Guard::new(
1272 GuardId::new([23; 32].into(), [23; 20].into()),
1273 vec![],
1274 None,
1275 now,
1276 );
1277 assert_eq!(guard23.listed_in(&netdir2), Some(true));
1278 assert_eq!(guard23.listed_in(&netdir3), None);
1279 guard23.update_from_universe(&netdir3);
1280 assert!(guard23.dir_info_missing);
1281 assert!(guard23.is_dir_cache);
1282 }
1283
1284 #[test]
1285 fn pending() {
1286 let mut g = basic_guard();
1287 let t1 = Instant::get();
1288 let t2 = t1 + Duration::from_secs(100);
1289 let t3 = t1 + Duration::from_secs(200);
1290
1291 assert!(!g.exploratory_attempt_after(t1));
1292 assert!(!g.exploratory_circ_pending());
1293
1294 g.note_exploratory_circ(true);
1295 g.record_attempt(t2);
1296 assert!(g.exploratory_circ_pending());
1297 assert!(g.exploratory_attempt_after(t1));
1298 assert!(!g.exploratory_attempt_after(t3));
1299
1300 g.note_exploratory_circ(false);
1301 assert!(!g.exploratory_circ_pending());
1302 assert!(!g.exploratory_attempt_after(t1));
1303 assert!(!g.exploratory_attempt_after(t3));
1304 }
1305
1306 #[test]
1307 fn circ_history() {
1308 let mut h = CircHistory {
1309 n_successes: 3,
1310 n_failures: 4,
1311 n_indeterminate: 3,
1312 };
1313 assert!(h.indeterminate_ratio().is_none());
1314
1315 h.n_successes = 20;
1316 assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
1317 }
1318
1319 #[test]
1320 fn disable_on_failure() {
1321 let mut g = basic_guard();
1322 let params = GuardParams::default();
1323
1324 let now = SystemTime::get();
1325
1326 let _ignore = g.record_success(now, ¶ms);
1327 for _ in 0..13 {
1328 g.record_indeterminate_result();
1329 }
1330 assert!(g.disabled.is_none());
1332
1333 g.record_indeterminate_result();
1335 assert!(g.disabled.is_some());
1336
1337 #[allow(unreachable_patterns)]
1338 match g.disabled.unwrap().into_option().unwrap() {
1339 GuardDisabled::TooManyIndeterminateFailures {
1340 history: _,
1341 failure_ratio,
1342 threshold_ratio,
1343 } => {
1344 assert!((failure_ratio - 0.933).abs() < 0.01);
1345 assert!((threshold_ratio - 0.7).abs() < 0.01);
1346 }
1347 other => {
1348 panic!("Wrong variant: {:?}", other);
1349 }
1350 }
1351 }
1352
1353 #[test]
1354 fn mark_retriable() {
1355 let mut g = basic_guard();
1356 use super::Reachable::*;
1357
1358 assert_eq!(g.reachable(), Untried);
1359
1360 for (pre, post) in &[
1361 (Untried, Untried),
1362 (Unreachable, Retriable),
1363 (Reachable, Reachable),
1364 ] {
1365 g.reachable = *pre;
1366 g.mark_retriable();
1367 assert_eq!(g.reachable(), *post);
1368 }
1369 }
1370
1371 #[test]
1372 fn dir_status() {
1373 use crate::GuardUsageBuilder;
1377 let mut g = basic_guard();
1378 let inst = Instant::get();
1379 let st = SystemTime::get();
1380 let sec = Duration::from_secs(1);
1381 let params = GuardParams::default();
1382 let dir_usage = GuardUsageBuilder::new()
1383 .kind(GuardUsageKind::OneHopDirectory)
1384 .build()
1385 .unwrap();
1386 let data_usage = GuardUsage::default();
1387
1388 let _ = g.record_success(st, ¶ms);
1390 assert_eq!(g.next_retry(&dir_usage), None);
1391 assert!(g.ready_for_usage(&dir_usage, inst));
1392 assert_eq!(g.next_retry(&data_usage), None);
1393 assert!(g.ready_for_usage(&data_usage, inst));
1394
1395 g.record_external_failure(ExternalActivity::DirCache, inst);
1397 assert_eq!(g.next_retry(&data_usage), None);
1398 assert!(g.ready_for_usage(&data_usage, inst));
1399 let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1400 assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1401 assert!(!g.ready_for_usage(&dir_usage, inst));
1402 assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1403
1404 let _ = g.record_success(st, ¶ms);
1407 assert!(g.ready_for_usage(&data_usage, inst));
1408 assert!(!g.ready_for_usage(&dir_usage, inst));
1409
1410 g.record_failure(inst + sec * 10, true);
1412 let next_circ_retry = g.next_retry(&data_usage).unwrap();
1413 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1414 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1415 assert_eq!(
1416 g.next_retry(&dir_usage).unwrap(),
1417 std::cmp::max(next_circ_retry, next_dir_retry)
1418 );
1419
1420 g.record_external_success(ExternalActivity::DirCache);
1423 assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1424 assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1425 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1426 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1427 }
1428}