1use std::time::SystemTime;
4
5use rand::Rng;
6use tracing::instrument;
7
8use super::{AnonymousPathBuilder, TorPath};
9use crate::path::pick_path;
10use crate::{DirInfo, Error, PathConfig, Result, TargetPort};
11
12use tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable};
13use tor_linkspec::OwnedChanTarget;
14use tor_netdir::{NetDir, Relay};
15use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
16use tor_rtcompat::Runtime;
17#[cfg(feature = "geoip")]
18use {tor_geoip::CountryCode, tor_relay_selection::RelayRestriction};
19
20enum ExitPathBuilderInner {
22 WantsPorts(Vec<TargetPort>),
24
25 #[cfg(feature = "geoip")]
30 ExitInCountry {
31 country: CountryCode,
33 ports: Vec<TargetPort>,
37 },
38
39 AnyExit {
41 strict: bool,
44 },
45}
46
47pub(crate) struct ExitPathBuilder {
53 inner: ExitPathBuilderInner,
55 compatible_with: Option<OwnedChanTarget>,
57 require_stability: bool,
59}
60
61impl ExitPathBuilder {
62 pub(crate) fn from_target_ports(wantports: impl IntoIterator<Item = TargetPort>) -> Self {
67 let ports: Vec<TargetPort> = wantports.into_iter().collect();
68 if ports.is_empty() {
69 return Self::for_any_exit();
70 }
71 Self {
72 inner: ExitPathBuilderInner::WantsPorts(ports),
73 compatible_with: None,
74 require_stability: true,
75 }
76 }
77
78 #[cfg(feature = "geoip")]
79 pub(crate) fn in_given_country(
85 country: CountryCode,
86 wantports: impl IntoIterator<Item = TargetPort>,
87 ) -> Self {
88 let ports: Vec<TargetPort> = wantports.into_iter().collect();
89 Self {
90 inner: ExitPathBuilderInner::ExitInCountry { country, ports },
91 compatible_with: None,
92 require_stability: true,
93 }
94 }
95
96 pub(crate) fn for_any_exit() -> Self {
98 Self {
99 inner: ExitPathBuilderInner::AnyExit { strict: true },
100 compatible_with: None,
101 require_stability: false,
102 }
103 }
104
105 #[instrument(skip_all, level = "trace")]
108 pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
109 &self,
110 rng: &mut R,
111 netdir: DirInfo<'a>,
112 guards: &GuardMgr<RT>,
113 config: &PathConfig,
114 now: SystemTime,
115 ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
116 pick_path(self, rng, netdir, guards, config, now)
117 }
118
119 pub(crate) fn for_timeout_testing() -> Self {
122 Self {
123 inner: ExitPathBuilderInner::AnyExit { strict: false },
124 compatible_with: None,
125 require_stability: false,
126 }
127 }
128
129 pub(crate) fn require_stability(&mut self, require_stability: bool) -> &mut Self {
132 self.require_stability = require_stability;
133 self
134 }
135}
136
137impl AnonymousPathBuilder for ExitPathBuilder {
138 fn compatible_with(&self) -> Option<&OwnedChanTarget> {
139 self.compatible_with.as_ref()
140 }
141
142 fn pick_exit<'a, R: Rng>(
143 &self,
144 rng: &mut R,
145 netdir: &'a NetDir,
146 guard_exclusion: RelayExclusion<'a>,
147 rs_cfg: &RelaySelectionConfig<'_>,
148 ) -> Result<(Relay<'a>, RelayUsage)> {
149 let selector = match &self.inner {
150 ExitPathBuilderInner::AnyExit { strict } => {
151 let mut selector =
152 RelaySelector::new(RelayUsage::any_exit(rs_cfg), guard_exclusion);
153 if !strict {
154 selector.mark_usage_flexible();
155 }
156 selector
157 }
158
159 #[cfg(feature = "geoip")]
160 ExitPathBuilderInner::ExitInCountry { country, ports } => {
161 let mut selector = RelaySelector::new(
162 RelayUsage::exit_to_all_ports(rs_cfg, ports.clone()),
163 guard_exclusion,
164 );
165 selector.push_restriction(RelayRestriction::require_country_code(*country));
166 selector
167 }
168
169 ExitPathBuilderInner::WantsPorts(wantports) => RelaySelector::new(
170 RelayUsage::exit_to_all_ports(rs_cfg, wantports.clone()),
171 guard_exclusion,
172 ),
173 };
174
175 let (relay, info) = selector.select_relay(rng, netdir);
176 let relay = relay.ok_or_else(|| Error::NoRelay {
177 path_kind: self.path_kind(),
178 role: "final hop",
179 problem: info.to_string(),
180 })?;
181 Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
182 }
183
184 fn path_kind(&self) -> &'static str {
185 use ExitPathBuilderInner::*;
186 match &self.inner {
187 WantsPorts(_) => "exit circuit",
188 #[cfg(feature = "geoip")]
189 ExitInCountry { .. } => "country-specific exit circuit",
190 AnyExit { .. } => "testing circuit",
191 }
192 }
193}
194
195#[cfg(test)]
196mod test {
197 #![allow(clippy::bool_assert_comparison)]
199 #![allow(clippy::clone_on_copy)]
200 #![allow(clippy::dbg_macro)]
201 #![allow(clippy::mixed_attributes_style)]
202 #![allow(clippy::print_stderr)]
203 #![allow(clippy::print_stdout)]
204 #![allow(clippy::single_char_pattern)]
205 #![allow(clippy::unwrap_used)]
206 #![allow(clippy::unchecked_time_subtraction)]
207 #![allow(clippy::useless_vec)]
208 #![allow(clippy::needless_pass_by_value)]
209 #![allow(clippy::string_slice)] use super::*;
212 use crate::path::{
213 MaybeOwnedRelay, OwnedPath, TorPath, TorPathInner, assert_same_path_when_owned,
214 };
215 use std::collections::HashSet;
216 use tor_basic_utils::test_rng::testing_rng;
217 use tor_guardmgr::TestConfig;
218 use tor_linkspec::{HasRelayIds, RelayIds};
219 use tor_netdir::{FamilyRules, SubnetConfig, testnet};
220 use tor_persist::TestingStateMgr;
221 use tor_relay_selection::LowLevelRelayPredicate;
222 use tor_rtcompat::SleepProvider;
223 use web_time_compat::SystemTimeExt;
224
225 impl<'a> MaybeOwnedRelay<'a> {
226 fn can_share_circuit(
227 &self,
228 other: &MaybeOwnedRelay<'_>,
229 subnet_config: SubnetConfig,
230 family_rules: FamilyRules,
231 ) -> bool {
232 use MaybeOwnedRelay as M;
233 match (self, other) {
234 (M::Relay(a), M::Relay(b)) => {
235 let ports = Default::default();
236 let cfg = RelaySelectionConfig {
237 long_lived_ports: &ports,
238 subnet_config,
239 };
240 RelayExclusion::exclude_relays_in_same_family(
243 &cfg,
244 vec![a.clone()],
245 family_rules,
246 )
247 .low_level_predicate_permits_relay(b)
248 }
249 (a, b) => !subnet_config.any_addrs_in_same_subnet(a, b),
250 }
251 }
252 }
253
254 fn assert_exit_path_ok(relays: &[MaybeOwnedRelay<'_>], family_rules: FamilyRules) {
255 assert_eq!(relays.len(), 3);
256
257 let r1 = &relays[0];
258 let r2 = &relays[1];
259 let r3 = &relays[2];
260
261 if let MaybeOwnedRelay::Relay(r1) = r1 {
262 assert!(r1.low_level_details().is_suitable_as_guard());
263 }
264
265 assert!(!r1.same_relay_ids(r2));
266 assert!(!r1.same_relay_ids(r3));
267 assert!(!r2.same_relay_ids(r3));
268
269 let subnet_config = SubnetConfig::default();
270 assert!(r1.can_share_circuit(r2, subnet_config, family_rules));
271 assert!(r2.can_share_circuit(r3, subnet_config, family_rules));
272 assert!(r1.can_share_circuit(r3, subnet_config, family_rules));
273 }
274
275 #[test]
276 fn by_ports() {
277 tor_rtcompat::test_with_all_runtimes!(|rt| async move {
278 let mut rng = testing_rng();
279 let family_rules = FamilyRules::all_family_info();
280 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
281 let ports = vec![TargetPort::ipv4(443), TargetPort::ipv4(1119)];
282 let dirinfo = (&netdir).into();
283 let config = PathConfig::default();
284 let statemgr = TestingStateMgr::new();
285 let guards =
286 tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
287 guards.install_test_netdir(&netdir);
288 let now = SystemTime::get();
289
290 for _ in 0..1000 {
291 let (path, _, _) = ExitPathBuilder::from_target_ports(ports.clone())
292 .pick_path(&mut rng, dirinfo, &guards, &config, now)
293 .unwrap();
294
295 assert_same_path_when_owned(&path);
296
297 if let TorPathInner::Path(p) = path.inner {
298 assert_exit_path_ok(&p[..], family_rules);
299 let exit = match &p[2] {
300 MaybeOwnedRelay::Relay(r) => r,
301 MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
302 };
303 assert!(exit.low_level_details().ipv4_policy().allows_port(1119));
304 } else {
305 panic!("Generated the wrong kind of path");
306 }
307 }
308 });
309 }
310
311 #[test]
312 fn any_exit() {
313 tor_rtcompat::test_with_all_runtimes!(|rt| async move {
314 let mut rng = testing_rng();
315 let family_rules = FamilyRules::all_family_info();
316 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
317 let dirinfo = (&netdir).into();
318 let statemgr = TestingStateMgr::new();
319 let guards =
320 tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
321 guards.install_test_netdir(&netdir);
322 let now = SystemTime::get();
323
324 let config = PathConfig::default();
325 for _ in 0..1000 {
326 let (path, _, _) = ExitPathBuilder::for_any_exit()
327 .pick_path(&mut rng, dirinfo, &guards, &config, now)
328 .unwrap();
329 assert_same_path_when_owned(&path);
330 if let TorPathInner::Path(p) = path.inner {
331 assert_exit_path_ok(&p[..], family_rules);
332 let exit = match &p[2] {
333 MaybeOwnedRelay::Relay(r) => r,
334 MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
335 };
336 assert!(exit.low_level_details().policies_allow_some_port());
337 } else {
338 panic!("Generated the wrong kind of path");
339 }
340 }
341 });
342 }
343
344 #[test]
345 fn empty_path() {
346 let bogus_path = TorPath {
349 inner: TorPathInner::Path(vec![]),
350 };
351
352 assert!(bogus_path.exit_relay().is_none());
353 assert!(bogus_path.exit_policy().is_none());
354 assert_eq!(bogus_path.len(), 0);
355
356 let owned: Result<OwnedPath> = (&bogus_path).try_into();
357 assert!(owned.is_err());
358 }
359
360 #[test]
361 fn no_exits() {
362 tor_rtcompat::test_with_all_runtimes!(|rt| async move {
363 let netdir = testnet::construct_custom_netdir(|_idx, bld, _| {
365 bld.md.parse_ipv4_policy("reject 1-65535").unwrap();
366 })
367 .unwrap()
368 .unwrap_if_sufficient()
369 .unwrap();
370 let mut rng = testing_rng();
371 let dirinfo = (&netdir).into();
372 let statemgr = TestingStateMgr::new();
373 let guards =
374 tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
375 guards.install_test_netdir(&netdir);
376 let config = PathConfig::default();
377 let now = SystemTime::get();
378
379 let outcome = ExitPathBuilder::from_target_ports(vec![TargetPort::ipv4(80)])
381 .pick_path(&mut rng, dirinfo, &guards, &config, now);
382 assert!(outcome.is_err());
383 assert!(matches!(outcome, Err(Error::NoRelay { .. })));
384
385 let outcome =
387 ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, &guards, &config, now);
388 assert!(outcome.is_err());
389 assert!(matches!(outcome, Err(Error::NoRelay { .. })));
390
391 let outcome = ExitPathBuilder::for_timeout_testing()
393 .pick_path(&mut rng, dirinfo, &guards, &config, now);
394 assert!(outcome.is_ok());
395 });
396 }
397
398 #[test]
399 fn exitpath_with_guards() {
400 use tor_guardmgr::GuardStatus;
401
402 tor_rtcompat::test_with_all_runtimes!(|rt| async move {
403 let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
404 let family_rules = FamilyRules::all_family_info();
405 let mut rng = testing_rng();
406 let dirinfo = (&netdir).into();
407 let statemgr = TestingStateMgr::new();
408 let guards =
409 tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
410 let config = PathConfig::default();
411 guards.install_test_netdir(&netdir);
412 let port443 = TargetPort::ipv4(443);
413
414 let mut distinct_guards = HashSet::new();
418 let mut distinct_mid = HashSet::new();
419 let mut distinct_exit = HashSet::new();
420 for _ in 0..20 {
421 let (path, mon, usable) = ExitPathBuilder::from_target_ports(vec![port443])
422 .pick_path(&mut rng, dirinfo, &guards, &config, rt.wallclock())
423 .unwrap();
424 assert_eq!(path.len(), 3);
425 assert_same_path_when_owned(&path);
426 if let TorPathInner::Path(p) = path.inner {
427 assert_exit_path_ok(&p[..], family_rules);
428 distinct_guards.insert(RelayIds::from_relay_ids(&p[0]));
429 distinct_mid.insert(RelayIds::from_relay_ids(&p[1]));
430 distinct_exit.insert(RelayIds::from_relay_ids(&p[2]));
431 } else {
432 panic!("Wrong kind of path");
433 }
434 assert!(matches!(
435 mon.inspect_pending_status(),
436 (GuardStatus::AttemptAbandoned, false)
437 ));
438 mon.succeeded();
439 assert!(usable.await.unwrap());
440 }
441 assert_eq!(distinct_guards.len(), 1);
442 assert_ne!(distinct_mid.len(), 1);
443 assert_ne!(distinct_exit.len(), 1);
444 });
445 }
446}