1
//! Code for building paths for HS circuits.
2
//!
3
//! The path builders defined here are used for creating hidden service circuit stems.
4
//! A circuit stem is the beginning portion of a hidden service circuit,
5
//! the structure of which depends on the types of vanguards, if any, that are in use.
6
//!
7
//! There are two types of circuit stems:
8
//!   * naive circuit stems, used for building circuits to a final hop that an adversary
9
//!     cannot easily control (for example if the target is randomly chosen by us)
10
//!   * guarded circuit stems, used for building circuits to a final hop that an adversary
11
//!     can easily control (for example if the target was not chosen by us)
12
//!
13
//! Circuit stems eventually become introduction, rendezvous, and HsDir circuits.
14
//! For all circuit types except client rendezvous, the stems must first be
15
//! extended by an extra hop:
16
//!
17
//! ```text
18
//!  Client hsdir:  GUARDED -> HsDir
19
//!  Client intro:  GUARDED -> Ipt
20
//!  Client rend:   GUARDED
21
//!  Service hsdir: NAIVE   -> HsDir
22
//!  Service intro: NAIVE   -> Ipt
23
//!  Service rend:  GUARDED -> Rpt
24
//! ```
25
//!
26
//! > Note: the client rendezvous case is an exception to this rule:
27
//! > the rendezvous point is selected by the client, so it cannot easily be
28
//! > controlled by an attacker.
29
//! >
30
//! > This type of circuit would more accurately be described as a NAIVE circuit
31
//! > that gets extended by an extra hop if Full-Vanguards are in use
32
//! > (this is necessary to avoid using the L3 guard as a rendezvous point).
33
//! > However, for the sake of simplicity, we define these circuits in terms of
34
//! > GUARDED.
35
//! >
36
//! > Note: in the client rendezvous case, the last node from the GUARDED
37
//! > circuit stem is the rendezvous point.
38
//!
39
//! If vanguards are disabled, naive circuit stems (NAIVE),
40
//! and guarded circuit stems (GUARDED) are the same,
41
//! and are built using
42
//! [`ExitPathBuilder`](crate::path::exitpath::ExitPathBuilder)'s
43
//! path selection rules.
44
//!
45
//! If vanguards are enabled, the path is built without applying family
46
//! or same-subnet restrictions at all, the guard is not prohibited
47
//! from appearing as either of the last two hops of the circuit,
48
//! and the two circuit stem kinds are built differently
49
//! depending on the type of vanguards that are in use:
50
//!
51
//!   * with lite vanguards enabled:
52
//!      ```text
53
//!         NAIVE   = G -> L2 -> M
54
//!         GUARDED = G -> L2 -> M
55
//!      ```
56
//!
57
//!   * with full vanguards enabled:
58
//!      ```text
59
//!         NAIVE   = G -> L2 -> L3
60
//!         GUARDED = G -> L2 -> L3 -> M
61
//!      ```
62

            
63
#[cfg(feature = "vanguards")]
64
mod vanguards;
65

            
66
use rand::Rng;
67
use tor_error::internal;
68
use tor_linkspec::{HasRelayIds, OwnedChanTarget};
69
use tor_netdir::{NetDir, Relay};
70
use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
71
use tracing::instrument;
72

            
73
use crate::{Error, Result, hspool::HsCircKind, hspool::HsCircStemKind};
74

            
75
use super::AnonymousPathBuilder;
76

            
77
use {
78
    crate::path::{TorPath, pick_path},
79
    crate::{DirInfo, PathConfig},
80
    std::time::SystemTime,
81
    tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable},
82
    tor_rtcompat::Runtime,
