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

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

            
74
use super::AnonymousPathBuilder;
75

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

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

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

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

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

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

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

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

            
168
56
        let vanguard_path_builder = VanguardHsPathBuilder {
169
56
            stem_kind: self.stem_kind,
170
56
            circ_kind: self.circ_kind,
171
56
            compatible_with: self.compatible_with.clone(),
172
56
        };
173
56

            
174
56
        vanguard_path_builder.pick_path(rng, netdir, guards, vanguards)
175
56
    }
176
}
177

            
178
impl AnonymousPathBuilder for HsPathBuilder {
179
808
    fn compatible_with(&self) -> Option<&OwnedChanTarget> {
180
808
        self.compatible_with.as_ref()
181
808
    }
182

            
183
4
    fn path_kind(&self) -> &'static str {
184
4
        "onion-service circuit"
185
4
    }
186

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

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

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

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

            
243
        // Select the guard, allowing it to appear as
244
        // either of the last two hops of the circuit.
245
56
        let (l1_guard, mon, usable) = select_guard(netdir, guards, None)?;
246

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

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

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

            
286
40
        Ok((path, mon, usable))
287
56
    }
288

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

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

            
314
32
        let path = vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard);
315

            
316
32
        let path = path
317
32
            .add_vanguard(&l2_selector, Layer::Layer2)?
318
32
            .add_vanguard(&l3_selector, Layer::Layer3)?;
319

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

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

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

            
351
24
        vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard)
352
24
            .add_vanguard(&l2_selector, Layer::Layer2)?
353
24
            .add_middle(&mid_selector)?
354
16
            .build()
355
24
    }
356
}
357

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

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

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

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

            
419
    use std::sync::Arc;
420

            
421
    use super::*;
422

            
423
    use tor_linkspec::{ChannelMethod, OwnedCircTarget};
424
    use tor_netdir::{testnet::NodeBuilders, testprovider::TestNetDirProvider, NetDirProvider};
425
    use tor_netdoc::doc::netstatus::{RelayFlags, RelayWeight};
426
    use tor_rtmock::MockRuntime;
427

            
428
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
429
    use {
430
        crate::path::OwnedPath, tor_basic_utils::test_rng::testing_rng,
431
        tor_guardmgr::VanguardMgrError, tor_netdir::testnet::construct_custom_netdir,
432
    };
433

            
434
    /// The maximum number of relays in a test network.
435
    const MAX_NET_SIZE: usize = 40;
436

            
437
    /// Construct a test network of the specified size.
438
    fn construct_test_network<F>(size: usize, mut set_family: F) -> NetDir
439
    where
440
        F: FnMut(usize, &mut NodeBuilders),
441
    {
442
        assert!(
443
            size <= MAX_NET_SIZE,
444
            "the test network supports at most {MAX_NET_SIZE} relays"
445
        );
446
        let netdir = construct_custom_netdir(|pos, nb, _| {
447
            nb.omit_rs = pos >= size;
448
            if !nb.omit_rs {
449
                let f = RelayFlags::RUNNING
450
                    | RelayFlags::VALID
451
                    | RelayFlags::V2DIR
452
                    | RelayFlags::FAST
453
                    | RelayFlags::STABLE;
454
                nb.rs.set_flags(f | RelayFlags::GUARD);
455
                nb.rs.weight(RelayWeight::Measured(10_000));
456

            
457
                set_family(pos, nb);
458
            }
459
        })
460
        .unwrap()
461
        .unwrap_if_sufficient()
462
        .unwrap();
463

            
464
        assert_eq!(netdir.all_relays().count(), size);
465

            
466
        netdir
467
    }
468

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

            
478
            nb.md.family(family.parse().unwrap());
479
        })
480
    }
481

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

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

            
506
        assert_eq!(
507
            has_dupes, expect_dupes,
508
            "expected path to {msg} duplicate hops: {:?}",
509
            hops
510
        );
511
    }
512

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

            
523
        assert_eq!(
524
            path.len(),
525
            stem_kind.num_hops(mode).unwrap(),
526
            "invalid path length for {stem_kind} {mode}-vanguards circuit"
527
        );
528

            
529
        let hops = path_hops(path);
530
        for (hop1, hop2, hop3) in hops.iter().tuple_windows() {
531
            if hop1.has_any_relay_id_from(hop2)
532
                || hop1.has_any_relay_id_from(hop3)
533
                || hop2.has_any_relay_id_from(hop3)
534
            {
535
                panic!(
536
                    "neighboring hops should be distinct: [{}], [{}], [{}]",
537
                    hop1.display_relay_ids(),
538
                    hop2.display_relay_ids(),
539
                    hop3.display_relay_ids(),
540
                );
541
            }
542
        }
