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.
4
mod config;
5
mod pool;
6

            
7
use std::{
8
    ops::Deref,
9
    sync::{Arc, Mutex, Weak},
10
    time::Duration,
11
};
12

            
13
use crate::{
14
    AbstractTunnel, CircMgr, CircMgrInner, ClientOnionServiceDataTunnel,
15
    ClientOnionServiceDirTunnel, ClientOnionServiceIntroTunnel, Error, Result,
16
    ServiceOnionServiceDataTunnel, ServiceOnionServiceDirTunnel, ServiceOnionServiceIntroTunnel,
17
    build::{TunnelBuilder, onion_circparams_from_netparams},
18
    mgr::AbstractTunnelBuilder,
19
    path::hspath::hs_stem_terminal_hop_usage,
20
    timeouts,
21
};
22
use futures::{StreamExt, TryFutureExt, task::SpawnExt};
23
use once_cell::sync::OnceCell;
24
use tor_error::{Bug, debug_report};
25
use tor_error::{bad_api_usage, internal};
26
use tor_guardmgr::VanguardMode;
27
use tor_linkspec::{
28
    CircTarget, HasRelayIds as _, IntoOwnedChanTarget, OwnedChanTarget, OwnedCircTarget,
29
};
30
use tor_netdir::{NetDir, NetDirProvider, Relay};
31
use tor_proto::client::circuit::{self, CircParameters};
32
use tor_relay_selection::{LowLevelRelayPredicate, RelayExclusion};
33
use tor_rtcompat::{
34
    Runtime, SleepProviderExt,
35
    scheduler::{TaskHandle, TaskSchedule},
36
};
37
use tracing::{debug, trace, warn};
38

            
39
use std::result::Result as StdResult;
40

            
41
pub use config::HsCircPoolConfig;
42

            
43
use self::pool::HsCircPrefs;
44

            
45
#[cfg(all(feature = "vanguards", feature = "hs-common"))]
46
use 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]
56
pub 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

            
71
impl 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].
101
pub(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

            
108
impl<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

            
122
impl<C: AbstractTunnel> Deref for HsCircStem<C> {
123
    type Target = C;
124

            
125
    fn deref(&self) -> &Self::Target {
126
        &self.circ
127
    }
128
}
129

            
130
impl<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]
173
pub(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

            
188
impl HsCircStemKind {
189
    /// Return the number of hops this `HsCircKind` ought to have when using the specified
190
    /// [`VanguardMode`].
191
80
    pub(crate) fn num_hops(&self, mode: VanguardMode) -> StdResult<usize, Bug> {
192
        use HsCircStemKind::*;
193
        use VanguardMode::*;
194

            
195
80
        let len = match (mode, self) {
196
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
197
32
            (Lite, _) => 3,
198
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
199
24
            (Full, Naive) => 3,
200
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
201
24
            (Full, Guarded) => 4,
202
            (Disabled, _) => 3,
203
            (_, _) => {
204
                return Err(internal!("Unsupported vanguard mode {mode}"));
205
            }
206
        };
207

            
208
80
        Ok(len)
209
80
    }
210
}
211

            
212
/// An object to provide circuits for implementing onion services.
213
pub struct HsCircPool<R: Runtime>(Arc<HsCircPoolInner<TunnelBuilder<R>, R>>);
214

            
215
impl<R: Runtime> HsCircPool<R> {
216
    /// Create a new `HsCircPool`.
217
    ///
218
    /// This will not work properly before "launch_background_tasks" is called.
219
22
    pub fn new(circmgr: &Arc<CircMgr<R>>) -> Self {
220
22
        Self(Arc::new(HsCircPoolInner::new(circmgr)))
221
22
    }
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
    pub async fn get_or_launch_client_dir<T>(
227
        &self,
228
        netdir: &NetDir,
229
        target: T,
230
    ) -> Result<ClientOnionServiceDirTunnel>
231
    where
232
        T: CircTarget + Sync,
233
    {
234
        let tunnel = self
235
            .0
236
            .get_or_launch_specific(netdir, HsCircKind::ClientHsDir, target)
237
            .await?;
238
        Ok(tunnel.into())
239
    }
240

            
241
    /// Create a client introduction circuit ending at the chosen hop `target`.
242
    ///
243
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
244
    pub async fn get_or_launch_client_intro<T>(
245
        &self,
246
        netdir: &NetDir,
247
        target: T,
248
    ) -> Result<ClientOnionServiceIntroTunnel>
249
    where
250
        T: CircTarget + Sync,
251
    {
252
        let tunnel = self
253
            .0
254
            .get_or_launch_specific(netdir, HsCircKind::ClientIntro, target)
255
            .await?;
256
        Ok(tunnel.into())
257
    }
258

            
259
    /// Create a service directory circuit ending at the chosen hop `target`.
260
    ///
261
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
262
    pub async fn get_or_launch_svc_dir<T>(
263
        &self,
264
        netdir: &NetDir,
265
        target: T,
266
    ) -> Result<ServiceOnionServiceDirTunnel>
267
    where
268
        T: CircTarget + Sync,
269
    {
270
        let tunnel = self
271
            .0
272
            .get_or_launch_specific(netdir, HsCircKind::SvcHsDir, target)
273
            .await?;
274
        Ok(tunnel.into())
275
    }
276

            
277
    /// Create a service introduction circuit ending at the chosen hop `target`.
278
    ///
279
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
280
    pub async fn get_or_launch_svc_intro<T>(
281
        &self,
282
        netdir: &NetDir,
283
        target: T,
284
    ) -> Result<ServiceOnionServiceIntroTunnel>
285
    where
286
        T: CircTarget + Sync,
287
    {
288
        let tunnel = self
289
            .0
290
            .get_or_launch_specific(netdir, HsCircKind::SvcIntro, target)
291
            .await?;
292
        Ok(tunnel.into())
293
    }
294

            
295
    /// Create a service rendezvous (data) circuit ending at the chosen hop `target`.
296
    ///
297
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
298
    pub async fn get_or_launch_svc_rend<T>(
299
        &self,
300
        netdir: &NetDir,
301
        target: T,
302
    ) -> Result<ServiceOnionServiceDataTunnel>
303
    where
304
        T: CircTarget + Sync,
305
    {
306
        let tunnel = self
307
            .0
308
            .get_or_launch_specific(netdir, HsCircKind::SvcRend, target)
309
            .await?;
310
        Ok(tunnel.into())
311
    }
312

            
313
    /// Create a circuit suitable for use as a rendezvous circuit by a client.
314
    ///
315
    /// Return the circuit, along with a [`Relay`] from `netdir` representing its final hop.
316
    ///
317
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
318
    pub async fn get_or_launch_client_rend<'a>(
319
        &self,
320
        netdir: &'a NetDir,
321
    ) -> Result<(ClientOnionServiceDataTunnel, Relay<'a>)> {
322
        let (tunnel, relay) = self.0.get_or_launch_client_rend(netdir).await?;
323
        Ok((tunnel.into(), relay))
324
    }
325

            
326
    /// Return an estimate-based delay for how long a given
327
    /// [`Action`](timeouts::Action) should be allowed to complete.
328
    ///
329
    /// This function has the same semantics as
330
    /// [`CircMgr::estimate_timeout`].
331
    /// See the notes there.
332
    ///
333
    /// In particular **you do not need to use this function** in order to get
334
    /// reasonable timeouts for the circuit-building operations provided by `HsCircPool`.
335
    //
336
    // In principle we could have made this available by making `HsCircPool` `Deref`
337
    // to `CircMgr`, but we don't want to do that because `CircMgr` has methods that
338
    // operate on *its* pool which is separate from the pool maintained by `HsCircPool`.
339
    //
340
    // We *might* want to provide a method to access the underlying `CircMgr`
341
    // but that has the same issues, albeit less severely.
342
    pub fn estimate_timeout(&self, timeout_action: &timeouts::Action) -> std::time::Duration {
343
        self.0.estimate_timeout(timeout_action)
344
    }
345

            
346
    /// Launch the periodic daemon tasks required by the manager to function properly.
347
    ///
348
    /// Returns a set of [`TaskHandle`]s that can be used to manage the daemon tasks.
349
14
    pub fn launch_background_tasks(
350
14
        self: &Arc<Self>,
351
14
        runtime: &R,
352
14
        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
353
14
    ) -> Result<Vec<TaskHandle>> {
354
14
        HsCircPoolInner::launch_background_tasks(&self.0.clone(), runtime, netdir_provider)
355
14
    }
356

            
357
    /// Retire the circuits in this pool.
358
    ///
359
    /// This is used for handling vanguard configuration changes:
360
    /// if the [`VanguardMode`] changes, we need to empty the pool and rebuild it,
361
    /// because the old circuits are no longer suitable for use.
362
    pub fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
363
        self.0.retire_all_circuits()
364
    }