83
};
84

            
85
#[cfg(feature = "vanguards")]
86
use {
87
    crate::path::{MaybeOwnedRelay, select_guard},
88
    tor_error::bad_api_usage,
89
    tor_guardmgr::VanguardMode,
90
    tor_guardmgr::vanguards::Layer,
91
    tor_guardmgr::vanguards::VanguardMgr,
92
};
93

            
94
#[cfg(feature = "vanguards")]
95
pub(crate) use vanguards::select_middle_for_vanguard_circ;
96

            
97
/// A path builder for hidden service circuits.
98
///
99
/// See the [hspath](crate::path::hspath) docs for more details.
100
pub(crate) struct HsPathBuilder {
101
    /// If present, a "target" that every chosen relay must be able to share a circuit with with.
102
    ///
103
    /// Ignored if vanguards are in use.
104
    compatible_with: Option<OwnedChanTarget>,
105
    /// The type of circuit stem to build.
106
    ///
107
    /// This is only used if `vanguards` are enabled.
108
    #[cfg_attr(not(feature = "vanguards"), allow(dead_code))]
109
    stem_kind: HsCircStemKind,
110

            
111
    /// If present, ensure that the circuit stem is suitable for use as (a stem for) the given kind
112
    /// of circuit.
113
    circ_kind: Option<HsCircKind>,
114
}
115

            
116
impl HsPathBuilder {
117
    /// Create a new builder that will try to build a three-hop non-exit path
118
    /// for use with the onion services protocols
119
    /// that is compatible with being extended to an optional given relay.
120
    ///
121
    /// (The provided relay is _not_ included in the built path: we only ensure
122
    /// that the path we build does not have any features that would stop us
123
    /// extending it to that relay as a fourth hop.)
124
460
    pub(crate) fn new(
125
460
        compatible_with: Option<OwnedChanTarget>,
126
460
        stem_kind: HsCircStemKind,
127
460
        circ_kind: Option<HsCircKind>,
128
460
    ) -> Self {
129
460
        Self {
130
460
            compatible_with,
131
460
            stem_kind,
132
460
            circ_kind,
133
460
        }
134
460
    }
135

            
136
    /// Try to create and return a path for a hidden service circuit stem.
137
    #[cfg_attr(feature = "vanguards", allow(unused))]
138
    #[instrument(skip_all, level = "trace")]
139
404
    pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
140
404
        &self,
141
404
        rng: &mut R,
142
404
        netdir: DirInfo<'a>,
143
404
        guards: &GuardMgr<RT>,
144
404
        config: &PathConfig,
145
404
        now: SystemTime,
146
404
    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
147
404
        pick_path(self, rng, netdir, guards, config, now)
148
404
    }
149

            
150
    /// Try to create and return a path for a hidden service circuit stem.
151
    ///
152
    /// If vanguards are disabled, this has the same behavior as
153
    /// [pick_path](HsPathBuilder::pick_path).
154
    #[cfg(feature = "vanguards")]
155
    #[cfg_attr(not(feature = "vanguards"), allow(unused))]
156
    #[instrument(skip_all, level = "trace")]
157
56
    pub(crate) fn pick_path_with_vanguards<'a, R: Rng, RT: Runtime>(
158
56
        &self,
159
56
        rng: &mut R,
160
56
        netdir: DirInfo<'a>,
161
56
        guards: &GuardMgr<RT>,
162
56
        vanguards: &VanguardMgr<RT>,
163
56
        config: &PathConfig,
164
56
        now: SystemTime,
165
56
    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
166
56
        let mode = vanguards.mode();
167
56
        if mode == VanguardMode::Disabled {
168
            return pick_path(self, rng, netdir, guards, config, now);
169
56
        }
170

            
171
56
        let vanguard_path_builder = VanguardHsPathBuilder {
172
56
            stem_kind: self.stem_kind,
173
56
            circ_kind: self.circ_kind,
174
56
            compatible_with: self.compatible_with.clone(),
175
56
        };
176

            
177
56
        vanguard_path_builder.pick_path(rng, netdir, guards, vanguards)
178
56
    }
179
}
180

            
181
impl AnonymousPathBuilder for HsPathBuilder {
182
808
    fn compatible_with(&self) -> Option<&OwnedChanTarget> {
183
808
        self.compatible_with.as_ref()
184
808
    }
185

            
186
4
    fn path_kind(&self) -> &'static str {
187
4
        "onion-service circuit"
188
4
    }
189

            
190
404
    fn pick_exit<'a, R: Rng>(
191
404
        &self,
192
404
        rng: &mut R,
193
404
        netdir: &'a NetDir,
194
404
        guard_exclusion: RelayExclusion<'a>,
195
404
        _rs_cfg: &RelaySelectionConfig<'_>,
196
404
    ) -> Result<(Relay<'a>, RelayUsage)> {
197
404
        let selector =
198
404
            RelaySelector::new(hs_stem_terminal_hop_usage(self.circ_kind), guard_exclusion);
199

            
200
404
        let (relay, info) = selector.select_relay(rng, netdir);
201
404
        let relay = relay.ok_or_else(|| Error::NoRelay {
202
2
            path_kind: self.path_kind(),
203
            role: "final hop",
204
2
            problem: info.to_string(),
205
2
        })?;
206
402
        Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
207
404
    }
