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    // This is needed because in default cfg, we do not have await in this function
744    #[allow(clippy::unused_async)]
745    async fn maybe_extend_stem_circuit<T>(
746        &self,
747        netdir: &NetDir,
748        circuit: HsCircStem<B::Tunnel>,
749        avoid_target: Option<&T>,
750        stem_kind: HsCircStemKind,
751        circ_kind: HsCircKind,
752    ) -> Result<HsCircStem<B::Tunnel>>
753    where
754        T: CircTarget + Sync,
755    {
756        match self.vanguard_mode() {
757            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
758            VanguardMode::Full => {
759                // NAIVE circuit stems need to be extended by one hop to become GUARDED stems
760                // if we're using full vanguards.
761                self.extend_full_vanguards_circuit(
762                    netdir,
763                    circuit,
764                    avoid_target,
765                    stem_kind,
766                    circ_kind,
767                )
768                .await
769            }
770            _ => {
771                let HsCircStem { circ, kind: _ } = circuit;
772
773                Ok(HsCircStem {
774                    circ,
775                    kind: stem_kind,
776                })
777            }
778        }
779    }
780
781    /// Extend the specified full vanguard circuit if necessary.
782    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
783    async fn extend_full_vanguards_circuit<T>(
784        &self,
785        netdir: &NetDir,
786        circuit: HsCircStem<B::Tunnel>,
787        avoid_target: Option<&T>,
788        stem_kind: HsCircStemKind,
789        circ_kind: HsCircKind,
790    ) -> Result<HsCircStem<B::Tunnel>>
791    where
792        T: CircTarget + Sync,
793    {
794        use crate::path::hspath::hs_stem_terminal_hop_usage;
795        use tor_relay_selection::RelaySelector;
796
797        match (circuit.kind, stem_kind) {
798            (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
799                debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
800                let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
801                let circ_path = circuit
802                    .circ
803                    .single_path()
804                    .map_err(|error| Error::Protocol {
805                        action: "extending full vanguards circuit",
806                        peer: None, // Either party could be to blame.
807                        unique_id: Some(circuit.unique_id()),
808                        error,
809                    })?;
810
811                // A NAIVE circuit is a 3-hop circuit.
812                debug_assert_eq!(circ_path.hops().len(), 3);
813
814                let target_exclusion = if let Some(target) = &avoid_target {
815                    RelayExclusion::exclude_identities(
816                        target.identities().map(|id| id.to_owned()).collect(),
817                    )
818                } else {
819                    RelayExclusion::no_relays_excluded()
820                };
821                let selector = RelaySelector::new(
822                    hs_stem_terminal_hop_usage(Some(circ_kind)),
823                    target_exclusion,
824                );
825                let hops = circ_path
826                    .iter()
827                    .flat_map(|hop| hop.as_chan_target())
828                    .map(IntoOwnedChanTarget::to_owned)
829                    .collect::<Vec<OwnedChanTarget>>();
830
831                let extra_hop =
832                    select_middle_for_vanguard_circ(&hops, netdir, &selector, &mut rand::rng())?;
833
834                // Since full vanguards are enabled and the circuit we got is NAIVE,
835                // we need to extend it by another hop to make it GUARDED before returning it
836                let circ = self.extend_circ(circuit, params, extra_hop).await?;
837
838                Ok(HsCircStem {
839                    circ,
840                    kind: stem_kind,
841                })
842            }
843            (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
844                Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
845            }
846            _ => {
847                trace!("Wanted {stem_kind} circuit, got {}", circuit.kind);
848                // Nothing to do: the circuit stem we got is of the kind we wanted
849                Ok(circuit)
850            }
851        }
852    }
853
854    /// Ensure `circ` is compatible with `target`, and has the correct length for its `kind`.
855    fn ensure_suitable_circuit<T>(
856        &self,
857        circ: &B::Tunnel,
858        target: Option<&T>,
859        kind: HsCircStemKind,
860    ) -> Result<()>
861    where
862        T: CircTarget + Sync,
863    {
864        Self::ensure_circuit_can_extend_to_target(circ, target)?;
865        self.ensure_circuit_length_valid(circ, kind)?;
866
867        Ok(())
868    }
869
870    /// Ensure the specified circuit of type `kind` has the right length.
871    fn ensure_circuit_length_valid(&self, tunnel: &B::Tunnel, kind: HsCircStemKind) -> Result<()> {
872        let circ_path_len = tunnel.n_hops().map_err(|error| Error::Protocol {
873            action: "validating circuit length",
874            peer: None, // Either party could be to blame.
875            unique_id: Some(tunnel.unique_id()),
876            error,
877        })?;
878
879        let mode = self.vanguard_mode();
880
881        // TODO(#1457): somehow unify the path length checks
882        let expected_len = kind.num_hops(mode)?;
883
884        if circ_path_len != expected_len {
885            return Err(internal!(
886                "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
887                kind,
888                expected_len,
889                circ_path_len
890            )
891            .into());
892        }
893
894        Ok(())
895    }
896
897    /// Ensure that it is possible to extend `circ` to `target`.
898    ///
899    /// Returns an error if either of the last 2 hops of the circuit are the same as `target`,
900    /// because:
901    ///   * a relay won't let you extend the circuit to itself
902    ///   * relays won't let you extend the circuit to their previous hop
903    fn ensure_circuit_can_extend_to_target<T>(tunnel: &B::Tunnel, target: Option<&T>) -> Result<()>
904    where
905        T: CircTarget + Sync,
906    {
907        if let Some(target) = target {
908            let take_n = 2;
909            if let Some(hop) = tunnel
910                .single_path()
911                .map_err(|error| Error::Protocol {
912                    action: "validating circuit compatibility with target",
913                    peer: None, // Either party could be to blame.
914                    unique_id: Some(tunnel.unique_id()),
915                    error,
916                })?
917                .hops()
918                .iter()
919                .rev()
920                .take(take_n)
921                .flat_map(|hop| hop.as_chan_target())
922                .find(|hop| hop.has_any_relay_id_from(target))
923            {
924                return Err(internal!(
925                    "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
926                    target.display_relay_ids(),
927                    hop.display_relay_ids()
928                ).into());
929            }
930        }
931
932        Ok(())
933    }
934
935    /// Internal: Remove every closed circuit from this pool.
936    fn remove_closed(&self) {
937        let mut inner = self.inner.lock().expect("lock poisoned");
938        inner.pool.retain(|circ| !circ.is_closing());
939    }
940
941    /// Internal: Remove every circuit form this pool for which any relay is not
942    /// listed in `netdir`.
943    fn remove_unlisted(&self, netdir: &NetDir) {
944        let mut inner = self.inner.lock().expect("lock poisoned");
945        inner
946            .pool
947            .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true, |_last_hop| true));
948    }
949
950    /// Returns the current [`VanguardMode`].
951    fn vanguard_mode(&self) -> VanguardMode {
952        cfg_if::cfg_if! {
953            if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
954                self
955                    .circmgr
956                    .mgr
957                    .peek_builder()
958                    .vanguardmgr()
959                    .mode()
960            } else {
961                VanguardMode::Disabled
962            }
963        }
964    }
965
966    /// Internal implementation for [`HsCircPool::estimate_timeout`].
967    pub(crate) fn estimate_timeout(
968        &self,
969        timeout_action: &timeouts::Action,
970    ) -> std::time::Duration {
971        self.circmgr.estimate_timeout(timeout_action)
972    }
973}
974
975/// Return true if we can extend a pre-built circuit `circ` to `target`.
976///
977/// We require that the circuit is open, that every hop  in the circuit is
978/// listed in `netdir`, and that no hop in the circuit shares a family with
979/// `target`.
980fn circuit_compatible_with_target<C: AbstractTunnel>(
981    netdir: &NetDir,
982    circ: &HsCircStem<C>,
983    circ_kind: HsCircKind,
984    exclude_target: &RelayExclusion,
985) -> bool {
986    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
987
988    // NOTE, TODO #504:
989    // This uses a RelayExclusion directly, when we would be better off
990    // using a RelaySelector to make sure that we had checked every relevant
991    // property.
992    //
993    // The behavior is okay, since we already checked all the properties of the
994    // circuit's relays when we first constructed the circuit.  Still, it would
995    // be better to use refactor and a RelaySelector instead.
996    circuit_still_useable(
997        netdir,
998        circ,
999        |relay| exclude_target.low_level_predicate_permits_relay(relay),
1000        |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
1001    )
1002}
1003
1004/// Return true if we can extend a pre-built vanguards circuit `circ` to `target`.
1005///
1006/// We require that the circuit is open, that it can become the specified
1007/// kind of [`HsCircStem`], that every hop in the circuit is listed in `netdir`,
1008/// and that the last two hops are different from the specified target.
1009fn vanguards_circuit_compatible_with_target<C: AbstractTunnel, T>(
1010    netdir: &NetDir,
1011    circ: &HsCircStem<C>,
1012    kind: HsCircStemKind,
1013    circ_kind: HsCircKind,
1014    avoid_target: Option<&T>,
1015) -> bool
1016where
1017    T: CircTarget + Sync,
1018{
1019    if let Some(target) = avoid_target {
1020        let Ok(circ_path) = circ.circ.single_path() else {
1021            // Circuit is unusable, so we can't use it.
1022            return false;
1023        };
1024        // The last 2 hops of the circuit must be different from the circuit target, because:
1025        //   * a relay won't let you extend the circuit to itself
1026        //   * relays won't let you extend the circuit to their previous hop
1027        let take_n = 2;
1028        if circ_path
1029            .hops()
1030            .iter()
1031            .rev()
1032            .take(take_n)
1033            .flat_map(|hop| hop.as_chan_target())
1034            .any(|hop| hop.has_any_relay_id_from(target))
1035        {
1036            return false;
1037        }
1038    }
1039
1040    // TODO #504: usage of low_level_predicate_permits_relay is inherently dubious.
1041    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
1042
1043    circ.can_become(kind)
1044        && circuit_still_useable(
1045            netdir,
1046            circ,
1047            |_relay| true,
1048            |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
1049        )
1050}
1051
1052/// Return true if we can still use a given pre-build circuit.
1053///
1054/// We require that the circuit is open, that every hop  in the circuit is
1055/// listed in `netdir`, and that `relay_okay` returns true for every hop on the
1056/// circuit.
1057fn circuit_still_useable<C, F1, F2>(
1058    netdir: &NetDir,
1059    circ: &HsCircStem<C>,
1060    relay_okay: F1,
1061    last_hop_ok: F2,
1062) -> bool
1063where
1064    C: AbstractTunnel,
1065    F1: Fn(&Relay<'_>) -> bool,
1066    F2: Fn(&Relay<'_>) -> bool,
1067{
1068    let circ = &circ.circ;
1069    if circ.is_closing() {
1070        return false;
1071    }
1072
1073    let Ok(path) = circ.single_path() else {
1074        // Circuit is unusable, so we can't use it.
1075        return false;
1076    };
1077    let last_hop = path.hops().last().expect("No hops in circuit?!");
1078    match relay_for_path_ent(netdir, last_hop) {
1079        Err(NoRelayForPathEnt::HopWasVirtual) => {}
1080        Err(NoRelayForPathEnt::NoSuchRelay) => {
1081            return false;
1082        }
1083        Ok(r) => {
1084            if !last_hop_ok(&r) {
1085                return false;
1086            }
1087        }
1088    };
1089
1090    path.iter().all(|ent: &circuit::PathEntry| {
1091        match relay_for_path_ent(netdir, ent) {
1092            Err(NoRelayForPathEnt::HopWasVirtual) => {
1093                // This is a virtual hop; it's necessarily compatible with everything.
1094                true
1095            }
1096            Err(NoRelayForPathEnt::NoSuchRelay) => {
1097                // We require that every relay in this circuit is still listed; an
1098                // unlisted relay means "reject".
1099                false
1100            }
1101            Ok(r) => {
1102                // Now it's all down to the predicate.
1103                relay_okay(&r)
1104            }
1105        }
1106    })
1107}
1108
1109/// A possible error condition when trying to look up a PathEntry
1110//
1111// Only used for one module-internal function, so doesn't derive Error.
1112#[derive(Clone, Debug)]
1113enum NoRelayForPathEnt {
1114    /// This was a virtual hop; it doesn't have a relay.
1115    HopWasVirtual,
1116    /// The relay wasn't found in the netdir.
1117    NoSuchRelay,
1118}
1119
1120/// Look up a relay in a netdir corresponding to `ent`
1121fn relay_for_path_ent<'a>(
1122    netdir: &'a NetDir,
1123    ent: &circuit::PathEntry,
1124) -> StdResult<Relay<'a>, NoRelayForPathEnt> {
1125    let Some(c) = ent.as_chan_target() else {
1126        return Err(NoRelayForPathEnt::HopWasVirtual);
1127    };
1128    let Some(relay) = netdir.by_ids(c) else {
1129        return Err(NoRelayForPathEnt::NoSuchRelay);
1130    };
1131    Ok(relay)
1132}
1133
1134/// Background task to launch onion circuits as needed.
1135#[instrument(level = "trace", skip_all)]
1136async fn launch_hs_circuits_as_needed<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
1137    pool: Weak<HsCircPoolInner<B, R>>,
1138    netdir_provider: Weak<dyn NetDirProvider + 'static>,
1139    mut schedule: TaskSchedule<R>,
1140) {
1141    /// Default delay when not told to fire explicitly. Chosen arbitrarily.
1142    const DELAY: Duration = Duration::from_secs(30);
1143
1144    while schedule.next().await.is_some() {
1145        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1146            (Some(x), Some(y)) => (x, y),
1147            _ => {
1148                break;
1149            }
1150        };
1151        let now = pool.circmgr.mgr.peek_runtime().now();
1152        pool.remove_closed();
1153        let mut circs_to_launch = {
1154            let mut inner = pool.inner.lock().expect("poisioned_lock");
1155            inner.pool.update_target_size(now);
1156            inner.pool.circs_to_launch()
1157        };
1158        let n_to_launch = circs_to_launch.n_to_launch();
1159        let mut max_attempts = n_to_launch * 2;
1160
1161        if n_to_launch > 0 {
1162            debug!(
1163                "launching {} NAIVE  and {} GUARDED circuits",
1164                circs_to_launch.stem(),
1165                circs_to_launch.guarded_stem()
1166            );
1167        }
1168
1169        // TODO: refactor this to launch the circuits in parallel
1170        'inner: while circs_to_launch.n_to_launch() > 0 {
1171            max_attempts -= 1;
1172            if max_attempts == 0 {
1173                // We want to avoid retrying over and over in a tight loop if all our attempts
1174                // are failing.
1175                warn!("Too many preemptive onion service circuits failed; waiting a while.");
1176                break 'inner;
1177            }
1178            if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1179                // We want to launch a circuit, and we have a netdir that we can use
1180                // to launch it.
1181                //
1182                // TODO: Possibly we should be doing this in a background task, and
1183                // launching several of these in parallel.  If we do, we should think about
1184                // whether taking the fastest will expose us to any attacks.
1185                let no_target: Option<&OwnedCircTarget> = None;
1186                let for_launch = circs_to_launch.for_launch();
1187
1188                // TODO HS: We should catch panics, here or in launch_hs_unmanaged.
1189                match pool
1190                    .circmgr
1191                    .launch_hs_unmanaged(no_target, &netdir, for_launch.kind(), None)
1192                    .await
1193                {
1194                    Ok(circ) => {
1195                        let kind = for_launch.kind();
1196                        let circ = HsCircStem { circ, kind };
1197                        pool.inner.lock().expect("poisoned lock").pool.insert(circ);
1198                        trace!("successfully launched {kind} circuit");
1199                        for_launch.note_circ_launched();
1200                    }
1201                    Err(err) => {
1202                        debug_report!(err, "Unable to build preemptive circuit for onion services");
1203                    }
1204                }
1205            } else {
1206                // We'd like to launch a circuit, but we don't have a netdir that we
1207                // can use.
1208                //
1209                // TODO HS possibly instead of a fixed delay we want to wait for more
1210                // netdir info?
1211                break 'inner;
1212            }
1213        }
1214
1215        // We have nothing to launch now, so we'll try after a while.
1216        schedule.fire_in(DELAY);
1217    }
1218}
1219
1220/// Background task to remove unusable circuits whenever the directory changes.
1221async fn remove_unusable_circuits<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
1222    pool: Weak<HsCircPoolInner<B, R>>,
1223    netdir_provider: Weak<dyn NetDirProvider + 'static>,
1224) {
1225    let mut event_stream = match netdir_provider.upgrade() {
1226        Some(nd) => nd.events(),
1227        None => return,
1228    };
1229
1230    // Note: We only look at the event stream here, not any kind of TaskSchedule.
1231    // That's fine, since this task only wants to fire when the directory changes,
1232    // and the directory will not change while we're dormant.
1233    //
1234    // Removing closed circuits is also handled above in launch_hs_circuits_as_needed.
1235    while event_stream.next().await.is_some() {
1236        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1237            (Some(x), Some(y)) => (x, y),
1238            _ => {
1239                break;
1240            }
1241        };
1242        pool.remove_closed();
1243        if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1244            pool.remove_unlisted(&netdir);
1245        }
1246    }
1247}
1248
1249#[cfg(test)]
1250mod test {
1251    // @@ begin test lint list maintained by maint/add_warning @@
1252    #![allow(clippy::bool_assert_comparison)]
1253    #![allow(clippy::clone_on_copy)]
1254    #![allow(clippy::dbg_macro)]
1255    #![allow(clippy::mixed_attributes_style)]
1256    #![allow(clippy::print_stderr)]
1257    #![allow(clippy::print_stdout)]
1258    #![allow(clippy::single_char_pattern)]
1259    #![allow(clippy::unwrap_used)]
1260    #![allow(clippy::unchecked_time_subtraction)]
1261    #![allow(clippy::useless_vec)]
1262    #![allow(clippy::needless_pass_by_value)]
1263    #![allow(clippy::string_slice)] // See arti#2571
1264    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1265
1266    use tor_config::ExplicitOrAuto;
1267    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1268    use tor_guardmgr::VanguardConfigBuilder;
1269    use tor_guardmgr::VanguardMode;
1270    use tor_memquota::ArcMemoryQuotaTrackerExt as _;
1271    use tor_proto::memquota::ToplevelAccount;
1272    use tor_rtmock::MockRuntime;
1273
1274    use super::*;
1275    use crate::{CircMgrInner, TestConfig};
1276
1277    /// Create a `CircMgr` with an underlying `VanguardMgr` that runs in the specified `mode`.
1278    fn circmgr_with_vanguards<R: Runtime>(
1279        runtime: R,
1280        mode: VanguardMode,
1281    ) -> Arc<CircMgrInner<crate::build::TunnelBuilder<R>, R>> {
1282        let chanmgr = tor_chanmgr::ChanMgr::new(
1283            runtime.clone(),
1284            Default::default(),
1285            tor_chanmgr::Dormancy::Dormant,
1286            &Default::default(),
1287            ToplevelAccount::new_noop(),
1288        )
1289        .unwrap();
1290        let guardmgr = tor_guardmgr::GuardMgr::new(
1291            runtime.clone(),
1292            tor_persist::TestingStateMgr::new(),
1293            &tor_guardmgr::TestConfig::default(),
1294        )
1295        .unwrap();
1296
1297        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1298        let vanguard_config = VanguardConfigBuilder::default()
1299            .mode(ExplicitOrAuto::Explicit(mode))
1300            .build()
1301            .unwrap();
1302
1303        let config = TestConfig {
1304            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1305            vanguard_config,
1306            ..Default::default()
1307        };
1308
1309        CircMgrInner::new(
1310            &config,
1311            tor_persist::TestingStateMgr::new(),
1312            &runtime,
1313            Arc::new(chanmgr),
1314            &guardmgr,
1315        )
1316        .unwrap()
1317        .into()
1318    }
1319
1320    // Prevents TROVE-2024-005 (arti#1424)
1321    #[test]
1322    fn pool_with_vanguards_disabled() {
1323        MockRuntime::test_with_various(|runtime| async move {
1324            let circmgr = circmgr_with_vanguards(runtime, VanguardMode::Disabled);
1325            let circpool = HsCircPoolInner::new_internal(&circmgr);
1326            assert!(circpool.vanguard_mode() == VanguardMode::Disabled);
1327        });
1328    }
1329
1330    #[test]
1331    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1332    fn pool_with_vanguards_enabled() {
1333        MockRuntime::test_with_various(|runtime| async move {
1334            for mode in [VanguardMode::Lite, VanguardMode::Full] {
1335                let circmgr = circmgr_with_vanguards(runtime.clone(), mode);
1336                let circpool = HsCircPoolInner::new_internal(&circmgr);
1337                assert!(circpool.vanguard_mode() == mode);
1338            }
1339        });
1340    }
1341}