365
}
366

            
367
/// An object to provide circuits for implementing onion services.
368
pub(crate) struct HsCircPoolInner<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> {
369
    /// An underlying circuit manager, used for constructing circuits.
370
    circmgr: Arc<CircMgrInner<B, R>>,
371
    /// A task handle for making the background circuit launcher fire early.
372
    //
373
    // TODO: I think we may want to move this into the same Mutex as Pool
374
    // eventually.  But for now, this is fine, since it's just an implementation
375
    // detail.
376
    //
377
    // TODO MSRV TBD: Replace with OnceLock (#1996)
378
    launcher_handle: OnceCell<TaskHandle>,
379
    /// The mutable state of this pool.
380
    inner: Mutex<Inner<B::Tunnel>>,
381
}
382

            
383
/// The mutable state of an [`HsCircPool`]
384
struct Inner<C: AbstractTunnel> {
385
    /// A collection of pre-constructed circuits.
386
    pool: pool::Pool<C>,
387
}
388

            
389
impl<R: Runtime> HsCircPoolInner<TunnelBuilder<R>, R> {
390
    /// Internal implementation for [`HsCircPool::new`].
391
22
    pub(crate) fn new(circmgr: &CircMgr<R>) -> Self {
392
22
        Self::new_internal(&circmgr.0)
393
22
    }
394
}
395

            
396
impl<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> HsCircPoolInner<B, R> {
397
    /// Create a new [`HsCircPoolInner`] from a [`CircMgrInner`].
398
34
    pub(crate) fn new_internal(circmgr: &Arc<CircMgrInner<B, R>>) -> Self {
399
34
        let circmgr = Arc::clone(circmgr);
400
34
        let pool = pool::Pool::default();
401
34
        Self {
402
34
            circmgr,
403
34
            launcher_handle: OnceCell::new(),
404
34
            inner: Mutex::new(Inner { pool }),
405
34
        }
406
34
    }
407

            
408
    /// Internal implementation for [`HsCircPool::launch_background_tasks`].
409
14
    pub(crate) fn launch_background_tasks(
410
14
        self: &Arc<Self>,
411
14
        runtime: &R,
412
14
        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
413
14
    ) -> Result<Vec<TaskHandle>> {
414
14
        let handle = self.launcher_handle.get_or_try_init(|| {
415
14
            runtime
416
14
                .spawn(remove_unusable_circuits(
417
14
                    Arc::downgrade(self),
418
14
                    Arc::downgrade(netdir_provider),
419
                ))
420
14
                .map_err(|e| Error::from_spawn("preemptive onion circuit expiration task", e))?;
421

            
422
14
            let (schedule, handle) = TaskSchedule::new(runtime.clone());
423
14
            runtime
424
14
                .spawn(launch_hs_circuits_as_needed(
425
14
                    Arc::downgrade(self),
426
14
                    Arc::downgrade(netdir_provider),
427
14
                    schedule,
428
                ))
429
14
                .map_err(|e| Error::from_spawn("preemptive onion circuit builder task", e))?;
430

            
431
14
            Result::<TaskHandle>::Ok(handle)
432
14
        })?;
433

            
434
14
        Ok(vec![handle.clone()])
435
14
    }
436

            
437
    /// Internal implementation for [`HsCircPool::get_or_launch_client_rend`].
438
    pub(crate) async fn get_or_launch_client_rend<'a>(
439
        &self,
440
        netdir: &'a NetDir,
441
    ) -> Result<(B::Tunnel, Relay<'a>)> {
442
        // For rendezvous points, clients use 3-hop circuits.
443
        // Note that we aren't using any special rules for the last hop here; we
444
        // are relying on the fact that:
445
        //   * all suitable middle relays that we use in these circuit stems are
446
        //     suitable renedezvous points, and
447
        //   * the weighting rules for selecting rendezvous points are the same
448
        //     as those for selecting an arbitrary middle relay.
449
        let circ = self
450
            .take_or_launch_stem_circuit::<OwnedCircTarget>(netdir, None, HsCircKind::ClientRend)
451
            .await?;
452

            
453
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
454
        if matches!(
455
            self.vanguard_mode(),
456
            VanguardMode::Full | VanguardMode::Lite
457
        ) && circ.kind != HsCircStemKind::Guarded
458
        {
459
            return Err(internal!("wanted a GUARDED circuit, but got NAIVE?!").into());
460
        }
461

            
462
        let path = circ.single_path().map_err(|error| Error::Protocol {
463
            action: "launching a client rend circuit",
464
            peer: None, // Either party could be to blame.
465
            unique_id: Some(circ.unique_id()),
466
            error,
467
        })?;
468

            
469
        match path.hops().last() {
470
            Some(ent) => {
471
                let Some(ct) = ent.as_chan_target() else {
472
                    return Err(
473
                        internal!("HsPool gave us a circuit with a virtual last hop!?").into(),
474
                    );
475
                };
476
                match netdir.by_ids(ct) {
477
                    Some(relay) => Ok((circ.circ, relay)),
478
                    // This can't happen, since launch_hs_unmanaged() only takes relays from the netdir
479
                    // it is given, and circuit_compatible_with_target() ensures that
480
                    // every relay in the circuit is listed.
481
                    //
482
                    // TODO: Still, it's an ugly place in our API; maybe we should return the last hop
483
                    // from take_or_launch_stem_circuit()?  But in many cases it won't be needed...
484
                    None => Err(internal!("Got circuit with unknown last hop!?").into()),
485
                }
486
            }
487
            None => Err(internal!("Circuit with an empty path!?").into()),
488
        }
489
    }
