1mod 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#[cfg(feature = "hs-common")]
51#[derive(Debug, Clone, Copy, Eq, PartialEq)]
52#[non_exhaustive]
53pub enum HsCircKind {
54 SvcHsDir,
56 SvcIntro,
58 SvcRend,
60 ClientHsDir,
62 ClientIntro,
64 ClientRend,
66}
67
68impl HsCircKind {
69 fn stem_kind(&self) -> HsCircStemKind {
71 match self {
72 HsCircKind::SvcIntro => HsCircStemKind::Naive,
73 HsCircKind::SvcHsDir => {
74 HsCircStemKind::Naive
76 }
77 HsCircKind::ClientRend => {
78 HsCircStemKind::Guarded
85 }
86 HsCircKind::SvcRend | HsCircKind::ClientHsDir | HsCircKind::ClientIntro => {
87 HsCircStemKind::Guarded
88 }
89 }
90 }
91}
92
93pub(crate) struct HsCircStem<C: AbstractCirc> {
99 pub(crate) circ: Arc<C>,
101 pub(crate) kind: HsCircStemKind,
103}
104
105impl<C: AbstractCirc> HsCircStem<C> {
106 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 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#[derive(Copy, Clone, Debug, PartialEq, derive_more::Display)]
169#[non_exhaustive]
170pub(crate) enum HsCircStemKind {
171 #[display("NAIVE")]
176 Naive,
177 #[display("GUARDED")]
182 Guarded,
183}
184
185impl HsCircStemKind {
186 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
209pub struct HsCircPool<R: Runtime>(Arc<HsCircPoolInner<CircuitBuilder<R>, R>>);
211
212impl<R: Runtime> HsCircPool<R> {
213 pub fn new(circmgr: &Arc<CircMgr<R>>) -> Self {
217 Self(Arc::new(HsCircPoolInner::new(circmgr)))
218 }
219
220 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 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 pub fn estimate_timeout(&self, timeout_action: &timeouts::Action) -> std::time::Duration {
264 self.0.estimate_timeout(timeout_action)
265 }
266
267 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 pub fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
284 self.0.retire_all_circuits()
285 }
286}
287
288pub(crate) struct HsCircPoolInner<B: AbstractCircBuilder<R> + 'static, R: Runtime> {
290 circmgr: Arc<CircMgrInner<B, R>>,
292 launcher_handle: OnceCell<TaskHandle>,
306 inner: Mutex<Inner<B::Circ>>,
308}
309
310struct Inner<C: AbstractCirc> {
312 pool: pool::Pool<C>,
314}
315
316impl<R: Runtime> HsCircPoolInner<CircuitBuilder<R>, R> {
317 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 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 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 pub(crate) async fn get_or_launch_client_rend<'a>(
366 &self,
367 netdir: &'a NetDir,
368 ) -> Result<(Arc<B::Circ>, Relay<'a>)> {
369 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, 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 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 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 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 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 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, unique_id: Some(circ.unique_id()),
488 error,
489 };
490
491 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 let extend_future = circ.extend(&target, params).map_err(protocol_err);
503
504 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 Ok(circ.circ)
514 }
515
516 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 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 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 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 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 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 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 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 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 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 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 #[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, unique_id: Some(circuit.unique_id()),
706 error,
707 })?;
708
709 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 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 Ok(circuit)
748 }
749 }
750 }
751
752 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 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, unique_id: Some(circ.unique_id()),
774 error,
775 })?;
776
777 let mode = self.vanguard_mode();
778
779 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 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, 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 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 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 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 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
873fn 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 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
902fn 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 return false;
921 };
922 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 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
950fn 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 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 let all_compatible = path.iter().all(|ent: &circuit::PathEntry| {
990 match relay_for_path_ent(netdir, ent) {
991 Err(NoRelayForPathEnt::HopWasVirtual) => {
992 true
994 }
995 Err(NoRelayForPathEnt::NoSuchRelay) => {
996 false
999 }
1000 Ok(r) => {
1001 relay_okay(&r)
1003 }
1004 }
1005 });
1006 all_compatible
1007}
1008
1009#[derive(Clone, Debug)]
1013enum NoRelayForPathEnt {
1014 HopWasVirtual,
1016 NoSuchRelay,
1018}
1019
1020fn 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#[allow(clippy::cognitive_complexity)] async 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 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 'inner: while circs_to_launch.n_to_launch() > 0 {
1071 max_attempts -= 1;
1072 if max_attempts == 0 {
1073 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 let no_target: Option<&OwnedCircTarget> = None;
1086 let for_launch = circs_to_launch.for_launch();
1087
1088 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 break 'inner;
1112 }
1113 }
1114
1115 schedule.fire_in(DELAY);
1117 }
1118}
1119
1120async 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 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 #![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 #![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 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 #[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}