tor_circmgr/
hspool.rs

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