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};
71
72use crate::{hspool::HsCircKind, hspool::HsCircStemKind, Error, Result};
73
74use super::AnonymousPathBuilder;
75
76use {
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")]
85use {
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")]
94pub(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.
99pub(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
115impl 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    pub(crate) fn new(
124        compatible_with: Option<OwnedChanTarget>,
125        stem_kind: HsCircStemKind,
126        circ_kind: Option<HsCircKind>,
127    ) -> Self {
128        Self {
129            compatible_with,
130            stem_kind,
131            circ_kind,
132        }
133    }
134
135    /// Try to create and return a path for a hidden service circuit stem.
136    #[cfg_attr(feature = "vanguards", allow(unused))]
137    pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
138        &self,
139        rng: &mut R,
140        netdir: DirInfo<'a>,
141        guards: &GuardMgr<RT>,
142        config: &PathConfig,
143        now: SystemTime,
144    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
145        pick_path(self, rng, netdir, guards, config, now)
146    }
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    pub(crate) fn pick_path_with_vanguards<'a, R: Rng, RT: Runtime>(
155        &self,
156        rng: &mut R,
157        netdir: DirInfo<'a>,
158        guards: &GuardMgr<RT>,
159        vanguards: &VanguardMgr<RT>,
160        config: &PathConfig,
161        now: SystemTime,
162    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
163        let mode = vanguards.mode();
164        if mode == VanguardMode::Disabled {
165            return pick_path(self, rng, netdir, guards, config, now);
166        }
167
168        let vanguard_path_builder = VanguardHsPathBuilder {
169            stem_kind: self.stem_kind,
170            circ_kind: self.circ_kind,
171            compatible_with: self.compatible_with.clone(),
172        };
173
174        vanguard_path_builder.pick_path(rng, netdir, guards, vanguards)
175    }
176}
177
178impl AnonymousPathBuilder for HsPathBuilder {
179    fn compatible_with(&self) -> Option<&OwnedChanTarget> {
180        self.compatible_with.as_ref()
181    }
182
183    fn path_kind(&self) -> &'static str {
184        "onion-service circuit"
185    }
186
187    fn pick_exit<'a, R: Rng>(
188        &self,
189        rng: &mut R,
190        netdir: &'a NetDir,
191        guard_exclusion: RelayExclusion<'a>,
192        _rs_cfg: &RelaySelectionConfig<'_>,
193    ) -> Result<(Relay<'a>, RelayUsage)> {
194        let selector =
195            RelaySelector::new(hs_stem_terminal_hop_usage(self.circ_kind), guard_exclusion);
196
197        let (relay, info) = selector.select_relay(rng, netdir);
198        let relay = relay.ok_or_else(|| Error::NoRelay {
199            path_kind: self.path_kind(),
200            role: "final hop",
201            problem: info.to_string(),
202        })?;
203        Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
204    }
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")]
213struct 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")]
224impl VanguardHsPathBuilder {
225    /// Try to create and return a path for a hidden service circuit stem.
226    fn pick_path<'a, R: Rng, RT: Runtime>(
227        &self,
228        rng: &mut R,
229        netdir: DirInfo<'a>,
230        guards: &GuardMgr<RT>,
231        vanguards: &VanguardMgr<RT>,
232    ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
233        let netdir = match netdir {
234            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        let (l1_guard, mon, usable) = select_guard(netdir, guards, None)?;
246
247        let target_exclusion = if let Some(target) = self.compatible_with.as_ref() {
248            RelayExclusion::exclude_identities(
249                target.identities().map(|id| id.to_owned()).collect(),
250            )
251        } else {
252            RelayExclusion::no_relays_excluded()
253        };
254
255        let mode = vanguards.mode();
256        let path = match mode {
257            VanguardMode::Lite => {
258                self.pick_lite_vanguard_path(rng, netdir, vanguards, l1_guard, &target_exclusion)?
259            }
260            VanguardMode::Full => {
261                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        let actual_len = path.len();
275        let expected_len = self.stem_kind.num_hops(mode)?;
276        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        }
285
286        Ok((path, mon, usable))
287    }
288
289    /// Create a path for a hidden service circuit stem using full vanguards.
290    fn pick_full_vanguard_path<'n, R: Rng, RT: Runtime>(
291        &self,
292        rng: &mut R,
293        netdir: &'n NetDir,
294        vanguards: &VanguardMgr<RT>,
295        l1_guard: MaybeOwnedRelay<'n>,
296        target_exclusion: &RelayExclusion<'n>,
297    ) -> 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        let l2_target_exclusion = match self.stem_kind {
303            HsCircStemKind::Guarded => RelayExclusion::no_relays_excluded(),
304            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        let l3_usage = match self.stem_kind {
308            HsCircStemKind::Naive => hs_stem_terminal_hop_usage(self.circ_kind),
309            HsCircStemKind::Guarded => hs_intermediate_hop_usage(),
310        };
311        let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), l2_target_exclusion);
312        let l3_selector = RelaySelector::new(l3_usage, target_exclusion.clone());
313
314        let path = vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard);
315
316        let path = path
317            .add_vanguard(&l2_selector, Layer::Layer2)?
318            .add_vanguard(&l3_selector, Layer::Layer3)?;
319
320        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                let mid_selector = RelaySelector::new(
327                    hs_stem_terminal_hop_usage(self.circ_kind),
328                    target_exclusion.clone(),
329                );
330                path.add_middle(&mid_selector)?.build()
331            }
332            HsCircStemKind::Naive => path.build(),
333        }
334    }
335
336    /// Create a path for a hidden service circuit stem using lite vanguards.
337    fn pick_lite_vanguard_path<'n, R: Rng, RT: Runtime>(
338        &self,
339        rng: &mut R,
340        netdir: &'n NetDir,
341        vanguards: &VanguardMgr<RT>,
342        l1_guard: MaybeOwnedRelay<'n>,
343        target_exclusion: &RelayExclusion<'n>,
344    ) -> Result<TorPath<'n>> {
345        let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), target_exclusion.clone());
346        let mid_selector = RelaySelector::new(
347            hs_stem_terminal_hop_usage(self.circ_kind),
348            target_exclusion.clone(),
349        );
350
351        vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard)
352            .add_vanguard(&l2_selector, Layer::Layer2)?
353            .add_middle(&mid_selector)?
354            .build()
355    }
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.)
363pub(crate) fn hs_intermediate_hop_usage() -> RelayUsage {
364    // Restrict our intermediary relays to the set of middle relays we could use when building a new
365    // intro circuit.
366
367    // TODO: This usage is a bit convoluted, and some onion-service-
368    // related circuits don't really need this much stability.
369    //
370    // TODO: new_intro_point() isn't really accurate here, but it _is_
371    // the most restrictive target-usage we can use.
372    RelayUsage::middle_relay(Some(&RelayUsage::new_intro_point()))
373}
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.
379pub(crate) fn hs_stem_terminal_hop_usage(kind: Option<HsCircKind>) -> RelayUsage {
380    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        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}
402
403#[cfg(test)]
404mod 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}