1
//! Code for building paths to an exit relay.
2

            
3
use std::time::SystemTime;
4

            
5
use rand::Rng;
6
use tracing::instrument;
7

            
8
use super::{AnonymousPathBuilder, TorPath};
9
use crate::path::pick_path;
10
use crate::{DirInfo, Error, PathConfig, Result, TargetPort};
11

            
12
use tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable};
13
use tor_linkspec::OwnedChanTarget;
14
use tor_netdir::{NetDir, Relay};
15
use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
16
use tor_rtcompat::Runtime;
17
#[cfg(feature = "geoip")]
18
use {tor_geoip::CountryCode, tor_relay_selection::RelayRestriction};
19

            
20
/// Internal representation of PathBuilder.
21
enum 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.
52
pub(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

            
61
impl 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
12264
    pub(crate) fn from_target_ports(wantports: impl IntoIterator<Item = TargetPort>) -> Self {
67
12264
        let ports: Vec<TargetPort> = wantports.into_iter().collect();
68
12264
        if ports.is_empty() {
69
            return Self::for_any_exit();
70
12264
        }
71
12264
        Self {
72
12264
            inner: ExitPathBuilderInner::WantsPorts(ports),
73
12264
            compatible_with: None,
74
12264
            require_stability: true,
75
12264
        }
76
12264
    }
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
12012
    pub(crate) fn for_any_exit() -> Self {
98
12012
        Self {
99
12012
            inner: ExitPathBuilderInner::AnyExit { strict: true },
100
12012
            compatible_with: None,
101
12012
            require_stability: false,
102
12012
        }
103
12012
    }
104

            
105
    /// Try to create and return a path corresponding to the requirements of
106
    /// this builder.
107
    #[instrument(skip_all, level = "trace")]
108
24312
    pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
109
24312
        &self,
110
24312
        rng: &mut R,
111
24312
        netdir: DirInfo<'a>,
112
24312
        guards: &GuardMgr<RT>,
113
24312
        config: &PathConfig,
114
24312
        now: SystemTime,
115
24312
    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
116
24312
        pick_path(self, rng, netdir, guards, config, now)
117
24312
    }
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
36
    pub(crate) fn for_timeout_testing() -> Self {
122
36
        Self {
123
36
            inner: ExitPathBuilderInner::AnyExit { strict: false },
124
36
            compatible_with: None,
125
36
            require_stability: false,
126
36
        }
127
36
    }
128

            
129
    /// Indicate that middle and exit relays on this circuit need (or do not
130
    /// need) to have the Stable flag.
131
36
    pub(crate) fn require_stability(&mut self, require_stability: bool) -> &mut Self {
132
36
        self.require_stability = require_stability;
133
36
        self
134
36
    }
135
}
136

            
137
impl AnonymousPathBuilder for ExitPathBuilder {
138
48624
    fn compatible_with(&self) -> Option<&OwnedChanTarget> {
139
48624
        self.compatible_with.as_ref()
140
48624
    }
141

            
142
24312
    fn pick_exit<'a, R: Rng>(
143
24312
        &self,
144
24312
        rng: &mut R,
145
24312
        netdir: &'a NetDir,
146
24312
        guard_exclusion: RelayExclusion<'a>,
147
24312
        rs_cfg: &RelaySelectionConfig<'_>,
148
24312
    ) -> Result<(Relay<'a>, RelayUsage)> {
149
24312
        let selector = match &self.inner {
150
12048
            ExitPathBuilderInner::AnyExit { strict } => {
151
12048
                let mut selector =
152
12048
                    RelaySelector::new(RelayUsage::any_exit(rs_cfg), guard_exclusion);
153
12048
                if !strict {
154
36
                    selector.mark_usage_flexible();
155
12012
                }
156
12048
                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
12264
            ExitPathBuilderInner::WantsPorts(wantports) => RelaySelector::new(
170
12264
                RelayUsage::exit_to_all_ports(rs_cfg, wantports.clone()),
171
12264
                guard_exclusion,
172
            ),
173
        };
174

            
175
24312
        let (relay, info) = selector.select_relay(rng, netdir);
176
24312
        let relay = relay.ok_or_else(|| Error::NoRelay {
177
24
            path_kind: self.path_kind(),
178
            role: "final hop",
179
24
            problem: info.to_string(),
180
24
        })?;
181
24288
        Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
182
24312
    }
183

            
184
24
    fn path_kind(&self) -> &'static str {
185
        use ExitPathBuilderInner::*;
186
24
        match &self.inner {
187
12
            WantsPorts(_) => "exit circuit",
188
            #[cfg(feature = "geoip")]
189
            ExitInCountry { .. } => "country-specific exit circuit",
190
12
            AnyExit { .. } => "testing circuit",
191
        }
192
24
    }
193
}
194

            
195
#[cfg(test)]
196
mod 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
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
210
    use super::*;
211
    use crate::path::{
212
        MaybeOwnedRelay, OwnedPath, TorPath, TorPathInner, assert_same_path_when_owned,
213
    };
