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
    build::{onion_circparams_from_netparams, CircuitBuilder},
15
    mgr::AbstractCircBuilder,
16
    timeouts, AbstractCirc, CircMgr, CircMgrInner, Error, Result,
17
};
18
use futures::{task::SpawnExt, StreamExt, TryFutureExt};
19
use once_cell::sync::OnceCell;
20
use tor_error::{bad_api_usage, internal};
21
use tor_error::{debug_report, Bug};
22
use tor_guardmgr::VanguardMode;
23
use tor_linkspec::{
24
    CircTarget, HasRelayIds as _, IntoOwnedChanTarget, OwnedChanTarget, OwnedCircTarget,
25
};
26
use tor_netdir::{NetDir, NetDirProvider, Relay};
27
use tor_proto::circuit::{self, CircParameters, ClientCirc};
28
use tor_relay_selection::{LowLevelRelayPredicate, RelayExclusion};
29
use tor_rtcompat::{
30
    scheduler::{TaskHandle, TaskSchedule},
31
    Runtime, SleepProviderExt,
32
};
33
use tracing::{debug, trace, warn};
34

            
35
use std::result::Result as StdResult;
36

            
37
pub use config::HsCircPoolConfig;
38

            
39
use self::pool::HsCircPrefs;
40

            
41
#[cfg(all(feature = "vanguards", feature = "hs-common"))]
42
use crate::path::hspath::select_middle_for_vanguard_circ;
43

            
44
/// The (onion-service-related) purpose for which a given circuit is going to be
45
/// used.
46
///
47
/// We will use this to tell how the path for a given circuit is to be
48
/// constructed.
49
#[cfg(feature = "hs-common")]
50
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
51
#[non_exhaustive]
52
pub enum HsCircKind {
53
    /// Circuit from an onion service to an HsDir.
54
    SvcHsDir,
55
    /// Circuit from an onion service to an Introduction Point.
56
    SvcIntro,
57
    /// Circuit from an onion service to a Rendezvous Point.
58
    SvcRend,
59
    /// Circuit from an onion service client to an HsDir.
60
    ClientHsDir,
61
    /// Circuit from an onion service client to an Introduction Point.
62
    ClientIntro,
63
    /// Circuit from an onion service client to a Rendezvous Point.
64
    ClientRend,
65
}
66

            
67
impl HsCircKind {
68
    /// Return the [`HsCircStemKind`] needed to build this type of circuit.
69
    fn stem_kind(&self) -> HsCircStemKind {
70
        match self {
71
            HsCircKind::ClientRend | HsCircKind::SvcIntro => HsCircStemKind::Naive,
72
            HsCircKind::SvcHsDir => {
73
                // TODO: we might want this to be GUARDED
74
                HsCircStemKind::Naive
75
            }
76
            HsCircKind::SvcRend | HsCircKind::ClientHsDir | HsCircKind::ClientIntro => {
77
                HsCircStemKind::Guarded
78
            }
79
        }
80
    }
81
}
82

            
83
/// A hidden service circuit stem.
84
///
85
/// This represents a hidden service circuit that has not yet been extended to a target.
86
///
87
/// See [HsCircStemKind].
88
pub(crate) struct HsCircStem<C: AbstractCirc> {
89
    /// The circuit.
90
    pub(crate) circ: Arc<C>,
91
    /// Whether the circuit is NAIVE  or GUARDED.
92
    pub(crate) kind: HsCircStemKind,
93
}
94

            
95
impl<C: AbstractCirc> HsCircStem<C> {
96
    /// Whether this circuit satisfies _all_ the [`HsCircPrefs`].
97
    ///
98
    /// Returns `false` if any of the `prefs` are not satisfied.
99
    fn satisfies_prefs(&self, prefs: &HsCircPrefs) -> bool {
100
        let HsCircPrefs { kind_prefs } = prefs;
101

            
102
        match kind_prefs {
103
            Some(kind) => *kind == self.kind,
104
            None => true,
105
        }
106
    }
107
}
108

            
109
impl<C: AbstractCirc> Deref for HsCircStem<C> {
110
    type Target = Arc<C>;
111

            
112
    fn deref(&self) -> &Self::Target {
113
        &self.circ
114
    }
115
}
116

            
117
impl<C: AbstractCirc> HsCircStem<C> {
118
    /// Check if this circuit stem is of the specified `kind`
119
    /// or can be extended to become that kind.
120
    ///
121
    /// Returns `true` if this `HsCircStem`'s kind is equal to `other`,
122
    /// or if its kind is [`Naive`](HsCircStemKind::Naive)
123
    /// and `other` is [`Guarded`](HsCircStemKind::Guarded).
124
    pub(crate) fn can_become(&self, other: HsCircStemKind) -> bool {
125
        use HsCircStemKind::*;
126

            
127
        match (self.kind, other) {
128
            (Naive, Naive) | (Guarded, Guarded) | (Naive, Guarded) => true,
129
            (Guarded, Naive) => false,
130
        }
131
    }
132
}
133

            
134
#[allow(rustdoc::private_intra_doc_links)]
135
/// A kind of hidden service circuit stem.
136
///
137
/// See [hspath](crate::path::hspath) docs for more information.
138
///
139
/// The structure of a circuit stem depends on whether vanguards are enabled:
140
///
141
///   * with vanguards disabled:
142
///      ```text
143
///         NAIVE   = G -> M -> M
144
///         GUARDED = G -> M -> M
145
///      ```
146
///
147
///   * with lite vanguards enabled:
148
///      ```text
149
///         NAIVE   = G -> L2 -> M
150
///         GUARDED = G -> L2 -> M
151
///      ```
152
///
153
///   * with full vanguards enabled:
154
///      ```text
155
///         NAIVE    = G -> L2 -> L3
156
///         GUARDED = G -> L2 -> L3 -> M
157
///      ```
158
#[derive(Copy, Clone, Debug, PartialEq, derive_more::Display)]
159
#[non_exhaustive]
160
pub(crate) enum HsCircStemKind {
161
    /// A naive circuit stem.
162
    ///
163
    /// Used for building circuits to a final hop that an adversary cannot easily control,
164
    /// for example if the final hop is is randomly chosen by us.
165
    #[display("NAIVE")]
166
    Naive,
167
    /// An guarded circuit stem.
168
    ///
169
    /// Used for building circuits to a final hop that an adversary can easily control,
170
    /// for example if the final hop is not chosen by us.
171
    #[display("GUARDED")]
172
    Guarded,
173
}
174

            
175
impl HsCircStemKind {
176
    /// Return the number of hops this `HsCircKind` ought to have when using the specified
177
    /// [`VanguardMode`].
178
80
    pub(crate) fn num_hops(&self, mode: VanguardMode) -> StdResult<usize, Bug> {
179
        use HsCircStemKind::*;
180
        use VanguardMode::*;
181

            
182
80
        let len = match (mode, self) {
183
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
184
32
            (Lite, _) => 3,
185
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
186
24
            (Full, Naive) => 3,
187
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
188
24
            (Full, Guarded) => 4,
189
            (Disabled, _) => 3,
190
            (_, _) => {
191
                return Err(internal!("Unsupported vanguard mode {mode}"));
192
            }
193
        };
194

            
195
80
        Ok(len)
196
80
    }
197
}
198

            
199
/// An object to provide circuits for implementing onion services.
200
pub struct HsCircPool<R: Runtime>(Arc<HsCircPoolInner<CircuitBuilder<R>, R>>);
201

            
202
impl<R: Runtime> HsCircPool<R> {
203
    /// Create a new `HsCircPool`.
204
    ///
205
    /// This will not work properly before "launch_background_tasks" is called.
206
16
    pub fn new(circmgr: &Arc<CircMgr<R>>) -> Self {
207
16
        Self(Arc::new(HsCircPoolInner::new(circmgr)))
208
16
    }
209

            
210
    /// Create a circuit suitable for use for `kind`, ending at the chosen hop `target`.
211
    ///
212
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
213
    pub async fn get_or_launch_specific<T>(
214
        &self,
215
        netdir: &NetDir,
216
        kind: HsCircKind,
217
        target: T,
218
    ) -> Result<Arc<ClientCirc>>
219
    where
220
        T: CircTarget + std::marker::Sync,
221
    {
222
        self.0.get_or_launch_specific(netdir, kind, target).await
223
    }
224

            
225
    /// Create a circuit suitable for use as a rendezvous circuit by a client.
226
    ///
227
    /// Return the circuit, along with a [`Relay`] from `netdir` representing its final hop.
228
    ///
229
    /// Only makes  a single attempt; the caller needs to loop if they want to retry.
230
    pub async fn get_or_launch_client_rend<'a>(
231
        &self,
232
        netdir: &'a NetDir,
233
    ) -> Result<(Arc<ClientCirc>, Relay<'a>)> {
234
        self.0.get_or_launch_client_rend(netdir).await
235
    }
