tor_circmgr/path/
exitpath.rs

1//! Code for building paths to an exit relay.
2
3use 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
19/// Internal representation of PathBuilder.
20enum ExitPathBuilderInner {
21    /// Request a path that allows exit to the given `TargetPort`s.
22    WantsPorts(Vec<TargetPort>),
23
24    /// Request a path that allows exit with a relay in the given country.
25    // TODO GEOIP: refactor this builder to allow conjunction!
26    // See discussion here:
27    // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1537#note_2942218
28    #[cfg(feature = "geoip")]
29    ExitInCountry {
30        /// The country to exit in.
31        country: CountryCode,
32        /// Some target ports to use (works like `WantsPorts`).
33        ///
34        /// HACK(eta): This is a horrible hack to work around the lack of conjunction.
35        ports: Vec<TargetPort>,
36    },
37
38    /// Request a path that allows exit to _any_ port.
39    AnyExit {
40        /// If false, then we fall back to non-exit nodes if we can't find an
41        /// exit.
42        strict: bool,
43    },
44}
45
46/// A PathBuilder that builds a path to an exit relay supporting a given
47/// set of ports.
48///
49/// NOTE: The name of this type is no longer completely apt: given some circuits,
50/// it is happy to build a circuit ending at a non-exit.
51pub(crate) struct ExitPathBuilder {
52    /// The inner ExitPathBuilder state.
53    inner: ExitPathBuilderInner,
54    /// If present, a "target" that every chosen relay must be able to share a circuit with with.
55    compatible_with: Option<OwnedChanTarget>,
56    /// If true, all relays on this path must be Stable.
57    require_stability: bool,
58}
59
60impl ExitPathBuilder {
61    /// Create a new builder that will try to get an exit relay
62    /// containing all the ports in `ports`.
63    ///
64    /// If the list of ports is empty, tries to get any exit relay at all.
65    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    /// Create a new builder that will try to get an exit relay in `country`,
80    /// containing all the ports in `ports`.
81    ///
82    /// If the list of ports is empty, it is disregarded.
83    // TODO GEOIP: this method is hacky, and should be refactored.
84    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    /// Create a new builder that will try to get any exit relay at all.
97    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    /// Try to create and return a path corresponding to the requirements of
106    /// this builder.
107    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    /// Create a new builder that will try to get an exit relay, but which
119    /// will be satisfied with a non-exit relay.
120    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    /// Indicate that middle and exit relays on this circuit need (or do not
129    /// need) to have the Stable flag.
130    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    // @@ begin test lint list maintained by maint/add_warning @@
197    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
209    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                    // This use of "low_level_predicate_permits_relay" is okay because
238                    // because we're in tests.
239                    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        // This shouldn't actually be constructable IRL, but let's test to
344        // make sure our code can handle it.
345        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            // Construct a netdir with no exits.
361            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            // With target ports
377            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            // For any exit
383            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            // For any exit (non-strict, so this will work).
389            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            // We're going to just have these all succeed and make sure
412            // that they pick the same guard.  We won't test failing
413            // cases here, since those are tested in guardmgr.
414            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}