use tor_basic_utils::retry::RetryDelay;
use educe::Educe;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::time::{Duration, Instant, SystemTime};
use tracing::{info, trace, warn};
use crate::dirstatus::DirStatus;
use crate::sample::Candidate;
use crate::skew::SkewObservation;
use crate::util::randomize_time;
use crate::{ids::GuardId, GuardParams, GuardRestriction, GuardUsage};
use crate::{sample, ExternalActivity, GuardSetSelector, GuardUsageKind};
#[cfg(feature = "bridge-client")]
use safelog::Redactable as _;
use tor_linkspec::{
ChanTarget, ChannelMethod, HasAddrs, HasChanMethod, HasRelayIds, PtTarget, RelayIds,
};
use tor_persist::{Futureproof, JsonValue};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Educe)]
#[educe(Default)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum Reachable {
Reachable,
Unreachable,
#[educe(Default)]
Untried,
Retriable,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CrateId {
#[serde(rename = "crate")]
crate_name: String,
version: String,
}
impl CrateId {
fn this_crate() -> Option<Self> {
let crate_name = option_env!("CARGO_PKG_NAME")?.to_string();
let version = option_env!("CARGO_PKG_VERSION")?.to_string();
Some(CrateId {
crate_name,
version,
})
}
}
#[derive(Clone, Debug, Educe)]
#[educe(Default)]
pub(crate) enum DisplayRule {
#[educe(Default)]
Sensitive,
#[cfg(feature = "bridge-client")]
Redacted,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Guard {
id: GuardId,
orports: Vec<SocketAddr>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pt_targets: Vec<PtTarget>,
#[serde(with = "humantime_serde")]
added_at: SystemTime,
added_by: Option<CrateId>,
#[serde(default)]
disabled: Option<Futureproof<GuardDisabled>>,
#[serde(with = "humantime_serde")]
confirmed_at: Option<SystemTime>,
#[serde(with = "humantime_serde")]
unlisted_since: Option<SystemTime>,
#[serde(skip)]
dir_info_missing: bool,
#[serde(skip)]
last_tried_to_connect_at: Option<Instant>,
#[serde(skip)]
retry_at: Option<Instant>,
#[serde(skip)]
retry_schedule: Option<RetryDelay>,
#[serde(skip)]
reachable: Reachable,
#[serde(skip)]
is_dir_cache: bool,
#[serde(skip, default = "guard_dirstatus")]
dir_status: DirStatus,
#[serde(skip)]
exploratory_circ_pending: bool,
#[serde(skip)]
circ_history: CircHistory,
#[serde(skip)]
suspicious_behavior_warned: bool,
#[serde(skip)]
clock_skew: Option<SkewObservation>,
#[serde(skip)]
sensitivity: DisplayRule,
#[serde(flatten)]
unknown_fields: HashMap<String, JsonValue>,
}
const GUARD_DIR_RETRY_FLOOR: Duration = Duration::from_secs(60);
fn guard_dirstatus() -> DirStatus {
DirStatus::new(GUARD_DIR_RETRY_FLOOR)
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum NewlyConfirmed {
Yes,
No,
}
impl Guard {
pub(crate) fn from_candidate(
candidate: Candidate,
now: SystemTime,
params: &GuardParams,
) -> Self {
let Candidate {
is_dir_cache,
full_dir_info,
owned_target,
..
} = candidate;
Guard {
is_dir_cache,
dir_info_missing: !full_dir_info,
..Self::from_chan_target(&owned_target, now, params)
}
}
fn from_chan_target<T>(relay: &T, now: SystemTime, params: &GuardParams) -> Self
where
T: ChanTarget,
{
let added_at = randomize_time(
&mut rand::thread_rng(),
now,
params.lifetime_unconfirmed / 10,
);
let pt_target = match relay.chan_method() {
#[cfg(feature = "pt-client")]
ChannelMethod::Pluggable(pt) => Some(pt),
_ => None,
};
Self::new(
GuardId::from_relay_ids(relay),
relay.addrs().into(),
pt_target,
added_at,
)
}
fn new(
id: GuardId,
orports: Vec<SocketAddr>,
pt_target: Option<PtTarget>,
added_at: SystemTime,
) -> Self {
Guard {
id,
orports,
pt_targets: pt_target.into_iter().collect(),
added_at,
added_by: CrateId::this_crate(),
disabled: None,
confirmed_at: None,
unlisted_since: None,
dir_info_missing: false,
last_tried_to_connect_at: None,
reachable: Reachable::Untried,
retry_at: None,
dir_status: guard_dirstatus(),
retry_schedule: None,
is_dir_cache: true,
exploratory_circ_pending: false,
circ_history: CircHistory::default(),
suspicious_behavior_warned: false,
clock_skew: None,
unknown_fields: Default::default(),
sensitivity: DisplayRule::Sensitive,
}
}
pub(crate) fn guard_id(&self) -> &GuardId {
&self.id
}
pub(crate) fn reachable(&self) -> Reachable {
self.reachable
}
pub(crate) fn next_retry(&self, usage: &GuardUsage) -> Option<Instant> {
match &usage.kind {
GuardUsageKind::Data => self.retry_at,
GuardUsageKind::OneHopDirectory => [self.retry_at, self.dir_status.next_retriable()]
.iter()
.flatten()
.max()
.copied(),
}
}
pub(crate) fn usable(&self) -> bool {
self.unlisted_since.is_none() && self.disabled.is_none()
}
pub(crate) fn ready_for_usage(&self, usage: &GuardUsage, now: Instant) -> bool {
if let Some(retry_at) = self.retry_at {
if retry_at > now {
return false;
}
}
match usage.kind {
GuardUsageKind::Data => true,
GuardUsageKind::OneHopDirectory => self.dir_status.usable_at(now),
}
}
pub(crate) fn copy_ephemeral_status_into_newly_loaded_state(self, other: Guard) -> Guard {
assert!(self.same_relay_ids(&other));
Guard {
id: self.id,
pt_targets: self.pt_targets,
orports: self.orports,
added_at: self.added_at,
added_by: self.added_by,
disabled: self.disabled,
confirmed_at: self.confirmed_at,
unlisted_since: self.unlisted_since,
unknown_fields: self.unknown_fields,
last_tried_to_connect_at: other.last_tried_to_connect_at,
retry_at: other.retry_at,
retry_schedule: other.retry_schedule,
reachable: other.reachable,
is_dir_cache: other.is_dir_cache,
exploratory_circ_pending: other.exploratory_circ_pending,
dir_info_missing: other.dir_info_missing,
circ_history: other.circ_history,
suspicious_behavior_warned: other.suspicious_behavior_warned,
dir_status: other.dir_status,
clock_skew: other.clock_skew,
sensitivity: other.sensitivity,
}
}
fn set_reachable(&mut self, r: Reachable) {
use Reachable as R;
if self.reachable != r {
match (self.reachable, r) {
(_, R::Reachable) => info!("We have found that {} is usable.", self),
(R::Untried | R::Reachable, R::Unreachable) => warn!(
"Could not connect to {}. We'll retry later, and let you know if it succeeds.",
self
),
(_, _) => {} }
trace!(guard_id = ?self.id, old=?self.reachable, new=?r, "Guard status changed.");
self.reachable = r;
}
}
pub(crate) fn exploratory_circ_pending(&self) -> bool {
self.exploratory_circ_pending
}
pub(crate) fn note_exploratory_circ(&mut self, pending: bool) {
self.exploratory_circ_pending = pending;
}
pub(crate) fn consider_retry(&mut self, now: Instant) {
if let Some(retry_at) = self.retry_at {
debug_assert!(self.reachable == Reachable::Unreachable);
if retry_at <= now {
self.mark_retriable();
}
}
}
pub(crate) fn mark_retriable(&mut self) {
if self.reachable == Reachable::Unreachable {
self.set_reachable(Reachable::Retriable);
self.retry_at = None;
self.retry_schedule = None;
}
}
fn obeys_restrictions(&self, restrictions: &[GuardRestriction]) -> bool {
restrictions.iter().all(|r| self.obeys_restriction(r))
}
fn obeys_restriction(&self, r: &GuardRestriction) -> bool {
match r {
GuardRestriction::AvoidId(avoid_id) => !self.id.0.has_identity(avoid_id.as_ref()),
GuardRestriction::AvoidAllIds(avoid_ids) => {
self.id.0.identities().all(|id| !avoid_ids.contains(id))
}
}
}
pub(crate) fn conforms_to_usage(&self, usage: &GuardUsage) -> bool {
match usage.kind {
GuardUsageKind::OneHopDirectory => {
if !self.is_dir_cache {
return false;
}
}
GuardUsageKind::Data => {
if self.dir_info_missing {
return false;
}
}
}
self.obeys_restrictions(&usage.restrictions[..])
}
pub(crate) fn listed_in<U: sample::Universe>(&self, universe: &U) -> Option<bool> {
universe.contains(self)
}
pub(crate) fn update_from_universe<U: sample::Universe>(&mut self, universe: &U) {
use sample::CandidateStatus::*;
let listed_as_guard = match universe.status(self) {
Present(Candidate {
listed_as_guard,
is_dir_cache,
full_dir_info,
owned_target,
sensitivity,
}) => {
self.orports = owned_target.addrs().into();
self.pt_targets = match owned_target.chan_method() {
#[cfg(feature = "pt-client")]
ChannelMethod::Pluggable(pt) => vec![pt],
_ => Vec::new(),
};
self.is_dir_cache = is_dir_cache;
assert!(owned_target.has_all_relay_ids_from(self));
self.id = GuardId(RelayIds::from_relay_ids(&owned_target));
self.dir_info_missing = !full_dir_info;
self.sensitivity = sensitivity;
listed_as_guard
}
Absent => false, Uncertain => {
self.dir_info_missing = true;
return;
}
};
self.dir_info_missing = false;
if listed_as_guard {
self.mark_listed();
} else {
self.mark_unlisted(universe.timestamp());
}
}
fn mark_listed(&mut self) {
if self.unlisted_since.is_some() {
trace!(guard_id = ?self.id, "Guard is now listed again.");
self.unlisted_since = None;
}
}
fn mark_unlisted(&mut self, now: SystemTime) {
if self.unlisted_since.is_none() {
trace!(guard_id = ?self.id, "Guard is now unlisted.");
self.unlisted_since = Some(now);
}
}
pub(crate) fn is_expired(&self, params: &GuardParams, now: SystemTime) -> bool {
fn expired_by(t1: SystemTime, d: Duration, t2: SystemTime) -> bool {
if let Ok(elapsed) = t2.duration_since(t1) {
elapsed > d
} else {
false
}
}
if self.disabled.is_some() {
return false;
}
if let Some(confirmed_at) = self.confirmed_at {
if expired_by(confirmed_at, params.lifetime_confirmed, now) {
return true;
}
} else if expired_by(self.added_at, params.lifetime_unconfirmed, now) {
return true;
}
if let Some(unlisted_since) = self.unlisted_since {
if expired_by(unlisted_since, params.lifetime_unlisted, now) {
return true;
}
}
false
}
pub(crate) fn record_failure(&mut self, now: Instant, is_primary: bool) {
self.set_reachable(Reachable::Unreachable);
self.exploratory_circ_pending = false;
let mut rng = rand::thread_rng();
let retry_interval = self
.retry_schedule
.get_or_insert_with(|| retry_schedule(is_primary))
.next_delay(&mut rng);
self.retry_at = Some(now + retry_interval);
self.circ_history.n_failures += 1;
}
pub(crate) fn record_attempt(&mut self, connect_attempt: Instant) {
self.last_tried_to_connect_at = self
.last_tried_to_connect_at
.map(|last| last.max(connect_attempt))
.or(Some(connect_attempt));
}
pub(crate) fn exploratory_attempt_after(&self, when: Instant) -> bool {
self.exploratory_circ_pending
&& self.last_tried_to_connect_at.map(|t| t > when) == Some(true)
}
#[must_use = "You need to check whether a succeeding guard is confirmed."]
pub(crate) fn record_success(
&mut self,
now: SystemTime,
params: &GuardParams,
) -> NewlyConfirmed {
self.retry_at = None;
self.retry_schedule = None;
self.set_reachable(Reachable::Reachable);
self.exploratory_circ_pending = false;
self.circ_history.n_successes += 1;
if self.confirmed_at.is_none() {
self.confirmed_at = Some(
randomize_time(
&mut rand::thread_rng(),
now,
params.lifetime_unconfirmed / 10,
)
.max(self.added_at),
);
trace!(guard_id = ?self.id, "Newly confirmed");
NewlyConfirmed::Yes
} else {
NewlyConfirmed::No
}
}
pub(crate) fn record_external_success(&mut self, how: ExternalActivity) {
match how {
ExternalActivity::DirCache => {
self.dir_status.note_success();
}
}
}
pub(crate) fn record_external_failure(&mut self, how: ExternalActivity, now: Instant) {
match how {
ExternalActivity::DirCache => {
self.dir_status.note_failure(now);
}
}
}
pub(crate) fn record_indeterminate_result(&mut self) {
self.circ_history.n_indeterminate += 1;
if let Some(ratio) = self.circ_history.indeterminate_ratio() {
const DISABLE_THRESHOLD: f64 = 0.7;
const WARN_THRESHOLD: f64 = 0.5;
if ratio > DISABLE_THRESHOLD {
let reason = GuardDisabled::TooManyIndeterminateFailures {
history: self.circ_history.clone(),
failure_ratio: ratio,
threshold_ratio: DISABLE_THRESHOLD,
};
warn!(guard=?self.id, "Disabling guard: {:.1}% of circuits died under mysterious circumstances, exceeding threshold of {:.1}%", ratio*100.0, (DISABLE_THRESHOLD*100.0));
self.disabled = Some(reason.into());
} else if ratio > WARN_THRESHOLD && !self.suspicious_behavior_warned {
warn!(guard=?self.id, "Questionable guard: {:.1}% of circuits died under mysterious circumstances.", ratio*100.0);
self.suspicious_behavior_warned = true;
}
}
}
pub(crate) fn get_external_rep(&self, selection: GuardSetSelector) -> crate::FirstHop {
crate::FirstHop {
sample: Some(selection),
inner: crate::FirstHopInner::Chan(tor_linkspec::OwnedChanTarget::from_chan_target(
self,
)),
}
}
pub(crate) fn note_skew(&mut self, observation: SkewObservation) {
self.clock_skew = Some(observation);
}
pub(crate) fn skew(&self) -> Option<&SkewObservation> {
self.clock_skew.as_ref()
}
#[cfg(test)]
pub(crate) fn confirmed(&self) -> bool {
self.confirmed_at.is_some()
}
}
impl tor_linkspec::HasAddrs for Guard {
fn addrs(&self) -> &[SocketAddr] {
&self.orports[..]
}
}
impl tor_linkspec::HasRelayIds for Guard {
fn identity(
&self,
key_type: tor_linkspec::RelayIdType,
) -> Option<tor_linkspec::RelayIdRef<'_>> {
self.id.0.identity(key_type)
}
}
impl tor_linkspec::HasChanMethod for Guard {
fn chan_method(&self) -> ChannelMethod {
match &self.pt_targets[..] {
#[cfg(feature = "pt-client")]
[first, ..] => ChannelMethod::Pluggable(first.clone()),
#[cfg(not(feature = "pt-client"))]
[_first, ..] => ChannelMethod::Direct(vec![]), [] => ChannelMethod::Direct(self.orports.clone()),
}
}
}
impl tor_linkspec::ChanTarget for Guard {}
impl std::fmt::Display for Guard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.sensitivity {
DisplayRule::Sensitive => safelog::sensitive(self.display_chan_target()).fmt(f),
#[cfg(feature = "bridge-client")]
DisplayRule::Redacted => self.display_chan_target().redacted().fmt(f),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum GuardDisabled {
TooManyIndeterminateFailures {
history: CircHistory,
failure_ratio: f64,
threshold_ratio: f64,
},
}
fn retry_schedule(is_primary: bool) -> RetryDelay {
let minimum = if is_primary {
Duration::from_secs(30)
} else {
Duration::from_secs(150)
};
RetryDelay::from_duration(minimum)
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct CircHistory {
n_successes: u32,
#[allow(dead_code)] n_failures: u32,
n_indeterminate: u32,
}
impl CircHistory {
fn indeterminate_ratio(&self) -> Option<f64> {
const MIN_OBSERVATIONS: u32 = 15;
let total = self.n_successes + self.n_indeterminate;
if total < MIN_OBSERVATIONS {
return None;
}
Some(f64::from(self.n_indeterminate) / f64::from(total))
}
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::ids::FirstHopId;
use tor_linkspec::{HasRelayIds, RelayId};
use tor_llcrypto::pk::ed25519::Ed25519Identity;
#[test]
fn crate_id() {
let id = CrateId::this_crate().unwrap();
assert_eq!(&id.crate_name, "tor-guardmgr");
assert_eq!(Some(id.version.as_ref()), option_env!("CARGO_PKG_VERSION"));
}
fn basic_id() -> GuardId {
GuardId::new([13; 32].into(), [37; 20].into())
}
fn basic_guard() -> Guard {
let id = basic_id();
let ports = vec!["127.0.0.7:7777".parse().unwrap()];
let added = SystemTime::now();
Guard::new(id, ports, None, added)
}
#[test]
fn simple_accessors() {
fn ed(id: [u8; 32]) -> RelayId {
RelayId::Ed25519(id.into())
}
let id = basic_id();
let g = basic_guard();
assert_eq!(g.guard_id(), &id);
assert!(g.same_relay_ids(&FirstHopId::in_sample(GuardSetSelector::Default, id)));
assert_eq!(g.addrs(), &["127.0.0.7:7777".parse().unwrap()]);
assert_eq!(g.reachable(), Reachable::Untried);
assert_eq!(g.reachable(), Reachable::default());
use crate::GuardUsageBuilder;
let mut usage1 = GuardUsageBuilder::new();
usage1
.restrictions()
.push(GuardRestriction::AvoidId(ed([22; 32])));
let usage1 = usage1.build().unwrap();
let mut usage2 = GuardUsageBuilder::new();
usage2
.restrictions()
.push(GuardRestriction::AvoidId(ed([13; 32])));
let usage2 = usage2.build().unwrap();
let usage3 = GuardUsage::default();
let mut usage4 = GuardUsageBuilder::new();
usage4
.restrictions()
.push(GuardRestriction::AvoidId(ed([22; 32])));
usage4
.restrictions()
.push(GuardRestriction::AvoidId(ed([13; 32])));
let usage4 = usage4.build().unwrap();
let mut usage5 = GuardUsageBuilder::new();
usage5.restrictions().push(GuardRestriction::AvoidAllIds(
vec![ed([22; 32]), ed([13; 32])].into_iter().collect(),
));
let usage5 = usage5.build().unwrap();
let mut usage6 = GuardUsageBuilder::new();
usage6.restrictions().push(GuardRestriction::AvoidAllIds(
vec![ed([99; 32]), ed([100; 32])].into_iter().collect(),
));
let usage6 = usage6.build().unwrap();
assert!(g.conforms_to_usage(&usage1));
assert!(!g.conforms_to_usage(&usage2));
assert!(g.conforms_to_usage(&usage3));
assert!(!g.conforms_to_usage(&usage4));
assert!(!g.conforms_to_usage(&usage5));
assert!(g.conforms_to_usage(&usage6));
}
#[allow(clippy::redundant_clone)]
#[test]
fn trickier_usages() {
let g = basic_guard();
use crate::{GuardUsageBuilder, GuardUsageKind};
let data_usage = GuardUsageBuilder::new()
.kind(GuardUsageKind::Data)
.build()
.unwrap();
let dir_usage = GuardUsageBuilder::new()
.kind(GuardUsageKind::OneHopDirectory)
.build()
.unwrap();
assert!(g.conforms_to_usage(&data_usage));
assert!(g.conforms_to_usage(&dir_usage));
let mut g2 = g.clone();
g2.dir_info_missing = true;
assert!(!g2.conforms_to_usage(&data_usage));
assert!(g2.conforms_to_usage(&dir_usage));
let mut g3 = g.clone();
g3.is_dir_cache = false;
assert!(g3.conforms_to_usage(&data_usage));
assert!(!g3.conforms_to_usage(&dir_usage));
}
#[test]
fn record_attempt() {
let t1 = Instant::now() - Duration::from_secs(10);
let t2 = Instant::now() - Duration::from_secs(5);
let t3 = Instant::now();
let mut g = basic_guard();
assert!(g.last_tried_to_connect_at.is_none());
g.record_attempt(t1);
assert_eq!(g.last_tried_to_connect_at, Some(t1));
g.record_attempt(t3);
assert_eq!(g.last_tried_to_connect_at, Some(t3));
g.record_attempt(t2);
assert_eq!(g.last_tried_to_connect_at, Some(t3));
}
#[test]
fn record_failure() {
let t1 = Instant::now() - Duration::from_secs(10);
let t2 = Instant::now();
let mut g = basic_guard();
g.record_failure(t1, true);
assert!(g.retry_schedule.is_some());
assert_eq!(g.reachable(), Reachable::Unreachable);
let retry1 = g.retry_at.unwrap();
assert_eq!(retry1, t1 + Duration::from_secs(30));
g.record_failure(t2, true);
let retry2 = g.retry_at.unwrap();
assert!(retry2 >= t2 + Duration::from_secs(30));
assert!(retry2 <= t2 + Duration::from_secs(200));
}
#[test]
fn record_success() {
let t1 = Instant::now() - Duration::from_secs(10);
let now = SystemTime::now();
let t2 = now + Duration::from_secs(300 * 86400);
let t3 = Instant::now() + Duration::from_secs(310 * 86400);
let t4 = now + Duration::from_secs(320 * 86400);
let mut g = basic_guard();
g.record_failure(t1, true);
assert_eq!(g.reachable(), Reachable::Unreachable);
let conf = g.record_success(t2, &GuardParams::default());
assert_eq!(g.reachable(), Reachable::Reachable);
assert_eq!(conf, NewlyConfirmed::Yes);
assert!(g.retry_at.is_none());
assert!(g.confirmed_at.unwrap() <= t2);
assert!(g.confirmed_at.unwrap() >= t2 - Duration::from_secs(12 * 86400));
let confirmed_at_orig = g.confirmed_at;
g.record_failure(t3, true);
assert_eq!(g.reachable(), Reachable::Unreachable);
let conf = g.record_success(t4, &GuardParams::default());
assert_eq!(conf, NewlyConfirmed::No);
assert_eq!(g.reachable(), Reachable::Reachable);
assert!(g.retry_at.is_none());
assert_eq!(g.confirmed_at, confirmed_at_orig);
}
#[test]
fn retry() {
let t1 = Instant::now();
let mut g = basic_guard();
g.record_failure(t1, true);
assert!(g.retry_at.is_some());
assert_eq!(g.reachable(), Reachable::Unreachable);
g.consider_retry(t1);
assert!(g.retry_at.is_some());
assert_eq!(g.reachable(), Reachable::Unreachable);
g.consider_retry(g.retry_at.unwrap() - Duration::from_secs(1));
assert!(g.retry_at.is_some());
assert_eq!(g.reachable(), Reachable::Unreachable);
g.consider_retry(g.retry_at.unwrap() + Duration::from_secs(1));
assert!(g.retry_at.is_none());
assert_eq!(g.reachable(), Reachable::Retriable);
}
#[test]
fn expiration() {
const DAY: Duration = Duration::from_secs(24 * 60 * 60);
let params = GuardParams::default();
let now = SystemTime::now();
let g = basic_guard();
assert!(!g.is_expired(¶ms, now));
assert!(!g.is_expired(¶ms, now + 10 * DAY));
assert!(!g.is_expired(¶ms, now + 25 * DAY));
assert!(!g.is_expired(¶ms, now + 70 * DAY));
assert!(g.is_expired(¶ms, now + 200 * DAY));
let mut g = basic_guard();
let _ = g.record_success(now, ¶ms);
assert!(!g.is_expired(¶ms, now));
assert!(!g.is_expired(¶ms, now + 10 * DAY));
assert!(!g.is_expired(¶ms, now + 25 * DAY));
assert!(g.is_expired(¶ms, now + 70 * DAY));
let mut g = basic_guard();
g.mark_unlisted(now);
assert!(!g.is_expired(¶ms, now));
assert!(!g.is_expired(¶ms, now + 10 * DAY));
assert!(g.is_expired(¶ms, now + 25 * DAY)); }
#[test]
fn netdir_integration() {
use tor_netdir::testnet;
let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
let params = GuardParams::default();
let now = SystemTime::now();
let relay22 = netdir.by_id(&Ed25519Identity::from([22; 32])).unwrap();
let guard22 = Guard::from_chan_target(&relay22, now, ¶ms);
assert!(guard22.same_relay_ids(&relay22));
assert!(Some(guard22.added_at) <= Some(now));
let id = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id);
let r = id.get_relay(&netdir).unwrap();
assert!(r.same_relay_ids(&relay22));
let guard255 = Guard::new(
GuardId::new([255; 32].into(), [255; 20].into()),
vec![],
None,
now,
);
let id = FirstHopId::in_sample(GuardSetSelector::Default, guard255.id);
assert!(id.get_relay(&netdir).is_none());
}
#[test]
fn update_from_netdir() {
use tor_netdir::testnet;
let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
let netdir2 = testnet::construct_custom_netdir(|idx, mut node| {
if idx == 22 {
node.omit_rs = true;
}
})
.unwrap()
.unwrap_if_sufficient()
.unwrap();
let netdir3 = testnet::construct_custom_netdir(|idx, mut node| {
if idx == 22 {
node.omit_rs = true;
} else if idx == 23 {
node.omit_md = true;
}
})
.unwrap()
.unwrap_if_sufficient()
.unwrap();
let now = SystemTime::now();
let mut guard255 = Guard::new(
GuardId::new([255; 32].into(), [255; 20].into()),
vec!["8.8.8.8:53".parse().unwrap()],
None,
now,
);
assert_eq!(guard255.unlisted_since, None);
assert_eq!(guard255.listed_in(&netdir), Some(false));
guard255.update_from_universe(&netdir);
assert_eq!(
guard255.unlisted_since,
Some(netdir.lifetime().valid_after())
);
assert!(!guard255.orports.is_empty());
let mut guard22 = Guard::new(
GuardId::new([22; 32].into(), [22; 20].into()),
vec![],
None,
now,
);
let id22: FirstHopId = FirstHopId::in_sample(GuardSetSelector::Default, guard22.id.clone());
let relay22 = id22.get_relay(&netdir).unwrap();
assert_eq!(guard22.listed_in(&netdir), Some(true));
guard22.update_from_universe(&netdir);
assert_eq!(guard22.unlisted_since, None); assert_eq!(&guard22.orports, relay22.addrs()); assert_eq!(guard22.listed_in(&netdir2), Some(false));
guard22.update_from_universe(&netdir2);
assert_eq!(
guard22.unlisted_since,
Some(netdir2.lifetime().valid_after())
);
assert_eq!(&guard22.orports, relay22.addrs()); assert!(!guard22.dir_info_missing);
let mut guard23 = Guard::new(
GuardId::new([23; 32].into(), [23; 20].into()),
vec![],
None,
now,
);
assert_eq!(guard23.listed_in(&netdir2), Some(true));
assert_eq!(guard23.listed_in(&netdir3), None);
guard23.update_from_universe(&netdir3);
assert!(guard23.dir_info_missing);
assert!(guard23.is_dir_cache);
}
#[test]
fn pending() {
let mut g = basic_guard();
let t1 = Instant::now();
let t2 = t1 + Duration::from_secs(100);
let t3 = t1 + Duration::from_secs(200);
assert!(!g.exploratory_attempt_after(t1));
assert!(!g.exploratory_circ_pending());
g.note_exploratory_circ(true);
g.record_attempt(t2);
assert!(g.exploratory_circ_pending());
assert!(g.exploratory_attempt_after(t1));
assert!(!g.exploratory_attempt_after(t3));
g.note_exploratory_circ(false);
assert!(!g.exploratory_circ_pending());
assert!(!g.exploratory_attempt_after(t1));
assert!(!g.exploratory_attempt_after(t3));
}
#[test]
fn circ_history() {
let mut h = CircHistory {
n_successes: 3,
n_failures: 4,
n_indeterminate: 3,
};
assert!(h.indeterminate_ratio().is_none());
h.n_successes = 20;
assert!((h.indeterminate_ratio().unwrap() - 3.0 / 23.0).abs() < 0.0001);
}
#[test]
fn disable_on_failure() {
let mut g = basic_guard();
let params = GuardParams::default();
let now = SystemTime::now();
let _ignore = g.record_success(now, ¶ms);
for _ in 0..13 {
g.record_indeterminate_result();
}
assert!(g.disabled.is_none());
g.record_indeterminate_result();
assert!(g.disabled.is_some());
#[allow(unreachable_patterns)]
match g.disabled.unwrap().into_option().unwrap() {
GuardDisabled::TooManyIndeterminateFailures {
history: _,
failure_ratio,
threshold_ratio,
} => {
assert!((failure_ratio - 0.933).abs() < 0.01);
assert!((threshold_ratio - 0.7).abs() < 0.01);
}
other => {
panic!("Wrong variant: {:?}", other);
}
}
}
#[test]
fn mark_retriable() {
let mut g = basic_guard();
use super::Reachable::*;
assert_eq!(g.reachable(), Untried);
for (pre, post) in &[
(Untried, Untried),
(Unreachable, Retriable),
(Reachable, Reachable),
] {
g.reachable = *pre;
g.mark_retriable();
assert_eq!(g.reachable(), *post);
}
}
#[test]
fn dir_status() {
use crate::GuardUsageBuilder;
let mut g = basic_guard();
let inst = Instant::now();
let st = SystemTime::now();
let sec = Duration::from_secs(1);
let params = GuardParams::default();
let dir_usage = GuardUsageBuilder::new()
.kind(GuardUsageKind::OneHopDirectory)
.build()
.unwrap();
let data_usage = GuardUsage::default();
let _ = g.record_success(st, ¶ms);
assert_eq!(g.next_retry(&dir_usage), None);
assert!(g.ready_for_usage(&dir_usage, inst));
assert_eq!(g.next_retry(&data_usage), None);
assert!(g.ready_for_usage(&data_usage, inst));
g.record_external_failure(ExternalActivity::DirCache, inst);
assert_eq!(g.next_retry(&data_usage), None);
assert!(g.ready_for_usage(&data_usage, inst));
let next_dir_retry = g.next_retry(&dir_usage).unwrap();
assert!(next_dir_retry >= inst + GUARD_DIR_RETRY_FLOOR);
assert!(!g.ready_for_usage(&dir_usage, inst));
assert!(g.ready_for_usage(&dir_usage, next_dir_retry));
let _ = g.record_success(st, ¶ms);
assert!(g.ready_for_usage(&data_usage, inst));
assert!(!g.ready_for_usage(&dir_usage, inst));
g.record_failure(inst + sec * 10, true);
let next_circ_retry = g.next_retry(&data_usage).unwrap();
assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
assert_eq!(
g.next_retry(&dir_usage).unwrap(),
std::cmp::max(next_circ_retry, next_dir_retry)
);
g.record_external_success(ExternalActivity::DirCache);
assert_eq!(g.next_retry(&data_usage).unwrap(), next_circ_retry);
assert_eq!(g.next_retry(&dir_usage).unwrap(), next_circ_retry);
assert!(!g.ready_for_usage(&dir_usage, inst + sec * 10));
assert!(!g.ready_for_usage(&data_usage, inst + sec * 10));
}
}