208
}
209

            
210
/// A path builder for hidden service circuits that use vanguards.
211
///
212
/// Used by [`HsPathBuilder`] when vanguards are enabled.
213
///
214
/// See the [`HsPathBuilder`] documentation for more details.
215
#[cfg(feature = "vanguards")]
216
struct VanguardHsPathBuilder {
217
    /// The kind of circuit stem we are building
218
    stem_kind: HsCircStemKind,
219
    /// If present, ensure that the circuit stem is suitable for use as (a stem for) the given kind
220
    /// of circuit.
221
    circ_kind: Option<HsCircKind>,
222
    /// The target we are about to extend the circuit to.
223
    compatible_with: Option<OwnedChanTarget>,
224
}
225

            
226
#[cfg(feature = "vanguards")]
227
impl VanguardHsPathBuilder {
228
    /// Try to create and return a path for a hidden service circuit stem.
229
    #[instrument(skip_all, level = "trace")]
230
56
    fn pick_path<'a, R: Rng, RT: Runtime>(
231
56
        &self,
232
56
        rng: &mut R,
233
56
        netdir: DirInfo<'a>,
234
56
        guards: &GuardMgr<RT>,
235
56
        vanguards: &VanguardMgr<RT>,
236
56
    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
237
56
        let netdir = match netdir {
238
56
            DirInfo::Directory(d) => d,
239
            _ => {
240
                return Err(bad_api_usage!(
241
                    "Tried to build a multihop path without a network directory"
242
                )
243
                .into());
244
            }
245
        };
246

            
247
        // Select the guard, allowing it to appear as
248
        // either of the last two hops of the circuit.
249
56
        let (l1_guard, mon, usable) = select_guard(netdir, guards, None)?;
250

            
251
56
        let target_exclusion = if let Some(target) = self.compatible_with.as_ref() {
252
16
            RelayExclusion::exclude_identities(
253
16
                target.identities().map(|id| id.to_owned()).collect(),
254
            )
255
        } else {
256
40
            RelayExclusion::no_relays_excluded()
257
        };
258

            
259
56
        let mode = vanguards.mode();
260
56
        let path = match mode {
261
            VanguardMode::Lite => {
262
24
                self.pick_lite_vanguard_path(rng, netdir, vanguards, l1_guard, &target_exclusion)?
263
            }
264
            VanguardMode::Full => {
265
32
                self.pick_full_vanguard_path(rng, netdir, vanguards, l1_guard, &target_exclusion)?
266
            }
267
            VanguardMode::Disabled => {
268
                return Err(internal!(
269
                    "VanguardHsPathBuilder::pick_path called, but vanguards are disabled?!"
270
                )
271
                .into());
272
            }
273
            _ => {
274
                return Err(internal!("unrecognized vanguard mode {mode}").into());
275
            }
276
        };
277

            
278
40
        let actual_len = path.len();
279
40
        let expected_len = self.stem_kind.num_hops(mode)?;
280
40
        if actual_len != expected_len {
281
            return Err(internal!(
282
                "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
283
                self.stem_kind,
284
                expected_len,
285
                actual_len
286
            )
287
            .into());
288
40
        }
289

            
290
40
        Ok((path, mon, usable))
291
56
    }
292

            
293
    /// Create a path for a hidden service circuit stem using full vanguards.
294
32
    fn pick_full_vanguard_path<'n, R: Rng, RT: Runtime>(
295
32
        &self,
296
32
        rng: &mut R,
297
32
        netdir: &'n NetDir,
298
32
        vanguards: &VanguardMgr<RT>,
299
32
        l1_guard: MaybeOwnedRelay<'n>,
300
32
        target_exclusion: &RelayExclusion<'n>,
301
32
    ) -> Result<TorPath<'n>> {
302
        // NOTE: if the we are using full vanguards and building an GUARDED circuit stem,
303
        // we do *not* exclude the target from occurring as the second hop
304
        // (circuits of the form G - L2 - L3 - M - L2 are valid)
305

            
306
32
        let l2_target_exclusion = match self.stem_kind {
307
16
            HsCircStemKind::Guarded => RelayExclusion::no_relays_excluded(),
308
16
            HsCircStemKind::Naive => target_exclusion.clone(),
309
        };
310
        // We have to pick the usage based on whether this hop is the last one of the stem.
311
32
        let l3_usage = match self.stem_kind {
312
16
            HsCircStemKind::Naive => hs_stem_terminal_hop_usage(self.circ_kind),
313
16
            HsCircStemKind::Guarded => hs_intermediate_hop_usage(),
314
        };
315
32
        let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), l2_target_exclusion);
