//! Code for building paths to an exit relay. 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}; /// Internal representation of PathBuilder. enum ExitPathBuilderInner { /// Request a path that allows exit to the given `TargetPort`s. WantsPorts(Vec), /// Request a path that allows exit with a relay in the given country. // TODO GEOIP: refactor this builder to allow conjunction! // See discussion here: // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1537#note_2942218 #[cfg(feature = "geoip")] ExitInCountry { /// The country to exit in. country: CountryCode, /// Some target ports to use (works like `WantsPorts`). /// /// HACK(eta): This is a horrible hack to work around the lack of conjunction. ports: Vec, }, /// Request a path that allows exit to _any_ port. AnyExit { /// If false, then we fall back to non-exit nodes if we can't find an /// exit. strict: bool, }, } /// A PathBuilder that builds a path to an exit relay supporting a given /// set of ports. /// /// NOTE: The name of this type is no longer completely apt: given some circuits, /// it is happy to build a circuit ending at a non-exit. pub(crate) struct ExitPathBuilder { /// The inner ExitPathBuilder state. inner: ExitPathBuilderInner, /// If present, a "target" that every chosen relay must be able to share a circuit with with. compatible_with: Option, /// If true, all relays on this path must be Stable. require_stability: bool, } impl ExitPathBuilder { /// Create a new builder that will try to get an exit relay /// containing all the ports in `ports`. /// /// If the list of ports is empty, tries to get any exit relay at all. pub(crate) fn from_target_ports(wantports: impl IntoIterator) -> Self { let ports: Vec = 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")] /// Create a new builder that will try to get an exit relay in `country`, /// containing all the ports in `ports`. /// /// If the list of ports is empty, it is disregarded. // TODO GEOIP: this method is hacky, and should be refactored. pub(crate) fn in_given_country( country: CountryCode, wantports: impl IntoIterator, ) -> Self { let ports: Vec = wantports.into_iter().collect(); Self { inner: ExitPathBuilderInner::ExitInCountry { country, ports }, compatible_with: None, require_stability: true, } } /// Create a new builder that will try to get any exit relay at all. pub(crate) fn for_any_exit() -> Self { Self { inner: ExitPathBuilderInner::AnyExit { strict: true }, compatible_with: None, require_stability: false, } } /// Try to create and return a path corresponding to the requirements of /// this builder. #[instrument(skip_all, level = "trace")] pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>( &self, rng: &mut R, netdir: DirInfo<'a>, guards: &GuardMgr, config: &PathConfig, now: SystemTime, ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> { pick_path(self, rng, netdir, guards, config, now) } /// Create a new builder that will try to get an exit relay, but which /// will be satisfied with a non-exit relay. pub(crate) fn for_timeout_testing() -> Self { Self { inner: ExitPathBuilderInner::AnyExit { strict: false }, compatible_with: None, require_stability: false, } } /// Indicate that middle and exit relays on this circuit need (or do not /// need) to have the Stable flag. 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 { // @@ begin test lint list maintained by maint/add_warning @@ #![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; 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, }; // This use of "low_level_predicate_permits_relay" is okay because // because we're in tests. 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::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[..], 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::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[..], 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() { // This shouldn't actually be constructable IRL, but let's test to // make sure our code can handle it. 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 = (&bogus_path).try_into(); assert!(owned.is_err()); } #[test] fn no_exits() { tor_rtcompat::test_with_all_runtimes!(|rt| async move { // Construct a netdir with 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 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::now(); // With target ports 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 { .. }))); // For any exit let outcome = ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, &guards, &config, now); assert!(outcome.is_err()); assert!(matches!(outcome, Err(Error::NoRelay { .. }))); // For any exit (non-strict, so this will work). 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); // We're going to just have these all succeed and make sure // that they pick the same guard. We won't test failing // cases here, since those are tested in guardmgr. 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); }); } }