490

            
491
    /// Helper for the [`HsCircPool`] functions that launch rendezvous,
492
    /// introduction, or directory circuits.
493
    pub(crate) async fn get_or_launch_specific<T>(
494
        &self,
495
        netdir: &NetDir,
496
        kind: HsCircKind,
497
        target: T,
498
    ) -> Result<B::Tunnel>
499
    where
500
        T: CircTarget + Sync,
501
    {
502
        if kind == HsCircKind::ClientRend {
503
            return Err(bad_api_usage!("get_or_launch_specific with ClientRend circuit!?").into());
504
        }
505

            
506
        let wanted_kind = kind.stem_kind();
507

            
508
        // For most* of these circuit types, we want to build our circuit with
509
        // an extra hop, since the target hop is under somebody else's control.
510
        //
511
        // * The exceptions are ClientRend, which we handle in a different
512
        //   method, and SvcIntro, where we will eventually  want an extra hop
513
        //   to avoid vanguard discovery attacks.
514

            
515
        // Get an unfinished circuit that's compatible with our target.
516
        let circ = self
517
            .take_or_launch_stem_circuit(netdir, Some(&target), kind)
518
            .await?;
519

            
520
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
521
        if matches!(
522
            self.vanguard_mode(),
523
            VanguardMode::Full | VanguardMode::Lite
524
        ) && circ.kind != wanted_kind
525
        {
526
            return Err(internal!(
527
                "take_or_launch_stem_circuit() returned {:?}, but we need {wanted_kind:?}",
528
                circ.kind
529
            )
530
            .into());
531
        }
532

            
533
        let mut params = onion_circparams_from_netparams(netdir.params())?;
534

            
535
        // If this is a HsDir circuit, establish a limit on the number of incoming cells from
536
        // the last hop.
537
        params.n_incoming_cells_permitted = match kind {
538
            HsCircKind::ClientHsDir => Some(netdir.params().hsdir_dl_max_reply_cells.into()),
539
            HsCircKind::SvcHsDir => Some(netdir.params().hsdir_ul_max_reply_cells.into()),
540
            HsCircKind::SvcIntro
541
            | HsCircKind::SvcRend
542
            | HsCircKind::ClientIntro
543
            | HsCircKind::ClientRend => None,
544
        };
545
        self.extend_circ(circ, params, target).await
546
    }