316
32
        let l3_selector = RelaySelector::new(l3_usage, target_exclusion.clone());
317

            
318
32
        let path = vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard);
319

            
320
32
        let path = path
321
32
            .add_vanguard(&l2_selector, Layer::Layer2)?
322
32
            .add_vanguard(&l3_selector, Layer::Layer3)?;
323

            
324
24
        match self.stem_kind {
325
            HsCircStemKind::Guarded => {
326
                // If full vanguards are enabled, we need an extra hop for the GUARDED stem:
327
                //     NAIVE   = G -> L2 -> L3
328
                //     GUARDED = G -> L2 -> L3 -> M
329

            
330
12
                let mid_selector = RelaySelector::new(
331
12
                    hs_stem_terminal_hop_usage(self.circ_kind),
332
12
                    target_exclusion.clone(),
333
                );
334
12
                path.add_middle(&mid_selector)?.build()
335
            }
336
12
            HsCircStemKind::Naive => path.build(),
337
        }
338
32
    }
339

            
340
    /// Create a path for a hidden service circuit stem using lite vanguards.
341
24
    fn pick_lite_vanguard_path<'n, R: Rng, RT: Runtime>(
342
24
        &self,
343
24
        rng: &mut R,
344
24
        netdir: &'n NetDir,
345
24
        vanguards: &VanguardMgr<RT>,
346
24
        l1_guard: MaybeOwnedRelay<'n>,
347
24
        target_exclusion: &RelayExclusion<'n>,
348
24
    ) -> Result<TorPath<'n>> {
349
24
        let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), target_exclusion.clone());
350
24
        let mid_selector = RelaySelector::new(
351
24
            hs_stem_terminal_hop_usage(self.circ_kind),
352
24
            target_exclusion.clone(),
353
        );
354

            
355
24
        vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard)
356
24
            .add_vanguard(&l2_selector, Layer::Layer2)?
357
24
            .add_middle(&mid_selector)?
358
16
            .build()
359
24
    }
