use std::time::SystemTime;
use rand::Rng;
use tracing::instrument;
use super::{AnonymousPathBuilder, TorPath};
use crate::path::pick_path;
use crate::{DirInfo, Error, PathConfig, Result, TargetPort};
use tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable};
use tor_linkspec::OwnedChanTarget;
use tor_netdir::{NetDir, Relay};
use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
use tor_rtcompat::Runtime;
#[cfg(feature = "geoip")]
use {tor_geoip::CountryCode, tor_relay_selection::RelayRestriction};
enum ExitPathBuilderInner {
WantsPorts(Vec<TargetPort>),
#[cfg(feature = "geoip")]
ExitInCountry {
country: CountryCode,
ports: Vec<TargetPort>,
},
AnyExit {
strict: bool,
},
}
pub(crate) struct ExitPathBuilder {
inner: ExitPathBuilderInner,
compatible_with: Option<OwnedChanTarget>,
require_stability: bool,
}
impl ExitPathBuilder {
pub(crate) fn from_target_ports(wantports: impl IntoIterator<Item = TargetPort>) -> Self {
let ports: Vec<TargetPort> = wantports.into_iter().collect();
if ports.is_empty() {
return Self::for_any_exit();
}
Self {
inner: ExitPathBuilderInner::WantsPorts(ports),
compatible_with: None,
require_stability: true,
}
}
#[cfg(feature = "geoip")]
pub(crate) fn in_given_country(
country: CountryCode,
wantports: impl IntoIterator<Item = TargetPort>,
) -> Self {
let ports: Vec<TargetPort> = wantports.into_iter().collect();
Self {
inner: ExitPathBuilderInner::ExitInCountry { country, ports },
compatible_with: None,
require_stability: true,
}
}
pub(crate) fn for_any_exit() -> Self {
Self {
inner: ExitPathBuilderInner::AnyExit { strict: true },
compatible_with: None,
require_stability: false,
}
}
#[instrument(skip_all, level = "trace")]
pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
&self,
rng: &mut R,
netdir: DirInfo<'a>,
guards: &GuardMgr<RT>,
config: &PathConfig,
now: SystemTime,
) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
pick_path(self, rng, netdir, guards, config, now)
}
pub(crate) fn for_timeout_testing() -> Self {
Self {
inner: ExitPathBuilderInner::AnyExit { strict: false },
compatible_with: None,
require_stability: false,
}
}
pub(crate) fn require_stability(&mut self, require_stability: bool) -> &mut Self {
self.require_stability = require_stability;
self
}
}
impl AnonymousPathBuilder for ExitPathBuilder {
fn compatible_with(&self) -> Option<&OwnedChanTarget> {
self.compatible_with.as_ref()
}
fn pick_exit<'a, R: Rng>(
&self,
rng: &mut R,
netdir: &'a NetDir,
guard_exclusion: RelayExclusion<'a>,
rs_cfg: &RelaySelectionConfig<'_>,
) -> Result<(Relay<'a>, RelayUsage)> {
let selector = match &self.inner {
ExitPathBuilderInner::AnyExit { strict } => {
let mut selector =
RelaySelector::new(RelayUsage::any_exit(rs_cfg), guard_exclusion);
if !strict {
selector.mark_usage_flexible();
}
selector
}
#[cfg(feature = "geoip")]
ExitPathBuilderInner::ExitInCountry { country, ports } => {
let mut selector = RelaySelector::new(
RelayUsage::exit_to_all_ports(rs_cfg, ports.clone()),
guard_exclusion,
);
selector.push_restriction(RelayRestriction::require_country_code(*country));
selector
}
ExitPathBuilderInner::WantsPorts(wantports) => RelaySelector::new(
RelayUsage::exit_to_all_ports(rs_cfg, wantports.clone()),
guard_exclusion,
),
};
let (relay, info) = selector.select_relay(rng, netdir);
let relay = relay.ok_or_else(|| Error::NoRelay {
path_kind: self.path_kind(),
role: "final hop",
problem: info.to_string(),
})?;
Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
}
fn path_kind(&self) -> &'static str {
use ExitPathBuilderInner::*;
match &self.inner {
WantsPorts(_) => "exit circuit",
#[cfg(feature = "geoip")]
ExitInCountry { .. } => "country-specific exit circuit",
AnyExit { .. } => "testing circuit",
}
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use crate::path::{
MaybeOwnedRelay, OwnedPath, TorPath, TorPathInner, assert_same_path_when_owned,
};
use std::collections::HashSet;
use tor_basic_utils::test_rng::testing_rng;
use tor_guardmgr::TestConfig;
use tor_linkspec::{HasRelayIds, RelayIds};
use tor_netdir::{FamilyRules, SubnetConfig, testnet};
use tor_persist::TestingStateMgr;
use tor_relay_selection::LowLevelRelayPredicate;
use tor_rtcompat::SleepProvider;
use web_time_compat::SystemTimeExt;
impl<'a> MaybeOwnedRelay<'a> {
fn can_share_circuit(
&self,
other: &MaybeOwnedRelay<'_>,
subnet_config: SubnetConfig,
family_rules: FamilyRules,
) -> bool {
use MaybeOwnedRelay as M;
match (self, other) {
(M::Relay(a), M::Relay(b)) => {
let ports = Default::default();
let cfg = RelaySelectionConfig {
long_lived_ports: &ports,
subnet_config,
};
RelayExclusion::exclude_relays_in_same_family(
&cfg,
vec![a.clone()],
family_rules,
)
.low_level_predicate_permits_relay(b)
}
(a, b) => !subnet_config.any_addrs_in_same_subnet(a, b),
}
}
}
fn assert_exit_path_ok(relays: &[MaybeOwnedRelay<'_>], family_rules: FamilyRules) {
assert_eq!(relays.len(), 3);
let r1 = &relays[0];
let r2 = &relays[1];
let r3 = &relays[2];
if let MaybeOwnedRelay::Relay(r1) = r1 {
assert!(r1.low_level_details().is_suitable_as_guard());
}
assert!(!r1.same_relay_ids(r2));
assert!(!r1.same_relay_ids(r3));
assert!(!r2.same_relay_ids(r3));
let subnet_config = SubnetConfig::default();
assert!(r1.can_share_circuit(r2, subnet_config, family_rules));
assert!(r2.can_share_circuit(r3, subnet_config, family_rules));
assert!(r1.can_share_circuit(r3, subnet_config, family_rules));
}
#[test]
fn by_ports() {
tor_rtcompat::test_with_all_runtimes!(|rt| async move {
let mut rng = testing_rng();
let family_rules = FamilyRules::all_family_info();
let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
let ports = vec![TargetPort::ipv4(443), TargetPort::ipv4(1119)];
let dirinfo = (&netdir).into();
let config = PathConfig::default();
let statemgr = TestingStateMgr::new();
let guards =
tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
guards.install_test_netdir(&netdir);
let now = SystemTime::get();
for _ in 0..1000 {
let (path, _, _) = ExitPathBuilder::from_target_ports(ports.clone())
.pick_path(&mut rng, dirinfo, &guards, &config, now)
.unwrap();
assert_same_path_when_owned(&path);
if let TorPathInner::Path(p) = path.inner {
assert_exit_path_ok(&p[..], family_rules);
let exit = match &p[2] {
MaybeOwnedRelay::Relay(r) => r,
MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
};
assert!(exit.low_level_details().ipv4_policy().allows_port(1119));
} else {
panic!("Generated the wrong kind of path");
}
}
});
}
#[test]
fn any_exit() {
tor_rtcompat::test_with_all_runtimes!(|rt| async move {
let mut rng = testing_rng();
let family_rules = FamilyRules::all_family_info();
let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
let dirinfo = (&netdir).into();
let statemgr = TestingStateMgr::new();
let guards =
tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
guards.install_test_netdir(&netdir);
let now = SystemTime::get();
let config = PathConfig::default();
for _ in 0..1000 {
let (path, _, _) = ExitPathBuilder::for_any_exit()
.pick_path(&mut rng, dirinfo, &guards, &config, now)
.unwrap();
assert_same_path_when_owned(&path);
if let TorPathInner::Path(p) = path.inner {
assert_exit_path_ok(&p[..], family_rules);
let exit = match &p[2] {
MaybeOwnedRelay::Relay(r) => r,
MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
};
assert!(exit.low_level_details().policies_allow_some_port());
} else {
panic!("Generated the wrong kind of path");
}
}
});
}
#[test]
fn empty_path() {
let bogus_path = TorPath {
inner: TorPathInner::Path(vec![]),
};
assert!(bogus_path.exit_relay().is_none());
assert!(bogus_path.exit_policy().is_none());
assert_eq!(bogus_path.len(), 0);
let owned: Result<OwnedPath> = (&bogus_path).try_into();
assert!(owned.is_err());
}
#[test]
fn no_exits() {
tor_rtcompat::test_with_all_runtimes!(|rt| async move {
let netdir = testnet::construct_custom_netdir(|_idx, bld, _| {
bld.md.parse_ipv4_policy("reject 1-65535").unwrap();
})
.unwrap()
.unwrap_if_sufficient()
.unwrap();
let mut rng = testing_rng();
let dirinfo = (&netdir).into();
let statemgr = TestingStateMgr::new();
let guards =
tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
guards.install_test_netdir(&netdir);
let config = PathConfig::default();
let now = SystemTime::get();
let outcome = ExitPathBuilder::from_target_ports(vec![TargetPort::ipv4(80)])
.pick_path(&mut rng, dirinfo, &guards, &config, now);
assert!(outcome.is_err());
assert!(matches!(outcome, Err(Error::NoRelay { .. })));
let outcome =
ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, &guards, &config, now);
assert!(outcome.is_err());
assert!(matches!(outcome, Err(Error::NoRelay { .. })));
let outcome = ExitPathBuilder::for_timeout_testing()
.pick_path(&mut rng, dirinfo, &guards, &config, now);
assert!(outcome.is_ok());
});
}
#[test]
fn exitpath_with_guards() {
use tor_guardmgr::GuardStatus;
tor_rtcompat::test_with_all_runtimes!(|rt| async move {
let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
let family_rules = FamilyRules::all_family_info();
let mut rng = testing_rng();
let dirinfo = (&netdir).into();
let statemgr = TestingStateMgr::new();
let guards =
tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
let config = PathConfig::default();
guards.install_test_netdir(&netdir);
let port443 = TargetPort::ipv4(443);
let mut distinct_guards = HashSet::new();
let mut distinct_mid = HashSet::new();
let mut distinct_exit = HashSet::new();
for _ in 0..20 {
let (path, mon, usable) = ExitPathBuilder::from_target_ports(vec![port443])
.pick_path(&mut rng, dirinfo, &guards, &config, rt.wallclock())
.unwrap();
assert_eq!(path.len(), 3);
assert_same_path_when_owned(&path);
if let TorPathInner::Path(p) = path.inner {
assert_exit_path_ok(&p[..], family_rules);
distinct_guards.insert(RelayIds::from_relay_ids(&p[0]));
distinct_mid.insert(RelayIds::from_relay_ids(&p[1]));
distinct_exit.insert(RelayIds::from_relay_ids(&p[2]));
} else {
panic!("Wrong kind of path");
}
assert!(matches!(
mon.inspect_pending_status(),
(GuardStatus::AttemptAbandoned, false)
));
mon.succeeded();
assert!(usable.await.unwrap());
}
assert_eq!(distinct_guards.len(), 1);
assert_ne!(distinct_mid.len(), 1);
assert_ne!(distinct_exit.len(), 1);
});
}
}