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

            
3
use std::time::SystemTime;
4

            
5
use rand::Rng;
6

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

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

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

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

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

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

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

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

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

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

            
194
#[cfg(test)]
195
mod 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
}