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};
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, SpawnExt,
35
    scheduler::{TaskHandle, TaskSchedule},
36
};
37
use tracing::{debug, instrument, 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
30
    pub fn new(circmgr: &Arc<CircMgr<R>>) -> Self {
220
30
        Self(Arc::new(HsCircPoolInner::new(circmgr)))
221
30
    }
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
22
    pub fn launch_background_tasks(
356
22
        self: &Arc<Self>,
357
22
        runtime: &R,
358
22
        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
359
22
    ) -> Result<Vec<TaskHandle>> {
360
22
        HsCircPoolInner::launch_background_tasks(&self.0.clone(), runtime, netdir_provider)
361
22
    }
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) -> std::time::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) -> std::time::SystemTime {
382
        self.0.circmgr.mgr.peek_runtime().wallclock()
383
    }
384
}
385

            
386
/// An object to provide circuits for implementing onion services.
387
pub(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`]
403
struct Inner<C: AbstractTunnel> {
404
    /// A collection of pre-constructed circuits.
405
    pool: pool::Pool<C>,
406
}
407

            
408
impl<R: Runtime> HsCircPoolInner<TunnelBuilder<R>, R> {
409
    /// Internal implementation for [`HsCircPool::new`].
410
30
    pub(crate) fn new(circmgr: &CircMgr<R>) -> Self {
411
30
        Self::new_internal(&circmgr.0)
412
30
    }
413
}
414

            
415
impl<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> HsCircPoolInner<B, R> {
416
    /// Create a new [`HsCircPoolInner`] from a [`CircMgrInner`].
417
42
    pub(crate) fn new_internal(circmgr: &Arc<CircMgrInner<B, R>>) -> Self {
418
42
        let circmgr = Arc::clone(circmgr);
419
42
        let pool = pool::Pool::default();
420
42
        Self {
421
42
            circmgr,
422
42
            launcher_handle: OnceCell::new(),
423
42
            inner: Mutex::new(Inner { pool }),
424
42
        }
425
42
    }
426

            
427
    /// Internal implementation for [`HsCircPool::launch_background_tasks`].
428
    #[instrument(level = "trace", skip_all)]
429
22
    pub(crate) fn launch_background_tasks(
430
22
        self: &Arc<Self>,
431
22
        runtime: &R,
432
22
        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
433
22
    ) -> Result<Vec<TaskHandle>> {
434
22
        let handle = self.launcher_handle.get_or_try_init(|| {
435
22
            runtime
436
22
                .spawn(remove_unusable_circuits(
437
22
                    Arc::downgrade(self),
438
22
                    Arc::downgrade(netdir_provider),
439
                ))
440
22
                .map_err(|e| Error::from_spawn("preemptive onion circuit expiration task", e))?;
441

            
442
22
            let (schedule, handle) = TaskSchedule::new(runtime.clone());
443
22
            runtime
444
22
                .spawn(launch_hs_circuits_as_needed(
445
22
                    Arc::downgrade(self),
446
22
                    Arc::downgrade(netdir_provider),
447
22
                    schedule,
448
                ))
449
22
                .map_err(|e| Error::from_spawn("preemptive onion circuit builder task", e))?;
450

            
451
22
            Result::<TaskHandle>::Ok(handle)
452
22
        })?;
453

            
454
22
        Ok(vec![handle.clone()])
455
22
    }
456

            
457
    /// Internal implementation for [`HsCircPool::get_or_launch_client_rend`].
458
    #[instrument(level = "trace", skip_all)]
459
    pub(crate) async fn get_or_launch_client_rend<'a>(
460
        &self,
461
        netdir: &'a NetDir,
462
    ) -> Result<(B::Tunnel, Relay<'a>)> {
463
        // For rendezvous points, clients use 3-hop circuits.
464
        // Note that we aren't using any special rules for the last hop here; we
465
        // are relying on the fact that:
466
        //   * all suitable middle relays that we use in these circuit stems are
467
        //     suitable renedezvous points, and
468
        //   * the weighting rules for selecting rendezvous points are the same
469
        //     as those for selecting an arbitrary middle relay.
470
        let circ = self
471
            .take_or_launch_stem_circuit::<OwnedCircTarget>(netdir, None, HsCircKind::ClientRend)
472
            .await?;
473

            
474
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
475
        if matches!(
476
            self.vanguard_mode(),
477
            VanguardMode::Full | VanguardMode::Lite
478
        ) && circ.kind != HsCircStemKind::Guarded
479
        {
480
            return Err(internal!("wanted a GUARDED circuit, but got NAIVE?!").into());
481
        }
482

            
483
        let path = circ.single_path().map_err(|error| Error::Protocol {
484
            action: "launching a client rend circuit",
485
            peer: None, // Either party could be to blame.
486
            unique_id: Some(circ.unique_id()),
487
            error,
488
        })?;
489

            
490
        match path.hops().last() {
491
            Some(ent) => {
492
                let Some(ct) = ent.as_chan_target() else {
493
                    return Err(
494
                        internal!("HsPool gave us a circuit with a virtual last hop!?").into(),
495
                    );
496
                };
497
                match netdir.by_ids(ct) {
498
                    Some(relay) => Ok((circ.circ, relay)),
499
                    // This can't happen, since launch_hs_unmanaged() only takes relays from the netdir
500
                    // it is given, and circuit_compatible_with_target() ensures that
501
                    // every relay in the circuit is listed.
502
                    //
503
                    // TODO: Still, it's an ugly place in our API; maybe we should return the last hop
504
                    // from take_or_launch_stem_circuit()?  But in many cases it won't be needed...
505
                    None => Err(internal!("Got circuit with unknown last hop!?").into()),
506
                }
507
            }
508
            None => Err(internal!("Circuit with an empty path!?").into()),
509
        }
510
    }
511

            
512
    /// Helper for the [`HsCircPool`] functions that launch rendezvous,
513
    /// introduction, or directory circuits.
514
    #[instrument(level = "trace", skip_all)]
515
    pub(crate) async fn get_or_launch_specific<T>(
516
        &self,
517
        netdir: &NetDir,
518
        kind: HsCircKind,
519
        target: T,
520
    ) -> Result<B::Tunnel>
521
    where
522
        T: CircTarget + Sync,
523
    {
524
        if kind == HsCircKind::ClientRend {
525
            return Err(bad_api_usage!("get_or_launch_specific with ClientRend circuit!?").into());
526
        }
527

            
528
        let wanted_kind = kind.stem_kind();
529

            
530
        // For most* of these circuit types, we want to build our circuit with
531
        // an extra hop, since the target hop is under somebody else's control.
532
        //
533
        // * The exceptions are ClientRend, which we handle in a different
534
        //   method, and SvcIntro, where we will eventually  want an extra hop
535
        //   to avoid vanguard discovery attacks.
536

            
537
        // Get an unfinished circuit that's compatible with our target.
538
        let circ = self
539
            .take_or_launch_stem_circuit(netdir, Some(&target), kind)
540
            .await?;
541

            
542
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
543
        if matches!(
544
            self.vanguard_mode(),
545
            VanguardMode::Full | VanguardMode::Lite
546
        ) && circ.kind != wanted_kind
547
        {
548
            return Err(internal!(
549
                "take_or_launch_stem_circuit() returned {:?}, but we need {wanted_kind:?}",
550
                circ.kind
551
            )
552
            .into());
553
        }
554

            
555
        let mut params = onion_circparams_from_netparams(netdir.params())?;
556

            
557
        // If this is a HsDir circuit, establish a limit on the number of incoming cells from
558
        // the last hop.
559
        params.n_incoming_cells_permitted = match kind {
560
            HsCircKind::ClientHsDir => Some(netdir.params().hsdir_dl_max_reply_cells.into()),
561
            HsCircKind::SvcHsDir => Some(netdir.params().hsdir_ul_max_reply_cells.into()),
562
            HsCircKind::SvcIntro
563
            | HsCircKind::SvcRend
564
            | HsCircKind::ClientIntro
565
            | HsCircKind::ClientRend => None,
566
        };
567
        self.extend_circ(circ, params, target).await
568
    }
569

            
570
    /// Try to extend a circuit to the specified target hop.
571
    async fn extend_circ<T>(
572
        &self,
573
        circ: HsCircStem<B::Tunnel>,
574
        params: CircParameters,
575
        target: T,
576
    ) -> Result<B::Tunnel>
577
    where
578
        T: CircTarget + Sync,
579
    {
580
        let protocol_err = |error| Error::Protocol {
581
            action: "extending to chosen HS hop",
582
            peer: None, // Either party could be to blame.
583
            unique_id: Some(circ.unique_id()),
584
            error,
585
        };
586

            
587
        // Estimate how long it will take to extend it one more hop, and
588
        // construct a timeout as appropriate.
589
        let n_hops = circ.n_hops().map_err(protocol_err)?;
590
        let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
591
            &crate::timeouts::Action::ExtendCircuit {
592
                initial_length: n_hops,
593
                final_length: n_hops + 1,
594
            },
595
        );
596

            
597
        // Make a future to extend the circuit.
598
        let extend_future = circ.extend(&target, params).map_err(protocol_err);
599

            
600
        // Wait up to the timeout for the future to complete.
601
        self.circmgr
602
            .mgr
603
            .peek_runtime()
604
            .timeout(extend_timeout, extend_future)
605
            .await
606
            .map_err(|_| Error::CircTimeout(Some(circ.unique_id())))??;
607

            
608
        // With any luck, return the circuit.
609
        Ok(circ.circ)
610
    }
611

            
612
    /// Internal implementation for [`HsCircPool::retire_all_circuits`].
613
    pub(crate) fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
614
        self.inner
615
            .lock()
616
            .expect("poisoned lock")
617
            .pool
618
            .retire_all_circuits()?;
619

            
620
        Ok(())
621
    }
622

            
623
    /// Take and return a circuit from our pool suitable for being extended to `avoid_target`.
624
    ///
625
    /// If vanguards are enabled, this will try to build a circuit stem appropriate for use
626
    /// as the specified `kind`.
627
    ///
628
    /// If vanguards are disabled, `kind` is unused.
629
    ///
630
    /// If there is no such circuit, build and return a new one.
631
    #[instrument(level = "trace", skip_all)]
632
    async fn take_or_launch_stem_circuit<T>(
633
        &self,
634
        netdir: &NetDir,
635
        avoid_target: Option<&T>,
636
        kind: HsCircKind,
637
    ) -> Result<HsCircStem<B::Tunnel>>
638
    where
639
        // TODO #504: It would be better if this were a type that had to include
640
        // family info.
641
        T: CircTarget + Sync,
642
    {
643
        let stem_kind = kind.stem_kind();
644
        let vanguard_mode = self.vanguard_mode();
645
        trace!(
646
            vanguards=%vanguard_mode,
647
            kind=%stem_kind,
648
            "selecting HS circuit stem"
649
        );
650

            
651
        // First, look for a circuit that is already built, if any is suitable.
652

            
653
        let target_exclusion = {
654
            let path_cfg = self.circmgr.builder().path_config();
655
            let cfg = path_cfg.relay_selection_config();
656
            match avoid_target {
657
                // TODO #504: This is an unaccompanied RelayExclusion, and is therefore a
658
                // bit suspect.  We should consider whether we like this behavior.
659
                Some(ct) => RelayExclusion::exclude_channel_target_family(&cfg, ct, netdir),
660
                None => RelayExclusion::no_relays_excluded(),
661
            }
662
        };
663

            
664
        let found_usable_circ = {
665
            let mut inner = self.inner.lock().expect("lock poisoned");
666

            
667
            let restrictions = |circ: &HsCircStem<B::Tunnel>| {
668
                // If vanguards are enabled, we no longer apply same-family or same-subnet
669
                // restrictions, and we allow the guard to appear as either of the last
670
                // two hope of the circuit.
671
                match vanguard_mode {
672
                    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
673
                    VanguardMode::Lite | VanguardMode::Full => {
674
                        vanguards_circuit_compatible_with_target(
675
                            netdir,
676
                            circ,
677
                            stem_kind,
678
                            kind,
679
                            avoid_target,
680
                        )
681
                    }
682
                    VanguardMode::Disabled => {
683
                        circuit_compatible_with_target(netdir, circ, kind, &target_exclusion)
684
                    }
685
                    _ => {
686
                        warn!("unknown vanguard mode {vanguard_mode}");
687
                        false
688
                    }
689
                }
690
            };
691

            
692
            let mut prefs = HsCircPrefs::default();
693

            
694
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
695
            if matches!(vanguard_mode, VanguardMode::Full | VanguardMode::Lite) {
696
                prefs.preferred_stem_kind(stem_kind);
697
            }
698

            
699
            let found_usable_circ =
700
                inner
701
                    .pool
702
                    .take_one_where(&mut rand::rng(), restrictions, &prefs);
703

            
704
            // Tell the background task to fire immediately if we have very few circuits
705
            // circuits left, or if we found nothing.
706
            if inner.pool.very_low() || found_usable_circ.is_none() {
707
                let handle = self.launcher_handle.get().ok_or_else(|| {
708
                    Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
709
                })?;
710
                handle.fire();
711
            }
712
            found_usable_circ
713
        };
714
        // Return the circuit we found before, if any.
715
        if let Some(circuit) = found_usable_circ {
716
            let circuit = self
717
                .maybe_extend_stem_circuit(netdir, circuit, avoid_target, stem_kind, kind)
718
                .await?;
719
            self.ensure_suitable_circuit(&circuit, avoid_target, stem_kind)?;
720
            return Ok(circuit);
721
        }
722

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

            
728
        // TODO: We could in launch multiple circuits in parallel here?
729
        let circ = self
730
            .circmgr
731
            .launch_hs_unmanaged(avoid_target, netdir, stem_kind, Some(kind))
732
            .await?;
733

            
734
        self.ensure_suitable_circuit(&circ, avoid_target, stem_kind)?;
735

            
736
        Ok(HsCircStem {
737
            circ,
738
            kind: stem_kind,
739
        })
740
    }
741

            
742
    /// Return a circuit of the specified `kind`, built from `circuit`.
743
    async fn maybe_extend_stem_circuit<T>(
744
        &self,
745
        netdir: &NetDir,
746
        circuit: HsCircStem<B::Tunnel>,
747
        avoid_target: Option<&T>,
748
        stem_kind: HsCircStemKind,
749
        circ_kind: HsCircKind,
750
    ) -> Result<HsCircStem<B::Tunnel>>
751
    where
752
        T: CircTarget + Sync,
753
    {
754
        match self.vanguard_mode() {
755
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
756
            VanguardMode::Full => {
757
                // NAIVE circuit stems need to be extended by one hop to become GUARDED stems
758
                // if we're using full vanguards.
759
                self.extend_full_vanguards_circuit(
760
                    netdir,
761
                    circuit,
762
                    avoid_target,
763
                    stem_kind,
764
                    circ_kind,
765
                )
766
                .await
767
            }
768
            _ => {
769
                let HsCircStem { circ, kind: _ } = circuit;
770

            
771
                Ok(HsCircStem {
772
                    circ,
773
                    kind: stem_kind,
774
                })
775
            }
776
        }
777
    }
778

            
779
    /// Extend the specified full vanguard circuit if necessary.
780
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
781
    async fn extend_full_vanguards_circuit<T>(
782
        &self,
783
        netdir: &NetDir,
784
        circuit: HsCircStem<B::Tunnel>,
785
        avoid_target: Option<&T>,
786
        stem_kind: HsCircStemKind,
787
        circ_kind: HsCircKind,
788
    ) -> Result<HsCircStem<B::Tunnel>>
789
    where
790
        T: CircTarget + Sync,
791
    {
792
        use crate::path::hspath::hs_stem_terminal_hop_usage;
793
        use tor_relay_selection::RelaySelector;
794

            
795
        match (circuit.kind, stem_kind) {
796
            (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
797
                debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
798
                let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
799
                let circ_path = circuit
800
                    .circ
801
                    .single_path()
802
                    .map_err(|error| Error::Protocol {
803
                        action: "extending full vanguards circuit",
804
                        peer: None, // Either party could be to blame.
805
                        unique_id: Some(circuit.unique_id()),
806
                        error,
807
                    })?;
808

            
809
                // A NAIVE circuit is a 3-hop circuit.
810
                debug_assert_eq!(circ_path.hops().len(), 3);
811

            
812
                let target_exclusion = if let Some(target) = &avoid_target {
813
                    RelayExclusion::exclude_identities(
814
                        target.identities().map(|id| id.to_owned()).collect(),
815
                    )
816
                } else {
817
                    RelayExclusion::no_relays_excluded()
818
                };
819
                let selector = RelaySelector::new(
820
                    hs_stem_terminal_hop_usage(Some(circ_kind)),
821
                    target_exclusion,
822
                );
823
                let hops = circ_path
824
                    .iter()
825
                    .flat_map(|hop| hop.as_chan_target())
826
                    .map(IntoOwnedChanTarget::to_owned)
827
                    .collect::<Vec<OwnedChanTarget>>();
828

            
829
                let extra_hop =
830
                    select_middle_for_vanguard_circ(&hops, netdir, &selector, &mut rand::rng())?;
831

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

            
836
                Ok(HsCircStem {
837
                    circ,
838
                    kind: stem_kind,
839
                })
840
            }
841
            (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
842
                Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
843
            }
844
            _ => {
845
                trace!("Wanted {stem_kind} circuit, got {}", circuit.kind);
846
                // Nothing to do: the circuit stem we got is of the kind we wanted
847
                Ok(circuit)
848
            }
849
        }
850
    }
851

            
852
    /// Ensure `circ` is compatible with `target`, and has the correct length for its `kind`.
853
    fn ensure_suitable_circuit<T>(
854
        &self,
855
        circ: &B::Tunnel,
856
        target: Option<&T>,
857
        kind: HsCircStemKind,
858
    ) -> Result<()>
859
    where
860
        T: CircTarget + Sync,
861
    {
862
        Self::ensure_circuit_can_extend_to_target(circ, target)?;
863
        self.ensure_circuit_length_valid(circ, kind)?;
864

            
865
        Ok(())
866
    }
867

            
868
    /// Ensure the specified circuit of type `kind` has the right length.
869
    fn ensure_circuit_length_valid(&self, tunnel: &B::Tunnel, kind: HsCircStemKind) -> Result<()> {
870
        let circ_path_len = tunnel.n_hops().map_err(|error| Error::Protocol {
871
            action: "validating circuit length",
872
            peer: None, // Either party could be to blame.
873
            unique_id: Some(tunnel.unique_id()),
874
            error,
875
        })?;
876

            
877
        let mode = self.vanguard_mode();
878

            
879
        // TODO(#1457): somehow unify the path length checks
880
        let expected_len = kind.num_hops(mode)?;
881

            
882
        if circ_path_len != expected_len {
883
            return Err(internal!(
884
                "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
885
                kind,
886
                expected_len,
887
                circ_path_len
888
            )
889
            .into());
890
        }
891

            
892
        Ok(())
893
    }
894

            
895
    /// Ensure that it is possible to extend `circ` to `target`.
896
    ///
897
    /// Returns an error if either of the last 2 hops of the circuit are the same as `target`,
898
    /// because:
899
    ///   * a relay won't let you extend the circuit to itself
900
    ///   * relays won't let you extend the circuit to their previous hop
901
    fn ensure_circuit_can_extend_to_target<T>(tunnel: &B::Tunnel, target: Option<&T>) -> Result<()>
902
    where
903
        T: CircTarget + Sync,
904
    {
905
        if let Some(target) = target {
906
            let take_n = 2;
907
            if let Some(hop) = tunnel
908
                .single_path()
909
                .map_err(|error| Error::Protocol {
910
                    action: "validating circuit compatibility with target",
911
                    peer: None, // Either party could be to blame.
912
                    unique_id: Some(tunnel.unique_id()),
913
                    error,
914
                })?
915
                .hops()
916
                .iter()
917
                .rev()
918
                .take(take_n)
919
                .flat_map(|hop| hop.as_chan_target())
920
                .find(|hop| hop.has_any_relay_id_from(target))
921
            {
922
                return Err(internal!(
923
                    "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
924
                    target.display_relay_ids(),
925
                    hop.display_relay_ids()
926
                ).into());
927
            }
928
        }
929

            
930
        Ok(())
931
    }
932

            
933
    /// Internal: Remove every closed circuit from this pool.
934
22
    fn remove_closed(&self) {
935
22
        let mut inner = self.inner.lock().expect("lock poisoned");
936
22
        inner.pool.retain(|circ| !circ.is_closing());
937
22
    }
938

            
939
    /// Internal: Remove every circuit form this pool for which any relay is not
940
    /// listed in `netdir`.
941
    fn remove_unlisted(&self, netdir: &NetDir) {
942
        let mut inner = self.inner.lock().expect("lock poisoned");
943
        inner
944
            .pool
945
            .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true, |_last_hop| true));
946
    }
947

            
948
    /// Returns the current [`VanguardMode`].
949
12
    fn vanguard_mode(&self) -> VanguardMode {
950
        cfg_if::cfg_if! {
951
            if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
952
12
                self
953
12
                    .circmgr
954
12
                    .mgr
955
12
                    .peek_builder()
956
12
                    .vanguardmgr()
957
12
                    .mode()
958
            } else {
959
                VanguardMode::Disabled
960
            }
961
        }
962
12
    }
963

            
964
    /// Internal implementation for [`HsCircPool::estimate_timeout`].
965
    pub(crate) fn estimate_timeout(
966
        &self,
967
        timeout_action: &timeouts::Action,
968
    ) -> std::time::Duration {
969
        self.circmgr.estimate_timeout(timeout_action)
970
    }
971
}
972

            
973
/// Return true if we can extend a pre-built circuit `circ` to `target`.
974
///
975
/// We require that the circuit is open, that every hop  in the circuit is
976
/// listed in `netdir`, and that no hop in the circuit shares a family with
977
/// `target`.
978
fn circuit_compatible_with_target<C: AbstractTunnel>(
979
    netdir: &NetDir,
980
    circ: &HsCircStem<C>,
981
    circ_kind: HsCircKind,
982
    exclude_target: &RelayExclusion,
983
) -> bool {
984
    let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
985

            
986
    // NOTE, TODO #504:
987
    // This uses a RelayExclusion directly, when we would be better off
988
    // using a RelaySelector to make sure that we had checked every relevant
989
    // property.
990
    //
991
    // The behavior is okay, since we already checked all the properties of the
992
    // circuit's relays when we first constructed the circuit.  Still, it would
993
    // be better to use refactor and a RelaySelector instead.
994
    circuit_still_useable(
995
        netdir,
996
        circ,
997
        |relay| exclude_target.low_level_predicate_permits_relay(relay),
998
        |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
999
    )
}
/// Return true if we can extend a pre-built vanguards circuit `circ` to `target`.
///
/// We require that the circuit is open, that it can become the specified
/// kind of [`HsCircStem`], that every hop in the circuit is listed in `netdir`,
/// and that the last two hops are different from the specified target.
fn vanguards_circuit_compatible_with_target<C: AbstractTunnel, T>(
    netdir: &NetDir,
    circ: &HsCircStem<C>,
    kind: HsCircStemKind,
    circ_kind: HsCircKind,
    avoid_target: Option<&T>,
) -> bool
where
    T: CircTarget + Sync,
{
    if let Some(target) = avoid_target {
        let Ok(circ_path) = circ.circ.single_path() else {
            // Circuit is unusable, so we can't use it.
            return false;
        };
        // 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.
#[instrument(level = "trace", skip_all)]
22
async fn launch_hs_circuits_as_needed<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
22
    pool: Weak<HsCircPoolInner<B, R>>,
22
    netdir_provider: Weak<dyn NetDirProvider + 'static>,
22
    mut schedule: TaskSchedule<R>,
22
) {
    /// Default delay when not told to fire explicitly. Chosen arbitrarily.
    const DELAY: Duration = Duration::from_secs(30);
    while schedule.next().await.is_some() {
        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
            (Some(x), Some(y)) => (x, y),
            _ => {
                break;
            }
        };
        let now = pool.circmgr.mgr.peek_runtime().now();
        pool.remove_closed();
        let mut circs_to_launch = {
            let mut inner = pool.inner.lock().expect("poisioned_lock");
            inner.pool.update_target_size(now);
            inner.pool.circs_to_launch()
        };
        let n_to_launch = circs_to_launch.n_to_launch();
        let mut max_attempts = n_to_launch * 2;
        if n_to_launch > 0 {
            debug!(
                "launching {} NAIVE  and {} GUARDED circuits",
                circs_to_launch.stem(),
                circs_to_launch.guarded_stem()
            );
        }
        // TODO: refactor this to launch the circuits in parallel
        'inner: while circs_to_launch.n_to_launch() > 0 {
            max_attempts -= 1;
            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;
            }
            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?
                break 'inner;
            }
        }
        // We have nothing to launch now, so we'll try after a while.
        schedule.fire_in(DELAY);
    }
}
/// Background task to remove unusable circuits whenever the directory changes.
22
async fn remove_unusable_circuits<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
22
    pool: Weak<HsCircPoolInner<B, R>>,
22
    netdir_provider: Weak<dyn NetDirProvider + 'static>,
22
) {
22
    let mut event_stream = match netdir_provider.upgrade() {
22
        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.
22
    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_time_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(),
        )
        .unwrap();
        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);
            }
        });
    }
}