360
}
361

            
362
/// Return the usage that we should use when selecting an intermediary hop (vanguard or middle) of
363
/// an HS circuit or stem circuit.
364
///
365
/// (This isn't called "middle hop", since we want to avoid confusion with the M hop in vanguard
366
/// circuits.)
367
528
pub(crate) fn hs_intermediate_hop_usage() -> RelayUsage {
368
    // Restrict our intermediary relays to the set of middle relays we could use when building a new
369
    // intro circuit.
370

            
371
    // TODO: This usage is a bit convoluted, and some onion-service-
372
    // related circuits don't really need this much stability.
373
    //
374
    // TODO: new_intro_point() isn't really accurate here, but it _is_
375
    // the most restrictive target-usage we can use.
376
528
    RelayUsage::middle_relay(Some(&RelayUsage::new_intro_point()))
377
528
}
378

            
379
/// Return the usage that we should use when selecting the last hop of a stem circuit.
380
///
381
/// If `kind` is provided, we need to make sure that the last hop will yield a stem circuit
382
/// that's fit for that kind of circuit.
383
456
pub(crate) fn hs_stem_terminal_hop_usage(kind: Option<HsCircKind>) -> RelayUsage {
384
456
    let Some(kind) = kind else {
385
        // For unknown HsCircKinds, we'll pick an arbitrary last hop, and check later
386
        // that it is really suitable for whatever purpose we had in mind.
387
456
        return hs_intermediate_hop_usage();
388
    };
389
    match kind {
390
        HsCircKind::ClientRend => {
391
            // This stem circuit going to get used as-is for a ClientRend circuit,
392
            // and so the last hop of the stem circuit needs to be suitable as a rendezvous point.
393
            RelayUsage::new_rend_point()
394
        }
395
        HsCircKind::SvcHsDir
396
        | HsCircKind::SvcIntro
397
        | HsCircKind::SvcRend
398
        | HsCircKind::ClientHsDir
399
        | HsCircKind::ClientIntro => {
400
            // For all other HSCircKind cases, the last hop will be added to the stem,
401
            // so we have no additional restrictions on the usage.
402
            hs_intermediate_hop_usage()
403
        }
404
    }
405
456
}
406

            
407
#[cfg(test)]
408
mod test {
409
    // @@ begin test lint list maintained by maint/add_warning @@
410
    #![allow(clippy::bool_assert_comparison)]
411
    #![allow(clippy::clone_on_copy)]
412
    #![allow(clippy::dbg_macro)]
413
    #![allow(clippy::mixed_attributes_style)]
414
    #![allow(clippy::print_stderr)]
415
    #![allow(clippy::print_stdout)]
416
    #![allow(clippy::single_char_pattern)]
417
    #![allow(clippy::unwrap_used)]
418
    #![allow(clippy::unchecked_time_subtraction)]
419
    #![allow(clippy::useless_vec)]
420
    #![allow(clippy::needless_pass_by_value)]
421
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
422

            
423
    use std::sync::Arc;
424

            
425
    use super::*;
426

            
427
    use tor_linkspec::{ChannelMethod, OwnedCircTarget};
428
    use tor_netdir::{NetDirProvider, testnet::NodeBuilders, testprovider::TestNetDirProvider};
429
    use tor_netdoc::doc::netstatus::RelayWeight;
430
    use tor_netdoc::types::relay_flags::RelayFlag;
431
    use tor_rtmock::MockRuntime;
432
    use web_time_compat::SystemTimeExt;
433

            
434
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
435
    use {
436
        crate::path::OwnedPath, tor_basic_utils::test_rng::testing_rng,
437
        tor_guardmgr::VanguardMgrError, tor_netdir::testnet::construct_custom_netdir,
438
    };
439

            
440
    /// The maximum number of relays in a test network.
441
    const MAX_NET_SIZE: usize = 40;
442

            
443
    /// Construct a test network of the specified size.
444
    fn construct_test_network<F>(size: usize, mut set_family: F) -> NetDir
445
    where
446
        F: FnMut(usize, &mut NodeBuilders),
447
    {
448
        assert!(
449
            size <= MAX_NET_SIZE,
450
            "the test network supports at most {MAX_NET_SIZE} relays"
451
        );
452
        let netdir = construct_custom_netdir(|pos, nb, _| {
453
            nb.omit_rs = pos >= size;
454
            if !nb.omit_rs {
455
                let f = RelayFlag::Running
456
                    | RelayFlag::Valid
457
                    | RelayFlag::V2Dir
458
                    | RelayFlag::Fast
459
                    | RelayFlag::Stable;
460
                nb.rs.set_flags(f | RelayFlag::Guard);
461
                nb.rs.weight(RelayWeight::Measured(10_000));
462

            
463
                set_family(pos, nb);
464
            }
465
        })
466
        .unwrap()
467
        .unwrap_if_sufficient()
468
        .unwrap();
469

            
470
        assert_eq!(netdir.all_relays().count(), size);
471

            
472
        netdir
473
    }
474

            
475
    /// Construct a test network where every relay is in the same family with everyone else.
476
    fn same_family_test_network(size: usize) -> NetDir {
477
        construct_test_network(size, |_pos, nb| {
478
            // Everybody is in the same family with everyone else
479
            let family = (0..MAX_NET_SIZE)
480
                .map(|i| hex::encode([i as u8; 20]))
481
                .collect::<Vec<_>>()
482
                .join(" ");
483

            
484
            nb.md.family(family.parse().unwrap());
485
        })
486
    }
487

            
488
    /// Helper for extracting the hops in a `TorPath`.
489
    fn path_hops(path: &TorPath) -> Vec<OwnedCircTarget> {
490
        let path: OwnedPath = path.try_into().unwrap();
491
        match path {
492
            OwnedPath::ChannelOnly(_) => {
493
                panic!("expected OwnedPath::Normal, got OwnedPath::ChannelOnly")
494
            }
495
            OwnedPath::Normal(ref v) => v.clone(),
496
        }
497
    }
498

            
499
    /// Check the uniqueness of the hops from the specified `TorPath`.
500
    ///
501
    /// If `expect_dupes` is `true`, asserts that the path has some duplicate hops.
502
    /// Otherwise, asserts that there are no duplicate hops in the path.
503
    fn assert_duplicate_hops(path: &TorPath, expect_dupes: bool) {
504
        let hops = path_hops(path);
505
        let has_dupes = hops.iter().enumerate().any(|(i, hop)| {
506
            hops.iter()
507
                .skip(i + 1)
508
                .any(|h| h.has_any_relay_id_from(hop))
509
        });
510
        let msg = if expect_dupes { "have" } else { "not have any" };
511

            
512
        assert_eq!(
513
            has_dupes, expect_dupes,
514
            "expected path to {msg} duplicate hops: {:?}",
515
            hops
516
        );
517
    }
518

            
519
    /// Assert that the specified `TorPath` is a valid path for a circuit using vanguards.
520
    #[cfg(feature = "vanguards")]
521
    fn assert_vanguard_path_ok(
522
        path: &TorPath,
523
        stem_kind: HsCircStemKind,
524
        mode: VanguardMode,
525
        target: Option<&OwnedChanTarget>,
526
    ) {
527
        use itertools::Itertools;
528

            
529
        assert_eq!(
530
            path.len(),
531
            stem_kind.num_hops(mode).unwrap(),
532
            "invalid path length for {stem_kind} {mode}-vanguards circuit"
533
        );
534

            
535
        let hops = path_hops(path);
536
        for (hop1, hop2, hop3) in hops.iter().tuple_windows() {
537
            if hop1.has_any_relay_id_from(hop2)
538
                || hop1.has_any_relay_id_from(hop3)
539
                || hop2.has_any_relay_id_from(hop3)
540
            {
541
                panic!(
542
                    "neighboring hops should be distinct: [{}], [{}], [{}]",
543
                    hop1.display_relay_ids(),
544
                    hop2.display_relay_ids(),
545
                    hop3.display_relay_ids(),
546
                );
547
            }
548
        }
549

            
550
        // If the circuit had a target, make sure its last 2 hops are compatible with it.
551
        if let Some(target) = target {
552
            for hop in hops.iter().rev().take(2) {
553
                if hop.has_any_relay_id_from(target) {
554
                    panic!(
555
                        "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
556
                        hop.display_relay_ids(),
557
                        target.display_relay_ids(),
558
                    );
559
                }
560
            }
561
        }
562
    }
563

            
564
    /// Assert that the specified `TorPath` is a valid HS path.
565
    fn assert_hs_path_ok(path: &TorPath, target: Option<&OwnedChanTarget>) {
566
        assert_eq!(path.len(), 3);
567
        assert_duplicate_hops(path, false);
568
        if let Some(target) = target {
569
            for hop in path_hops(path) {
570
                if hop.has_any_relay_id_from(target) {
571
                    panic!(
572
                        "invalid path: hop {} is the same relay as the circuit target {}",
573
                        hop.display_relay_ids(),
574
                        target.display_relay_ids()
575
                    )
576
                }
577
            }
578
        }
579
    }
580

            
581
    /// Helper for calling `HsPathBuilder::pick_path_with_vanguards`.
582
    async fn pick_vanguard_path<'a>(
583
        runtime: &MockRuntime,
584
        netdir: &'a NetDir,
585
        stem_kind: HsCircStemKind,
586
        circ_kind: Option<HsCircKind>,
587
        mode: VanguardMode,
588
        target: Option<&OwnedChanTarget>,
589
    ) -> Result<TorPath<'a>> {