547

            
548
    /// Try to extend a circuit to the specified target hop.
549
    async fn extend_circ<T>(
550
        &self,
551
        circ: HsCircStem<B::Tunnel>,
552
        params: CircParameters,
553
        target: T,
554
    ) -> Result<B::Tunnel>
555
    where
556
        T: CircTarget + Sync,
557
    {
558
        let protocol_err = |error| Error::Protocol {
559
            action: "extending to chosen HS hop",
560
            peer: None, // Either party could be to blame.
561
            unique_id: Some(circ.unique_id()),
562
            error,
563
        };
564

            
565
        // Estimate how long it will take to extend it one more hop, and
566
        // construct a timeout as appropriate.
567
        let n_hops = circ.n_hops().map_err(protocol_err)?;
568
        let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
569
            &crate::timeouts::Action::ExtendCircuit {
570
                initial_length: n_hops,
571
                final_length: n_hops + 1,
572
            },
573
        );
574

            
575
        // Make a future to extend the circuit.
576
        let extend_future = circ.extend(&target, params).map_err(protocol_err);
577

            
578
        // Wait up to the timeout for the future to complete.
579
        self.circmgr
580
            .mgr
581
            .peek_runtime()
582
            .timeout(extend_timeout, extend_future)
583
            .await
584
            .map_err(|_| Error::CircTimeout(Some(circ.unique_id())))??;
585

            
586
        // With any luck, return the circuit.
587
        Ok(circ.circ)
588
    }
589

            
590
    /// Internal implementation for [`HsCircPool::retire_all_circuits`].
591
    pub(crate) fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
592
        self.inner
593
            .lock()
594
            .expect("poisoned lock")
595
            .pool
596
            .retire_all_circuits()?;
597

            
598
        Ok(())
599
    }
600

            
601
    /// Take and return a circuit from our pool suitable for being extended to `avoid_target`.
602
    ///
603
    /// If vanguards are enabled, this will try to build a circuit stem appropriate for use
604
    /// as the specified `kind`.
605
    ///
606
    /// If vanguards are disabled, `kind` is unused.
607
    ///
608
    /// If there is no such circuit, build and return a new one.
609
    async fn take_or_launch_stem_circuit<T>(
610
        &self,
611
        netdir: &NetDir,
612
        avoid_target: Option<&T>,
613
        kind: HsCircKind,
614
    ) -> Result<HsCircStem<B::Tunnel>>
615
    where
616
        // TODO #504: It would be better if this were a type that had to include
617
        // family info.
618
        T: CircTarget + Sync,
619
    {
620
        let stem_kind = kind.stem_kind();
621
        let vanguard_mode = self.vanguard_mode();
622
        trace!(
623
            vanguards=%vanguard_mode,
624
            kind=%stem_kind,
625
            "selecting HS circuit stem"
626
        );
627

            
628
        // First, look for a circuit that is already built, if any is suitable.
629

            
630
        let target_exclusion = {
631
            let path_cfg = self.circmgr.builder().path_config();
632
            let cfg = path_cfg.relay_selection_config();
633
            match avoid_target {
634
                // TODO #504: This is an unaccompanied RelayExclusion, and is therefore a
635
                // bit suspect.  We should consider whether we like this behavior.
636
                Some(ct) => RelayExclusion::exclude_channel_target_family(&cfg, ct, netdir),
637
                None => RelayExclusion::no_relays_excluded(),
638
            }
639
        };
640

            
641
        let found_usable_circ = {
642
            let mut inner = self.inner.lock().expect("lock poisoned");
643

            
644
            let restrictions = |circ: &HsCircStem<B::Tunnel>| {
645
                // If vanguards are enabled, we no longer apply same-family or same-subnet
646
                // restrictions, and we allow the guard to appear as either of the last
647
                // two hope of the circuit.
648
                match vanguard_mode {
649
                    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
650
                    VanguardMode::Lite | VanguardMode::Full => {
651
                        vanguards_circuit_compatible_with_target(
652
                            netdir,
653
                            circ,
654
                            stem_kind,
655
                            kind,
656
                            avoid_target,
657
                        )
658
                    }
659
                    VanguardMode::Disabled => {
660
                        circuit_compatible_with_target(netdir, circ, kind, &target_exclusion)
661
                    }
662
                    _ => {
663
                        warn!("unknown vanguard mode {vanguard_mode}");
664
                        false
665
                    }
666
                }
667
            };
668

            
669
            let mut prefs = HsCircPrefs::default();
670

            
671
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
672
            if matches!(vanguard_mode, VanguardMode::Full | VanguardMode::Lite) {
673
                prefs.preferred_stem_kind(stem_kind);
674
            }
675

            
676
            let found_usable_circ =
677
                inner
678
                    .pool
679
                    .take_one_where(&mut rand::rng(), restrictions, &prefs);
680

            
681
            // Tell the background task to fire immediately if we have very few circuits
682
            // circuits left, or if we found nothing.
683
            if inner.pool.very_low() || found_usable_circ.is_none() {
684
                let handle = self.launcher_handle.get().ok_or_else(|| {
685
                    Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
686
                })?;
687
                handle.fire();
688
            }
689
            found_usable_circ
690
        };
691
        // Return the circuit we found before, if any.
692
        if let Some(circuit) = found_usable_circ {
693
            let circuit = self
694
                .maybe_extend_stem_circuit(netdir, circuit, avoid_target, stem_kind, kind)
695
                .await?;
696
            self.ensure_suitable_circuit(&circuit, avoid_target, stem_kind)?;
697
            return Ok(circuit);
698
        }
699

            
700
        // TODO: There is a possible optimization here. Instead of only waiting
701
        // for the circuit we launch below to finish, we could also wait for any
702
        // of our in-progress preemptive circuits to finish.  That would,
703
        // however, complexify our logic quite a bit.
704

            
705
        // TODO: We could in launch multiple circuits in parallel here?
706
        let circ = self
707
            .circmgr
708
            .launch_hs_unmanaged(avoid_target, netdir, stem_kind, Some(kind))
709
            .await?;
710

            
711
        self.ensure_suitable_circuit(&circ, avoid_target, stem_kind)?;
712

            
713
        Ok(HsCircStem {
714
            circ,
715
            kind: stem_kind,
716
        })
717
    }
