Skip to main content

tor_circmgr/
hspool.rs

1//! Manage a pool of circuits for usage with onion services.
2//
3// TODO HS TEST: We need tests here. First, though, we need a testing strategy.
4mod config;
5mod pool;
6
7use std::{
8    ops::Deref,
9    sync::{Arc, Mutex, Weak},
10};
11
12use crate::{
13    AbstractTunnel, CircMgr, CircMgrInner, ClientOnionServiceDataTunnel,
14    ClientOnionServiceDirTunnel, ClientOnionServiceIntroTunnel, Error, Result,
15    ServiceOnionServiceDataTunnel, ServiceOnionServiceDirTunnel, ServiceOnionServiceIntroTunnel,
16    build::{TunnelBuilder, onion_circparams_from_netparams},
17    mgr::AbstractTunnelBuilder,
18    path::hspath::hs_stem_terminal_hop_usage,
19    timeouts,
20};
21use futures::{StreamExt, TryFutureExt};
22use once_cell::sync::OnceCell;
23use tor_error::{Bug, debug_report};
24use tor_error::{bad_api_usage, internal};
25use tor_guardmgr::VanguardMode;
26use tor_linkspec::{
27    CircTarget, HasRelayIds as _, IntoOwnedChanTarget, OwnedChanTarget, OwnedCircTarget,
28};
29use tor_netdir::{NetDir, NetDirProvider, Relay};
30use tor_proto::client::circuit::{self, CircParameters};
31use tor_relay_selection::{LowLevelRelayPredicate, RelayExclusion};
32use tor_rtcompat::{
33    Runtime, SleepProviderExt, SpawnExt,
34    scheduler::{TaskHandle, TaskSchedule},
35};
36use tracing::{debug, instrument, trace, warn};
37use web_time_compat::{Duration, Instant, SystemTime};
38
39use std::result::Result as StdResult;
40
41pub use config::HsCircPoolConfig;
42
43use self::pool::HsCircPrefs;
44
45#[cfg(all(feature = "vanguards", feature = "hs-common"))]
46use crate::path::hspath::select_middle_for_vanguard_circ;
47
48/// The (onion-service-related) purpose for which a given circuit is going to be
49/// used.
50///
51/// We will use this to tell how the path for a given circuit is to be
52/// constructed.
53#[cfg(feature = "hs-common")]
54#[derive(Debug, Clone, Copy, Eq, PartialEq)]
55#[non_exhaustive]
56pub enum HsCircKind {
57    /// Circuit from an onion service to an HsDir.
58    SvcHsDir,
59    /// Circuit from an onion service to an Introduction Point.
60    SvcIntro,
61    /// Circuit from an onion service to a Rendezvous Point.
62    SvcRend,
63    /// Circuit from an onion service client to an HsDir.
64    ClientHsDir,
65    /// Circuit from an onion service client to an Introduction Point.
66    ClientIntro,
67    /// Circuit from an onion service client to a Rendezvous Point.
68    ClientRend,
69}
70
71impl HsCircKind {
72    /// Return the [`HsCircStemKind`] needed to build this type of circuit.
73    fn stem_kind(&self) -> HsCircStemKind {
74        match self {
75            HsCircKind::SvcIntro => HsCircStemKind::Naive,
76            HsCircKind::SvcHsDir => {
77                // TODO: we might want this to be GUARDED
78                HsCircStemKind::Naive
79            }
80            HsCircKind::ClientRend => {
81                // NOTE: Technically, client rendezvous circuits don't need a "guarded"
82                // stem kind, because the rendezvous point is selected by the client,
83                // so it cannot easily be controlled by an attacker.
84                //
85                // However, to keep the implementation simple, we use "guarded" circuit stems,
86                // and designate the last hop of the stem as the rendezvous point.
87                HsCircStemKind::Guarded
88            }
89            HsCircKind::SvcRend | HsCircKind::ClientHsDir | HsCircKind::ClientIntro => {
90                HsCircStemKind::Guarded
91            }
92        }
93    }
94}
95
96/// A hidden service circuit stem.
97///
98/// This represents a hidden service circuit that has not yet been extended to a target.
99///
100/// See [HsCircStemKind].
101pub(crate) struct HsCircStem<C: AbstractTunnel> {
102    /// The circuit.
103    pub(crate) circ: C,
104    /// Whether the circuit is NAIVE  or GUARDED.
105    pub(crate) kind: HsCircStemKind,
106}
107
108impl<C: AbstractTunnel> HsCircStem<C> {
109    /// Whether this circuit satisfies _all_ the [`HsCircPrefs`].
110    ///
111    /// Returns `false` if any of the `prefs` are not satisfied.
112    fn satisfies_prefs(&self, prefs: &HsCircPrefs) -> bool {
113        let HsCircPrefs { kind_prefs } = prefs;
114
115        match kind_prefs {
116            Some(kind) => *kind == self.kind,
117            None => true,
118        }
119    }
120}
121
122impl<C: AbstractTunnel> Deref for HsCircStem<C> {
123    type Target = C;
124
125    fn deref(&self) -> &Self::Target {
126        &self.circ
127    }
128}
129
130impl<C: AbstractTunnel> HsCircStem<C> {
131    /// Check if this circuit stem is of the specified `kind`
132    /// or can be extended to become that kind.
133    ///
134    /// Returns `true` if this `HsCircStem`'s kind is equal to `other`,
135    /// or if its kind is [`Naive`](HsCircStemKind::Naive)
136    /// and `other` is [`Guarded`](HsCircStemKind::Guarded).
137    pub(crate) fn can_become(&self, other: HsCircStemKind) -> bool {
138        use HsCircStemKind::*;
139
140        match (self.kind, other) {
141            (Naive, Naive) | (Guarded, Guarded) | (Naive, Guarded) => true,
142            (Guarded, Naive) => false,
143        }
144    }
145}
146
147#[allow(rustdoc::private_intra_doc_links)]
148/// A kind of hidden service circuit stem.
149///
150/// See [hspath](crate::path::hspath) docs for more information.
151///
152/// The structure of a circuit stem depends on whether vanguards are enabled:
153///
154///   * with vanguards disabled:
155///      ```text
156///         NAIVE   = G -> M -> M
157///         GUARDED = G -> M -> M
158///      ```
159///
160///   * with lite vanguards enabled:
161///      ```text
162///         NAIVE   = G -> L2 -> M
163///         GUARDED = G -> L2 -> M
164///      ```
165///
166///   * with full vanguards enabled:
167///      ```text
168///         NAIVE    = G -> L2 -> L3
169///         GUARDED = G -> L2 -> L3 -> M
170///      ```
171#[derive(Copy, Clone, Debug, PartialEq, derive_more::Display)]
172#[non_exhaustive]
173pub(crate) enum HsCircStemKind {
174    /// A naive circuit stem.
175    ///
176    /// Used for building circuits to a final hop that an adversary cannot easily control,
177    /// for example if the final hop is is randomly chosen by us.
178    #[display("NAIVE")]
179    Naive,
180    /// An guarded circuit stem.
181    ///
182    /// Used for building circuits to a final hop that an adversary can easily control,
183    /// for example if the final hop is not chosen by us.
184    #[display("GUARDED")]
185    Guarded,
186}
187
188impl HsCircStemKind {
189    /// Return the number of hops this `HsCircKind` ought to have when using the specified
190    /// [`VanguardMode`].
191    pub(crate) fn num_hops(&self, mode: VanguardMode) -> StdResult<usize, Bug> {
192        use HsCircStemKind::*;
193        use VanguardMode::*;
194
195        let len = match (mode, self) {
196            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
197            (Lite, _) => 3,
198            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
199            (Full, Naive) => 3,
200            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
201            (Full, Guarded) => 4,
202            (Disabled, _) => 3,
203            (_, _) => {
204                return Err(internal!("Unsupported vanguard mode {mode}"));
205            }
206        };
207
208        Ok(len)
209    }
210}
211
212/// An object to provide circuits for implementing onion services.
213pub struct HsCircPool<R: Runtime>(Arc<HsCircPoolInner<TunnelBuilder<R>, R>>);
214
215impl<R: Runtime> HsCircPool<R> {
216    /// Create a new `HsCircPool`.
217    ///
218    /// This will not work properly before "launch_background_tasks" is called.
219    pub fn new(circmgr: &Arc<CircMgr<R>>) -> Self {
220        Self(Arc::new(HsCircPoolInner::new(circmgr)))
221    }
222
223    /// Create a client directory circuit ending at the chosen hop `target`.
224    ///
225    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
226    #[instrument(level = "trace", skip_all)]
227    pub async fn get_or_launch_client_dir<T>(
228        &self,
229        netdir: &NetDir,
230        target: T,
231    ) -> Result<ClientOnionServiceDirTunnel>
232    where
233        T: CircTarget + Sync,
234    {
235        let tunnel = self
236            .0
237            .get_or_launch_specific(netdir, HsCircKind::ClientHsDir, target)
238            .await?;
239        Ok(tunnel.into())
240    }
241
242    /// Create a client introduction circuit ending at the chosen hop `target`.
243    ///
244    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
245    #[instrument(level = "trace", skip_all)]
246    pub async fn get_or_launch_client_intro<T>(
247        &self,
248        netdir: &NetDir,
249        target: T,
250    ) -> Result<ClientOnionServiceIntroTunnel>
251    where
252        T: CircTarget + Sync,
253    {
254        let tunnel = self
255            .0
256            .get_or_launch_specific(netdir, HsCircKind::ClientIntro, target)
257            .await?;
258        Ok(tunnel.into())
259    }
260
261    /// Create a service directory circuit ending at the chosen hop `target`.
262    ///
263    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
264    #[instrument(level = "trace", skip_all)]
265    pub async fn get_or_launch_svc_dir<T>(
266        &self,
267        netdir: &NetDir,
268        target: T,
269    ) -> Result<ServiceOnionServiceDirTunnel>
270    where
271        T: CircTarget + Sync,
272    {
273        let tunnel = self
274            .0
275            .get_or_launch_specific(netdir, HsCircKind::SvcHsDir, target)
276            .await?;
277        Ok(tunnel.into())
278    }
279
280    /// Create a service introduction circuit ending at the chosen hop `target`.
281    ///
282    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
283    #[instrument(level = "trace", skip_all)]
284    pub async fn get_or_launch_svc_intro<T>(
285        &self,
286        netdir: &NetDir,
287        target: T,
288    ) -> Result<ServiceOnionServiceIntroTunnel>
289    where
290        T: CircTarget + Sync,
291    {
292        let tunnel = self
293            .0
294            .get_or_launch_specific(netdir, HsCircKind::SvcIntro, target)
295            .await?;
296        Ok(tunnel.into())
297    }
298
299    /// Create a service rendezvous (data) circuit ending at the chosen hop `target`.
300    ///
301    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
302    #[instrument(level = "trace", skip_all)]
303    pub async fn get_or_launch_svc_rend<T>(
304        &self,
305        netdir: &NetDir,
306        target: T,
307    ) -> Result<ServiceOnionServiceDataTunnel>
308    where
309        T: CircTarget + Sync,
310    {
311        let tunnel = self
312            .0
313            .get_or_launch_specific(netdir, HsCircKind::SvcRend, target)
314            .await?;
315        Ok(tunnel.into())
316    }
317
318    /// Create a circuit suitable for use as a rendezvous circuit by a client.
319    ///
320    /// Return the circuit, along with a [`Relay`] from `netdir` representing its final hop.
321    ///
322    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
323    #[instrument(level = "trace", skip_all)]
324    pub async fn get_or_launch_client_rend<'a>(
325        &self,
326        netdir: &'a NetDir,
327    ) -> Result<(ClientOnionServiceDataTunnel, Relay<'a>)> {
328        let (tunnel, relay) = self.0.get_or_launch_client_rend(netdir).await?;
329        Ok((tunnel.into(), relay))
330    }
331
332    /// Return an estimate-based delay for how long a given
333    /// [`Action`](timeouts::Action) should be allowed to complete.
334    ///
335    /// This function has the same semantics as
336    /// [`CircMgr::estimate_timeout`].
337    /// See the notes there.
338    ///
339    /// In particular **you do not need to use this function** in order to get
340    /// reasonable timeouts for the circuit-building operations provided by `HsCircPool`.
341    //
342    // In principle we could have made this available by making `HsCircPool` `Deref`
343    // to `CircMgr`, but we don't want to do that because `CircMgr` has methods that
344    // operate on *its* pool which is separate from the pool maintained by `HsCircPool`.
345    //
346    // We *might* want to provide a method to access the underlying `CircMgr`
347    // but that has the same issues, albeit less severely.
348    pub fn estimate_timeout(&self, timeout_action: &timeouts::Action) -> std::time::Duration {
349        self.0.estimate_timeout(timeout_action)
350    }
351
352    /// Launch the periodic daemon tasks required by the manager to function properly.
353    ///
354    /// Returns a set of [`TaskHandle`]s that can be used to manage the daemon tasks.
355    pub fn launch_background_tasks(
356        self: &Arc<Self>,
357        runtime: &R,
358        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
359    ) -> Result<Vec<TaskHandle>> {
360        HsCircPoolInner::launch_background_tasks(&self.0.clone(), runtime, netdir_provider)
361    }
362
363    /// Retire the circuits in this pool.
364    ///
365    /// This is used for handling vanguard configuration changes:
366    /// if the [`VanguardMode`] changes, we need to empty the pool and rebuild it,
367    /// because the old circuits are no longer suitable for use.
368    pub fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
369        self.0.retire_all_circuits()
370    }
371
372    /// Return the current time instant from the runtime.
373    ///
374    /// This provides mockable time for use in error tracking and other
375    /// time-sensitive operations.
376    pub fn now(&self) -> Instant {
377        self.0.circmgr.mgr.peek_runtime().now()
378    }
379
380    /// Return the current wall-clock time from the runtime.
381    pub fn wallclock(&self) -> SystemTime {
382        self.0.circmgr.mgr.peek_runtime().wallclock()
383    }
384}
385
386/// An object to provide circuits for implementing onion services.
387pub(crate) struct HsCircPoolInner<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> {
388    /// An underlying circuit manager, used for constructing circuits.
389    circmgr: Arc<CircMgrInner<B, R>>,
390    /// A task handle for making the background circuit launcher fire early.
391    //
392    // TODO: I think we may want to move this into the same Mutex as Pool
393    // eventually.  But for now, this is fine, since it's just an implementation
394    // detail.
395    //
396    // TODO MSRV TBD: Replace with OnceLock (#1996)
397    launcher_handle: OnceCell<TaskHandle>,
398    /// The mutable state of this pool.
399    inner: Mutex<Inner<B::Tunnel>>,
400}
401
402/// The mutable state of an [`HsCircPool`]
403struct Inner<C: AbstractTunnel> {
404    /// A collection of pre-constructed circuits.
405    pool: pool::Pool<C>,
406}
407
408impl<R: Runtime> HsCircPoolInner<TunnelBuilder<R>, R> {
409    /// Internal implementation for [`HsCircPool::new`].
410    pub(crate) fn new(circmgr: &CircMgr<R>) -> Self {
411        Self::new_internal(&circmgr.0)
412    }
413}
414
415impl<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> HsCircPoolInner<B, R> {
416    /// Create a new [`HsCircPoolInner`] from a [`CircMgrInner`].
417    pub(crate) fn new_internal(circmgr: &Arc<CircMgrInner<B, R>>) -> Self {
418        let circmgr = Arc::clone(circmgr);
419        let pool = pool::Pool::default();
420        Self {
421            circmgr,
422            launcher_handle: OnceCell::new(),
423            inner: Mutex::new(Inner { pool }),
424        }
425    }
426
427    /// Internal implementation for [`HsCircPool::launch_background_tasks`].
428    #[instrument(level = "trace", skip_all)]
429    pub(crate) fn launch_background_tasks(
430        self: &Arc<Self>,
431        runtime: &R,
432        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
433    ) -> Result<Vec<TaskHandle>> {
434        let handle = self.launcher_handle.get_or_try_init(|| {
435            runtime
436                .spawn(remove_unusable_circuits(
437                    Arc::downgrade(self),
438                    Arc::downgrade(netdir_provider),
439                ))
440                .map_err(|e| Error::from_spawn("preemptive onion circuit expiration task", e))?;
441
442            let (schedule, handle) = TaskSchedule::new(runtime.clone());
443            runtime
444                .spawn(launch_hs_circuits_as_needed(
445                    Arc::downgrade(self),
446                    Arc::downgrade(netdir_provider),
447                    schedule,
448                ))
449                .map_err(|e| Error::from_spawn("preemptive onion circuit builder task", e))?;
450
451            Result::<TaskHandle>::Ok(handle)
452        })?;
453
454        Ok(vec![handle.clone()])
455    }
456
457    /// Internal implementation for [`HsCircPool::get_or_launch_client_rend`].
458    #[instrument(level = "trace", skip_all)]
459    pub(crate) async fn get_or_launch_client_rend<'a>(
460        &self,
461        netdir: &'a NetDir,
462    ) -> Result<(B::Tunnel, Relay<'a>)> {
463        // For rendezvous points, clients use 3-hop circuits.
464        // Note that we aren't using any special rules for the last hop here; we
465        // are relying on the fact that:
466        //   * all suitable middle relays that we use in these circuit stems are
467        //     suitable renedezvous points, and
468        //   * the weighting rules for selecting rendezvous points are the same
469        //     as those for selecting an arbitrary middle relay.
470        let circ = self
471            .take_or_launch_stem_circuit::<OwnedCircTarget>(netdir, None, HsCircKind::ClientRend)
472            .await?;
473
474        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
475        if matches!(
476            self.vanguard_mode(),
477            VanguardMode::Full | VanguardMode::Lite
478        ) && circ.kind != HsCircStemKind::Guarded
479        {
480            return Err(internal!("wanted a GUARDED circuit, but got NAIVE?!").into());
481        }
482
483        let path = circ.single_path().map_err(|error| Error::Protocol {
484            action: "launching a client rend circuit",
485            peer: None, // Either party could be to blame.
486            unique_id: Some(circ.unique_id()),
487            error,
488        })?;
489
490        match path.hops().last() {
491            Some(ent) => {
492                let Some(ct) = ent.as_chan_target() else {
493                    return Err(
494                        internal!("HsPool gave us a circuit with a virtual last hop!?").into(),
495                    );
496                };
497                match netdir.by_ids(ct) {
498                    Some(relay) => Ok((circ.circ, relay)),
499                    // This can't happen, since launch_hs_unmanaged() only takes relays from the netdir
500                    // it is given, and circuit_compatible_with_target() ensures that
501                    // every relay in the circuit is listed.
502                    //
503                    // TODO: Still, it's an ugly place in our API; maybe we should return the last hop
504                    // from take_or_launch_stem_circuit()?  But in many cases it won't be needed...
505                    None => Err(internal!("Got circuit with unknown last hop!?").into()),
506                }
507            }
508            None => Err(internal!("Circuit with an empty path!?").into()),
509        }
510    }
511
512    /// Helper for the [`HsCircPool`] functions that launch rendezvous,
513    /// introduction, or directory circuits.
514    #[instrument(level = "trace", skip_all)]
515    pub(crate) async fn get_or_launch_specific<T>(
516        &self,
517        netdir: &NetDir,
518        kind: HsCircKind,
519        target: T,
520    ) -> Result<B::Tunnel>
521    where
522        T: CircTarget + Sync,
523    {
524        if kind == HsCircKind::ClientRend {
525            return Err(bad_api_usage!("get_or_launch_specific with ClientRend circuit!?").into());
526        }
527
528        let wanted_kind = kind.stem_kind();
529
530        // For most* of these circuit types, we want to build our circuit with
531        // an extra hop, since the target hop is under somebody else's control.
532        //
533        // * The exceptions are ClientRend, which we handle in a different
534        //   method, and SvcIntro, where we will eventually  want an extra hop
535        //   to avoid vanguard discovery attacks.
536
537        // Get an unfinished circuit that's compatible with our target.
538        let circ = self
539            .take_or_launch_stem_circuit(netdir, Some(&target), kind)
540            .await?;
541
542        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
543        if matches!(
544            self.vanguard_mode(),
545            VanguardMode::Full | VanguardMode::Lite
546        ) && circ.kind != wanted_kind
547        {
548            return Err(internal!(
549                "take_or_launch_stem_circuit() returned {:?}, but we need {wanted_kind:?}",
550                circ.kind
551            )
552            .into());
553        }
554
555        let mut params = onion_circparams_from_netparams(netdir.params())?;
556
557        // If this is a HsDir circuit, establish a limit on the number of incoming cells from
558        // the last hop.
559        params.n_incoming_cells_permitted = match kind {
560            HsCircKind::ClientHsDir => Some(netdir.params().hsdir_dl_max_reply_cells.into()),
561            HsCircKind::SvcHsDir => Some(netdir.params().hsdir_ul_max_reply_cells.into()),
562            HsCircKind::SvcIntro
563            | HsCircKind::SvcRend
564            | HsCircKind::ClientIntro
565            | HsCircKind::ClientRend => None,
566        };
567        self.extend_circ(circ, params, target).await
568    }
569
570    /// Try to extend a circuit to the specified target hop.
571    async fn extend_circ<T>(
572        &self,
573        circ: HsCircStem<B::Tunnel>,
574        params: CircParameters,
575        target: T,
576    ) -> Result<B::Tunnel>
577    where
578        T: CircTarget + Sync,
579    {
580        let protocol_err = |error| Error::Protocol {
581            action: "extending to chosen HS hop",
582            peer: None, // Either party could be to blame.
583            unique_id: Some(circ.unique_id()),
584            error,
585        };
586
587        // Estimate how long it will take to extend it one more hop, and
588        // construct a timeout as appropriate.
589        let n_hops = circ.n_hops().map_err(protocol_err)?;
590        let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
591            &crate::timeouts::Action::ExtendCircuit {
592                initial_length: n_hops,
593                final_length: n_hops + 1,
594            },
595        );
596
597        // Make a future to extend the circuit.
598        let extend_future = circ.extend(&target, params).map_err(protocol_err);
599
600        // Wait up to the timeout for the future to complete.
601        self.circmgr
602            .mgr
603            .peek_runtime()
604            .timeout(extend_timeout, extend_future)
605            .await
606            .map_err(|_| Error::CircTimeout(Some(circ.unique_id())))??;
607
608        // With any luck, return the circuit.
609        Ok(circ.circ)
610    }
611
612    /// Internal implementation for [`HsCircPool::retire_all_circuits`].
613    pub(crate) fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
614        self.inner
615            .lock()
616            .expect("poisoned lock")
617            .pool
618            .retire_all_circuits()?;
619
620        Ok(())
621    }
622
623    /// Take and return a circuit from our pool suitable for being extended to `avoid_target`.
624    ///
625    /// If vanguards are enabled, this will try to build a circuit stem appropriate for use
626    /// as the specified `kind`.
627    ///
628    /// If vanguards are disabled, `kind` is unused.
629    ///
630    /// If there is no such circuit, build and return a new one.
631    #[instrument(level = "trace", skip_all)]
632    async fn take_or_launch_stem_circuit<T>(
633        &self,
634        netdir: &NetDir,
635        avoid_target: Option<&T>,
636        kind: HsCircKind,
637    ) -> Result<HsCircStem<B::Tunnel>>
638    where
639        // TODO #504: It would be better if this were a type that had to include
640        // family info.
641        T: CircTarget + Sync,
642    {
643        let stem_kind = kind.stem_kind();
644        let vanguard_mode = self.vanguard_mode();
645        trace!(
646            vanguards=%vanguard_mode,
647            kind=%stem_kind,
648            "selecting HS circuit stem"
649        );
650
651        // First, look for a circuit that is already built, if any is suitable.
652
653        let target_exclusion = {
654            let path_cfg = self.circmgr.builder().path_config();
655            let cfg = path_cfg.relay_selection_config();
656            match avoid_target {
657                // TODO #504: This is an unaccompanied RelayExclusion, and is therefore a
658                // bit suspect.  We should consider whether we like this behavior.
659                Some(ct) => RelayExclusion::exclude_channel_target_family(&cfg, ct, netdir),
660                None => RelayExclusion::no_relays_excluded(),
661            }
662        };
663
664        let found_usable_circ = {
665            let mut inner = self.inner.lock().expect("lock poisoned");
666
667            let restrictions = |circ: &HsCircStem<B::Tunnel>| {
668                // If vanguards are enabled, we no longer apply same-family or same-subnet
669                // restrictions, and we allow the guard to appear as either of the last
670                // two hope of the circuit.
671                match vanguard_mode {
672                    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
673                    VanguardMode::Lite | VanguardMode::Full => {
674                        vanguards_circuit_compatible_with_target(
675                            netdir,
676                            circ,
677                            stem_kind,
678                            kind,
679                            avoid_target,
680                        )
681                    }
682                    VanguardMode::Disabled => {
683                        circuit_compatible_with_target(netdir, circ, kind, &target_exclusion)
684                    }
685                    _ => {
686                        warn!("unknown vanguard mode {vanguard_mode}");
687                        false
688                    }
689                }
690            };
691
692            let mut prefs = HsCircPrefs::default();
693
694            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
695            if matches!(vanguard_mode, VanguardMode::Full | VanguardMode::Lite) {
696                prefs.preferred_stem_kind(stem_kind);
697            }
698
699            let found_usable_circ =
700                inner
701                    .pool
702                    .take_one_where(&mut rand::rng(), restrictions, &prefs);
703
704            // Tell the background task to fire immediately if we have very few circuits
705            // circuits left, or if we found nothing.
706            if inner.pool.very_low() || found_usable_circ.is_none() {
707                let handle = self.launcher_handle.get().ok_or_else(|| {
708                    Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
709                })?;
710                handle.fire();
711            }
712            found_usable_circ
713        };
714        // Return the circuit we found before, if any.
715        if let Some(circuit) = found_usable_circ {
716            let circuit = self
717                .maybe_extend_stem_circuit(netdir, circuit, avoid_target, stem_kind, kind)
718                .await?;
719            self.ensure_suitable_circuit(&circuit, avoid_target, stem_kind)?;
720            return Ok(circuit);
721        }
722
723        // TODO: There is a possible optimization here. Instead of only waiting
724        // for the circuit we launch below to finish, we could also wait for any
725        // of our in-progress preemptive circuits to finish.  That would,
726        // however, complexify our logic quite a bit.
727
728        // TODO: We could in launch multiple circuits in parallel here?
729        let circ = self
730            .circmgr
731            .launch_hs_unmanaged(avoid_target, netdir, stem_kind, Some(kind))
732            .await?;
733
734        self.ensure_suitable_circuit(&circ, avoid_target, stem_kind)?;
735
736        Ok(HsCircStem {
737            circ,
738            kind: stem_kind,
739        })
740    }
741
742    /// Return a circuit of the specified `kind`, built from `circuit`.
743    async fn maybe_extend_stem_circuit<T>(
744        &self,
745        netdir: &NetDir,
746        circuit: HsCircStem<B::Tunnel>,
747        avoid_target: Option<&T>,
748        stem_kind: HsCircStemKind,
749        circ_kind: HsCircKind,
750    ) -> Result<HsCircStem<B::Tunnel>>
751    where
752        T: CircTarget + Sync,
753    {
754        match self.vanguard_mode() {
755            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
756            VanguardMode::Full => {
757                // NAIVE circuit stems need to be extended by one hop to become GUARDED stems
758                // if we're using full vanguards.
759                self.extend_full_vanguards_circuit(
760                    netdir,
761                    circuit,
762                    avoid_target,
763                    stem_kind,
764                    circ_kind,
765                )
766                .await
767            }
768            _ => {
769                let HsCircStem { circ, kind: _ } = circuit;
770
771                Ok(HsCircStem {
772                    circ,
773                    kind: stem_kind,
774                })
775            }
776        }
777    }
778
779    /// Extend the specified full vanguard circuit if necessary.
780    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
781    async fn extend_full_vanguards_circuit<T>(
782        &self,
783        netdir: &NetDir,
784        circuit: HsCircStem<B::Tunnel>,
785        avoid_target: Option<&T>,
786        stem_kind: HsCircStemKind,
787        circ_kind: HsCircKind,
788    ) -> Result<HsCircStem<B::Tunnel>>
789    where
790        T: CircTarget + Sync,
791    {
792        use crate::path::hspath::hs_stem_terminal_hop_usage;
793        use tor_relay_selection::RelaySelector;
794
795        match (circuit.kind, stem_kind) {
796            (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
797                debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
798                let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
799                let circ_path = circuit
800                    .circ
801                    .single_path()
802                    .map_err(|error| Error::Protocol {
803                        action: "extending full vanguards circuit",
804                        peer: None, // Either party could be to blame.
805                        unique_id: Some(circuit.unique_id()),
806                        error,
807                    })?;
808
809                // A NAIVE circuit is a 3-hop circuit.
810                debug_assert_eq!(circ_path.hops().len(), 3);
811
812                let target_exclusion = if let Some(target) = &avoid_target {
813                    RelayExclusion::exclude_identities(
814                        target.identities().map(|id| id.to_owned()).collect(),
815                    )
816                } else {
817                    RelayExclusion::no_relays_excluded()
818                };
819                let selector = RelaySelector::new(
820                    hs_stem_terminal_hop_usage(Some(circ_kind)),
821                    target_exclusion,
822                );
823                let hops = circ_path
824                    .iter()
825                    .flat_map(|hop| hop.as_chan_target())
826                    .map(IntoOwnedChanTarget::to_owned)
827                    .collect::<Vec<OwnedChanTarget>>();
828
829                let extra_hop =
830                    select_middle_for_vanguard_circ(&hops, netdir, &selector, &mut rand::rng())?;
831
832                // Since full vanguards are enabled and the circuit we got is NAIVE,
833                // we need to extend it by another hop to make it GUARDED before returning it
834                let circ = self.extend_circ(circuit, params, extra_hop).await?;
835
836                Ok(HsCircStem {
837                    circ,
838                    kind: stem_kind,
839                })
840            }
841            (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
842                Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
843            }
844            _ => {
845                trace!("Wanted {stem_kind} circuit, got {}", circuit.kind);
846                // Nothing to do: the circuit stem we got is of the kind we wanted
847                Ok(circuit)
848            }
849        }
850    }
851
852    /// Ensure `circ` is compatible with `target`, and has the correct length for its `kind`.
853    fn ensure_suitable_circuit<T>(
854        &self,
855        circ: &B::Tunnel,
856        target: Option<&T>,
857        kind: HsCircStemKind,
858    ) -> Result<()>
859    where
860        T: CircTarget + Sync,
861    {
862        Self::ensure_circuit_can_extend_to_target(circ, target)?;
863        self.ensure_circuit_length_valid(circ, kind)?;
864
865        Ok(())
866    }
867
868    /// Ensure the specified circuit of type `kind` has the right length.
869    fn ensure_circuit_length_valid(&self, tunnel: &B::Tunnel, kind: HsCircStemKind) -> Result<()> {
870        let circ_path_len = tunnel.n_hops().map_err(|error| Error::Protocol {
871            action: "validating circuit length",
872            peer: None, // Either party could be to blame.
873            unique_id: Some(tunnel.unique_id()),
874            error,
875        })?;
876
877        let mode = self.vanguard_mode();
878
879        // TODO(#1457): somehow unify the path length checks
880        let expected_len = kind.num_hops(mode)?;
881
882        if circ_path_len != expected_len {
883            return Err(internal!(
884                "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
885                kind,
886                expected_len,
887                circ_path_len
888            )
889            .into());
890        }
891
892        Ok(())
893    }
894
895    /// Ensure that it is possible to extend `circ` to `target`.
896    ///
897    /// Returns an error if either of the last 2 hops of the circuit are the same as `target`,
898    /// because:
899    ///   * a relay won't let you extend the circuit to itself
900    ///   * relays won't let you extend the circuit to their previous hop
901    fn ensure_circuit_can_extend_to_target<T>(tunnel: &B::Tunnel, target: Option<&T>) -> Result<()>
902    where
903        T: CircTarget + Sync,
904    {
905        if let Some(target) = target {
906            let take_n = 2;
907            if let Some(hop) = tunnel
908                .single_path()
909                .map_err(|error| Error::Protocol {
910                    action: "validating circuit compatibility with target",
911                    peer: None, // Either party could be to blame.
912                    unique_id: Some(tunnel.unique_id()),
913                    error,
914                })?
915                .hops()
916                .iter()
917                .rev()
918                .take(take_n)
919                .flat_map(|hop| hop.as_chan_target())
920                .find(|hop| hop.has_any_relay_id_from(target))
921            {
922                return Err(internal!(
923                    "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
924                    target.display_relay_ids(),
925                    hop.display_relay_ids()
926                ).into());
927            }
928        }
929
930        Ok(())
931    }
932
933    /// Internal: Remove every closed circuit from this pool.
934    fn remove_closed(&self) {
935        let mut inner = self.inner.lock().expect("lock poisoned");
936        inner.pool.retain(|circ| !circ.is_closing());
937    }
938
939    /// Internal: Remove every circuit form this pool for which any relay is not
940    /// listed in `netdir`.
941    fn remove_unlisted(&self, netdir: &NetDir) {
942        let mut inner = self.inner.lock().expect("lock poisoned");
943        inner
944            .pool
945            .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true, |_last_hop| true));
946    }
947
948    /// Returns the current [`VanguardMode`].
949    fn vanguard_mode(&self) -> VanguardMode {
950        cfg_if::cfg_if! {
951            if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
952                self
953                    .circmgr
954                    .mgr
955                    .peek_builder()
956                    .vanguardmgr()
957                    .mode()
958            } else {
959                VanguardMode::Disabled
960            }
961        }
962    }
963
964    /// Internal implementation for [`HsCircPool::estimate_timeout`].
965    pub(crate) fn estimate_timeout(
966        &self,
967        timeout_action: &timeouts::Action,
968    ) -> std::time::Duration {
969        self.circmgr.estimate_timeout(timeout_action)
970    }
971}
972
973/// Return true if we can extend a pre-built circuit `circ` to `target`.
974///
975/// We require that the circuit is open, that every hop  in the circuit is
976/// listed in `netdir`, and that no hop in the circuit shares a family with
977/// `target`.
978fn circuit_compatible_with_target<C: AbstractTunnel>(
979    netdir: &NetDir,
980    circ: &HsCircStem<C>,
981    circ_kind: HsCircKind,
982    exclude_target: &RelayExclusion,
983) -> bool {
984    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
985
986    // NOTE, TODO #504:
987    // This uses a RelayExclusion directly, when we would be better off
988    // using a RelaySelector to make sure that we had checked every relevant
989    // property.
990    //
991    // The behavior is okay, since we already checked all the properties of the
992    // circuit's relays when we first constructed the circuit.  Still, it would
993    // be better to use refactor and a RelaySelector instead.
994    circuit_still_useable(
995        netdir,
996        circ,
997        |relay| exclude_target.low_level_predicate_permits_relay(relay),
998        |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
999    )
1000}
1001
1002/// Return true if we can extend a pre-built vanguards circuit `circ` to `target`.
1003///
1004/// We require that the circuit is open, that it can become the specified
1005/// kind of [`HsCircStem`], that every hop in the circuit is listed in `netdir`,
1006/// and that the last two hops are different from the specified target.
1007fn vanguards_circuit_compatible_with_target<C: AbstractTunnel, T>(
1008    netdir: &NetDir,
1009    circ: &HsCircStem<C>,
1010    kind: HsCircStemKind,
1011    circ_kind: HsCircKind,
1012    avoid_target: Option<&T>,
1013) -> bool
1014where
1015    T: CircTarget + Sync,
1016{
1017    if let Some(target) = avoid_target {
1018        let Ok(circ_path) = circ.circ.single_path() else {
1019            // Circuit is unusable, so we can't use it.
1020            return false;
1021        };
1022        // The last 2 hops of the circuit must be different from the circuit target, because:
1023        //   * a relay won't let you extend the circuit to itself
1024        //   * relays won't let you extend the circuit to their previous hop
1025        let take_n = 2;
1026        if circ_path
1027            .hops()
1028            .iter()
1029            .rev()
1030            .take(take_n)
1031            .flat_map(|hop| hop.as_chan_target())
1032            .any(|hop| hop.has_any_relay_id_from(target))
1033        {
1034            return false;
1035        }
1036    }
1037
1038    // TODO #504: usage of low_level_predicate_permits_relay is inherently dubious.
1039    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
1040
1041    circ.can_become(kind)
1042        && circuit_still_useable(
1043            netdir,
1044            circ,
1045            |_relay| true,
1046            |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
1047        )
1048}
1049
1050/// Return true if we can still use a given pre-build circuit.
1051///
1052/// We require that the circuit is open, that every hop  in the circuit is
1053/// listed in `netdir`, and that `relay_okay` returns true for every hop on the
1054/// circuit.
1055fn circuit_still_useable<C, F1, F2>(
1056    netdir: &NetDir,
1057    circ: &HsCircStem<C>,
1058    relay_okay: F1,
1059    last_hop_ok: F2,
1060) -> bool
1061where
1062    C: AbstractTunnel,
1063    F1: Fn(&Relay<'_>) -> bool,
1064    F2: Fn(&Relay<'_>) -> bool,
1065{
1066    let circ = &circ.circ;
1067    if circ.is_closing() {
1068        return false;
1069    }
1070
1071    let Ok(path) = circ.single_path() else {
1072        // Circuit is unusable, so we can't use it.
1073        return false;
1074    };
1075    let last_hop = path.hops().last().expect("No hops in circuit?!");
1076    match relay_for_path_ent(netdir, last_hop) {
1077        Err(NoRelayForPathEnt::HopWasVirtual) => {}
1078        Err(NoRelayForPathEnt::NoSuchRelay) => {
1079            return false;
1080        }
1081        Ok(r) => {
1082            if !last_hop_ok(&r) {
1083                return false;
1084            }
1085        }
1086    };
1087
1088    path.iter().all(|ent: &circuit::PathEntry| {
1089        match relay_for_path_ent(netdir, ent) {
1090            Err(NoRelayForPathEnt::HopWasVirtual) => {
1091                // This is a virtual hop; it's necessarily compatible with everything.
1092                true
1093            }
1094            Err(NoRelayForPathEnt::NoSuchRelay) => {
1095                // We require that every relay in this circuit is still listed; an
1096                // unlisted relay means "reject".
1097                false
1098            }
1099            Ok(r) => {
1100                // Now it's all down to the predicate.
1101                relay_okay(&r)
1102            }
1103        }
1104    })
1105}
1106
1107/// A possible error condition when trying to look up a PathEntry
1108//
1109// Only used for one module-internal function, so doesn't derive Error.
1110#[derive(Clone, Debug)]
1111enum NoRelayForPathEnt {
1112    /// This was a virtual hop; it doesn't have a relay.
1113    HopWasVirtual,
1114    /// The relay wasn't found in the netdir.
1115    NoSuchRelay,
1116}
1117
1118/// Look up a relay in a netdir corresponding to `ent`
1119fn relay_for_path_ent<'a>(
1120    netdir: &'a NetDir,
1121    ent: &circuit::PathEntry,
1122) -> StdResult<Relay<'a>, NoRelayForPathEnt> {
1123    let Some(c) = ent.as_chan_target() else {
1124        return Err(NoRelayForPathEnt::HopWasVirtual);
1125    };
1126    let Some(relay) = netdir.by_ids(c) else {
1127        return Err(NoRelayForPathEnt::NoSuchRelay);
1128    };
1129    Ok(relay)
1130}
1131
1132/// Background task to launch onion circuits as needed.
1133#[allow(clippy::cognitive_complexity)] // TODO #2010: Refactor, after !3007 is in.
1134#[instrument(level = "trace", skip_all)]
1135async fn launch_hs_circuits_as_needed<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
1136    pool: Weak<HsCircPoolInner<B, R>>,
1137    netdir_provider: Weak<dyn NetDirProvider + 'static>,
1138    mut schedule: TaskSchedule<R>,
1139) {
1140    /// Default delay when not told to fire explicitly. Chosen arbitrarily.
1141    const DELAY: Duration = Duration::from_secs(30);
1142
1143    while schedule.next().await.is_some() {
1144        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1145            (Some(x), Some(y)) => (x, y),
1146            _ => {
1147                break;
1148            }
1149        };
1150        let now = pool.circmgr.mgr.peek_runtime().now();
1151        pool.remove_closed();
1152        let mut circs_to_launch = {
1153            let mut inner = pool.inner.lock().expect("poisioned_lock");
1154            inner.pool.update_target_size(now);
1155            inner.pool.circs_to_launch()
1156        };
1157        let n_to_launch = circs_to_launch.n_to_launch();
1158        let mut max_attempts = n_to_launch * 2;
1159
1160        if n_to_launch > 0 {
1161            debug!(
1162                "launching {} NAIVE  and {} GUARDED circuits",
1163                circs_to_launch.stem(),
1164                circs_to_launch.guarded_stem()
1165            );
1166        }
1167
1168        // TODO: refactor this to launch the circuits in parallel
1169        'inner: while circs_to_launch.n_to_launch() > 0 {
1170            max_attempts -= 1;
1171            if max_attempts == 0 {
1172                // We want to avoid retrying over and over in a tight loop if all our attempts
1173                // are failing.
1174                warn!("Too many preemptive onion service circuits failed; waiting a while.");
1175                break 'inner;
1176            }
1177            if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1178                // We want to launch a circuit, and we have a netdir that we can use
1179                // to launch it.
1180                //
1181                // TODO: Possibly we should be doing this in a background task, and
1182                // launching several of these in parallel.  If we do, we should think about
1183                // whether taking the fastest will expose us to any attacks.
1184                let no_target: Option<&OwnedCircTarget> = None;
1185                let for_launch = circs_to_launch.for_launch();
1186
1187                // TODO HS: We should catch panics, here or in launch_hs_unmanaged.
1188                match pool
1189                    .circmgr
1190                    .launch_hs_unmanaged(no_target, &netdir, for_launch.kind(), None)
1191                    .await
1192                {
1193                    Ok(circ) => {
1194                        let kind = for_launch.kind();
1195                        let circ = HsCircStem { circ, kind };
1196                        pool.inner.lock().expect("poisoned lock").pool.insert(circ);
1197                        trace!("successfully launched {kind} circuit");
1198                        for_launch.note_circ_launched();
1199                    }
1200                    Err(err) => {
1201                        debug_report!(err, "Unable to build preemptive circuit for onion services");
1202                    }
1203                }
1204            } else {
1205                // We'd like to launch a circuit, but we don't have a netdir that we
1206                // can use.
1207                //
1208                // TODO HS possibly instead of a fixed delay we want to wait for more
1209                // netdir info?
1210                break 'inner;
1211            }
1212        }
1213
1214        // We have nothing to launch now, so we'll try after a while.
1215        schedule.fire_in(DELAY);
1216    }
1217}
1218
1219/// Background task to remove unusable circuits whenever the directory changes.
1220async fn remove_unusable_circuits<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
1221    pool: Weak<HsCircPoolInner<B, R>>,
1222    netdir_provider: Weak<dyn NetDirProvider + 'static>,
1223) {
1224    let mut event_stream = match netdir_provider.upgrade() {
1225        Some(nd) => nd.events(),
1226        None => return,
1227    };
1228
1229    // Note: We only look at the event stream here, not any kind of TaskSchedule.
1230    // That's fine, since this task only wants to fire when the directory changes,
1231    // and the directory will not change while we're dormant.
1232    //
1233    // Removing closed circuits is also handled above in launch_hs_circuits_as_needed.
1234    while event_stream.next().await.is_some() {
1235        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1236            (Some(x), Some(y)) => (x, y),
1237            _ => {
1238                break;
1239            }
1240        };
1241        pool.remove_closed();
1242        if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1243            pool.remove_unlisted(&netdir);
1244        }
1245    }
1246}
1247
1248#[cfg(test)]
1249mod test {
1250    // @@ begin test lint list maintained by maint/add_warning @@
1251    #![allow(clippy::bool_assert_comparison)]
1252    #![allow(clippy::clone_on_copy)]
1253    #![allow(clippy::dbg_macro)]
1254    #![allow(clippy::mixed_attributes_style)]
1255    #![allow(clippy::print_stderr)]
1256    #![allow(clippy::print_stdout)]
1257    #![allow(clippy::single_char_pattern)]
1258    #![allow(clippy::unwrap_used)]
1259    #![allow(clippy::unchecked_time_subtraction)]
1260    #![allow(clippy::useless_vec)]
1261    #![allow(clippy::needless_pass_by_value)]
1262    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1263    #![allow(clippy::cognitive_complexity)]
1264
1265    use tor_config::ExplicitOrAuto;
1266    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1267    use tor_guardmgr::VanguardConfigBuilder;
1268    use tor_guardmgr::VanguardMode;
1269    use tor_memquota::ArcMemoryQuotaTrackerExt as _;
1270    use tor_proto::memquota::ToplevelAccount;
1271    use tor_rtmock::MockRuntime;
1272
1273    use super::*;
1274    use crate::{CircMgrInner, TestConfig};
1275
1276    /// Create a `CircMgr` with an underlying `VanguardMgr` that runs in the specified `mode`.
1277    fn circmgr_with_vanguards<R: Runtime>(
1278        runtime: R,
1279        mode: VanguardMode,
1280    ) -> Arc<CircMgrInner<crate::build::TunnelBuilder<R>, R>> {
1281        let chanmgr = tor_chanmgr::ChanMgr::new(
1282            runtime.clone(),
1283            Default::default(),
1284            tor_chanmgr::Dormancy::Dormant,
1285            &Default::default(),
1286            ToplevelAccount::new_noop(),
1287        )
1288        .unwrap();
1289        let guardmgr = tor_guardmgr::GuardMgr::new(
1290            runtime.clone(),
1291            tor_persist::TestingStateMgr::new(),
1292            &tor_guardmgr::TestConfig::default(),
1293        )
1294        .unwrap();
1295
1296        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1297        let vanguard_config = VanguardConfigBuilder::default()
1298            .mode(ExplicitOrAuto::Explicit(mode))
1299            .build()
1300            .unwrap();
1301
1302        let config = TestConfig {
1303            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1304            vanguard_config,
1305            ..Default::default()
1306        };
1307
1308        CircMgrInner::new(
1309            &config,
1310            tor_persist::TestingStateMgr::new(),
1311            &runtime,
1312            Arc::new(chanmgr),
1313            &guardmgr,
1314        )
1315        .unwrap()
1316        .into()
1317    }
1318
1319    // Prevents TROVE-2024-005 (arti#1424)
1320    #[test]
1321    fn pool_with_vanguards_disabled() {
1322        MockRuntime::test_with_various(|runtime| async move {
1323            let circmgr = circmgr_with_vanguards(runtime, VanguardMode::Disabled);
1324            let circpool = HsCircPoolInner::new_internal(&circmgr);
1325            assert!(circpool.vanguard_mode() == VanguardMode::Disabled);
1326        });
1327    }
1328
1329    #[test]
1330    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1331    fn pool_with_vanguards_enabled() {
1332        MockRuntime::test_with_various(|runtime| async move {
1333            for mode in [VanguardMode::Lite, VanguardMode::Full] {
1334                let circmgr = circmgr_with_vanguards(runtime.clone(), mode);
1335                let circpool = HsCircPoolInner::new_internal(&circmgr);
1336                assert!(circpool.vanguard_mode() == mode);
1337            }
1338        });
1339    }
1340}