590
        let vanguardmgr = VanguardMgr::new_testing(runtime, mode).unwrap();
591
        let _provider = vanguardmgr.init_vanguard_sets(netdir).await.unwrap();
592

            
593
        let mut rng = testing_rng();
594
        let guards = tor_guardmgr::GuardMgr::new(
595
            runtime.clone(),
596
            tor_persist::TestingStateMgr::new(),
597
            &tor_guardmgr::TestConfig::default(),
598
        )
599
        .unwrap();
600
        let netdir_provider = Arc::new(TestNetDirProvider::new());
601
        netdir_provider.set_netdir(netdir.clone());
602
        let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
603
        guards.install_netdir_provider(&netdir_provider).unwrap();
604
        let config = PathConfig::default();
605
        // TODO #2428.  (This is just testing, though)
606
        let now = SystemTime::get();
607
        let dirinfo = (netdir).into();
608
        HsPathBuilder::new(target.cloned(), stem_kind, circ_kind)
609
            .pick_path_with_vanguards(&mut rng, dirinfo, &guards, &vanguardmgr, &config, now)
610
            .map(|res| res.0)
611
    }
612

            
613
    /// Helper for calling `HsPathBuilder::pick_path`.
614
    fn pick_hs_path_no_vanguards<'a>(
615
        netdir: &'a NetDir,
616
        target: Option<&OwnedChanTarget>,
617
        circ_kind: Option<HsCircKind>,
618
    ) -> Result<TorPath<'a>> {
619
        let mut rng = testing_rng();
620
        let config = PathConfig::default();
621
        // TODO #2428. (This is just testing, though)
622
        let now = SystemTime::get();
623
        let dirinfo = (netdir).into();
624
        let guards = tor_guardmgr::GuardMgr::new(
625
            MockRuntime::new(),
626
            tor_persist::TestingStateMgr::new(),
627
            &tor_guardmgr::TestConfig::default(),
628
        )
629
        .unwrap();