214
    use std::collections::HashSet;
215
    use tor_basic_utils::test_rng::testing_rng;
216
    use tor_guardmgr::TestConfig;
217
    use tor_linkspec::{HasRelayIds, RelayIds};
218
    use tor_netdir::{FamilyRules, SubnetConfig, testnet};
219
    use tor_persist::TestingStateMgr;
220
    use tor_relay_selection::LowLevelRelayPredicate;
221
    use tor_rtcompat::SleepProvider;
222
    use web_time_compat::SystemTimeExt;
223

            
224
    impl<'a> MaybeOwnedRelay<'a> {
225
        fn can_share_circuit(
226
            &self,
227
            other: &MaybeOwnedRelay<'_>,
228
            subnet_config: SubnetConfig,
229
            family_rules: FamilyRules,
230
        ) -> bool {
231
            use MaybeOwnedRelay as M;
232
            match (self, other) {
233
                (M::Relay(a), M::Relay(b)) => {
234
                    let ports = Default::default();
235
                    let cfg = RelaySelectionConfig {
236
                        long_lived_ports: &ports,
237
                        subnet_config,
238
                    };
239
                    // This use of "low_level_predicate_permits_relay" is okay because
240
                    // because we're in tests.
241
                    RelayExclusion::exclude_relays_in_same_family(
242
                        &cfg,
243
                        vec![a.clone()],
244
                        family_rules,
245
                    )
246
                    .low_level_predicate_permits_relay(b)
247
                }
248
                (a, b) => !subnet_config.any_addrs_in_same_subnet(a, b),
249
            }
250
        }
251
    }
252

            
253
    fn assert_exit_path_ok(relays: &[MaybeOwnedRelay<'_>], family_rules: FamilyRules) {
254
        assert_eq!(relays.len(), 3);
255

            
256
        let r1 = &relays[0];
257
        let r2 = &relays[1];
258
        let r3 = &relays[2];
259

            
260
        if let MaybeOwnedRelay::Relay(r1) = r1 {
261
            assert!(r1.low_level_details().is_suitable_as_guard());
262
        }
263

            
264
        assert!(!r1.same_relay_ids(r2));
265
        assert!(!r1.same_relay_ids(r3));
266
        assert!(!r2.same_relay_ids(r3));
267

            
268
        let subnet_config = SubnetConfig::default();
269
        assert!(r1.can_share_circuit(r2, subnet_config, family_rules));
270
        assert!(r2.can_share_circuit(r3, subnet_config, family_rules));
271
        assert!(r1.can_share_circuit(r3, subnet_config, family_rules));
272
    }
273

            
274
    #[test]
275
    fn by_ports() {
276
        tor_rtcompat::test_with_all_runtimes!(|rt| async move {
277
            let mut rng = testing_rng();
278
            let family_rules = FamilyRules::all_family_info();
279
            let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
280
            let ports = vec![TargetPort::ipv4(443), TargetPort::ipv4(1119)];
281
            let dirinfo = (&netdir).into();
282
            let config = PathConfig::default();
283
            let statemgr = TestingStateMgr::new();
284
            let guards =
285
                tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
286
            guards.install_test_netdir(&netdir);
287
            let now = SystemTime::get();
288

            
289
            for _ in 0..1000 {
290
                let (path, _, _) = ExitPathBuilder::from_target_ports(ports.clone())
291
                    .pick_path(&mut rng, dirinfo, &guards, &config, now)
292
                    .unwrap();
293

            
294
                assert_same_path_when_owned(&path);
295

            
296
                if let TorPathInner::Path(p) = path.inner {
297
                    assert_exit_path_ok(&p[..], family_rules);
298
                    let exit = match &p[2] {
299
                        MaybeOwnedRelay::Relay(r) => r,
300
                        MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
301
                    };
302
                    assert!(exit.low_level_details().ipv4_policy().allows_port(1119));
303
                } else {
304
                    panic!("Generated the wrong kind of path");
305
                }
306
            }
307
        });
308
    }
309

            
310
    #[test]
311
    fn any_exit() {
312
        tor_rtcompat::test_with_all_runtimes!(|rt| async move {
313
            let mut rng = testing_rng();
314
            let family_rules = FamilyRules::all_family_info();
315
            let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
316
            let dirinfo = (&netdir).into();
317
            let statemgr = TestingStateMgr::new();
318
            let guards =
319
                tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
320
            guards.install_test_netdir(&netdir);
321
            let now = SystemTime::get();
322

            
323
            let config = PathConfig::default();
324
            for _ in 0..1000 {
325
                let (path, _, _) = ExitPathBuilder::for_any_exit()
326
                    .pick_path(&mut rng, dirinfo, &guards, &config, now)
327
                    .unwrap();
328
                assert_same_path_when_owned(&path);
329
                if let TorPathInner::Path(p) = path.inner {
330
                    assert_exit_path_ok(&p[..], family_rules);
331
                    let exit = match &p[2] {
332
                        MaybeOwnedRelay::Relay(r) => r,
333
                        MaybeOwnedRelay::Owned(_) => panic!("Didn't asked for an owned target!"),
334
                    };
335
                    assert!(exit.low_level_details().policies_allow_some_port());
336
                } else {
337
                    panic!("Generated the wrong kind of path");
338
                }
339
            }
340
        });
341
    }
342

            
343
    #[test]
344
    fn empty_path() {
345
        // This shouldn't actually be constructable IRL, but let's test to
346
        // make sure our code can handle it.
347
        let bogus_path = TorPath {
348
            inner: TorPathInner::Path(vec![]),
349
        };
350

            
351
        assert!(bogus_path.exit_relay().is_none());
352
        assert!(bogus_path.exit_policy().is_none());
353
        assert_eq!(bogus_path.len(), 0);
354

            
355
        let owned: Result<OwnedPath> = (&bogus_path).try_into();
356
        assert!(owned.is_err());
357
    }
358

            
359
    #[test]
360
    fn no_exits() {
361
        tor_rtcompat::test_with_all_runtimes!(|rt| async move {
362
            // Construct a netdir with no exits.
363
            let netdir = testnet::construct_custom_netdir(|_idx, bld, _| {
364
                bld.md.parse_ipv4_policy("reject 1-65535").unwrap();
365
            })
366
            .unwrap()
367
            .unwrap_if_sufficient()
368
            .unwrap();
369
            let mut rng = testing_rng();
370
            let dirinfo = (&netdir).into();
371
            let statemgr = TestingStateMgr::new();
372
            let guards =
373
                tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
374
            guards.install_test_netdir(&netdir);
375
            let config = PathConfig::default();
376
            let now = SystemTime::get();
377

            
378
            // With target ports
379
            let outcome = ExitPathBuilder::from_target_ports(vec![TargetPort::ipv4(80)])
380
                .pick_path(&mut rng, dirinfo, &guards, &config, now);
381
            assert!(outcome.is_err());
382
            assert!(matches!(outcome, Err(Error::NoRelay { .. })));
383

            
384
            // For any exit
385
            let outcome =
386
                ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, &guards, &config, now);
387
            assert!(outcome.is_err());
388
            assert!(matches!(outcome, Err(Error::NoRelay { .. })));
389

            
390
            // For any exit (non-strict, so this will work).
391
            let outcome = ExitPathBuilder::for_timeout_testing()
392
                .pick_path(&mut rng, dirinfo, &guards, &config, now);
393
            assert!(outcome.is_ok());
394
        });
395
    }