236

            
237
    /// Return an estimate-based delay for how long a given
238
    /// [`Action`](timeouts::Action) should be allowed to complete.
239
    ///
240
    /// This function has the same semantics as
241
    /// [`CircMgr::estimate_timeout`].
242
    /// See the notes there.
243
    ///
244
    /// In particular **you do not need to use this function** in order to get
245
    /// reasonable timeouts for the circuit-building operations provided by `HsCircPool`.
246
    //
247
    // In principle we could have made this available by making `HsCircPool` `Deref`
248
    // to `CircMgr`, but we don't want to do that because `CircMgr` has methods that
249
    // operate on *its* pool which is separate from the pool maintained by `HsCircPool`.
250
    //
251
    // We *might* want to provide a method to access the underlying `CircMgr`
252
    // but that has the same issues, albeit less severely.
253
    pub fn estimate_timeout(&self, timeout_action: &timeouts::Action) -> std::time::Duration {
254
        self.0.estimate_timeout(timeout_action)
255
    }
256

            
257
    /// Launch the periodic daemon tasks required by the manager to function properly.
258
    ///
259
    /// Returns a set of [`TaskHandle`]s that can be used to manage the daemon tasks.
260
8
    pub fn launch_background_tasks(
261
8
        self: &Arc<Self>,
262
8
        runtime: &R,
263
8
        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
264
8
    ) -> Result<Vec<TaskHandle>> {
265
8
        HsCircPoolInner::launch_background_tasks(&self.0.clone(), runtime, netdir_provider)
266
8
    }
