1use tor_basic_utils::retry::RetryDelay;
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::time::{Duration, Instant, SystemTime};
9use tracing::{info, trace, warn};
10
11use crate::dirstatus::DirStatus;
12use crate::sample::Candidate;
13use crate::skew::SkewObservation;
14use crate::util::randomize_time;
15use crate::{ids::GuardId, GuardParams, GuardRestriction, GuardUsage};
16use crate::{sample, ExternalActivity, GuardSetSelector, GuardUsageKind};
17
18#[cfg(feature = "bridge-client")]
19use safelog::Redactable as _;
20
21use tor_linkspec::{
22 ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
23};
24use tor_persist::{Futureproof, JsonValue};
25
26#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
28#[allow(clippy::enum_variant_names)]
29pub(crate) enum Reachable {
30 Reachable,
33 Unreachable,
36 #[default]
39 Untried,
40 Retriable,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
52struct CrateId {
53 #[serde(rename = "crate")]
55 crate_name: String,
56 version: String,
58}
59
60impl CrateId {
61 fn this_crate() -> Option<Self> {
63 let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
64 let version = option_env!("CARGO_PKG_VERSION")?.to_string();
65 Some(CrateId {
66 crate_name,
67 version,
68 })
69 }
70}
71
72#[derive(Clone, Default, Debug)]
74pub(crate) enum DisplayRule {
75 #[default]
84 Sensitive,
85 #[cfg(feature = "bridge-client")]
90 Redacted,
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize)]
114pub(crate) struct Guard {
115 id: GuardId,
117
118 orports: Vec<SocketAddr>,
124
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pt_targets: Vec<PtTarget>,
140
141 #[serde(with = "humantime_serde")]
143 added_at: SystemTime,
144
145 added_by: Option<CrateId>,
147
148 #[serde(default)]
151 disabled: Option<Futureproof<GuardDisabled>>,
152
153 #[serde(with = "humantime_serde")]
158 confirmed_at: Option<SystemTime>,
159
160 #[serde(with = "humantime_serde")]
166 unlisted_since: Option<SystemTime>,
167
168 #[serde(skip)]
171 dir_info_missing: bool,
172
173 #[serde(skip)]
175 last_tried_to_connect_at: Option<Instant>,
176
177 #[serde(skip)]
183 retry_at: Option<Instant>, #[serde(skip)]
188 retry_schedule: Option<RetryDelay>,
189
190 #[serde(skip)]
192 reachable: Reachable,
193
194 #[serde(skip)]
197 is_dir_cache: bool,
198
199 #[serde(skip, default = "guard_dirstatus")]
206 dir_status: DirStatus,
207
208 #[serde(skip)]
215 exploratory_circ_pending: bool,
216
217 #[serde(skip)]
221 circ_history: CircHistory,
222
223 #[serde(skip)]
225 suspicious_behavior_warned: bool,
226
227 #[serde(skip)]
229 clock_skew: Option<SkewObservation>,
230
231 #[serde(skip)]
233 sensitivity: DisplayRule,
234
235 #[serde(flatten)]
238 unknown_fields: HashMap<String, JsonValue>,
239}
240
241const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
244
245fn guard_dirstatus() -> DirStatus {
247 DirStatus::new(GUARD_DIR_RETRY_FLOOR)
248}
249
250#[derive(Debug, Clone, Copy, Eq, PartialEq)]
253pub(crate) enum NewlyConfirmed {
254 Yes,
256 No,
258}
259
260impl Guard {
261 pub(crate) fn from_candidate(
263 candidate: Candidate,
264 now: SystemTime,
265 params: &GuardParams,
266 ) -> Self {
267 let Candidate {
268 is_dir_cache,
269 full_dir_info,
270 owned_target,
271 ..
272 } = candidate;
273
274 Guard {
275 is_dir_cache,
276 dir_info_missing: !full_dir_info,
277 ..Self::from_chan_target(&owned_target, now, params)
278 }
279 }
280
281 fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
286 where
287 T: ChanTarget,
288 {
289 let added_at = randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10);
290
291 let pt_target = match relay.chan_method() {
292 #[cfg(feature = "pt-client")]
293 ChannelMethod::Pluggable(pt) => Some(pt),
294 _ => None,
295 };
296
297 Self::new(
298 GuardId::from_relay_ids(relay),
299 relay.addrs().into(),
300 pt_target,
301 added_at,
302 )
303 }
304
305 fn new(
307 id: GuardId,
308 orports: Vec<SocketAddr>,
309 pt_target: Option<PtTarget>,
310 added_at: SystemTime,
311 ) -> Self {
312 Guard {
313 id,
314 orports,
315 pt_targets: pt_target.into_iter().collect(),
316 added_at,
317 added_by: CrateId::this_crate(),
318 disabled: None,
319 confirmed_at: None,
320 unlisted_since: None,
321 dir_info_missing: false,
322 last_tried_to_connect_at: None,
323 reachable: Reachable::Untried,
324 retry_at: None,
325 dir_status: guard_dirstatus(),
326 retry_schedule: None,
327 is_dir_cache: true,
328 exploratory_circ_pending: false,
329 circ_history: CircHistory::default(),
330 suspicious_behavior_warned: false,
331 clock_skew: None,
332 unknown_fields: Default::default(),
333 sensitivity: DisplayRule::Sensitive,
334 }
335 }
336
337 pub(crate) fn guard_id(&self) -> &GuardId {
339 &self.id
340 }
341
342 pub(crate) fn reachable(&self) -> Reachable {
344 self.reachable
345 }
346
347 pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
352 match &usage.kind {
353 GuardUsageKind::Data => self.retry_at,
354 GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
355 .iter()
356 .flatten()
357 .max()
358 .copied(),
359 }
360 }
361
362 pub(crate) fn usable(&self) -> bool {
366 self.unlisted_since.is_none() && self.disabled.is_none()
367 }
368
369 pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
372 if let Some(retry_at) = self.retry_at {
373 if retry_at > now {
374 return false;
375 }
376 }
377
378 match usage.kind {
379 GuardUsageKind::Data => true,
380 GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
381 }
382 }
383
384 pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
398 assert!(self.same_relay_ids(&other));
404
405 Guard {
406 id: self.id,
408 pt_targets: self.pt_targets,
409 orports: self.orports,
410 added_at: self.added_at,
411 added_by: self.added_by,
412 disabled: self.disabled,
413 confirmed_at: self.confirmed_at,
414 unlisted_since: self.unlisted_since,
415 unknown_fields: self.unknown_fields,
416
417 last_tried_to_connect_at: other.last_tried_to_connect_at,
419 retry_at: other.retry_at,
420 retry_schedule: other.retry_schedule,
421 reachable: other.reachable,
422 is_dir_cache: other.is_dir_cache,
423 exploratory_circ_pending: other.exploratory_circ_pending,
424 dir_info_missing: other.dir_info_missing,
425 circ_history: other.circ_history,
426 suspicious_behavior_warned: other.suspicious_behavior_warned,
427 dir_status: other.dir_status,
428 clock_skew: other.clock_skew,
429 sensitivity: other.sensitivity,
430 }
435 }
436
437 fn set_reachable(&mut self, r: Reachable) {
439 use Reachable as R;
440
441 if self.reachable != r {
442 match (self.reachable, r) {
444 (_, R::Reachable) => info!("We have found that guard {} is usable.", self),
445 (R::Untried | R::Reachable, R::Unreachable) => warn!(
446 "Could not connect to guard {}. We'll retry later, and let you know if it succeeds.",
447 self
448 ),
449 (_, _) => {} }
451 trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed.");
453 self.reachable = r;
454 }
455 }
456
457 pub(crate) fn exploratory_circ_pending(&self) -> bool {
467 self.exploratory_circ_pending
468 }
469
470 pub(crate) fn note_exploratory_circ(&mut self, pending: bool) {
473 self.exploratory_circ_pending = pending;
474 }
475
476 pub(crate) fn consider_retry(&mut self, now: Instant) {
483 if let Some(retry_at) = self.retry_at {
484 debug_assert!(self.reachable == Reachable::Unreachable);
485 if retry_at <= now {
486 self.mark_retriable();
487 }
488 }
489 }
490
491 pub(crate) fn mark_retriable(&mut self) {
494 if self.reachable == Reachable::Unreachable {
495 self.set_reachable(Reachable::Retriable);
496 self.retry_at = None;
497 self.retry_schedule = None;
498 }
499 }
500
501 fn obeys_restrictions(&self, restrictions: &[GuardRestriction]) -> bool {
503 restrictions.iter().all(|r| self.obeys_restriction(r))
504 }
505
506 fn obeys_restriction(&self, r: &GuardRestriction) -> bool {
508 match r {
509 GuardRestriction::AvoidId(avoid_id) => !self.id.0.has_identity(avoid_id.as_ref()),
510 GuardRestriction::AvoidAllIds(avoid_ids) => {
511 self.id.0.identities().all(|id| !avoid_ids.contains(id))
512 }
513 }
514 }
515
516 pub(crate) fn conforms_to_usage(&self, usage: &GuardUsage) -> bool {
518 match usage.kind {
519 GuardUsageKind::OneHopDirectory => {
520 if !self.is_dir_cache {
521 return false;
522 }
523 }
524 GuardUsageKind::Data => {
525 if self.dir_info_missing {
528 return false;
529 }
530 }
531 }
532 self.obeys_restrictions(&usage.restrictions[..])
533 }
534
535 pub(crate) fn listed_in<U: sample::Universe>(&self, universe: &U) -> Option<bool> {
542 universe.contains(self)
543 }
544
545 pub(crate) fn update_from_universe<U: sample::Universe>(&mut self, universe: &U) {
557 use sample::CandidateStatus::*;
560 let listed_as_guard = match universe.status(self) {
561 Present(Candidate {
562 listed_as_guard,
563 is_dir_cache,
564 full_dir_info,
565 owned_target,
566 sensitivity,
567 }) => {
568 self.orports = owned_target.addrs().into();
570 self.pt_targets = match owned_target.chan_method() {
572 #[cfg(feature = "pt-client")]
573 ChannelMethod::Pluggable(pt) => vec![pt],
574 _ => Vec::new(),
575 };
576 self.is_dir_cache = is_dir_cache;
578 assert!(owned_target.has_all_relay_ids_from(self));
580 self.id = GuardId(RelayIds::from_relay_ids(&owned_target));
581 self.dir_info_missing = !full_dir_info;
582 self.sensitivity = sensitivity;
583
584 listed_as_guard
585 }
586 Absent => false, Uncertain => {
588 self.dir_info_missing = true;
590 return;
591 }
592 };
593
594 if listed_as_guard {
595 self.mark_listed();
597 } else {
598 self.mark_unlisted(universe.timestamp());
600 }
601 }
602
603 fn mark_listed(&mut self) {
605 if self.unlisted_since.is_some() {
606 trace!(guard_id = ?self.id, "Guard is now listed again.");
607 self.unlisted_since = None;
608 }
609 }
610
611 fn mark_unlisted(&mut self, now: SystemTime) {
614 if self.unlisted_since.is_none() {
615 trace!(guard_id = ?self.id, "Guard is now unlisted.");
616 self.unlisted_since = Some(now);
617 }
618 }
619
620 pub(crate) fn is_expired(&self, params: &GuardParams, now: SystemTime) -> bool {
628 fn expired_by(t1: SystemTime, d: Duration, t2: SystemTime) -> bool {
630 if let Ok(elapsed) = t2.duration_since(t1) {
631 elapsed > d
632 } else {
633 false
634 }
635 }
636 if self.disabled.is_some() {
637 return false;
640 }
641 if let Some(confirmed_at) = self.confirmed_at {
642 if expired_by(confirmed_at, params.lifetime_confirmed, now) {
643 return true;
644 }
645 } else if expired_by(self.added_at, params.lifetime_unconfirmed, now) {
646 return true;
647 }
648
649 if let Some(unlisted_since) = self.unlisted_since {
650 if expired_by(unlisted_since, params.lifetime_unlisted, now) {
651 return true;
652 }
653 }
654
655 false
656 }
657
658 pub(crate) fn record_failure(&mut self, now: Instant, is_primary: bool) {
662 self.set_reachable(Reachable::Unreachable);
663 self.exploratory_circ_pending = false;
664
665 let mut rng = rand::rng();
666 let retry_interval = self
667 .retry_schedule
668 .get_or_insert_with(|| retry_schedule(is_primary))
669 .next_delay(&mut rng);
670
671 self.retry_at = Some(now + retry_interval);
673
674 self.circ_history.n_failures += 1;
675 }
676
677 pub(crate) fn record_attempt(&mut self, connect_attempt: Instant) {
682 self.last_tried_to_connect_at = self
683 .last_tried_to_connect_at
684 .map(|last| last.max(connect_attempt))
685 .or(Some(connect_attempt));
686 }
687
688 pub(crate) fn exploratory_attempt_after(&self, when: Instant) -> bool {
693 self.exploratory_circ_pending
694 && self.last_tried_to_connect_at.map(|t| t > when) == Some(true)
695 }
696
697 #[must_use = "You need to check whether a succeeding guard is confirmed."]
705 pub(crate) fn record_success(
706 &mut self,
707 now: SystemTime,
708 params: &GuardParams,
709 ) -> NewlyConfirmed {
710 self.retry_at = None;
711 self.retry_schedule = None;
712 self.set_reachable(Reachable::Reachable);
713 self.exploratory_circ_pending = false;
714 self.circ_history.n_successes += 1;
715
716 if self.confirmed_at.is_none() {
717 self.confirmed_at = Some(
718 randomize_time(&mut rand::rng(), now, params.lifetime_unconfirmed / 10)
719 .max(self.added_at),
720 );
721 trace!(guard_id = ?self.id, "Newly confirmed");
724 NewlyConfirmed::Yes
725 } else {
726 NewlyConfirmed::No
727 }
728 }
729
730 pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
732 match how {
733 ExternalActivity::DirCache => {
734 self.dir_status.note_success();
735 }
736 }
737 }
738
739 pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
741 match how {
742 ExternalActivity::DirCache => {
743 self.dir_status.note_failure(now);
744 }
745 }
746 }
747
748 pub(crate) fn record_indeterminate_result(&mut self) {
751 self.circ_history.n_indeterminate += 1;
752
753 if let Some(ratio) = self.circ_history.indeterminate_ratio() {
754 const DISABLE_THRESHOLD: f64 = 0.7;
759 const WARN_THRESHOLD: f64 = 0.5;
762
763 if ratio > DISABLE_THRESHOLD {
764 let reason = GuardDisabled::TooManyIndeterminateFailures {
765 history: self.circ_history.clone(),
766 failure_ratio: ratio,
767 threshold_ratio: DISABLE_THRESHOLD,
768 };
769 warn!(guard=?self.id, "Disabling guard: {:.1}% of circuits died under mysterious circumstances, exceeding threshold of {:.1}%", ratio*100.0, (DISABLE_THRESHOLD*100.0));
770 self.disabled = Some(reason.into());
771 } else if ratio > WARN_THRESHOLD && !self.suspicious_behavior_warned {
772 warn!(guard=?self.id, "Questionable guard: {:.1}% of circuits died under mysterious circumstances.", ratio*100.0);
773 self.suspicious_behavior_warned = true;
774 }
775 }
776 }
777
778 pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
780 crate::FirstHop {
781 sample: Some(selection),
782 inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
783 self,
784 )),
785 }
786 }
787
788 pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
790 self.clock_skew = Some(observation);
791 }
792
793 pub(crate) fn skew(&self) -> Option<&SkewObservation> {
796 self.clock_skew.as_ref()
797 }
798
799 #[cfg(test)]
801 pub(crate) fn confirmed(&self) -> bool {
802 self.confirmed_at.is_some()
803 }
804}
805
806impl tor_linkspec::HasAddrs for Guard {
807 fn addrs(&self) -> &[SocketAddr] {
808 &self.orports[..]
809 }
810}
811
812impl tor_linkspec::HasRelayIds for Guard {
813 fn identity(
814 &self,
815 key_type: tor_linkspec::RelayIdType,
816 ) -> Option<tor_linkspec::RelayIdRef<'_>> {
817 self.id.0.identity(key_type)
818 }
819}
820
821impl tor_linkspec::HasChanMethod for Guard {
822 fn chan_method(&self) -> ChannelMethod {
823 match &self.pt_targets[..] {
824 #[cfg(feature = "pt-client")]
825 [first, ..] => ChannelMethod::Pluggable(first.clone()),
826 #[cfg(not(feature = "pt-client"))]
827 [_first, ..] => ChannelMethod::Direct(vec![]), [] => ChannelMethod::Direct(self.orports.clone()),
829 }
830 }
831}
832
833impl tor_linkspec::ChanTarget for Guard {}
834
835impl std::fmt::Display for Guard {
836 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
837 match self.sensitivity {
838 DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f),
839 #[cfg(feature = "bridge-client")]
840 DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f),
841 }
842 }
843}
844
845#[derive(Clone, Debug, Serialize, Deserialize)]
847#[serde(tag = "type")]
848enum GuardDisabled {
849 TooManyIndeterminateFailures {
851 history: CircHistory,
853 failure_ratio: f64,
855 threshold_ratio: f64,
857 },
858}
859
860fn retry_schedule(is_primary: bool) -> RetryDelay {
864 let minimum = if is_primary {
865 Duration::from_secs(30)
866 } else {
867 Duration::from_secs(150)
868 };
869
870 RetryDelay::from_duration(minimum)
871}
872
873#[derive(Debug, Clone, Default, Serialize, Deserialize)]
898pub(crate) struct CircHistory {
899 n_successes: u32,
901 #[allow(dead_code)] n_failures: u32,
904 n_indeterminate: u32,
906}
907
908impl CircHistory {
909 fn indeterminate_ratio(&self) -> Option<f64> {
912 const MIN_OBSERVATIONS: u32 = 15;
916
917 let total = self.n_successes + self.n_indeterminate;
918 if total < MIN_OBSERVATIONS {
919 return None;
920 }
921
922 Some(f64::from(self.n_indeterminate) / f64::from(total))
923 }
924}
925
926#[cfg(test)]
927mod test {
928 #![allow(clippy::bool_assert_comparison)]
930 #![allow(clippy::clone_on_copy)]
931 #![allow(clippy::dbg_macro)]
932 #![allow(clippy::mixed_attributes_style)]
933 #![allow(clippy::print_stderr)]
934 #![allow(clippy::print_stdout)]
935 #![allow(clippy::single_char_pattern)]
936 #![allow(clippy::unwrap_used)]
937 #![allow(clippy::unchecked_duration_subtraction)]
938 #![allow(clippy::useless_vec)]
939 #![allow(clippy::needless_pass_by_value)]
940 use super::*;
942 use crate::ids::FirstHopId;
943 use tor_linkspec::{HasRelayIds, RelayId};
944 use tor_llcrypto::pk::ed25519::Ed25519Identity;
945
946 #[test]
947 fn crate_id() {
948 let id = CrateId::this_crate().unwrap();
949 assert_eq!(&id.crate_name, "tor-guardmgr");
950 assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
951 }
952
953 fn basic_id() -> GuardId {
954 GuardId::new([13; 32].into(), [37; 20].into())
955 }
956 fn basic_guard() -> Guard {
957 let id = basic_id();
958 let ports = vec!["127.0.0.7:7777".parse().unwrap()];
959 let added = SystemTime::now();
960 Guard::new(id, ports, None, added)
961 }
962
963 #[test]
964 fn simple_accessors() {
965 fn ed(id: [u8; 32]) -> RelayId {
966 RelayId::Ed25519(id.into())
967 }
968 let id = basic_id();
969 let g = basic_guard();
970
971 assert_eq!(g.guard_id(), &id);
972 assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
973 assert_eq!(g.addrs(), &["127.0.0.7:7777".parse().unwrap()]);
974 assert_eq!(g.reachable(), Reachable::Untried);
975 assert_eq!(g.reachable(), Reachable::default());
976
977 use crate::GuardUsageBuilder;
978 let mut usage1 = GuardUsageBuilder::new();
979
980 usage1
981 .restrictions()
982 .push(GuardRestriction::AvoidId(ed([22; 32])));
983 let usage1 = usage1.build().unwrap();
984 let mut usage2 = GuardUsageBuilder::new();
985 usage2
986 .restrictions()
987 .push(GuardRestriction::AvoidId(ed([13; 32])));
988 let usage2 = usage2.build().unwrap();
989 let usage3 = GuardUsage::default();
990 let mut usage4 = GuardUsageBuilder::new();
991 usage4
992 .restrictions()
993 .push(GuardRestriction::AvoidId(ed([22; 32])));
994 usage4
995 .restrictions()
996 .push(GuardRestriction::AvoidId(ed([13; 32])));
997 let usage4 = usage4.build().unwrap();
998 let mut usage5 = GuardUsageBuilder::new();
999 usage5.restrictions().push(GuardRestriction::AvoidAllIds(
1000 vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
1001 ));
1002 let usage5 = usage5.build().unwrap();
1003 let mut usage6 = GuardUsageBuilder::new();
1004 usage6.restrictions().push(GuardRestriction::AvoidAllIds(
1005 vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
1006 ));
1007 let usage6 = usage6.build().unwrap();
1008
1009 assert!(g.conforms_to_usage(&usage1));
1010 assert!(!g.conforms_to_usage(&usage2));
1011 assert!(g.conforms_to_usage(&usage3));
1012 assert!(!g.conforms_to_usage(&usage4));
1013 assert!(!g.conforms_to_usage(&usage5));
1014 assert!(g.conforms_to_usage(&usage6));
1015 }
1016
1017 #[allow(clippy::redundant_clone)]
1018 #[test]
1019 fn trickier_usages() {
1020 let g = basic_guard();
1021 use crate::{GuardUsageBuilder, GuardUsageKind};
1022 let data_usage = GuardUsageBuilder::new()
1023 .kind(GuardUsageKind::Data)
1024 .build()
1025 .unwrap();
1026 let dir_usage = GuardUsageBuilder::new()
1027 .kind(GuardUsageKind::OneHopDirectory)
1028 .build()
1029 .unwrap();
1030 assert!(g.conforms_to_usage(&data_usage));
1031 assert!(g.conforms_to_usage(&dir_usage));
1032
1033 let mut g2 = g.clone();
1034 g2.dir_info_missing = true;
1035 assert!(!g2.conforms_to_usage(&data_usage));
1036 assert!(g2.conforms_to_usage(&dir_usage));
1037
1038 let mut g3 = g.clone();
1039 g3.is_dir_cache = false;
1040 assert!(g3.conforms_to_usage(&data_usage));
1041 assert!(!g3.conforms_to_usage(&dir_usage));
1042 }
1043
1044 #[test]
1045 fn record_attempt() {
1046 let t1 = Instant::now() - Duration::from_secs(10);
1047 let t2 = Instant::now() - Duration::from_secs(5);
1048 let t3 = Instant::now();
1049
1050 let mut g = basic_guard();
1051
1052 assert!(g.last_tried_to_connect_at.is_none());
1053 g.record_attempt(t1);
1054 assert_eq!(g.last_tried_to_connect_at, Some(t1));
1055 g.record_attempt(t3);
1056 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1057 g.record_attempt(t2);
1058 assert_eq!(g.last_tried_to_connect_at, Some(t3));
1059 }
1060
1061 #[test]
1062 fn record_failure() {
1063 let t1 = Instant::now() - Duration::from_secs(10);
1064 let t2 = Instant::now();
1065
1066 let mut g = basic_guard();
1067 g.record_failure(t1, true);
1068 assert!(g.retry_schedule.is_some());
1069 assert_eq!(g.reachable(), Reachable::Unreachable);
1070 let retry1 = g.retry_at.unwrap();
1071 assert_eq!(retry1, t1 + Duration::from_secs(30));
1072
1073 g.record_failure(t2, true);
1074 let retry2 = g.retry_at.unwrap();
1075 assert!(retry2 >= t2 + Duration::from_secs(30));
1076 assert!(retry2 <= t2 + Duration::from_secs(200));
1077 }
1078
1079 #[test]
1080 fn record_success() {
1081 let t1 = Instant::now() - Duration::from_secs(10);
1082 let now = SystemTime::now();
1084 let t2 = now + Duration::from_secs(300 * 86400);
1085 let t3 = Instant::now() + Duration::from_secs(310 * 86400);
1086 let t4 = now + Duration::from_secs(320 * 86400);
1087
1088 let mut g = basic_guard();
1089 g.record_failure(t1, true);
1090 assert_eq!(g.reachable(), Reachable::Unreachable);
1091
1092 let conf = g.record_success(t2, &GuardParams::default());
1093 assert_eq!(g.reachable(), Reachable::Reachable);
1094 assert_eq!(conf, NewlyConfirmed::Yes);
1095 assert!(g.retry_at.is_none());
1096 assert!(g.confirmed_at.unwrap() <= t2);
1097 assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
1098 let confirmed_at_orig = g.confirmed_at;
1099
1100 g.record_failure(t3, true);
1101 assert_eq!(g.reachable(), Reachable::Unreachable);
1102
1103 let conf = g.record_success(t4, &GuardParams::default());
1104 assert_eq!(conf, NewlyConfirmed::No);
1105 assert_eq!(g.reachable(), Reachable::Reachable);
1106 assert!(g.retry_at.is_none());
1107 assert_eq!(g.confirmed_at, confirmed_at_orig);
1108 }
1109
1110 #[test]
1111 fn retry() {
1112 let t1 = Instant::now();
1113 let mut g = basic_guard();
1114
1115 g.record_failure(t1, true);
1116 assert!(g.retry_at.is_some());
1117 assert_eq!(g.reachable(), Reachable::Unreachable);
1118
1119 g.consider_retry(t1);
1121 assert!(g.retry_at.is_some());
1122 assert_eq!(g.reachable(), Reachable::Unreachable);
1123
1124 g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
1126 assert!(g.retry_at.is_some());
1127 assert_eq!(g.reachable(), Reachable::Unreachable);
1128
1129 g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
1131 assert!(g.retry_at.is_none());
1132 assert_eq!(g.reachable(), Reachable::Retriable);
1133 }
1134
1135 #[test]
1136 fn expiration() {
1137 const DAY: Duration = Duration::from_secs(24 * 60 * 60);
1138 let params = GuardParams::default();
1139 let now = SystemTime::now();
1140
1141 let g = basic_guard();
1142 assert!(!g.is_expired(¶ms, now));
1143 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1144 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1145 assert!(!g.is_expired(¶ms, now + 70 * DAY));
1146 assert!(g.is_expired(¶ms, now + 200 * DAY)); let mut g = basic_guard();
1149 let _ = g.record_success(now, ¶ms);
1150 assert!(!g.is_expired(¶ms, now));
1151 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1152 assert!(!g.is_expired(¶ms, now + 25 * DAY));
1153 assert!(g.is_expired(¶ms, now + 70 * DAY)); let mut g = basic_guard();
1156 g.mark_unlisted(now);
1157 assert!(!g.is_expired(¶ms, now));
1158 assert!(!g.is_expired(¶ms, now + 10 * DAY));
1159 assert!(g.is_expired(¶ms, now + 25 * DAY)); }
1161
1162 #[test]
1163 fn netdir_integration() {
1164 use tor_netdir::testnet;
1165 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1166 let params = GuardParams::default();
1167 let now = SystemTime::now();
1168
1169 let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
1171 let guard22 = Guard::from_chan_target(&relay22, now, ¶ms);
1172 assert!(guard22.same_relay_ids(&relay22));
1173 assert!(Some(guard22.added_at) <= Some(now));
1174
1175 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
1177 let r = id.get_relay(&netdir).unwrap();
1178 assert!(r.same_relay_ids(&relay22));
1179
1180 let guard255 = Guard::new(
1182 GuardId::new([255; 32].into(), [255; 20].into()),
1183 vec![],
1184 None,
1185 now,
1186 );
1187 let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
1188 assert!(id.get_relay(&netdir).is_none());
1189 }
1190
1191 #[test]
1192 fn update_from_netdir() {
1193 use tor_netdir::testnet;
1194 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
1195 let netdir2 = testnet::construct_custom_netdir(|idx, node, _| {
1197 if idx == 22 {
1198 node.omit_rs = true;
1199 }
1200 })
1201 .unwrap()
1202 .unwrap_if_sufficient()
1203 .unwrap();
1204 let netdir3 = testnet::construct_custom_netdir(|idx, node, _| {
1206 if idx == 22 {
1207 node.omit_rs = true;
1208 } else if idx == 23 {
1209 node.omit_md = true;
1210 }
1211 })
1212 .unwrap()
1213 .unwrap_if_sufficient()
1214 .unwrap();
1215
1216 let now = SystemTime::now();
1218
1219 let mut guard255 = Guard::new(
1221 GuardId::new([255; 32].into(), [255; 20].into()),
1222 vec!["8.8.8.8:53".parse().unwrap()],
1223 None,
1224 now,
1225 );
1226 assert_eq!(guard255.unlisted_since, None);
1227 assert_eq!(guard255.listed_in(&netdir), Some(false));
1228 guard255.update_from_universe(&netdir);
1229 assert_eq!(
1230 guard255.unlisted_since,
1231 Some(netdir.lifetime().valid_after())
1232 );
1233 assert!(!guard255.orports.is_empty());
1234
1235 let mut guard22 = Guard::new(
1237 GuardId::new([22; 32].into(), [22; 20].into()),
1238 vec![],
1239 None,
1240 now,
1241 );
1242 let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
1243 let relay22 = id22.get_relay(&netdir).unwrap();
1244 assert_eq!(guard22.listed_in(&netdir), Some(true));
1245 guard22.update_from_universe(&netdir);
1246 assert_eq!(guard22.unlisted_since, None); assert_eq!(&guard22.orports, relay22.addrs()); assert_eq!(guard22.listed_in(&netdir2), Some(false));
1249 guard22.update_from_universe(&netdir2);
1250 assert_eq!(
1251 guard22.unlisted_since,
1252 Some(netdir2.lifetime().valid_after())
1253 );
1254 assert_eq!(&guard22.orports, relay22.addrs()); assert!(!guard22.dir_info_missing);
1256
1257 let mut guard23 = Guard::new(
1259 GuardId::new([23; 32].into(), [23; 20].into()),
1260 vec![],
1261 None,
1262 now,
1263 );
1264 assert_eq!(guard23.listed_in(&netdir2), Some(true));
1265 assert_eq!(guard23.listed_in(&netdir3), None);
1266 guard23.update_from_universe(&netdir3);
1267 assert!(guard23.dir_info_missing);
1268 assert!(guard23.is_dir_cache);
1269 }
1270
1271 #[test]
1272 fn pending() {
1273 let mut g = basic_guard();
1274 let t1 = Instant::now();
1275 let t2 = t1 + Duration::from_secs(100);
1276 let t3 = t1 + Duration::from_secs(200);
1277
1278 assert!(!g.exploratory_attempt_after(t1));
1279 assert!(!g.exploratory_circ_pending());
1280
1281 g.note_exploratory_circ(true);
1282 g.record_attempt(t2);
1283 assert!(g.exploratory_circ_pending());
1284 assert!(g.exploratory_attempt_after(t1));
1285 assert!(!g.exploratory_attempt_after(t3));
1286
1287 g.note_exploratory_circ(false);
1288 assert!(!g.exploratory_circ_pending());
1289 assert!(!g.exploratory_attempt_after(t1));
1290 assert!(!g.exploratory_attempt_after(t3));
1291 }
1292
1293 #[test]
1294 fn circ_history() {
1295 let mut h = CircHistory {
1296 n_successes: 3,
1297 n_failures: 4,
1298 n_indeterminate: 3,
1299 };
1300 assert!(h.indeterminate_ratio().is_none());
1301
1302 h.n_successes = 20;
1303 assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
1304 }
1305
1306 #[test]
1307 fn disable_on_failure() {
1308 let mut g = basic_guard();
1309 let params = GuardParams::default();
1310
1311 let now = SystemTime::now();
1312
1313 let _ignore = g.record_success(now, ¶ms);
1314 for _ in 0..13 {
1315 g.record_indeterminate_result();
1316 }
1317 assert!(g.disabled.is_none());
1319
1320 g.record_indeterminate_result();
1322 assert!(g.disabled.is_some());
1323
1324 #[allow(unreachable_patterns)]
1325 match g.disabled.unwrap().into_option().unwrap() {
1326 GuardDisabled::TooManyIndeterminateFailures {
1327 history: _,
1328 failure_ratio,
1329 threshold_ratio,
1330 } => {
1331 assert!((failure_ratio - 0.933).abs() < 0.01);
1332 assert!((threshold_ratio - 0.7).abs() < 0.01);
1333 }
1334 other => {
1335 panic!("Wrong variant: {:?}", other);
1336 }
1337 }
1338 }
1339
1340 #[test]
1341 fn mark_retriable() {
1342 let mut g = basic_guard();
1343 use super::Reachable::*;
1344
1345 assert_eq!(g.reachable(), Untried);
1346
1347 for (pre, post) in &[
1348 (Untried, Untried),
1349 (Unreachable, Retriable),
1350 (Reachable, Reachable),
1351 ] {
1352 g.reachable = *pre;
1353 g.mark_retriable();
1354 assert_eq!(g.reachable(), *post);
1355 }
1356 }
1357
1358 #[test]
1359 fn dir_status() {
1360 use crate::GuardUsageBuilder;
1364 let mut g = basic_guard();
1365 let inst = Instant::now();
1366 let st = SystemTime::now();
1367 let sec = Duration::from_secs(1);
1368 let params = GuardParams::default();
1369 let dir_usage = GuardUsageBuilder::new()
1370 .kind(GuardUsageKind::OneHopDirectory)
1371 .build()
1372 .unwrap();
1373 let data_usage = GuardUsage::default();
1374
1375 let _ = g.record_success(st, ¶ms);
1377 assert_eq!(g.next_retry(&dir_usage), None);
1378 assert!(g.ready_for_usage(&dir_usage, inst));
1379 assert_eq!(g.next_retry(&data_usage), None);
1380 assert!(g.ready_for_usage(&data_usage, inst));
1381
1382 g.record_external_failure(ExternalActivity::DirCache, inst);
1384 assert_eq!(g.next_retry(&data_usage), None);
1385 assert!(g.ready_for_usage(&data_usage, inst));
1386 let next_dir_retry = g.next_retry(&dir_usage).unwrap();
1387 assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
1388 assert!(!g.ready_for_usage(&dir_usage, inst));
1389 assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
1390
1391 let _ = g.record_success(st, ¶ms);
1394 assert!(g.ready_for_usage(&data_usage, inst));
1395 assert!(!g.ready_for_usage(&dir_usage, inst));
1396
1397 g.record_failure(inst + sec * 10, true);
1399 let next_circ_retry = g.next_retry(&data_usage).unwrap();
1400 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1401 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1402 assert_eq!(
1403 g.next_retry(&dir_usage).unwrap(),
1404 std::cmp::max(next_circ_retry, next_dir_retry)
1405 );
1406
1407 g.record_external_success(ExternalActivity::DirCache);
1410 assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
1411 assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
1412 assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
1413 assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
1414 }
1415}