718

            
719
    /// Return a circuit of the specified `kind`, built from `circuit`.
720
    async fn maybe_extend_stem_circuit<T>(
721
        &self,
722
        netdir: &NetDir,
723
        circuit: HsCircStem<B::Tunnel>,
724
        avoid_target: Option<&T>,
725
        stem_kind: HsCircStemKind,
726
        circ_kind: HsCircKind,
727
    ) -> Result<HsCircStem<B::Tunnel>>
728
    where
729
        T: CircTarget + Sync,
730
    {
731
        match self.vanguard_mode() {
732
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
733
            VanguardMode::Full => {
734
                // NAIVE circuit stems need to be extended by one hop to become GUARDED stems
735
                // if we're using full vanguards.
736
                self.extend_full_vanguards_circuit(
737
                    netdir,
738
                    circuit,
739
                    avoid_target,
740
                    stem_kind,
741
                    circ_kind,
742
                )
743
                .await
744
            }
745
            _ => {
746
                let HsCircStem { circ, kind: _ } = circuit;
747

            
748
                Ok(HsCircStem {
749
                    circ,
750
                    kind: stem_kind,
751
                })
752
            }
753
        }
754
    }
755

            
756
    /// Extend the specified full vanguard circuit if necessary.
757
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
758
    async fn extend_full_vanguards_circuit<T>(
759
        &self,
760
        netdir: &NetDir,
761
        circuit: HsCircStem<B::Tunnel>,
762
        avoid_target: Option<&T>,
763
        stem_kind: HsCircStemKind,
764
        circ_kind: HsCircKind,
765
    ) -> Result<HsCircStem<B::Tunnel>>
766
    where
767
        T: CircTarget + Sync,
768
    {
769
        use crate::path::hspath::hs_stem_terminal_hop_usage;
770
        use tor_relay_selection::RelaySelector;
771

            
772
        match (circuit.kind, stem_kind) {
773
            (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
774
                debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
775
                let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
776
                let circ_path = circuit
777
                    .circ
778
                    .single_path()
779
                    .map_err(|error| Error::Protocol {
780
                        action: "extending full vanguards circuit",
781
                        peer: None, // Either party could be to blame.
782
                        unique_id: Some(circuit.unique_id()),
783
                        error,
784
                    })?;
785

            
786
                // A NAIVE circuit is a 3-hop circuit.
787
                debug_assert_eq!(circ_path.hops().len(), 3);
788

            
789
                let target_exclusion = if let Some(target) = &avoid_target {
790
                    RelayExclusion::exclude_identities(
791
                        target.identities().map(|id| id.to_owned()).collect(),
792
                    )
793
                } else {
794
                    RelayExclusion::no_relays_excluded()
795
                };
796
                let selector = RelaySelector::new(
797
                    hs_stem_terminal_hop_usage(Some(circ_kind)),
798
                    target_exclusion,
799
                );
800
                let hops = circ_path
801
                    .iter()
802
                    .flat_map(|hop| hop.as_chan_target())
803
                    .map(IntoOwnedChanTarget::to_owned)
804
                    .collect::<Vec<OwnedChanTarget>>();
805

            
806
                let extra_hop =
807
                    select_middle_for_vanguard_circ(&hops, netdir, &selector, &mut rand::rng())?;
808

            
809
                // Since full vanguards are enabled and the circuit we got is NAIVE,
810
                // we need to extend it by another hop to make it GUARDED before returning it
811
                let circ = self.extend_circ(circuit, params, extra_hop).await?;
812

            
813
                Ok(HsCircStem {
814
                    circ,
815
                    kind: stem_kind,
816
                })
817
            }
818
            (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
819
                Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
820
            }
821
            _ => {
822
                trace!("Wanted {stem_kind} circuit, got {}", circuit.kind);
823
                // Nothing to do: the circuit stem we got is of the kind we wanted
824
                Ok(circuit)
825
            }
826
        }
827
    }