267

            
268
    /// Retire the circuits in this pool.
269
    ///
270
    /// This is used for handling vanguard configuration changes:
271
    /// if the [`VanguardMode`] changes, we need to empty the pool and rebuild it,
272
    /// because the old circuits are no longer suitable for use.
273
    pub fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
274
        self.0.retire_all_circuits()
275
    }
276
}
277

            
278
/// An object to provide circuits for implementing onion services.
279
pub(crate) struct HsCircPoolInner<B: AbstractCircBuilder<R> + 'static, R: Runtime> {
280
    /// An underlying circuit manager, used for constructing circuits.
281
    circmgr: Arc<CircMgrInner<B, R>>,
282
    /// A task handle for making the background circuit launcher fire early.
283
    //
284
    // TODO: I think we may want to move this into the same Mutex as Pool
285
    // eventually.  But for now, this is fine, since it's just an implementation
286
    // detail.
287
    //
288
    // TODO MSRV TBD: If still relevant, see about replacing this usage of
289
    // [`once_cell::sync::OnceCell`] with [`std::sync::OnceLock`]. Waiting on
290
    // [`std::sync::OnceLock::get_or_try_init`] to stabilize and fall within our
291
    // MSRV. See [1] and [2] for more information.
292
    //
293
    // [1]: https://github.com/rust-lang/rust/issues/109737
294
    // [2]: https://doc.rust-lang.org/std/sync/struct.OnceLock.html#method.get_or_try_init
295
    launcher_handle: OnceCell<TaskHandle>,
296
    /// The mutable state of this pool.
297
    inner: Mutex<Inner<B::Circ>>,
298
}
299

            
300
/// The mutable state of an [`HsCircPool`]
301
struct Inner<C: AbstractCirc> {
302
    /// A collection of pre-constructed circuits.
303
    pool: pool::Pool<C>,
304
}
305

            
306
impl<R: Runtime> HsCircPoolInner<CircuitBuilder<R>, R> {
307
    /// Internal implementation for [`HsCircPool::new`].
308
16
    pub(crate) fn new(circmgr: &CircMgr<R>) -> Self {
309
16
        Self::new_internal(&circmgr.0)
310
16
    }
311
}
312

            
313
impl<B: AbstractCircBuilder<R> + 'static, R: Runtime> HsCircPoolInner<B, R> {
314
    /// Create a new [`HsCircPoolInner`] from a [`CircMgrInner`].
315
28
    pub(crate) fn new_internal(circmgr: &Arc<CircMgrInner<B, R>>) -> Self {
316
28
        let circmgr = Arc::clone(circmgr);
317
28
        let pool = pool::Pool::default();
318
28
        Self {
319
28
            circmgr,
320
28
            launcher_handle: OnceCell::new(),
321
28
            inner: Mutex::new(Inner { pool }),
322
28
        }
323
28
    }
324

            
325
    /// Internal implementation for [`HsCircPool::launch_background_tasks`].
326
8
    pub(crate) fn launch_background_tasks(
327
8
        self: &Arc<Self>,
328
8
        runtime: &R,
329
8
        netdir_provider: &Arc<dyn NetDirProvider + 'static>,
330
8
    ) -> Result<Vec<TaskHandle>> {
331
8
        let handle = self.launcher_handle.get_or_try_init(|| {
332
8
            runtime
333
8
                .spawn(remove_unusable_circuits(
334
8
                    Arc::downgrade(self),
335
8
                    Arc::downgrade(netdir_provider),
336
8
                ))
337
8
                .map_err(|e| Error::from_spawn("preemptive onion circuit expiration task", e))?;
338

            
339
8
            let (schedule, handle) = TaskSchedule::new(runtime.clone());
340
8
            runtime
341
8
                .spawn(launch_hs_circuits_as_needed(
342
8
                    Arc::downgrade(self),
343
8
                    Arc::downgrade(netdir_provider),
344
8
                    schedule,
345
8
                ))
346
8
                .map_err(|e| Error::from_spawn("preemptive onion circuit builder task", e))?;
347

            
348
8
            Result::<TaskHandle>::Ok(handle)
349
8
        })?;
350

            
351
8
        Ok(vec![handle.clone()])
352
8
    }
353

            
354
    /// Internal implementation for [`HsCircPool::get_or_launch_client_rend`].
355
    pub(crate) async fn get_or_launch_client_rend<'a>(
356
        &self,
357
        netdir: &'a NetDir,
