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