828

            
829
    /// Ensure `circ` is compatible with `target`, and has the correct length for its `kind`.
830
    fn ensure_suitable_circuit<T>(
831
        &self,
832
        circ: &B::Tunnel,
833
        target: Option<&T>,
834
        kind: HsCircStemKind,
835
    ) -> Result<()>
836
    where
837
        T: CircTarget + Sync,
838
    {
839
        Self::ensure_circuit_can_extend_to_target(circ, target)?;
840
        self.ensure_circuit_length_valid(circ, kind)?;
841

            
842
        Ok(())
843
    }
844

            
845
    /// Ensure the specified circuit of type `kind` has the right length.
846
    fn ensure_circuit_length_valid(&self, tunnel: &B::Tunnel, kind: HsCircStemKind) -> Result<()> {
847
        let circ_path_len = tunnel.n_hops().map_err(|error| Error::Protocol {
848
            action: "validating circuit length",
849
            peer: None, // Either party could be to blame.
850
            unique_id: Some(tunnel.unique_id()),
851
            error,
852
        })?;
853

            
854
        let mode = self.vanguard_mode();
855

            
856
        // TODO(#1457): somehow unify the path length checks
857
        let expected_len = kind.num_hops(mode)?;
858

            
859
        if circ_path_len != expected_len {
860
            return Err(internal!(
861
                "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
862
                kind,
863
                expected_len,
864
                circ_path_len
865
            )
866
            .into());
867
        }
868

            
869
        Ok(())
870
    }
871

            
872
    /// Ensure that it is possible to extend `circ` to `target`.
873
    ///
874
    /// Returns an error if either of the last 2 hops of the circuit are the same as `target`,
875
    /// because:
876
    ///   * a relay won't let you extend the circuit to itself
877
    ///   * relays won't let you extend the circuit to their previous hop
878
    fn ensure_circuit_can_extend_to_target<T>(tunnel: &B::Tunnel, target: Option<&T>) -> Result<()>
879
    where
880
        T: CircTarget + Sync,
881
    {
882
        if let Some(target) = target {
883
            let take_n = 2;
884
            if let Some(hop) = tunnel
885
                .single_path()
886
                .map_err(|error| Error::Protocol {
887
                    action: "validating circuit compatibility with target",
888
                    peer: None, // Either party could be to blame.
889
                    unique_id: Some(tunnel.unique_id()),
890
                    error,
891
                })?
892
                .hops()
893
                .iter()
894
                .rev()
895
                .take(take_n)
896
                .flat_map(|hop| hop.as_chan_target())
897
                .find(|hop| hop.has_any_relay_id_from(target))
898
            {
899
                return Err(internal!(
900
                    "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
901
                    target.display_relay_ids(),
902
                    hop.display_relay_ids()
903
                ).into());
904
            }
905
        }
906

            
907
        Ok(())
908
    }
909

            
910
    /// Internal: Remove every closed circuit from this pool.
911
14
    fn remove_closed(&self) {
912
14
        let mut inner = self.inner.lock().expect("lock poisoned");
913
14
        inner.pool.retain(|circ| !circ.is_closing());
914
14
    }
915

            
916
    /// Internal: Remove every circuit form this pool for which any relay is not
917
    /// listed in `netdir`.
918
    fn remove_unlisted(&self, netdir: &NetDir) {
919
        let mut inner = self.inner.lock().expect("lock poisoned");
920
        inner
921
            .pool
922
            .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true, |_last_hop| true));
923
    }
924

            
925
    /// Returns the current [`VanguardMode`].
926
12
    fn vanguard_mode(&self) -> VanguardMode {
927
        cfg_if::cfg_if! {
928
            if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
929
12
                self
930
12
                    .circmgr
931
12
                    .mgr
932
12
                    .peek_builder()
933
12
                    .vanguardmgr()
934
12
                    .mode()
935
            } else {
936
                VanguardMode::Disabled
937
            }
938
        }
939
12
    }
940

            
941
    /// Internal implementation for [`HsCircPool::estimate_timeout`].
942
    pub(crate) fn estimate_timeout(
943
        &self,
944
        timeout_action: &timeouts::Action,
945
    ) -> std::time::Duration {
946
        self.circmgr.estimate_timeout(timeout_action)
947
    }
948
}
949

            
950
/// Return true if we can extend a pre-built circuit `circ` to `target`.
951
///
952
/// We require that the circuit is open, that every hop  in the circuit is
953
/// listed in `netdir`, and that no hop in the circuit shares a family with
954
/// `target`.
955
fn circuit_compatible_with_target<C: AbstractTunnel>(
956
    netdir: &NetDir,
957
    circ: &HsCircStem<C>,
958
    circ_kind: HsCircKind,
959
    exclude_target: &RelayExclusion,
960
) -> bool {
961
    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
962

            
963
    // NOTE, TODO #504:
964
    // This uses a RelayExclusion directly, when we would be better off
965
    // using a RelaySelector to make sure that we had checked every relevant
966
    // property.
967
    //
968
    // The behavior is okay, since we already checked all the properties of the
969
    // circuit's relays when we first constructed the circuit.  Still, it would
970
    // be better to use refactor and a RelaySelector instead.
971
    circuit_still_useable(
972
        netdir,
973
        circ,
974
        |relay| exclude_target.low_level_predicate_permits_relay(relay),
975
        |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
976
    )
977
}
978

            
979
/// Return true if we can extend a pre-built vanguards circuit `circ` to `target`.
980
///
981
/// We require that the circuit is open, that it can become the specified
982
/// kind of [`HsCircStem`], that every hop in the circuit is listed in `netdir`,
983
/// and that the last two hops are different from the specified target.
984
fn vanguards_circuit_compatible_with_target<C: AbstractTunnel, T>(
985
    netdir: &NetDir,
986
    circ: &HsCircStem<C>,
987
    kind: HsCircStemKind,
988
    circ_kind: HsCircKind,
989
    avoid_target: Option<&T>,
990
) -> bool
991
where
992
    T: CircTarget + Sync,