358
    ) -> Result<(Arc<B::Circ>, Relay<'a>)> {
359
        // For rendezvous points, clients use 3-hop circuits.
360
        // Note that we aren't using any special rules for the last hop here; we
361
        // are relying on the fact that:
362
        //   * all suitable middle relays that we use in these circuit stems are
363
        //     suitable renedezvous points, and
364
        //   * the weighting rules for selecting rendezvous points are the same
365
        //     as those for selecting an arbitrary middle relay.
366
        let circ = self
367
            .take_or_launch_stem_circuit::<OwnedCircTarget>(netdir, None, HsCircStemKind::Guarded)
368
            .await?;
369

            
370
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
371
        if matches!(
372
            self.vanguard_mode(),
373
            VanguardMode::Full | VanguardMode::Lite
374
        ) && circ.kind != HsCircStemKind::Guarded
375
        {
376
            return Err(internal!("wanted a GUARDED circuit, but got NAIVE?!").into());
377
        }
378

            
379
        let path = circ.path_ref().map_err(|error| Error::Protocol {
380
            action: "launching a client rend circuit",
381
            peer: None, // Either party could be to blame.
382
            unique_id: Some(circ.unique_id()),
383
            error,
384
        })?;
385

            
386
        match path.hops().last() {
387
            Some(ent) => {
388
                let Some(ct) = ent.as_chan_target() else {
389
                    return Err(
390
                        internal!("HsPool gave us a circuit with a virtual last hop!?").into(),
391
                    );
392
                };
393
                match netdir.by_ids(ct) {
394
                    Some(relay) => Ok((circ.circ, relay)),
395
                    // This can't happen, since launch_hs_unmanaged() only takes relays from the netdir
396
                    // it is given, and circuit_compatible_with_target() ensures that
397
                    // every relay in the circuit is listed.
398
                    //
399
                    // TODO: Still, it's an ugly place in our API; maybe we should return the last hop
400
                    // from take_or_launch_stem_circuit()?  But in many cases it won't be needed...
401
                    None => Err(internal!("Got circuit with unknown last hop!?").into()),
402
                }
403
            }
404
            None => Err(internal!("Circuit with an empty path!?").into()),
405
        }
406
    }
407

            
408
    /// Internal implementation for [`HsCircPool::get_or_launch_specific`].
409
    pub(crate) async fn get_or_launch_specific<T>(
410
        &self,
411
        netdir: &NetDir,
412
        kind: HsCircKind,
413
        target: T,
414
    ) -> Result<Arc<B::Circ>>
415
    where
416
        T: CircTarget + std::marker::Sync,
417
    {
418
        if kind == HsCircKind::ClientRend {
419
            return Err(bad_api_usage!("get_or_launch_specific with ClientRend circuit!?").into());
420
        }
421

            
422
        let wanted_kind = kind.stem_kind();
423

            
424
        // For most* of these circuit types, we want to build our circuit with
425
        // an extra hop, since the target hop is under somebody else's control.
426
        //
427
        // * The exceptions are ClientRend, which we handle in a different
428
        //   method, and SvcIntro, where we will eventually  want an extra hop
429
        //   to avoid vanguard discovery attacks.
430

            
431
        // Get an unfinished circuit that's compatible with our target.
432
        let circ = self
433
            .take_or_launch_stem_circuit(netdir, Some(&target), wanted_kind)
434
            .await?;
435

            
436
        #[cfg(all(feature = "vanguards", feature = "hs-common"))]
437
        if matches!(
438
            self.vanguard_mode(),
439
            VanguardMode::Full | VanguardMode::Lite
440
        ) && circ.kind != wanted_kind
441
        {
442
            return Err(internal!(
443
                "take_or_launch_stem_circuit() returned {:?}, but we need {wanted_kind:?}",
444
                circ.kind
445
            )
446
            .into());
447
        }
448

            
449
        let params = onion_circparams_from_netparams(netdir.params())?;
450
        self.extend_circ(circ, params, target).await
451
    }
452

            
453
    /// Try to extend a circuit to the specified target hop.
454
    async fn extend_circ<T>(
455
        &self,
456
        circ: HsCircStem<B::Circ>,
457
        params: CircParameters,
458
        target: T,
459
    ) -> Result<Arc<B::Circ>>
460
    where
461
        T: CircTarget + std::marker::Sync,
462
    {
463
        let protocol_err = |error| Error::Protocol {
464
            action: "extending to chosen HS hop",
465
            peer: None, // Either party could be to blame.
466
            unique_id: Some(circ.unique_id()),
467
            error,
468
        };
469

            
470
        // Estimate how long it will take to extend it one more hop, and
471
        // construct a timeout as appropriate.
472
        let n_hops = circ.n_hops().map_err(protocol_err)?;
473
        let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
474
            &crate::timeouts::Action::ExtendCircuit {
475
                initial_length: n_hops,
476
                final_length: n_hops + 1,
477
            },
478
        );
479

            
480
        // Make a future to extend the circuit.
481
        let extend_future = circ.extend(&target, params).map_err(protocol_err);
482

            
483
        // Wait up to the timeout for the future to complete.
484
        self.circmgr
485
            .mgr
486
            .peek_runtime()
487
            .timeout(extend_timeout, extend_future)
