Skip to main content

tor_circmgr/path/
exitpath.rs

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