993
{
994
    if let Some(target) = avoid_target {
995
        let Ok(circ_path) = circ.circ.single_path() else {
996
            // Circuit is unusable, so we can't use it.
997
            return false;
998
        };
999
        // The last 2 hops of the circuit must be different from the circuit target, because:
        //   * a relay won't let you extend the circuit to itself
        //   * relays won't let you extend the circuit to their previous hop
        let take_n = 2;
        if circ_path
            .hops()
            .iter()
            .rev()
            .take(take_n)
            .flat_map(|hop| hop.as_chan_target())
            .any(|hop| hop.has_any_relay_id_from(target))
        {
            return false;
        }
    }
    // TODO #504: usage of low_level_predicate_permits_relay is inherently dubious.
    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
    circ.can_become(kind)
        && circuit_still_useable(
            netdir,
            circ,
            |_relay| true,
            |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
        )
}
/// Return true if we can still use a given pre-build circuit.
///
/// We require that the circuit is open, that every hop  in the circuit is
/// listed in `netdir`, and that `relay_okay` returns true for every hop on the
/// circuit.
fn circuit_still_useable<C, F1, F2>(
    netdir: &NetDir,
    circ: &HsCircStem<C>,
    relay_okay: F1,
    last_hop_ok: F2,
) -> bool
where
    C: AbstractTunnel,
    F1: Fn(&Relay<'_>) -> bool,
    F2: Fn(&Relay<'_>) -> bool,
{
    let circ = &circ.circ;
    if circ.is_closing() {
        return false;
    }
    let Ok(path) = circ.single_path() else {
        // Circuit is unusable, so we can't use it.
        return false;
    };
    let last_hop = path.hops().last().expect("No hops in circuit?!");
    match relay_for_path_ent(netdir, last_hop) {
        Err(NoRelayForPathEnt::HopWasVirtual) => {}
        Err(NoRelayForPathEnt::NoSuchRelay) => {
            return false;
        }
        Ok(r) => {
            if !last_hop_ok(&r) {
                return false;
            }
        }
    };
    path.iter().all(|ent: &circuit::PathEntry| {
        match relay_for_path_ent(netdir, ent) {
            Err(NoRelayForPathEnt::HopWasVirtual) => {
                // This is a virtual hop; it's necessarily compatible with everything.
                true
            }
            Err(NoRelayForPathEnt::NoSuchRelay) => {
                // We require that every relay in this circuit is still listed; an
                // unlisted relay means "reject".
                false
            }
            Ok(r) => {
                // Now it's all down to the predicate.
                relay_okay(&r)
            }
        }
    })
}
/// A possible error condition when trying to look up a PathEntry
//
// Only used for one module-internal function, so doesn't derive Error.
#[derive(Clone, Debug)]
enum NoRelayForPathEnt {
    /// This was a virtual hop; it doesn't have a relay.
    HopWasVirtual,
    /// The relay wasn't found in the netdir.
    NoSuchRelay,
}
/// Look up a relay in a netdir corresponding to `ent`
fn relay_for_path_ent<'a>(
    netdir: &'a NetDir,
    ent: &circuit::PathEntry,
) -> StdResult<Relay<'a>, NoRelayForPathEnt> {
    let Some(c) = ent.as_chan_target() else {
        return Err(NoRelayForPathEnt::HopWasVirtual);
    };
    let Some(relay) = netdir.by_ids(c) else {
        return Err(NoRelayForPathEnt::NoSuchRelay);
    };
    Ok(relay)
}
/// Background task to launch onion circuits as needed.
#[allow(clippy::cognitive_complexity)] // TODO #2010: Refactor, after !3007 is in.
14
async fn launch_hs_circuits_as_needed<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
14
    pool: Weak<HsCircPoolInner<B, R>>,
14
    netdir_provider: Weak<dyn NetDirProvider + 'static>,
14
    mut schedule: TaskSchedule<R>,