488
            .await
489
            .map_err(|_| Error::CircTimeout(Some(circ.unique_id())))??;
490

            
491
        // With any luck, return the circuit.
492
        Ok(circ.circ)
493
    }
494

            
495
    /// Internal implementation for [`HsCircPool::retire_all_circuits`].
496
    pub(crate) fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
497
        self.inner
498
            .lock()
499
            .expect("poisoned lock")
500
            .pool
501
            .retire_all_circuits()?;
502

            
503
        Ok(())
504
    }
505

            
506
    /// Take and return a circuit from our pool suitable for being extended to `avoid_target`.
507
    ///
508
    /// If vanguards are enabled, this will try to build a circuit stem of the specified
509
    /// [`HsCircStemKind`].
510
    ///
511
    /// If vanguards are disabled, `kind` is unused.
512
    ///
513
    /// If there is no such circuit, build and return a new one.
514
    async fn take_or_launch_stem_circuit<T>(
515
        &self,
516
        netdir: &NetDir,
517
        avoid_target: Option<&T>,
518
        kind: HsCircStemKind,
519
    ) -> Result<HsCircStem<B::Circ>>
520
    where
521
        // TODO #504: It would be better if this were a type that had to include
522
        // family info.
523
        T: CircTarget + std::marker::Sync,
524
    {
525
        let vanguard_mode = self.vanguard_mode();
526
        trace!(
527
            vanguards=%vanguard_mode,
528
            kind=%kind,
529
            "selecting HS circuit stem"
530
        );
531

            
532
        // First, look for a circuit that is already built, if any is suitable.
533

            
534
        let target_exclusion = {
535
            let path_cfg = self.circmgr.builder().path_config();
536
            let cfg = path_cfg.relay_selection_config();
537
            match avoid_target {
538
                // TODO #504: This is an unaccompanied RelayExclusion, and is therefore a
539
                // bit suspect.  We should consider whether we like this behavior.
540
                Some(ct) => RelayExclusion::exclude_channel_target_family(&cfg, ct, netdir),
541
                None => RelayExclusion::no_relays_excluded(),
542
            }
543
        };
544

            
545
        let found_usable_circ = {
546
            let mut inner = self.inner.lock().expect("lock poisoned");
547

            
548
            let restrictions = |circ: &HsCircStem<B::Circ>| {
549
                // If vanguards are enabled, we no longer apply same-family or same-subnet
550
                // restrictions, and we allow the guard to appear as either of the last
551
                // two hope of the circuit.
552
                match vanguard_mode {
553
                    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
554
                    VanguardMode::Lite | VanguardMode::Full => {
555
                        vanguards_circuit_compatible_with_target(netdir, circ, kind, avoid_target)
556
                    }
557
                    VanguardMode::Disabled => {
558
                        circuit_compatible_with_target(netdir, circ, &target_exclusion)
559
                    }
560
                    _ => {
561
                        warn!("unknown vanguard mode {vanguard_mode}");
562
                        false
563
                    }
564
                }
565
            };
566

            
567
            let mut prefs = HsCircPrefs::default();
568

            
569
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
570
            if matches!(vanguard_mode, VanguardMode::Full | VanguardMode::Lite) {
571
                prefs.preferred_stem_kind(kind);
572
            }
573

            
574
            let found_usable_circ =
575
                inner
576
                    .pool
577
                    .take_one_where(&mut rand::rng(), restrictions, &prefs);
578

            
579
            // Tell the background task to fire immediately if we have very few circuits
580
            // circuits left, or if we found nothing.
581
            if inner.pool.very_low() || found_usable_circ.is_none() {
582
                let handle = self.launcher_handle.get().ok_or_else(|| {
583
                    Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
584
                })?;
585
                handle.fire();
586
            }
587
            found_usable_circ
588
        };
589
        // Return the circuit we found before, if any.
590
        if let Some(circuit) = found_usable_circ {
591
            let circuit = self
592
                .maybe_extend_stem_circuit(netdir, circuit, avoid_target, kind)
593
                .await?;
594
            self.ensure_suitable_circuit(&circuit, avoid_target, kind)?;
595
            return Ok(circuit);
596
        }
597

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

            
603
        // TODO: We could in launch multiple circuits in parallel here?
604
        let circ = self
605
            .circmgr
606
            .launch_hs_unmanaged(avoid_target, netdir, kind)
607
            .await?;
608

            
609
        self.ensure_suitable_circuit(&circ, avoid_target, kind)?;
610

            
611
        Ok(HsCircStem { circ, kind })
612
    }
613

            
614
    /// Return a circuit of the specified `kind`, built from `circuit`.
615
    async fn maybe_extend_stem_circuit<T>(
616
        &self,
617
        netdir: &NetDir,
618
        circuit: HsCircStem<B::Circ>,
619
        avoid_target: Option<&T>,
620
        kind: HsCircStemKind,
621
    ) -> Result<HsCircStem<B::Circ>>
622
    where
623
        T: CircTarget + std::marker::Sync,
