pub(crate) mod dirpath;
pub(crate) mod exitpath;
#[cfg(feature = "hs-common")]
pub(crate) mod hspath;
use std::result::Result as StdResult;
use std::time::SystemTime;
use itertools::Either;
use rand::Rng;
use tor_dircommon::fallback::FallbackDir;
use tor_error::{Bug, bad_api_usage, internal};
#[cfg(feature = "geoip")]
use tor_geoip::{CountryCode, HasCountryCode};
use tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable};
use tor_linkspec::{HasAddrs, HasRelayIds, OwnedChanTarget, OwnedCircTarget, RelayIdSet};
use tor_netdir::{FamilyRules, NetDir, Relay};
use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
use tor_rtcompat::Runtime;
#[cfg(all(feature = "vanguards", feature = "hs-common"))]
use tor_guardmgr::vanguards::Vanguard;
use tracing::instrument;
use crate::usage::ExitPolicy;
use crate::{DirInfo, Error, PathConfig, Result};
pub struct TorPath<'a> {
inner: TorPathInner<'a>,
}
enum TorPathInner<'a> {
OneHop(Relay<'a>), FallbackOneHop(&'a FallbackDir),
OwnedOneHop(OwnedChanTarget),
Path(Vec<MaybeOwnedRelay<'a>>),
}
#[derive(Clone)]
enum MaybeOwnedRelay<'a> {
Relay(Relay<'a>),
Owned(Box<OwnedCircTarget>),
}
impl<'a> MaybeOwnedRelay<'a> {
fn to_owned(&self) -> OwnedCircTarget {
match self {
MaybeOwnedRelay::Relay(r) => OwnedCircTarget::from_circ_target(r),
MaybeOwnedRelay::Owned(o) => o.as_ref().clone(),
}
}
}
impl<'a> From<OwnedCircTarget> for MaybeOwnedRelay<'a> {
fn from(ct: OwnedCircTarget) -> Self {
MaybeOwnedRelay::Owned(Box::new(ct))
}
}
impl<'a> From<Relay<'a>> for MaybeOwnedRelay<'a> {
fn from(r: Relay<'a>) -> Self {
MaybeOwnedRelay::Relay(r)
}
}
impl<'a> HasAddrs for MaybeOwnedRelay<'a> {
fn addrs(&self) -> impl Iterator<Item = std::net::SocketAddr> {
match self {
MaybeOwnedRelay::Relay(r) => Either::Left(r.addrs()),
MaybeOwnedRelay::Owned(r) => Either::Right(r.addrs()),
}
}
}
impl<'a> HasRelayIds for MaybeOwnedRelay<'a> {
fn identity(
&self,
key_type: tor_linkspec::RelayIdType,
) -> Option<tor_linkspec::RelayIdRef<'_>> {
match self {
MaybeOwnedRelay::Relay(r) => r.identity(key_type),
MaybeOwnedRelay::Owned(r) => r.identity(key_type),
}
}
}
#[cfg(all(feature = "vanguards", feature = "hs-common"))]
impl<'a> From<Vanguard<'a>> for MaybeOwnedRelay<'a> {
fn from(r: Vanguard<'a>) -> Self {
MaybeOwnedRelay::Relay(r.relay().clone())
}
}
impl<'a> TorPath<'a> {
pub fn new_one_hop(relay: Relay<'a>) -> Self {
Self {
inner: TorPathInner::OneHop(relay),
}
}
pub fn new_fallback_one_hop(fallback_dir: &'a FallbackDir) -> Self {
Self {
inner: TorPathInner::FallbackOneHop(fallback_dir),
}
}
pub fn new_one_hop_owned<T: tor_linkspec::ChanTarget>(target: &T) -> Self {
Self {
inner: TorPathInner::OwnedOneHop(OwnedChanTarget::from_chan_target(target)),
}
}
pub fn new_multihop(relays: impl IntoIterator<Item = Relay<'a>>) -> Self {
Self {
inner: TorPathInner::Path(relays.into_iter().map(MaybeOwnedRelay::from).collect()),
}
}
fn new_multihop_from_maybe_owned(relays: Vec<MaybeOwnedRelay<'a>>) -> Self {
Self {
inner: TorPathInner::Path(relays),
}
}
fn exit_relay(&self) -> Option<&MaybeOwnedRelay<'a>> {
match &self.inner {
TorPathInner::Path(relays) if !relays.is_empty() => Some(&relays[relays.len() - 1]),
_ => None,
}
}
pub(crate) fn exit_policy(&self) -> Option<ExitPolicy> {
self.exit_relay().and_then(|r| match r {
MaybeOwnedRelay::Relay(r) => Some(ExitPolicy::from_relay(r)),
MaybeOwnedRelay::Owned(_) => None,
})
}
#[cfg(feature = "geoip")]
pub(crate) fn country_code(&self) -> Option<CountryCode> {
self.exit_relay().and_then(|r| match r {
MaybeOwnedRelay::Relay(r) => r.country_code(),
MaybeOwnedRelay::Owned(_) => None,
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
use TorPathInner::*;
match &self.inner {
OneHop(_) => 1,
FallbackOneHop(_) => 1,
OwnedOneHop(_) => 1,
Path(p) => p.len(),
}
}
pub(crate) fn appears_stable(&self) -> bool {
match &self.inner {
TorPathInner::OneHop(r) => r.low_level_details().is_flagged_stable(),
TorPathInner::FallbackOneHop(_) => true,
TorPathInner::OwnedOneHop(_) => true,
TorPathInner::Path(relays) => relays.iter().all(|maybe_owned| match maybe_owned {
MaybeOwnedRelay::Relay(r) => r.low_level_details().is_flagged_stable(),
MaybeOwnedRelay::Owned(_) => true,
}),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum OwnedPath {
ChannelOnly(OwnedChanTarget),
Normal(Vec<OwnedCircTarget>),
}
impl<'a> TryFrom<&TorPath<'a>> for OwnedPath {
type Error = crate::Error;
fn try_from(p: &TorPath<'a>) -> Result<OwnedPath> {
use TorPathInner::*;
Ok(match &p.inner {
FallbackOneHop(h) => OwnedPath::ChannelOnly(OwnedChanTarget::from_chan_target(*h)),
OneHop(h) => OwnedPath::Normal(vec![OwnedCircTarget::from_circ_target(h)]),
OwnedOneHop(owned) => OwnedPath::ChannelOnly(owned.clone()),
Path(p) if !p.is_empty() => {
OwnedPath::Normal(p.iter().map(MaybeOwnedRelay::to_owned).collect())
}
Path(_) => {
return Err(bad_api_usage!("Path with no entries!").into());
}
})
}
}
impl OwnedPath {
#[allow(clippy::len_without_is_empty)]
pub(crate) fn len(&self) -> usize {
match self {
OwnedPath::ChannelOnly(_) => 1,
OwnedPath::Normal(p) => p.len(),
}
}
pub(crate) fn first_hop_as_chantarget(&self) -> &OwnedChanTarget {
match self {
OwnedPath::ChannelOnly(ct) => ct,
OwnedPath::Normal(path) => path[0].chan_target(),
}
}
}
trait AnonymousPathBuilder {
fn compatible_with(&self) -> Option<&OwnedChanTarget>;
fn path_kind(&self) -> &'static str;
fn pick_exit<'a, R: Rng>(
&self,
rng: &mut R,
netdir: &'a NetDir,
guard_exclusion: RelayExclusion<'a>,
rs_cfg: &RelaySelectionConfig<'_>,
) -> Result<(Relay<'a>, RelayUsage)>;
}
#[instrument(skip_all, level = "trace")]
fn pick_path<'a, B: AnonymousPathBuilder, R: Rng, RT: Runtime>(
builder: &B,
rng: &mut R,
netdir: DirInfo<'a>,
guards: &GuardMgr<RT>,
config: &PathConfig,
_now: SystemTime,
) -> Result<(TorPath<'a>, GuardMonitor, 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 rs_cfg = config.relay_selection_config();
let family_rules = FamilyRules::from(netdir.params());
let target_exclusion = match builder.compatible_with() {
Some(ct) => {
let ids = RelayIdSet::from_iter(ct.identities().map(|id_ref| id_ref.to_owned()));
RelayExclusion::exclude_identities(ids)
}
None => RelayExclusion::no_relays_excluded(),
};
let (guard, mon, usable) = select_guard(netdir, guards, builder.compatible_with())?;
let guard_exclusion = match &guard {
MaybeOwnedRelay::Relay(r) => RelayExclusion::exclude_relays_in_same_family(
&config.relay_selection_config(),
vec![r.clone()],
family_rules,
),
MaybeOwnedRelay::Owned(ct) => RelayExclusion::exclude_channel_target_family(
&config.relay_selection_config(),
ct.as_ref(),
netdir,
),
};
let mut exclusion = guard_exclusion.clone();
exclusion.extend(&target_exclusion);
let (exit, middle_usage) = builder.pick_exit(rng, netdir, exclusion, &rs_cfg)?;
let mut family_exclusion =
RelayExclusion::exclude_relays_in_same_family(&rs_cfg, vec![exit.clone()], family_rules);
family_exclusion.extend(&guard_exclusion);
let mut exclusion = family_exclusion;
exclusion.extend(&target_exclusion);
let selector = RelaySelector::new(middle_usage, exclusion);
let (middle, info) = selector.select_relay(rng, netdir);
let middle = middle.ok_or_else(|| Error::NoRelay {
path_kind: builder.path_kind(),
role: "middle relay",
problem: info.to_string(),
})?;
let hops = vec![
guard,
MaybeOwnedRelay::from(middle),
MaybeOwnedRelay::from(exit),
];
ensure_unique_hops(&hops)?;
Ok((TorPath::new_multihop_from_maybe_owned(hops), mon, usable))
}
fn ensure_unique_hops<'a>(hops: &'a [MaybeOwnedRelay<'a>]) -> StdResult<(), Bug> {
for (i, hop) in hops.iter().enumerate() {
if let Some(hop2) = hops
.iter()
.skip(i + 1)
.find(|hop2| hop.clone().has_any_relay_id_from(*hop2))
{
return Err(internal!(
"invalid path: the IDs of hops {} and {} overlap?!",
hop.display_relay_ids(),
hop2.display_relay_ids()
));
}
}
Ok(())
}
#[instrument(skip_all, level = "trace")]
fn select_guard<'a, RT: Runtime>(
netdir: &'a NetDir,
guardmgr: &GuardMgr<RT>,
compatible_with: Option<&OwnedChanTarget>,
) -> Result<(MaybeOwnedRelay<'a>, GuardMonitor, GuardUsable)> {
let mut b = tor_guardmgr::GuardUsageBuilder::default();
b.kind(tor_guardmgr::GuardUsageKind::Data);
if let Some(avoid_target) = compatible_with {
let mut family = RelayIdSet::new();
family.extend(avoid_target.identities().map(|id| id.to_owned()));
if let Some(avoid_relay) = netdir.by_ids(avoid_target) {
family.extend(netdir.known_family_members(&avoid_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, 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()
};
Ok((guard, mon, usable))
}
#[cfg(test)]
fn assert_same_path_when_owned(path: &TorPath<'_>) {
#![allow(clippy::unwrap_used)]
let owned: OwnedPath = path.try_into().unwrap();
match (&owned, &path.inner) {
(OwnedPath::ChannelOnly(c), TorPathInner::FallbackOneHop(f)) => {
assert!(c.same_relay_ids(*f));
}
(OwnedPath::Normal(p), TorPathInner::OneHop(h)) => {
assert_eq!(p.len(), 1);
assert!(p[0].same_relay_ids(h));
}
(OwnedPath::Normal(p1), TorPathInner::Path(p2)) => {
assert_eq!(p1.len(), p2.len());
for (n1, n2) in p1.iter().zip(p2.iter()) {
assert!(n1.same_relay_ids(n2));
}
}
(_, _) => {
panic!("Mismatched path types.");
}
}
}