543

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

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

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

            
587
        let mut rng = testing_rng();
588
        let guards = tor_guardmgr::GuardMgr::new(
589
            runtime.clone(),
590
            tor_persist::TestingStateMgr::new(),
591
            &tor_guardmgr::TestConfig::default(),
592
        )
593
        .unwrap();
594
        let netdir_provider = Arc::new(TestNetDirProvider::new());
595
        netdir_provider.set_netdir(netdir.clone());
596
        let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
597
        guards.install_netdir_provider(&netdir_provider).unwrap();
598
        let config = PathConfig::default();
599
        let now = SystemTime::now();
600
        let dirinfo = (netdir).into();
601
        HsPathBuilder::new(target.cloned(), stem_kind, circ_kind)
602
            .pick_path_with_vanguards(&mut rng, dirinfo, &guards, &vanguardmgr, &config, now)
603
            .map(|res| res.0)
604
    }
605

            
606
    /// Helper for calling `HsPathBuilder::pick_path`.
607
    fn pick_hs_path_no_vanguards<'a>(
608
        netdir: &'a NetDir,
609
        target: Option<&OwnedChanTarget>,
610
        circ_kind: Option<HsCircKind>,
611
    ) -> Result<TorPath<'a>> {
612
        let mut rng = testing_rng();
613
        let config = PathConfig::default();
614
        let now = SystemTime::now();
615
        let dirinfo = (netdir).into();
616
        let guards = tor_guardmgr::GuardMgr::new(
617
            MockRuntime::new(),
618
            tor_persist::TestingStateMgr::new(),
619
            &tor_guardmgr::TestConfig::default(),
620
        )
621
        .unwrap();
622
        let netdir_provider = Arc::new(TestNetDirProvider::new());
623
        netdir_provider.set_netdir(netdir.clone());
624
        let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
625
        guards.install_netdir_provider(&netdir_provider).unwrap();
626
        HsPathBuilder::new(target.cloned(), HsCircStemKind::Naive, circ_kind)
627
            .pick_path(&mut rng, dirinfo, &guards, &config, now)
628
            .map(|res| res.0)
629
    }
630

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

            
647
    // Prevents TROVE-2024-006 (arti#1425).
648
    //
649
    // Note: this, and all the other tests that disable vanguards,
650
    // perhaps belong in ExitPathBuilder, as they are are effectively
651
    // testing the vanilla pick_path() implementation.
652
    #[test]
653
    fn hs_path_no_vanguards_incompatible_target() {
654
        // We target one of the relays known to be the network.
655
        let target = test_target();
656

            
657
        let netdir = construct_test_network(3, |pos, nb| {
658
            // The target is in a family with every other relay,
659
            // so any circuit we might build is going to be incompatible with it
660
            if pos == 0 {
661
                let family = (0..MAX_NET_SIZE)
662
                    .map(|i| hex::encode([i as u8; 20]))
663
                    .collect::<Vec<_>>()
664
                    .join(" ");
665

            
666
                nb.md.family(family.parse().unwrap());
667
            } else {
668
                nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
669
            }
670
        });
671
        // We'll fail to select a guard, because the network doesn't have any relays compatible
672
        // with the target
673
        let err = pick_hs_path_no_vanguards(&netdir, Some(&target), None)
674
            .map(|_| ())
675
            .unwrap_err();
676

            
677
        assert!(
678
            matches!(
679
                err,
680
                Error::NoRelay {
681
                    ref problem,
682
                    ..
683
                } if problem ==  "Failed: rejected 0/3 as not usable as middle relay; 3/3 as in same family as already selected"
684
            ),
685
            "{err:?}"
686
        );
687
    }
688

            
689
    #[test]
690
    fn hs_path_no_vanguards_reject_same_family() {
691
        // All the relays in the network are in the same family,
692
        // so building HS circuits should be impossible.
693
        let netdir = same_family_test_network(MAX_NET_SIZE);
694
        let err = match pick_hs_path_no_vanguards(&netdir, None, None) {
695
            Ok(path) => panic!(
696
                "expected error, but got valid path: {:?})",
697
                OwnedPath::try_from(&path).unwrap()
698
            ),
699
            Err(e) => e,
700
        };
701

            
702
        assert!(
703
            matches!(
704
                err,
705
                Error::NoRelay {
706
                    ref problem,
707
                    ..
708
                } if problem ==  "Failed: rejected 0/40 as not usable as middle relay; 40/40 as in same family as already selected"
709
            ),
710
            "{err:?}"
711
        );
712
    }