624
    {
625
        match self.vanguard_mode() {
626
            #[cfg(all(feature = "vanguards", feature = "hs-common"))]
627
            VanguardMode::Full => {
628
                // NAIVE circuit stems need to be extended by one hop to become GUARDED stems
629
                // if we're using full vanguards.
630
                self.extend_full_vanguards_circuit(netdir, circuit, avoid_target, kind)
631
                    .await
632
            }
633
            _ => {
634
                let HsCircStem { circ, kind: _ } = circuit;
635

            
636
                Ok(HsCircStem { circ, kind })
637
            }
638
        }
639
    }
640

            
641
    /// Extend the specified full vanguard circuit if necessary.
642
    #[cfg(all(feature = "vanguards", feature = "hs-common"))]
643
    async fn extend_full_vanguards_circuit<T>(
644
        &self,
645
        netdir: &NetDir,
646
        circuit: HsCircStem<B::Circ>,
647
        avoid_target: Option<&T>,
648
        kind: HsCircStemKind,
649
    ) -> Result<HsCircStem<B::Circ>>
650
    where
651
        T: CircTarget + std::marker::Sync,
652
    {
653
        match (circuit.kind, kind) {
654
            (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
655
                debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
656
                let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
657
                let circ_path = circuit.circ.path_ref().map_err(|error| Error::Protocol {
658
                    action: "extending full vanguards circuit",
659
                    peer: None, // Either party could be to blame.
660
                    unique_id: Some(circuit.unique_id()),
661
                    error,
662
                })?;
663

            
664
                // A NAIVE circuit is a 3-hop circuit.
665
                debug_assert_eq!(circ_path.hops().len(), 3);
666

            
667
                let target_exclusion = if let Some(target) = &avoid_target {
668
                    RelayExclusion::exclude_identities(
669
                        target.identities().map(|id| id.to_owned()).collect(),
670
                    )
671
                } else {
672
                    RelayExclusion::no_relays_excluded()
673
                };
674
                let hops = circ_path
675
                    .iter()
676
                    .flat_map(|hop| hop.as_chan_target())
677
                    .map(IntoOwnedChanTarget::to_owned)
678
                    .collect::<Vec<OwnedChanTarget>>();
679
                let extra_hop = select_middle_for_vanguard_circ(
680
                    &hops,
681
                    netdir,
682
                    &target_exclusion,
683
                    &mut rand::rng(),
684
                )?;
685

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

            
690
                Ok(HsCircStem { circ, kind })
691
            }
692
            (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
693
                Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
694
            }
695
            _ => {
696
                trace!("Wanted {kind} circuit, got {}", circuit.kind);
697
                // Nothing to do: the circuit stem we got is of the kind we wanted
698
                Ok(circuit)
699
            }
700
        }
701
    }
702

            
703
    /// Ensure `circ` is compatible with `target`, and has the correct length for its `kind`.
704
    fn ensure_suitable_circuit<T>(
705
        &self,
706
        circ: &Arc<B::Circ>,
707
        target: Option<&T>,
708
        kind: HsCircStemKind,
709
    ) -> Result<()>
710
    where
711
        T: CircTarget + std::marker::Sync,
712
    {
713
        Self::ensure_circuit_compatible_with_target(circ, target)?;
714
        self.ensure_circuit_length_valid(circ, kind)?;
715

            
716
        Ok(())
717
    }
718

            
719
    /// Ensure the specified circuit of type `kind` has the right length.
720
    fn ensure_circuit_length_valid(&self, circ: &Arc<B::Circ>, kind: HsCircStemKind) -> Result<()> {
721
        let circ_path_len = circ.n_hops().map_err(|error| Error::Protocol {
722
            action: "validating circuit length",
723
            peer: None, // Either party could be to blame.
724
            unique_id: Some(circ.unique_id()),
725
            error,
726
        })?;
727

            
728
        let mode = self.vanguard_mode();
729

            
730
        // TODO(#1457): somehow unify the path length checks
731
        let expected_len = kind.num_hops(mode)?;
732

            
733
        if circ_path_len != expected_len {
734
            return Err(internal!(
735
                "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
736
                kind,
737
                expected_len,
738
                circ_path_len
739
            )
740
            .into());
741
        }
742

            
743
        Ok(())
744
    }
745

            
746
    /// Ensure `circ` is compatible with `target`.
747
    ///
748
    /// Returns an error if either of the last 2 hops of the circuit are the same as `target`,
749
    /// because:
750
    ///   * a relay won't let you extend the circuit to itself
751
    ///   * relays won't let you extend the circuit to their previous hop
752
    fn ensure_circuit_compatible_with_target<T>(
753
        circ: &Arc<B::Circ>,
754
        target: Option<&T>,
755
    ) -> Result<()>
756
    where
757
        T: CircTarget + std::marker::Sync,