630
        let netdir_provider = Arc::new(TestNetDirProvider::new());
631
        netdir_provider.set_netdir(netdir.clone());
632
        let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
633
        guards.install_netdir_provider(&netdir_provider).unwrap();
634
        HsPathBuilder::new(target.cloned(), HsCircStemKind::Naive, circ_kind)
635
            .pick_path(&mut rng, dirinfo, &guards, &config, now)
636
            .map(|res| res.0)
637
    }
638

            
639
    /// Return an `OwnedChanTarget` to use as the target of a circuit.
640
    ///
641
    /// This will correspond to the "first" relay from the test network
642
    /// (the one with the $0000000000000000000000000000000000000000
643
    /// RSA identity fingerprint).
644
    fn test_target() -> OwnedChanTarget {
645
        // We target one of the relays known to be the network.
646
        OwnedChanTarget::builder()
647
            .addrs(vec!["127.0.0.3:9001".parse().unwrap()])
648
            .ed_identity([0xAA; 32].into())
649
            .rsa_identity([0x00; 20].into())
650
            .method(ChannelMethod::Direct(vec!["0.0.0.3:9001".parse().unwrap()]))
651
            .build()
652
            .unwrap()
653
    }
654

            
655
    // Prevents TROVE-2024-006 (arti#1425).
656
    //
657
    // Note: this, and all the other tests that disable vanguards,
658
    // perhaps belong in ExitPathBuilder, as they are effectively
659
    // testing the vanilla pick_path() implementation.
660
    #[test]
661
    fn hs_path_no_vanguards_incompatible_target() {
662
        // We target one of the relays known to be the network.
663
        let target = test_target();
664

            
665
        let netdir = construct_test_network(3, |pos, nb| {
666
            // The target is in a family with every other relay,
667
            // so any circuit we might build is going to be incompatible with it
668
            if pos == 0 {
669
                let family = (0..MAX_NET_SIZE)
670
                    .map(|i| hex::encode([i as u8; 20]))
671
                    .collect::<Vec<_>>()
672
                    .join(" ");
673

            
674
                nb.md.family(family.parse().unwrap());
675
            } else {
676
                nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
677
            }
678
        });
679
        // We'll fail to select a guard, because the network doesn't have any relays compatible
680
        // with the target
681
        let err = pick_hs_path_no_vanguards(&netdir, Some(&target), None)
682
            .map(|_| ())
683
            .unwrap_err();
684

            
685
        assert!(
686
            matches!(
687
                err,
688
                Error::NoRelay {
689
                    ref problem,
690
                    ..
691
                } if problem ==  "Failed: rejected 3/3 as in same family as already selected"
692
            ),
693
            "{err:?}"
694
        );
695
    }
696

            
697
    #[test]
698
    fn hs_path_no_vanguards_reject_same_family() {
699
        // All the relays in the network are in the same family,
700
        // so building HS circuits should be impossible.
701
        let netdir = same_family_test_network(MAX_NET_SIZE);
702
        let err = match pick_hs_path_no_vanguards(&netdir, None, None) {
703
            Ok(path) => panic!(
704
                "expected error, but got valid path: {:?})",
705
                OwnedPath::try_from(&path).unwrap()
706
            ),
707
            Err(e) => e,
708
        };
709

            
710
        assert!(
711
            matches!(
712
                err,
713
                Error::NoRelay {
714
                    ref problem,
715
                    ..
716
                } if problem ==  "Failed: rejected 40/40 as in same family as already selected"
717
            ),
718
            "{err:?}"
719
        );
720
    }
721

            
722
    #[test]
723
    fn hs_path_no_vanguards() {
724
        let netdir = construct_test_network(20, |pos, nb| {
725
            nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
726
        });
727
        // We target one of the relays known to be the network.
728
        let target = test_target();
729
        for _ in 0..100 {
730
            for target in [None, Some(target.clone())] {
731
                let path = pick_hs_path_no_vanguards(&netdir, target.as_ref(), None).unwrap();
732
                assert_hs_path_ok(&path, target.as_ref());
733
            }
734
        }
735
    }
736

            
737
    #[test]
738
    #[cfg(feature = "vanguards")]