713

            
714
    #[test]
715
    fn hs_path_no_vanguards() {
716
        let netdir = construct_test_network(20, |pos, nb| {
717
            nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
718
        });
719
        // We target one of the relays known to be the network.
720
        let target = test_target();
721
        for _ in 0..100 {
722
            for target in [None, Some(target.clone())] {
723
                let path = pick_hs_path_no_vanguards(&netdir, target.as_ref(), None).unwrap();
724
                assert_hs_path_ok(&path, target.as_ref());
725
            }
726
        }
727
    }
728

            
729
    #[test]
730
    #[cfg(feature = "vanguards")]
731
    fn lite_vanguard_path_insufficient_relays() {
732
        MockRuntime::test_with_various(|runtime| async move {
733
            let netdir = same_family_test_network(2);
734
            for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
735
                let err = pick_vanguard_path(
736
                    &runtime,
737
                    &netdir,
738
                    stem_kind,
739
                    None,
740
                    VanguardMode::Lite,
741
                    None,
742
                )
743
                .await
744
                .map(|_| ())
745
                .unwrap_err();
746

            
747
                // The test network is too small to build a 3-hop circuit.
748
                assert!(
749
                    matches!(
750
                        err,
751
                        Error::NoRelay {
752
                            ref problem,
753
                            ..
754
                        } if problem == "Failed: rejected 0/2 as not usable as middle relay; 2/2 as already selected",
755
                    ),
756
                    "{err:?}"
757
                );
758
            }
759
        });
760
    }
761

            
762
    // Prevents TROVE-2024-003 (arti#1409).
763
    #[test]
764
    #[cfg(feature = "vanguards")]
765
    fn lite_vanguard_path() {
766
        MockRuntime::test_with_various(|runtime| async move {
767
            // We target one of the relays known to be the network.
768
            let target = OwnedChanTarget::builder()
769
                .rsa_identity([0x00; 20].into())
770
                .build()
771
                .unwrap();
772
            let netdir = same_family_test_network(10);
773
            let mode = VanguardMode::Lite;
774

            
775
            for target in [None, Some(target)] {
776
                for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
777
                    let path = pick_vanguard_path(
778
                        &runtime,
779
                        &netdir,
780
                        stem_kind,
781
                        None,
782
                        mode,
783
                        target.as_ref(),
784
                    )
785
                    .await
786
                    .unwrap();
787
                    assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
788
                }
789
            }
790
        });
791
    }
792

            
793
    #[test]
794
    #[cfg(feature = "vanguards")]
795
    fn full_vanguard_path() {
796
        MockRuntime::test_with_various(|runtime| async move {
797
            let netdir = same_family_test_network(MAX_NET_SIZE);
798
            let mode = VanguardMode::Full;
799

            
800
            // We target one of the relays known to be the network.
801
            let target = OwnedChanTarget::builder()
802
                .rsa_identity([0x00; 20].into())
803
                .build()
804
                .unwrap();
805

            
806
            for target in [None, Some(target)] {
807
                for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
808
                    let path = pick_vanguard_path(
809
                        &runtime,
810
                        &netdir,
811
                        stem_kind,
812
                        None,
813
                        mode,
814
                        target.as_ref(),
815
                    )
816
                    .await
817
                    .unwrap();
818
                    assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
819
                }
820
            }
821
        });
822
    }
823

            
824
    #[test]
825
    #[cfg(feature = "vanguards")]
826
    fn full_vanguard_path_insufficient_relays() {
827
        MockRuntime::test_with_various(|runtime| async move {
828
            let netdir = same_family_test_network(2);
829

            
830
            for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
831
                let err = pick_vanguard_path(
832
                    &runtime,
833
                    &netdir,
834
                    stem_kind,
835
                    None,
836
                    VanguardMode::Full,
837
                    None,
838
                )
839
                .await
840
                .map(|_| ())
841
                .unwrap_err();
842
                assert!(
843
                    matches!(
844
                        err,
845
                        Error::VanguardMgrInit(VanguardMgrError::NoSuitableRelay(Layer::Layer3)),
846
                    ),
847
                    "{err:?}"
848
                );
849
            }
850

            
851
            // We *can* build circuit stems in a 3-relay network,
852
            // as long as they don't have a specified target
853
            let netdir = same_family_test_network(3);
854
            let mode = VanguardMode::Full;
855

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