758
    {
759
        if let Some(target) = target {
760
            let take_n = 2;
761
            if let Some(hop) = circ
762
                .path_ref()
763
                .map_err(|error| Error::Protocol {
764
                    action: "validating circuit compatibility with target",
765
                    peer: None, // Either party could be to blame.
766
                    unique_id: Some(circ.unique_id()),
767
                    error,
768
                })?
769
                .hops()
770
                .iter()
771
                .rev()
772
                .take(take_n)
773
                .flat_map(|hop| hop.as_chan_target())
774
                .find(|hop| hop.has_any_relay_id_from(target))
775
            {
776
                return Err(internal!(
777
                    "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
778
                    target.display_relay_ids(),
779
                    hop.display_relay_ids()
780
                ).into());
781
            }
782
        }
783

            
784
        Ok(())
785
    }
786

            
787
    /// Internal: Remove every closed circuit from this pool.
788
8
    fn remove_closed(&self) {
789
8
        let mut inner = self.inner.lock().expect("lock poisoned");
790
8
        inner.pool.retain(|circ| !circ.is_closing());
791
8
    }
792

            
793
    /// Internal: Remove every circuit form this pool for which any relay is not
794
    /// listed in `netdir`.
795
    fn remove_unlisted(&self, netdir: &NetDir) {
796
        let mut inner = self.inner.lock().expect("lock poisoned");
797
        inner
798
            .pool
799
            .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true));
800
    }
801

            
802
    /// Returns the current [`VanguardMode`].
803
12
    fn vanguard_mode(&self) -> VanguardMode {
804
12
        cfg_if::cfg_if! {
805
12
            if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
806
12
                self
807
12
                    .circmgr
808
12
                    .mgr
809
12
                    .peek_builder()
810
12
                    .vanguardmgr()
811
12
                    .mode()
812
12
            } else {
813
12
                VanguardMode::Disabled
814
12
            }
815
12
        }
816
12
    }
817

            
818
    /// Internal implementation for [`HsCircPool::estimate_timeout`].
819
    pub(crate) fn estimate_timeout(
820
        &self,
821
        timeout_action: &timeouts::Action,
822
    ) -> std::time::Duration {
823
        self.circmgr.estimate_timeout(timeout_action)
824
    }
825
}
826

            
827
/// Return true if we can extend a pre-built circuit `circ` to `target`.
828
///
829
/// We require that the circuit is open, that every hop  in the circuit is
830
/// listed in `netdir`, and that no hop in the circuit shares a family with
831
/// `target`.
832
fn circuit_compatible_with_target<C: AbstractCirc>(
833
    netdir: &NetDir,
834
    circ: &HsCircStem<C>,
835
    exclude_target: &RelayExclusion,
836
) -> bool {
837
    // NOTE, TODO #504:
838
    // This uses a RelayExclusion directly, when we would be better off
839
    // using a RelaySelector to make sure that we had checked every relevant
840
    // property.
841
    //
842
    // The behavior is okay, since we already checked all the properties of the
843
    // circuit's relays when we first constructed the circuit.  Still, it would
844
    // be better to use refactor and a RelaySelector instead.
845
    circuit_still_useable(netdir, circ, |relay| {
846
        exclude_target.low_level_predicate_permits_relay(relay)
847
    })
848
}
849

            
850
/// Return true if we can extend a pre-built vanguards circuit `circ` to `target`.
851
///
852
/// We require that the circuit is open, that it can become the specified
853
/// kind of [`HsCircStem`], that every hop in the circuit is listed in `netdir`,
854
/// and that the last two hops are different from the specified target.
855
fn vanguards_circuit_compatible_with_target<C: AbstractCirc, T>(
856
    netdir: &NetDir,
857
    circ: &HsCircStem<C>,
858
    kind: HsCircStemKind,
859
    avoid_target: Option<&T>,
860
) -> bool
861
where
862
    T: CircTarget + std::marker::Sync,
863
{
864
    if let Some(target) = avoid_target {
865
        let Ok(circ_path) = circ.circ.path_ref() else {
866
            // Circuit is unusable, so we can't use it.
867
            return false;
868
        };
869
        // The last 2 hops of the circuit must be different from the circuit target, because:
870
        //   * a relay won't let you extend the circuit to itself
871
        //   * relays won't let you extend the circuit to their previous hop
872
        let take_n = 2;
873
        if circ_path
874
            .hops()
875
            .iter()
876
            .rev()
877
            .take(take_n)
878
            .flat_map(|hop| hop.as_chan_target())
879
            .any(|hop| hop.has_any_relay_id_from(target))
880
        {
881
            return false;
882
        }
883
    }
884

            
885
    circ.can_become(kind) && circuit_still_useable(netdir, circ, |_relay| true)
886
}
887

            
888
/// Return true if we can still use a given pre-build circuit.
889
///
890
/// We require that the circuit is open, that every hop  in the circuit is
891
/// listed in `netdir`, and that `relay_okay` returns true for every hop on the
892
/// circuit.
893
fn circuit_still_useable<C, F>(netdir: &NetDir, circ: &HsCircStem<C>, relay_okay: F) -> bool
894
where
895
    C: AbstractCirc,