739
    fn lite_vanguard_path_insufficient_relays() {
740
        MockRuntime::test_with_various(|runtime| async move {
741
            let netdir = same_family_test_network(2);
742
            for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
743
                let err = pick_vanguard_path(
744
                    &runtime,
745
                    &netdir,
746
                    stem_kind,
747
                    None,
748
                    VanguardMode::Lite,
749
                    None,
750
                )
751
                .await
752
                .map(|_| ())
753
                .unwrap_err();
754

            
755
                // The test network is too small to build a 3-hop circuit.
756
                assert!(
757
                    matches!(
758
                        err,
759
                        Error::NoRelay {
760
                            ref problem,
761
                            ..
762
                        } if problem == "Failed: rejected 2/2 as already selected",
763
                    ),
764
                    "{err:?}"
765
                );
766
            }
767
        });
768
    }
769

            
770
    // Prevents TROVE-2024-003 (arti#1409).
771
    #[test]
772
    #[cfg(feature = "vanguards")]
773
    fn lite_vanguard_path() {
774
        MockRuntime::test_with_various(|runtime| async move {
775
            // We target one of the relays known to be the network.
776
            let target = OwnedChanTarget::builder()
777
                .rsa_identity([0x00; 20].into())
778
                .build()
779
                .unwrap();
780
            let netdir = same_family_test_network(10);
781
            let mode = VanguardMode::Lite;
782

            
783
            for target in [None, Some(target)] {
784
                for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
785
                    let path = pick_vanguard_path(
786
                        &runtime,
787
                        &netdir,
788
                        stem_kind,
789
                        None,
790
                        mode,
791
                        target.as_ref(),
792
                    )
793
                    .await
794
                    .unwrap();
795
                    assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
796
                }
797
            }
798
        });
799
    }
800

            
801
    #[test]
802
    #[cfg(feature = "vanguards")]
803
    fn full_vanguard_path() {
804
        MockRuntime::test_with_various(|runtime| async move {
805
            let netdir = same_family_test_network(MAX_NET_SIZE);
806
            let mode = VanguardMode::Full;
807

            
808
            // We target one of the relays known to be the network.
809
            let target = OwnedChanTarget::builder()
810
                .rsa_identity([0x00; 20].into())
811
                .build()
812
                .unwrap();
813

            
814
            for target in [None, Some(target)] {
815
                for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
816
                    let path = pick_vanguard_path(
817
                        &runtime,
818
                        &netdir,
819
                        stem_kind,
820
                        None,
821
                        mode,
822
                        target.as_ref(),
823
                    )
824
                    .await
825
                    .unwrap();
826
                    assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
827
                }
828
            }
829
        });
830
    }
831

            
832
    #[test]
833
    #[cfg(feature = "vanguards")]
834
    fn full_vanguard_path_insufficient_relays() {
835
        MockRuntime::test_with_various(|runtime| async move {
836
            let netdir = same_family_test_network(2);
837

            
838
            for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
839
                let err = pick_vanguard_path(
840
                    &runtime,
841
                    &netdir,
842
                    stem_kind,
843
                    None,
844
                    VanguardMode::Full,
845
                    None,
846
                )
847
                .await
848
                .map(|_| ())
849
                .unwrap_err();
850
                assert!(
851
                    matches!(
852
                        err,
853
                        Error::VanguardMgrInit(VanguardMgrError::NoSuitableRelay(Layer::Layer3)),
854
                    ),
855
                    "{err:?}"
856
                );
857
            }
858

            
859
            // We *can* build circuit stems in a 3-relay network,
860
            // as long as they don't have a specified target
861
            let netdir = same_family_test_network(3);
862
            let mode = VanguardMode::Full;
863

            
864
            for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
865
                let path = pick_vanguard_path(&runtime, &netdir, stem_kind, None, mode, None)
866
                    .await
867
                    .unwrap();
868
                assert_vanguard_path_ok(&path, stem_kind, mode, None);
869
                match stem_kind {
870
                    HsCircStemKind::Naive => {
871
                        // A 3-hop circuit can't contain duplicates,
872
                        // because that would mean it has one of the following
873
                        // configurations
874
                        //
875
                        //     A - A - A
876
                        //     A - A - B
877
                        //     A - B - A
878
                        //     A - B - B
879
                        //     B - A - A
880
                        //     B - A - B
881
                        //     B - B - A
882
                        //     B - B - B
883
                        //
884
                        // none of which are valid circuits, because a relay won't extend
885
                        // to itself or its predecessor.
886
                        assert_duplicate_hops(&path, false);
887
                    }
888
                    HsCircStemKind::Guarded => {
889
                        // There are only 3 relats in the network,
890
                        // so a 4-hop circuit must contain the same hop twice.
891
                        assert_duplicate_hops(&path, true);
892
                    }
893
                }
894
            }
895
        });
896
    }
897
}