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 params = onion_circparams_from_netparams(netdir.params())?;
460 self.extend_circ(circ, params, target).await
461 }
462
463 async fn extend_circ<T>(
465 &self,
466 circ: HsCircStem<B::Circ>,
467 params: CircParameters,
468 target: T,
469 ) -> Result<Arc<B::Circ>>
470 where
471 T: CircTarget + std::marker::Sync,
472 {
473 let protocol_err = |error| Error::Protocol {
474 action: "extending to chosen HS hop",
475 peer: None, unique_id: Some(circ.unique_id()),
477 error,
478 };
479
480 let n_hops = circ.n_hops().map_err(protocol_err)?;
483 let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
484 &crate::timeouts::Action::ExtendCircuit {
485 initial_length: n_hops,
486 final_length: n_hops + 1,
487 },
488 );
489
490 let extend_future = circ.extend(&target, params).map_err(protocol_err);
492
493 self.circmgr
495 .mgr
496 .peek_runtime()
497 .timeout(extend_timeout, extend_future)
498 .await
499 .map_err(|_| Error::CircTimeout(Some(circ.unique_id())))??;
500
501 Ok(circ.circ)
503 }
504
505 pub(crate) fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
507 self.inner
508 .lock()
509 .expect("poisoned lock")
510 .pool
511 .retire_all_circuits()?;
512
513 Ok(())
514 }
515
516 async fn take_or_launch_stem_circuit<T>(
525 &self,
526 netdir: &NetDir,
527 avoid_target: Option<&T>,
528 kind: HsCircKind,
529 ) -> Result<HsCircStem<B::Circ>>
530 where
531 T: CircTarget + std::marker::Sync,
534 {
535 let stem_kind = kind.stem_kind();
536 let vanguard_mode = self.vanguard_mode();
537 trace!(
538 vanguards=%vanguard_mode,
539 kind=%stem_kind,
540 "selecting HS circuit stem"
541 );
542
543 let target_exclusion = {
546 let path_cfg = self.circmgr.builder().path_config();
547 let cfg = path_cfg.relay_selection_config();
548 match avoid_target {
549 Some(ct) => RelayExclusion::exclude_channel_target_family(&cfg, ct, netdir),
552 None => RelayExclusion::no_relays_excluded(),
553 }
554 };
555
556 let found_usable_circ = {
557 let mut inner = self.inner.lock().expect("lock poisoned");
558
559 let restrictions = |circ: &HsCircStem<B::Circ>| {
560 match vanguard_mode {
564 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
565 VanguardMode::Lite | VanguardMode::Full => {
566 vanguards_circuit_compatible_with_target(
567 netdir,
568 circ,
569 stem_kind,
570 kind,
571 avoid_target,
572 )
573 }
574 VanguardMode::Disabled => {
575 circuit_compatible_with_target(netdir, circ, kind, &target_exclusion)
576 }
577 _ => {
578 warn!("unknown vanguard mode {vanguard_mode}");
579 false
580 }
581 }
582 };
583
584 let mut prefs = HsCircPrefs::default();
585
586 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
587 if matches!(vanguard_mode, VanguardMode::Full | VanguardMode::Lite) {
588 prefs.preferred_stem_kind(stem_kind);
589 }
590
591 let found_usable_circ =
592 inner
593 .pool
594 .take_one_where(&mut rand::rng(), restrictions, &prefs);
595
596 if inner.pool.very_low() || found_usable_circ.is_none() {
599 let handle = self.launcher_handle.get().ok_or_else(|| {
600 Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
601 })?;
602 handle.fire();
603 }
604 found_usable_circ
605 };
606 if let Some(circuit) = found_usable_circ {
608 let circuit = self
609 .maybe_extend_stem_circuit(netdir, circuit, avoid_target, stem_kind, kind)
610 .await?;
611 self.ensure_suitable_circuit(&circuit, avoid_target, stem_kind)?;
612 return Ok(circuit);
613 }
614
615 let circ = self
622 .circmgr
623 .launch_hs_unmanaged(avoid_target, netdir, stem_kind, Some(kind))
624 .await?;
625
626 self.ensure_suitable_circuit(&circ, avoid_target, stem_kind)?;
627
628 Ok(HsCircStem {
629 circ,
630 kind: stem_kind,
631 })
632 }
633
634 async fn maybe_extend_stem_circuit<T>(
636 &self,
637 netdir: &NetDir,
638 circuit: HsCircStem<B::Circ>,
639 avoid_target: Option<&T>,
640 stem_kind: HsCircStemKind,
641 circ_kind: HsCircKind,
642 ) -> Result<HsCircStem<B::Circ>>
643 where
644 T: CircTarget + std::marker::Sync,
645 {
646 match self.vanguard_mode() {
647 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
648 VanguardMode::Full => {
649 self.extend_full_vanguards_circuit(
652 netdir,
653 circuit,
654 avoid_target,
655 stem_kind,
656 circ_kind,
657 )
658 .await
659 }
660 _ => {
661 let HsCircStem { circ, kind: _ } = circuit;
662
663 Ok(HsCircStem {
664 circ,
665 kind: stem_kind,
666 })
667 }
668 }
669 }
670
671 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
673 async fn extend_full_vanguards_circuit<T>(
674 &self,
675 netdir: &NetDir,
676 circuit: HsCircStem<B::Circ>,
677 avoid_target: Option<&T>,
678 stem_kind: HsCircStemKind,
679 circ_kind: HsCircKind,
680 ) -> Result<HsCircStem<B::Circ>>
681 where
682 T: CircTarget + std::marker::Sync,
683 {
684 use crate::path::hspath::hs_stem_terminal_hop_usage;
685 use tor_relay_selection::RelaySelector;
686
687 match (circuit.kind, stem_kind) {
688 (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
689 debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
690 let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
691 let circ_path = circuit.circ.path_ref().map_err(|error| Error::Protocol {
692 action: "extending full vanguards circuit",
693 peer: None, unique_id: Some(circuit.unique_id()),
695 error,
696 })?;
697
698 debug_assert_eq!(circ_path.hops().len(), 3);
700
701 let target_exclusion = if let Some(target) = &avoid_target {
702 RelayExclusion::exclude_identities(
703 target.identities().map(|id| id.to_owned()).collect(),
704 )
705 } else {
706 RelayExclusion::no_relays_excluded()
707 };
708 let selector = RelaySelector::new(
709 hs_stem_terminal_hop_usage(Some(circ_kind)),
710 target_exclusion,
711 );
712 let hops = circ_path
713 .iter()
714 .flat_map(|hop| hop.as_chan_target())
715 .map(IntoOwnedChanTarget::to_owned)
716 .collect::<Vec<OwnedChanTarget>>();
717
718 let extra_hop =
719 select_middle_for_vanguard_circ(&hops, netdir, &selector, &mut rand::rng())?;
720
721 let circ = self.extend_circ(circuit, params, extra_hop).await?;
724
725 Ok(HsCircStem {
726 circ,
727 kind: stem_kind,
728 })
729 }
730 (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
731 Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
732 }
733 _ => {
734 trace!("Wanted {stem_kind} circuit, got {}", circuit.kind);
735 Ok(circuit)
737 }
738 }
739 }
740
741 fn ensure_suitable_circuit<T>(
743 &self,
744 circ: &Arc<B::Circ>,
745 target: Option<&T>,
746 kind: HsCircStemKind,
747 ) -> Result<()>
748 where
749 T: CircTarget + std::marker::Sync,
750 {
751 Self::ensure_circuit_can_extend_to_target(circ, target)?;
752 self.ensure_circuit_length_valid(circ, kind)?;
753
754 Ok(())
755 }
756
757 fn ensure_circuit_length_valid(&self, circ: &Arc<B::Circ>, kind: HsCircStemKind) -> Result<()> {
759 let circ_path_len = circ.n_hops().map_err(|error| Error::Protocol {
760 action: "validating circuit length",
761 peer: None, unique_id: Some(circ.unique_id()),
763 error,
764 })?;
765
766 let mode = self.vanguard_mode();
767
768 let expected_len = kind.num_hops(mode)?;
770
771 if circ_path_len != expected_len {
772 return Err(internal!(
773 "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
774 kind,
775 expected_len,
776 circ_path_len
777 )
778 .into());
779 }
780
781 Ok(())
782 }
783
784 fn ensure_circuit_can_extend_to_target<T>(circ: &Arc<B::Circ>, target: Option<&T>) -> Result<()>
791 where
792 T: CircTarget + std::marker::Sync,
793 {
794 if let Some(target) = target {
795 let take_n = 2;
796 if let Some(hop) = circ
797 .path_ref()
798 .map_err(|error| Error::Protocol {
799 action: "validating circuit compatibility with target",
800 peer: None, unique_id: Some(circ.unique_id()),
802 error,
803 })?
804 .hops()
805 .iter()
806 .rev()
807 .take(take_n)
808 .flat_map(|hop| hop.as_chan_target())
809 .find(|hop| hop.has_any_relay_id_from(target))
810 {
811 return Err(internal!(
812 "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
813 target.display_relay_ids(),
814 hop.display_relay_ids()
815 ).into());
816 }
817 }
818
819 Ok(())
820 }
821
822 fn remove_closed(&self) {
824 let mut inner = self.inner.lock().expect("lock poisoned");
825 inner.pool.retain(|circ| !circ.is_closing());
826 }
827
828 fn remove_unlisted(&self, netdir: &NetDir) {
831 let mut inner = self.inner.lock().expect("lock poisoned");
832 inner
833 .pool
834 .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true, |_last_hop| true));
835 }
836
837 fn vanguard_mode(&self) -> VanguardMode {
839 cfg_if::cfg_if! {
840 if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
841 self
842 .circmgr
843 .mgr
844 .peek_builder()
845 .vanguardmgr()
846 .mode()
847 } else {
848 VanguardMode::Disabled
849 }
850 }
851 }
852
853 pub(crate) fn estimate_timeout(
855 &self,
856 timeout_action: &timeouts::Action,
857 ) -> std::time::Duration {
858 self.circmgr.estimate_timeout(timeout_action)
859 }
860}
861
862fn circuit_compatible_with_target<C: AbstractCirc>(
868 netdir: &NetDir,
869 circ: &HsCircStem<C>,
870 circ_kind: HsCircKind,
871 exclude_target: &RelayExclusion,
872) -> bool {
873 let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
874
875 circuit_still_useable(
884 netdir,
885 circ,
886 |relay| exclude_target.low_level_predicate_permits_relay(relay),
887 |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
888 )
889}
890
891fn vanguards_circuit_compatible_with_target<C: AbstractCirc, T>(
897 netdir: &NetDir,
898 circ: &HsCircStem<C>,
899 kind: HsCircStemKind,
900 circ_kind: HsCircKind,
901 avoid_target: Option<&T>,
902) -> bool
903where
904 T: CircTarget + std::marker::Sync,
905{
906 if let Some(target) = avoid_target {
907 let Ok(circ_path) = circ.circ.path_ref() else {
908 return false;
910 };
911 let take_n = 2;
915 if circ_path
916 .hops()
917 .iter()
918 .rev()
919 .take(take_n)
920 .flat_map(|hop| hop.as_chan_target())
921 .any(|hop| hop.has_any_relay_id_from(target))
922 {
923 return false;
924 }
925 }
926
927 let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
929
930 circ.can_become(kind)
931 && circuit_still_useable(
932 netdir,
933 circ,
934 |_relay| true,
935 |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
936 )
937}
938
939fn circuit_still_useable<C, F1, F2>(
945 netdir: &NetDir,
946 circ: &HsCircStem<C>,
947 relay_okay: F1,
948 last_hop_ok: F2,
949) -> bool
950where
951 C: AbstractCirc,
952 F1: Fn(&Relay<'_>) -> bool,
953 F2: Fn(&Relay<'_>) -> bool,
954{
955 let circ = &circ.circ;
956 if circ.is_closing() {
957 return false;
958 }
959
960 let Ok(path) = circ.path_ref() else {
961 return false;
963 };
964 let last_hop = path.hops().last().expect("No hops in circuit?!");
965 match relay_for_path_ent(netdir, last_hop) {
966 Err(NoRelayForPathEnt::HopWasVirtual) => {}
967 Err(NoRelayForPathEnt::NoSuchRelay) => {
968 return false;
969 }
970 Ok(r) => {
971 if !last_hop_ok(&r) {
972 return false;
973 }
974 }
975 };
976
977 let all_compatible = path.iter().all(|ent: &circuit::PathEntry| {
979 match relay_for_path_ent(netdir, ent) {
980 Err(NoRelayForPathEnt::HopWasVirtual) => {
981 true
983 }
984 Err(NoRelayForPathEnt::NoSuchRelay) => {
985 false
988 }
989 Ok(r) => {
990 relay_okay(&r)
992 }
993 }
994 });
995 all_compatible
996}
997
998#[derive(Clone, Debug)]
1002enum NoRelayForPathEnt {
1003 HopWasVirtual,
1005 NoSuchRelay,
1007}
1008
1009fn relay_for_path_ent<'a>(
1011 netdir: &'a NetDir,
1012 ent: &circuit::PathEntry,
1013) -> StdResult<Relay<'a>, NoRelayForPathEnt> {
1014 let Some(c) = ent.as_chan_target() else {
1015 return Err(NoRelayForPathEnt::HopWasVirtual);
1016 };
1017 let Some(relay) = netdir.by_ids(c) else {
1018 return Err(NoRelayForPathEnt::NoSuchRelay);
1019 };
1020 Ok(relay)
1021}
1022
1023#[allow(clippy::cognitive_complexity)] async fn launch_hs_circuits_as_needed<B: AbstractCircBuilder<R> + 'static, R: Runtime>(
1026 pool: Weak<HsCircPoolInner<B, R>>,
1027 netdir_provider: Weak<dyn NetDirProvider + 'static>,
1028 mut schedule: TaskSchedule<R>,
1029) {
1030 const DELAY: Duration = Duration::from_secs(30);
1032
1033 while schedule.next().await.is_some() {
1034 let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1035 (Some(x), Some(y)) => (x, y),
1036 _ => {
1037 break;
1038 }
1039 };
1040 let now = pool.circmgr.mgr.peek_runtime().now();
1041 pool.remove_closed();
1042 let mut circs_to_launch = {
1043 let mut inner = pool.inner.lock().expect("poisioned_lock");
1044 inner.pool.update_target_size(now);
1045 inner.pool.circs_to_launch()
1046 };
1047 let n_to_launch = circs_to_launch.n_to_launch();
1048 let mut max_attempts = n_to_launch * 2;
1049
1050 if n_to_launch > 0 {
1051 debug!(
1052 "launching {} NAIVE and {} GUARDED circuits",
1053 circs_to_launch.stem(),
1054 circs_to_launch.guarded_stem()
1055 );
1056 }
1057
1058 'inner: while circs_to_launch.n_to_launch() > 0 {
1060 max_attempts -= 1;
1061 if max_attempts == 0 {
1062 warn!("Too many preemptive onion service circuits failed; waiting a while.");
1065 break 'inner;
1066 }
1067 if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1068 let no_target: Option<&OwnedCircTarget> = None;
1075 let for_launch = circs_to_launch.for_launch();
1076
1077 match pool
1079 .circmgr
1080 .launch_hs_unmanaged(no_target, &netdir, for_launch.kind(), None)
1081 .await
1082 {
1083 Ok(circ) => {
1084 let kind = for_launch.kind();
1085 let circ = HsCircStem { circ, kind };
1086 pool.inner.lock().expect("poisoned lock").pool.insert(circ);
1087 trace!("successfully launched {kind} circuit");
1088 for_launch.note_circ_launched();
1089 }
1090 Err(err) => {
1091 debug_report!(err, "Unable to build preemptive circuit for onion services");
1092 }
1093 }
1094 } else {
1095 break 'inner;
1101 }
1102 }
1103
1104 schedule.fire_in(DELAY);
1106 }
1107}
1108
1109async fn remove_unusable_circuits<B: AbstractCircBuilder<R> + 'static, R: Runtime>(
1111 pool: Weak<HsCircPoolInner<B, R>>,
1112 netdir_provider: Weak<dyn NetDirProvider + 'static>,
1113) {
1114 let mut event_stream = match netdir_provider.upgrade() {
1115 Some(nd) => nd.events(),
1116 None => return,
1117 };
1118
1119 while event_stream.next().await.is_some() {
1125 let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1126 (Some(x), Some(y)) => (x, y),
1127 _ => {
1128 break;
1129 }
1130 };
1131 pool.remove_closed();
1132 if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1133 pool.remove_unlisted(&netdir);
1134 }
1135 }
1136}
1137
1138#[cfg(test)]
1139mod test {
1140 #![allow(clippy::bool_assert_comparison)]
1142 #![allow(clippy::clone_on_copy)]
1143 #![allow(clippy::dbg_macro)]
1144 #![allow(clippy::mixed_attributes_style)]
1145 #![allow(clippy::print_stderr)]
1146 #![allow(clippy::print_stdout)]
1147 #![allow(clippy::single_char_pattern)]
1148 #![allow(clippy::unwrap_used)]
1149 #![allow(clippy::unchecked_duration_subtraction)]
1150 #![allow(clippy::useless_vec)]
1151 #![allow(clippy::needless_pass_by_value)]
1152 #![allow(clippy::cognitive_complexity)]
1154
1155 use tor_config::ExplicitOrAuto;
1156 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1157 use tor_guardmgr::VanguardConfigBuilder;
1158 use tor_guardmgr::VanguardMode;
1159 use tor_memquota::ArcMemoryQuotaTrackerExt as _;
1160 use tor_proto::memquota::ToplevelAccount;
1161 use tor_rtmock::MockRuntime;
1162
1163 use super::*;
1164 use crate::{CircMgrInner, TestConfig};
1165
1166 fn circmgr_with_vanguards<R: Runtime>(
1168 runtime: R,
1169 mode: VanguardMode,
1170 ) -> Arc<CircMgrInner<crate::build::CircuitBuilder<R>, R>> {
1171 let chanmgr = tor_chanmgr::ChanMgr::new(
1172 runtime.clone(),
1173 &Default::default(),
1174 tor_chanmgr::Dormancy::Dormant,
1175 &Default::default(),
1176 ToplevelAccount::new_noop(),
1177 );
1178 let guardmgr = tor_guardmgr::GuardMgr::new(
1179 runtime.clone(),
1180 tor_persist::TestingStateMgr::new(),
1181 &tor_guardmgr::TestConfig::default(),
1182 )
1183 .unwrap();
1184
1185 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1186 let vanguard_config = VanguardConfigBuilder::default()
1187 .mode(ExplicitOrAuto::Explicit(mode))
1188 .build()
1189 .unwrap();
1190
1191 let config = TestConfig {
1192 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1193 vanguard_config,
1194 ..Default::default()
1195 };
1196
1197 CircMgrInner::new(
1198 &config,
1199 tor_persist::TestingStateMgr::new(),
1200 &runtime,
1201 Arc::new(chanmgr),
1202 &guardmgr,
1203 )
1204 .unwrap()
1205 .into()
1206 }
1207
1208 #[test]
1210 fn pool_with_vanguards_disabled() {
1211 MockRuntime::test_with_various(|runtime| async move {
1212 let circmgr = circmgr_with_vanguards(runtime, VanguardMode::Disabled);
1213 let circpool = HsCircPoolInner::new_internal(&circmgr);
1214 assert!(circpool.vanguard_mode() == VanguardMode::Disabled);
1215 });
1216 }
1217
1218 #[test]
1219 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1220 fn pool_with_vanguards_enabled() {
1221 MockRuntime::test_with_various(|runtime| async move {
1222 for mode in [VanguardMode::Lite, VanguardMode::Full] {
1223 let circmgr = circmgr_with_vanguards(runtime.clone(), mode);
1224 let circpool = HsCircPoolInner::new_internal(&circmgr);
1225 assert!(circpool.vanguard_mode() == mode);
1226 }
1227 });
1228 }
1229}