896
    F: Fn(&Relay<'_>) -> bool,
897
{
898
    let circ = &circ.circ;
899
    if circ.is_closing() {
900
        return false;
901
    }
902

            
903
    let Ok(path) = circ.path_ref() else {
904
        // Circuit is unusable, so we can't use it.
905
        return false;
906
    };
907
    // (We have to use a binding here to appease borrowck.)
908
    let all_compatible = path.iter().all(|ent: &circuit::PathEntry| {
909
        let Some(c) = ent.as_chan_target() else {
910
            // This is a virtual hop; it's necessarily compatible with everything.
911
            return true;
912
        };
913
        let Some(relay) = netdir.by_ids(c) else {
914
            // We require that every relay in this circuit is still listed; an
915
            // unlisted relay means "reject".
916
            return false;
917
        };
918
        // Now it's all down to the predicate.
919
        relay_okay(&relay)
920
    });
921
    all_compatible
922
}
923

            
924
/// Background task to launch onion circuits as needed.
925
8
async fn launch_hs_circuits_as_needed<B: AbstractCircBuilder<R> + 'static, R: Runtime>(
926
8
    pool: Weak<HsCircPoolInner<B, R>>,
927
8
    netdir_provider: Weak<dyn NetDirProvider + 'static>,
928
8
    mut schedule: TaskSchedule<R>,
929
8
) {
930
    /// Default delay when not told to fire explicitly. Chosen arbitrarily.
931
    const DELAY: Duration = Duration::from_secs(30);
932

            
933
16
    while schedule.next().await.is_some() {
934
8
        let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
935
8
            (Some(x), Some(y)) => (x, y),
936
            _ => {
937
                break;
938
            }
939
        };
940
8
        let now = pool.circmgr.mgr.peek_runtime().now();
941
8
        pool.remove_closed();
942
8
        let mut circs_to_launch = {
943
8
            let mut inner = pool.inner.lock().expect("poisioned_lock");
944
8
            inner.pool.update_target_size(now);
945
8
            inner.pool.circs_to_launch()
946
8
        };
947
8
        let n_to_launch = circs_to_launch.n_to_launch();
948
8
        let mut max_attempts = n_to_launch * 2;
949
8

            
950
8
        if n_to_launch > 0 {
951
8
            debug!(
952
                "launching {} NAIVE  and {} GUARDED circuits",
953
                circs_to_launch.stem(),
954
                circs_to_launch.guarded_stem()
955
            );
956
        }
957

            
958
        // TODO: refactor this to launch the circuits in parallel
959
8
        'inner: while circs_to_launch.n_to_launch() > 0 {
960
8
            max_attempts -= 1;
961
8
            if max_attempts == 0 {
962
                // We want to avoid retrying over and over in a tight loop if all our attempts
963
                // are failing.
964
                warn!("Too many preemptive onion service circuits failed; waiting a while.");
965
                break 'inner;
966
8
            }
967
8
            if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
968
                // We want to launch a circuit, and we have a netdir that we can use
969
                // to launch it.
970
                //
971
                // TODO: Possibly we should be doing this in a background task, and
972
                // launching several of these in parallel.  If we do, we should think about
973
                // whether taking the fastest will expose us to any attacks.
974
                let no_target: Option<&OwnedCircTarget> = None;
975
                let for_launch = circs_to_launch.for_launch();
976

            
977
                // TODO HS: We should catch panics, here or in launch_hs_unmanaged.
978
                match pool
979
                    .circmgr
980
                    .launch_hs_unmanaged(no_target, &netdir, for_launch.kind())
981
                    .await
982
                {
983
                    Ok(circ) => {
984
                        let kind = for_launch.kind();
985
                        let circ = HsCircStem { circ, kind };
986
                        pool.inner.lock().expect("poisoned lock").pool.insert(circ);
987
                        trace!("successfully launched {kind} circuit");
988
                        for_launch.note_circ_launched();
989
                    }
990
                    Err(err) => {
991
                        debug_report!(err, "Unable to build preemptive circuit for onion services");
992
                    }
993
                }
994
            } else {
995
                // We'd like to launch a circuit, but we don't have a netdir that we
996
                // can use.
997
                //
998
                // TODO HS possibly instead of a fixed delay we want to wait for more
999
                // netdir info?
8
                break 'inner;
            }
        }
        // We have nothing to launch now, so we'll try after a while.
8
        schedule.fire_in(DELAY);
    }
}
/// Background task to remove unusable circuits whenever the directory changes.
8
async fn remove_unusable_circuits<B: AbstractCircBuilder<R> + 'static, R: Runtime>(
8
    pool: Weak<HsCircPoolInner<B, R>>,
8
    netdir_provider: Weak<dyn NetDirProvider + 'static>,
8
) {
8
    let mut event_stream = match netdir_provider.upgrade() {
8
        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.
8
    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::CircuitBuilder<R>, R>> {
        let chanmgr = tor_chanmgr::ChanMgr::new(
            runtime.clone(),
            &Default::default(),
            tor_chanmgr::Dormancy::Dormant,
            &Default::default(),
            ToplevelAccount::new_noop(),
        );
        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);
            }
        });
    }
}