use super::{MaybeOwnedRelay, TorPath};
use crate::{DirInfo, Error, PathConfig, Result, TargetPort};
use rand::Rng;
use std::time::SystemTime;
use tor_basic_utils::iter::FilterCount;
use tor_error::{bad_api_usage, internal};
use tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable};
use tor_linkspec::RelayIdSet;
use tor_netdir::{NetDir, Relay, SubnetConfig, WeightRole};
use tor_rtcompat::Runtime;
enum ExitPathBuilderInner<'a> {
WantsPorts(Vec<TargetPort>),
AnyExit {
strict: bool,
},
ChosenExit(Relay<'a>),
}
pub struct ExitPathBuilder<'a> {
inner: ExitPathBuilderInner<'a>,
}
impl<'a> ExitPathBuilder<'a> {
pub 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),
}
}
pub fn from_chosen_exit(exit_relay: Relay<'a>) -> Self {
Self {
inner: ExitPathBuilderInner::ChosenExit(exit_relay),
}
}
pub fn for_any_exit() -> Self {
Self {
inner: ExitPathBuilderInner::AnyExit { strict: true },
}
}
pub(crate) fn for_timeout_testing() -> Self {
Self {
inner: ExitPathBuilderInner::AnyExit { strict: false },
}
}
fn pick_exit<R: Rng>(
&self,
rng: &mut R,
netdir: &'a NetDir,
guard: Option<&MaybeOwnedRelay<'a>>,
config: SubnetConfig,
) -> Result<Relay<'a>> {
let mut can_share = FilterCount::default();
let mut correct_ports = FilterCount::default();
match &self.inner {
ExitPathBuilderInner::AnyExit { strict } => {
let exit = netdir.pick_relay(rng, WeightRole::Exit, |r| {
can_share.count(r.policies_allow_some_port())
&& correct_ports.count(relays_can_share_circuit_opt(r, guard, config))
});
match (exit, strict) {
(Some(exit), _) => return Ok(exit),
(None, true) => {
return Err(Error::NoExit {
can_share,
correct_ports,
})
}
(None, false) => {}
}
netdir
.pick_relay(rng, WeightRole::Exit, |r| {
can_share.count(relays_can_share_circuit_opt(r, guard, config))
})
.ok_or(Error::NoExit {
can_share,
correct_ports,
})
}
ExitPathBuilderInner::WantsPorts(wantports) => Ok(netdir
.pick_relay(rng, WeightRole::Exit, |r| {
can_share.count(relays_can_share_circuit_opt(r, guard, config))
&& correct_ports.count(wantports.iter().all(|p| p.is_supported_by(r)))
})
.ok_or(Error::NoExit {
can_share,
correct_ports,
})?),
ExitPathBuilderInner::ChosenExit(exit_relay) => {
Ok(exit_relay.clone())
}
}
}
pub fn pick_path<R: Rng, RT: Runtime>(
&self,
rng: &mut R,
netdir: DirInfo<'a>,
guards: Option<&GuardMgr<RT>>,
config: &PathConfig,
_now: SystemTime,
) -> Result<(TorPath<'a>, Option<GuardMonitor>, Option<GuardUsable>)> {
let netdir = match netdir {
DirInfo::Directory(d) => d,
_ => {
return Err(bad_api_usage!(
"Tried to build a multihop path without a network directory"
)
.into())
}
};
let subnet_config = config.subnet_config();
let chosen_exit = if let ExitPathBuilderInner::ChosenExit(e) = &self.inner {
Some(e)
} else {
None
};
let path_is_fully_random = chosen_exit.is_none();
let (guard, mon, usable) = match guards {
Some(guardmgr) => {
let mut b = tor_guardmgr::GuardUsageBuilder::default();
b.kind(tor_guardmgr::GuardUsageKind::Data);
if let Some(exit_relay) = chosen_exit {
let mut family = RelayIdSet::new();
family.insert(*exit_relay.id());
family.extend(netdir.known_family_members(exit_relay).map(|r| *r.id()));
b.restrictions()
.push(tor_guardmgr::GuardRestriction::AvoidAllIds(family));
}
let guard_usage = b.build().expect("Failed while building guard usage!");
let (guard, mut mon, usable) = guardmgr.select_guard(guard_usage)?;
let guard = if let Some(ct) = guard.as_circ_target() {
MaybeOwnedRelay::from(ct.clone())
} else {
guard
.get_relay(netdir)
.ok_or_else(|| {
internal!(
"Somehow the guardmgr gave us an unlisted guard {:?}!",
guard
)
})?
.into()
};
if !path_is_fully_random {
mon.ignore_indeterminate_status();
}
(guard, Some(mon), Some(usable))
}
None => {
let mut can_share = FilterCount::default();
let mut correct_usage = FilterCount::default();
let chosen_exit = chosen_exit.map(|relay| MaybeOwnedRelay::from(relay.clone()));
let entry = netdir
.pick_relay(rng, WeightRole::Guard, |r| {
can_share.count(relays_can_share_circuit_opt(
r,
chosen_exit.as_ref(),
subnet_config,
)) && correct_usage.count(r.is_flagged_guard())
})
.ok_or(Error::NoPath {
role: "entry relay",
can_share,
correct_usage,
})?;
(MaybeOwnedRelay::from(entry), None, None)
}
};
let exit: MaybeOwnedRelay = self
.pick_exit(rng, netdir, Some(&guard), subnet_config)?
.into();
let mut can_share = FilterCount::default();
let mut correct_usage = FilterCount::default();
let middle = netdir
.pick_relay(rng, WeightRole::Middle, |r| {
can_share.count(
relays_can_share_circuit(r, &exit, subnet_config)
&& relays_can_share_circuit(r, &guard, subnet_config),
) && correct_usage.count(true)
})
.ok_or(Error::NoPath {
role: "middle relay",
can_share,
correct_usage,
})?;
Ok((
TorPath::new_multihop_from_maybe_owned(vec![
guard,
MaybeOwnedRelay::from(middle),
exit,
]),
mon,
usable,
))
}
}
fn relays_can_share_circuit(
a: &Relay<'_>,
b: &MaybeOwnedRelay<'_>,
subnet_config: SubnetConfig,
) -> bool {
if let MaybeOwnedRelay::Relay(r) = b {
if a.in_same_family(r) {
return false;
};
}
!subnet_config.any_addrs_in_same_subnet(a, b)
}
fn relays_can_share_circuit_opt(
r1: &Relay<'_>,
r2: Option<&MaybeOwnedRelay<'_>>,
c: SubnetConfig,
) -> bool {
match r2 {
Some(r2) => relays_can_share_circuit(r1, r2, c),
None => true,
}
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
#![allow(clippy::clone_on_copy)]
use super::*;
use crate::path::{assert_same_path_when_owned, MaybeOwnedRelay, OwnedPath, TorPathInner};
use crate::test::OptDummyGuardMgr;
use std::collections::HashSet;
use tor_basic_utils::test_rng::testing_rng;
use tor_guardmgr::TestConfig;
use tor_linkspec::{HasRelayIds, RelayIds};
use tor_llcrypto::pk::ed25519::Ed25519Identity;
use tor_netdir::testnet;
use tor_rtcompat::SleepProvider;
impl<'a> MaybeOwnedRelay<'a> {
fn can_share_circuit(
&self,
other: &MaybeOwnedRelay<'_>,
subnet_config: SubnetConfig,
) -> bool {
match self {
MaybeOwnedRelay::Relay(r) => relays_can_share_circuit(r, other, subnet_config),
MaybeOwnedRelay::Owned(r) => {
!subnet_config.any_addrs_in_same_subnet(r.as_ref(), other)
}
}
}
}
fn assert_exit_path_ok(relays: &[MaybeOwnedRelay<'_>]) {
assert_eq!(relays.len(), 3);
let r1 = &relays[0];
let r2 = &relays[1];
let r3 = &relays[2];
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));
assert!(r2.can_share_circuit(r3, subnet_config));
assert!(r1.can_share_circuit(r3, subnet_config));
}
#[test]
fn by_ports() {
let mut rng = testing_rng();
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 guards: OptDummyGuardMgr<'_> = None;
let now = SystemTime::now();
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[..]);
let exit = match &p[2] {
MaybeOwnedRelay::Relay(r) => r,
MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
};
assert!(exit.ipv4_policy().allows_port(1119));
} else {
panic!("Generated the wrong kind of path");
}
}
let chosen = netdir.by_id(&Ed25519Identity::from([0x20; 32])).unwrap();
let config = PathConfig::default();
for _ in 0..1000 {
let (path, _, _) = ExitPathBuilder::from_chosen_exit(chosen.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[..]);
let exit = &p[2];
assert!(exit.same_relay_ids(&chosen));
} else {
panic!("Generated the wrong kind of path");
}
}
}
#[test]
fn any_exit() {
let mut rng = testing_rng();
let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
let dirinfo = (&netdir).into();
let guards: OptDummyGuardMgr<'_> = None;
let now = SystemTime::now();
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[..]);
let exit = match &p[2] {
MaybeOwnedRelay::Relay(r) => r,
MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
};
assert!(exit.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() {
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 guards: OptDummyGuardMgr<'_> = None;
let config = PathConfig::default();
let now = SystemTime::now();
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::NoExit { .. })));
let outcome =
ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, guards, &config, now);
assert!(outcome.is_err());
assert!(matches!(outcome, Err(Error::NoExit { .. })));
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 mut rng = testing_rng();
let dirinfo = (&netdir).into();
let statemgr = tor_persist::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, Some(&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[..]);
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");
}
let mon = mon.unwrap();
assert!(matches!(
mon.inspect_pending_status(),
(GuardStatus::AttemptAbandoned, false)
));
mon.succeeded();
assert!(usable.unwrap().await.unwrap());
}
assert_eq!(distinct_guards.len(), 1);
assert_ne!(distinct_mid.len(), 1);
assert_ne!(distinct_exit.len(), 1);
let guard_relay = netdir
.by_ids(distinct_guards.iter().next().unwrap())
.unwrap();
let exit_relay = netdir.by_ids(distinct_exit.iter().next().unwrap()).unwrap();
let (path, mon, usable) = ExitPathBuilder::from_chosen_exit(exit_relay.clone())
.pick_path(&mut rng, dirinfo, Some(&guards), &config, rt.wallclock())
.unwrap();
assert_eq!(path.len(), 3);
if let TorPathInner::Path(p) = path.inner {
assert_exit_path_ok(&p[..]);
assert_eq!(p[0].ed_identity(), guard_relay.ed_identity());
assert_eq!(p[2].ed_identity(), exit_relay.ed_identity());
} else {
panic!("Wrong kind of path");
}
let mon = mon.unwrap();
assert!(matches!(
mon.inspect_pending_status(),
(GuardStatus::AttemptAbandoned, true)
));
mon.succeeded();
assert!(usable.unwrap().await.unwrap());
let (path, mon, usable) = ExitPathBuilder::from_chosen_exit(guard_relay.clone())
.pick_path(&mut rng, dirinfo, Some(&guards), &config, rt.wallclock())
.unwrap();
assert_eq!(path.len(), 3);
if let TorPathInner::Path(p) = path.inner {
assert_ne!(p[0].ed_identity(), guard_relay.ed_identity());
assert_eq!(p[2].ed_identity(), guard_relay.ed_identity());
} else {
panic!("Wrong kind of path");
}
let mon = mon.unwrap();
assert!(matches!(
mon.inspect_pending_status(),
(GuardStatus::AttemptAbandoned, true)
));
mon.succeeded();
assert!(usable.unwrap().await.unwrap());
});
}
}