Skip to main content

tor_circmgr/path/
hspath.rs

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")]
64mod vanguards;
65
66use rand::Rng;
67use tor_error::internal;
68use tor_linkspec::{HasRelayIds, OwnedChanTarget};
69use tor_netdir::{NetDir, Relay};
70use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
71use tracing::instrument;
72
73use crate::{Error, Result, hspool::HsCircKind, hspool::HsCircStemKind};
74
75use super::AnonymousPathBuilder;
76
77use {
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")]
86use {
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")]
95pub(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.
100pub(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
116impl 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    pub(crate) fn new(
125        compatible_with: Option<OwnedChanTarget>,
126        stem_kind: HsCircStemKind,
127        circ_kind: Option<HsCircKind>,
128    ) -> Self {
129        Self {
130            compatible_with,
131            stem_kind,
132            circ_kind,
133        }
134    }
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    pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
140        &self,
141        rng: &mut R,
142        netdir: DirInfo<'a>,
143        guards: &GuardMgr<RT>,
144        config: &PathConfig,
145        now: SystemTime,
146    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
147        pick_path(self, rng, netdir, guards, config, now)
148    }
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    pub(crate) fn pick_path_with_vanguards<'a, R: Rng, RT: Runtime>(
158        &self,
159        rng: &mut R,
160        netdir: DirInfo<'a>,
161        guards: &GuardMgr<RT>,
162        vanguards: &VanguardMgr<RT>,
163        config: &PathConfig,
164        now: SystemTime,
165    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
166        let mode = vanguards.mode();
167        if mode == VanguardMode::Disabled {
168            return pick_path(self, rng, netdir, guards, config, now);
169        }
170
171        let vanguard_path_builder = VanguardHsPathBuilder {
172            stem_kind: self.stem_kind,
173            circ_kind: self.circ_kind,
174            compatible_with: self.compatible_with.clone(),
175        };
176
177        vanguard_path_builder.pick_path(rng, netdir, guards, vanguards)
178    }
179}
180
181impl AnonymousPathBuilder for HsPathBuilder {
182    fn compatible_with(&self) -> Option<&OwnedChanTarget> {
183        self.compatible_with.as_ref()
184    }
185
186    fn path_kind(&self) -> &'static str {
187        "onion-service circuit"
188    }
189
190    fn pick_exit<'a, R: Rng>(
191        &self,
192        rng: &mut R,
193        netdir: &'a NetDir,
194        guard_exclusion: RelayExclusion<'a>,
195        _rs_cfg: &RelaySelectionConfig<'_>,
196    ) -> Result<(Relay<'a>, RelayUsage)> {
197        let selector =
198            RelaySelector::new(hs_stem_terminal_hop_usage(self.circ_kind), guard_exclusion);
199
200        let (relay, info) = selector.select_relay(rng, netdir);
201        let relay = relay.ok_or_else(|| Error::NoRelay {
202            path_kind: self.path_kind(),
203            role: "final hop",
204            problem: info.to_string(),
205        })?;
206        Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
207    }
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")]
216struct 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")]
227impl VanguardHsPathBuilder {
228    /// Try to create and return a path for a hidden service circuit stem.
229    #[instrument(skip_all, level = "trace")]
230    fn pick_path<'a, R: Rng, RT: Runtime>(
231        &self,
232        rng: &mut R,
233        netdir: DirInfo<'a>,
234        guards: &GuardMgr<RT>,
235        vanguards: &VanguardMgr<RT>,
236    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
237        let netdir = match netdir {
238            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        let (l1_guard, mon, usable) = select_guard(netdir, guards, None)?;
250
251        let target_exclusion = if let Some(target) = self.compatible_with.as_ref() {
252            RelayExclusion::exclude_identities(
253                target.identities().map(|id| id.to_owned()).collect(),
254            )
255        } else {
256            RelayExclusion::no_relays_excluded()
257        };
258
259        let mode = vanguards.mode();
260        let path = match mode {
261            VanguardMode::Lite => {
262                self.pick_lite_vanguard_path(rng, netdir, vanguards, l1_guard, &target_exclusion)?
263            }
264            VanguardMode::Full => {
265                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        let actual_len = path.len();
279        let expected_len = self.stem_kind.num_hops(mode)?;
280        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        }
289
290        Ok((path, mon, usable))
291    }
292
293    /// Create a path for a hidden service circuit stem using full vanguards.
294    fn pick_full_vanguard_path<'n, R: Rng, RT: Runtime>(
295        &self,
296        rng: &mut R,
297        netdir: &'n NetDir,
298        vanguards: &VanguardMgr<RT>,
299        l1_guard: MaybeOwnedRelay<'n>,
300        target_exclusion: &RelayExclusion<'n>,
301    ) -> 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        let l2_target_exclusion = match self.stem_kind {
307            HsCircStemKind::Guarded => RelayExclusion::no_relays_excluded(),
308            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        let l3_usage = match self.stem_kind {
312            HsCircStemKind::Naive => hs_stem_terminal_hop_usage(self.circ_kind),
313            HsCircStemKind::Guarded => hs_intermediate_hop_usage(),
314        };
315        let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), l2_target_exclusion);
316        let l3_selector = RelaySelector::new(l3_usage, target_exclusion.clone());
317
318        let path = vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard);
319
320        let path = path
321            .add_vanguard(&l2_selector, Layer::Layer2)?
322            .add_vanguard(&l3_selector, Layer::Layer3)?;
323
324        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                let mid_selector = RelaySelector::new(
331                    hs_stem_terminal_hop_usage(self.circ_kind),
332                    target_exclusion.clone(),
333                );
334                path.add_middle(&mid_selector)?.build()
335            }
336            HsCircStemKind::Naive => path.build(),
337        }
338    }
339
340    /// Create a path for a hidden service circuit stem using lite vanguards.
341    fn pick_lite_vanguard_path<'n, R: Rng, RT: Runtime>(
342        &self,
343        rng: &mut R,
344        netdir: &'n NetDir,
345        vanguards: &VanguardMgr<RT>,
346        l1_guard: MaybeOwnedRelay<'n>,
347        target_exclusion: &RelayExclusion<'n>,
348    ) -> Result<TorPath<'n>> {
349        let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), target_exclusion.clone());
350        let mid_selector = RelaySelector::new(
351            hs_stem_terminal_hop_usage(self.circ_kind),
352            target_exclusion.clone(),
353        );
354
355        vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard)
356            .add_vanguard(&l2_selector, Layer::Layer2)?
357            .add_middle(&mid_selector)?
358            .build()
359    }
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.)
367pub(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    RelayUsage::middle_relay(Some(&RelayUsage::new_intro_point()))
377}
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.
383pub(crate) fn hs_stem_terminal_hop_usage(kind: Option<HsCircKind>) -> RelayUsage {
384    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        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}
406
407#[cfg(test)]
408mod 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    #![allow(clippy::string_slice)] // See arti#2571
422    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
423
424    use std::sync::Arc;
425
426    use super::*;
427
428    use tor_linkspec::{ChannelMethod, OwnedCircTarget};
429    use tor_netdir::{NetDirProvider, testnet::NodeBuilders, testprovider::TestNetDirProvider};
430    use tor_netdoc::doc::netstatus::RelayWeight;
431    use tor_netdoc::types::relay_flags::RelayFlag;
432    use tor_rtcompat::SleepProvider as _;
433    use tor_rtmock::MockRuntime;
434
435    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
436    use {
437        crate::path::OwnedPath, tor_basic_utils::test_rng::testing_rng,
438        tor_guardmgr::VanguardMgrError, tor_netdir::testnet::construct_custom_netdir,
439    };
440
441    /// The maximum number of relays in a test network.
442    const MAX_NET_SIZE: usize = 40;
443
444    /// Construct a test network of the specified size.
445    fn construct_test_network<F>(size: usize, mut set_family: F) -> NetDir
446    where
447        F: FnMut(usize, &mut NodeBuilders),
448    {
449        assert!(
450            size <= MAX_NET_SIZE,
451            "the test network supports at most {MAX_NET_SIZE} relays"
452        );
453        let netdir = construct_custom_netdir(|pos, nb, _| {
454            nb.omit_rs = pos >= size;
455            if !nb.omit_rs {
456                let f = RelayFlag::Running
457                    | RelayFlag::Valid
458                    | RelayFlag::V2Dir
459                    | RelayFlag::Fast
460                    | RelayFlag::Stable;
461                nb.rs.set_flags(f | RelayFlag::Guard);
462                nb.rs.weight(RelayWeight::Measured(10_000));
463
464                set_family(pos, nb);
465            }
466        })
467        .unwrap()
468        .unwrap_if_sufficient()
469        .unwrap();
470
471        assert_eq!(netdir.all_relays().count(), size);
472
473        netdir
474    }
475
476    /// Construct a test network where every relay is in the same family with everyone else.
477    fn same_family_test_network(size: usize) -> NetDir {
478        construct_test_network(size, |_pos, nb| {
479            // Everybody is in the same family with everyone else
480            let family = (0..MAX_NET_SIZE)
481                .map(|i| hex::encode([i as u8; 20]))
482                .collect::<Vec<_>>()
483                .join(" ");
484
485            nb.md.family(family.parse().unwrap());
486        })
487    }
488
489    /// Helper for extracting the hops in a `TorPath`.
490    fn path_hops(path: &TorPath) -> Vec<OwnedCircTarget> {
491        let path: OwnedPath = path.try_into().unwrap();
492        match path {
493            OwnedPath::ChannelOnly(_) => {
494                panic!("expected OwnedPath::Normal, got OwnedPath::ChannelOnly")
495            }
496            OwnedPath::Normal(ref v) => v.clone(),
497        }
498    }
499
500    /// Check the uniqueness of the hops from the specified `TorPath`.
501    ///
502    /// If `expect_dupes` is `true`, asserts that the path has some duplicate hops.
503    /// Otherwise, asserts that there are no duplicate hops in the path.
504    fn assert_duplicate_hops(path: &TorPath, expect_dupes: bool) {
505        let hops = path_hops(path);
506        let has_dupes = hops.iter().enumerate().any(|(i, hop)| {
507            hops.iter()
508                .skip(i + 1)
509                .any(|h| h.has_any_relay_id_from(hop))
510        });
511        let msg = if expect_dupes { "have" } else { "not have any" };
512
513        assert_eq!(
514            has_dupes, expect_dupes,
515            "expected path to {msg} duplicate hops: {:?}",
516            hops
517        );
518    }
519
520    /// Assert that the specified `TorPath` is a valid path for a circuit using vanguards.
521    #[cfg(feature = "vanguards")]
522    fn assert_vanguard_path_ok(
523        path: &TorPath,
524        stem_kind: HsCircStemKind,
525        mode: VanguardMode,
526        target: Option<&OwnedChanTarget>,
527    ) {
528        use itertools::Itertools;
529
530        assert_eq!(
531            path.len(),
532            stem_kind.num_hops(mode).unwrap(),
533            "invalid path length for {stem_kind} {mode}-vanguards circuit"
534        );
535
536        let hops = path_hops(path);
537        for (hop1, hop2, hop3) in hops.iter().tuple_windows() {
538            if hop1.has_any_relay_id_from(hop2)
539                || hop1.has_any_relay_id_from(hop3)
540                || hop2.has_any_relay_id_from(hop3)
541            {
542                panic!(
543                    "neighboring hops should be distinct: [{}], [{}], [{}]",
544                    hop1.display_relay_ids(),
545                    hop2.display_relay_ids(),
546                    hop3.display_relay_ids(),
547                );
548            }
549        }
550
551        // If the circuit had a target, make sure its last 2 hops are compatible with it.
552        if let Some(target) = target {
553            for hop in hops.iter().rev().take(2) {
554                if hop.has_any_relay_id_from(target) {
555                    panic!(
556                        "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
557                        hop.display_relay_ids(),
558                        target.display_relay_ids(),
559                    );
560                }
561            }
562        }
563    }
564
565    /// Assert that the specified `TorPath` is a valid HS path.
566    fn assert_hs_path_ok(path: &TorPath, target: Option<&OwnedChanTarget>) {
567        assert_eq!(path.len(), 3);
568        assert_duplicate_hops(path, false);
569        if let Some(target) = target {
570            for hop in path_hops(path) {
571                if hop.has_any_relay_id_from(target) {
572                    panic!(
573                        "invalid path: hop {} is the same relay as the circuit target {}",
574                        hop.display_relay_ids(),
575                        target.display_relay_ids()
576                    )
577                }
578            }
579        }
580    }
581
582    /// Helper for calling `HsPathBuilder::pick_path_with_vanguards`.
583    async fn pick_vanguard_path<'a>(
584        runtime: &MockRuntime,
585        netdir: &'a NetDir,
586        stem_kind: HsCircStemKind,
587        circ_kind: Option<HsCircKind>,
588        mode: VanguardMode,
589        target: Option<&OwnedChanTarget>,
590    ) -> Result<TorPath<'a>> {
591        let vanguardmgr = VanguardMgr::new_testing(runtime, mode).unwrap();
592        let _provider = vanguardmgr.init_vanguard_sets(netdir).await.unwrap();
593
594        let mut rng = testing_rng();
595        let guards = tor_guardmgr::GuardMgr::new(
596            runtime.clone(),
597            tor_persist::TestingStateMgr::new(),
598            &tor_guardmgr::TestConfig::default(),
599        )
600        .unwrap();
601        let netdir_provider = Arc::new(TestNetDirProvider::new());
602        netdir_provider.set_netdir(netdir.clone());
603        let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
604        guards.install_netdir_provider(&netdir_provider).unwrap();
605        let config = PathConfig::default();
606        let now = runtime.wallclock();
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        let runtime = MockRuntime::new();
622        let now = runtime.wallclock();
623        let dirinfo = (netdir).into();
624        let guards = tor_guardmgr::GuardMgr::new(
625            runtime,
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}