396

            
397
    #[test]
398
    fn exitpath_with_guards() {
399
        use tor_guardmgr::GuardStatus;
400

            
401
        tor_rtcompat::test_with_all_runtimes!(|rt| async move {
402
            let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
403
            let family_rules = FamilyRules::all_family_info();
404
            let mut rng = testing_rng();
405
            let dirinfo = (&netdir).into();
406
            let statemgr = TestingStateMgr::new();
407
            let guards =
408
                tor_guardmgr::GuardMgr::new(rt.clone(), statemgr, &TestConfig::default()).unwrap();
409
            let config = PathConfig::default();
410
            guards.install_test_netdir(&netdir);
411
            let port443 = TargetPort::ipv4(443);
412

            
413
            // We're going to just have these all succeed and make sure
414
            // that they pick the same guard.  We won't test failing
415
            // cases here, since those are tested in guardmgr.
416
            let mut distinct_guards = HashSet::new();
417
            let mut distinct_mid = HashSet::new();
418
            let mut distinct_exit = HashSet::new();
419
            for _ in 0..20 {
420
                let (path, mon, usable) = ExitPathBuilder::from_target_ports(vec![port443])
421
                    .pick_path(&mut rng, dirinfo, &guards, &config, rt.wallclock())
422
                    .unwrap();
423
                assert_eq!(path.len(), 3);
424
                assert_same_path_when_owned(&path);
425
                if let TorPathInner::Path(p) = path.inner {
426
                    assert_exit_path_ok(&p[..], family_rules);
427
                    distinct_guards.insert(RelayIds::from_relay_ids(&p[0]));
428
                    distinct_mid.insert(RelayIds::from_relay_ids(&p[1]));
429
                    distinct_exit.insert(RelayIds::from_relay_ids(&p[2]));
430
                } else {
431
                    panic!("Wrong kind of path");
432
                }
433
                assert!(matches!(
434
                    mon.inspect_pending_status(),
435
                    (GuardStatus::AttemptAbandoned, false)
436
                ));
437
                mon.succeeded();
438
                assert!(usable.await.unwrap());
439
            }
440
            assert_eq!(distinct_guards.len(), 1);
441
            assert_ne!(distinct_mid.len(), 1);
442
            assert_ne!(distinct_exit.len(), 1);
443
        });
444
    }
445
}