14
) {
    /// Default delay when not told to fire explicitly. Chosen arbitrarily.
    const DELAY: Duration = Duration::from_secs(30);
28
    while schedule.next().await.is_some() {
14
        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
14
            (Some(x), Some(y)) => (x, y),
            _ => {
                break;
            }
        };
14
        let now = pool.circmgr.mgr.peek_runtime().now();
14
        pool.remove_closed();
14
        let mut circs_to_launch = {
14
            let mut inner = pool.inner.lock().expect("poisioned_lock");
14
            inner.pool.update_target_size(now);
14
            inner.pool.circs_to_launch()
        };
14
        let n_to_launch = circs_to_launch.n_to_launch();
14
        let mut max_attempts = n_to_launch * 2;
14
        if n_to_launch > 0 {
14
            debug!(
                "launching {} NAIVE  and {} GUARDED circuits",
                circs_to_launch.stem(),
                circs_to_launch.guarded_stem()
            );
        }
        // TODO: refactor this to launch the circuits in parallel
14
        'inner: while circs_to_launch.n_to_launch() > 0 {
14
            max_attempts -= 1;
14
            if max_attempts == 0 {
                // We want to avoid retrying over and over in a tight loop if all our attempts
                // are failing.
                warn!("Too many preemptive onion service circuits failed; waiting a while.");
                break 'inner;
14
            }
14
            if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
                // We want to launch a circuit, and we have a netdir that we can use
                // to launch it.
                //
                // TODO: Possibly we should be doing this in a background task, and
                // launching several of these in parallel.  If we do, we should think about
                // whether taking the fastest will expose us to any attacks.
                let no_target: Option<&OwnedCircTarget> = None;
                let for_launch = circs_to_launch.for_launch();
                // TODO HS: We should catch panics, here or in launch_hs_unmanaged.
                match pool
                    .circmgr
                    .launch_hs_unmanaged(no_target, &netdir, for_launch.kind(), None)
                    .await
                {
                    Ok(circ) => {
                        let kind = for_launch.kind();
                        let circ = HsCircStem { circ, kind };
                        pool.inner.lock().expect("poisoned lock").pool.insert(circ);
                        trace!("successfully launched {kind} circuit");
                        for_launch.note_circ_launched();
                    }
                    Err(err) => {
                        debug_report!(err, "Unable to build preemptive circuit for onion services");
                    }
                }
            } else {
                // We'd like to launch a circuit, but we don't have a netdir that we
                // can use.
                //
                // TODO HS possibly instead of a fixed delay we want to wait for more
                // netdir info?
14
                break 'inner;
            }
        }
        // We have nothing to launch now, so we'll try after a while.
14
        schedule.fire_in(DELAY);
    }
}
/// Background task to remove unusable circuits whenever the directory changes.
14
async fn remove_unusable_circuits<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
14
    pool: Weak<HsCircPoolInner<B, R>>,
14
    netdir_provider: Weak<dyn NetDirProvider + 'static>,
14
) {
14
    let mut event_stream = match netdir_provider.upgrade() {
14
        Some(nd) => nd.events(),
        None => return,
    };
    // Note: We only look at the event stream here, not any kind of TaskSchedule.
    // That's fine, since this task only wants to fire when the directory changes,
    // and the directory will not change while we're dormant.
    //
    // Removing closed circuits is also handled above in launch_hs_circuits_as_needed.
14
    while event_stream.next().await.is_some() {
        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
            (Some(x), Some(y)) => (x, y),
            _ => {
                break;
            }
        };
        pool.remove_closed();
        if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
            pool.remove_unlisted(&netdir);
        }
    }
}
#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    #![allow(clippy::cognitive_complexity)]
    use tor_config::ExplicitOrAuto;
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
    use tor_guardmgr::VanguardConfigBuilder;
    use tor_guardmgr::VanguardMode;
    use tor_memquota::ArcMemoryQuotaTrackerExt as _;
    use tor_proto::memquota::ToplevelAccount;
    use tor_rtmock::MockRuntime;
    use super::*;
    use crate::{CircMgrInner, TestConfig};
    /// Create a `CircMgr` with an underlying `VanguardMgr` that runs in the specified `mode`.
    fn circmgr_with_vanguards<R: Runtime>(
        runtime: R,
        mode: VanguardMode,
    ) -> Arc<CircMgrInner<crate::build::TunnelBuilder<R>, R>> {
        let chanmgr = tor_chanmgr::ChanMgr::new(
            runtime.clone(),
            &Default::default(),
            tor_chanmgr::Dormancy::Dormant,
            &Default::default(),
            ToplevelAccount::new_noop(),
            None,
        );
        let guardmgr = tor_guardmgr::GuardMgr::new(
            runtime.clone(),
            tor_persist::TestingStateMgr::new(),
            &tor_guardmgr::TestConfig::default(),
        )
        .unwrap();
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
        let vanguard_config = VanguardConfigBuilder::default()
            .mode(ExplicitOrAuto::Explicit(mode))
            .build()
            .unwrap();
        let config = TestConfig {
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
            vanguard_config,
            ..Default::default()
        };
        CircMgrInner::new(
            &config,
            tor_persist::TestingStateMgr::new(),
            &runtime,
            Arc::new(chanmgr),
            &guardmgr,
        )
        .unwrap()
        .into()
    }
    // Prevents TROVE-2024-005 (arti#1424)
    #[test]
    fn pool_with_vanguards_disabled() {
        MockRuntime::test_with_various(|runtime| async move {
            let circmgr = circmgr_with_vanguards(runtime, VanguardMode::Disabled);
            let circpool = HsCircPoolInner::new_internal(&circmgr);
            assert!(circpool.vanguard_mode() == VanguardMode::Disabled);
        });
    }
    #[test]
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
    fn pool_with_vanguards_enabled() {
        MockRuntime::test_with_various(|runtime| async move {
            for mode in [VanguardMode::Lite, VanguardMode::Full] {
                let circmgr = circmgr_with_vanguards(runtime.clone(), mode);
                let circpool = HsCircPoolInner::new_internal(&circmgr);
                assert!(circpool.vanguard_mode() == mode);
            }
        });
    }
}