1mod config;
5mod pool;
6
7use std::{
8 ops::Deref,
9 sync::{Arc, Mutex, Weak},
10};
11
12use crate::{
13 AbstractTunnel, CircMgr, CircMgrInner, ClientOnionServiceDataTunnel,
14 ClientOnionServiceDirTunnel, ClientOnionServiceIntroTunnel, Error, Result,
15 ServiceOnionServiceDataTunnel, ServiceOnionServiceDirTunnel, ServiceOnionServiceIntroTunnel,
16 build::{TunnelBuilder, onion_circparams_from_netparams},
17 mgr::AbstractTunnelBuilder,
18 path::hspath::hs_stem_terminal_hop_usage,
19 timeouts,
20};
21use futures::{StreamExt, TryFutureExt};
22use once_cell::sync::OnceCell;
23use tor_error::{Bug, debug_report};
24use tor_error::{bad_api_usage, internal};
25use tor_guardmgr::VanguardMode;
26use tor_linkspec::{
27 CircTarget, HasRelayIds as _, IntoOwnedChanTarget, OwnedChanTarget, OwnedCircTarget,
28};
29use tor_netdir::{NetDir, NetDirProvider, Relay};
30use tor_proto::client::circuit::{self, CircParameters};
31use tor_relay_selection::{LowLevelRelayPredicate, RelayExclusion};
32use tor_rtcompat::{
33 Runtime, SleepProviderExt, SpawnExt,
34 scheduler::{TaskHandle, TaskSchedule},
35};
36use tracing::{debug, instrument, trace, warn};
37use web_time_compat::{Duration, Instant, SystemTime};
38
39use std::result::Result as StdResult;
40
41pub use config::HsCircPoolConfig;
42
43use self::pool::HsCircPrefs;
44
45#[cfg(all(feature = "vanguards", feature = "hs-common"))]
46use crate::path::hspath::select_middle_for_vanguard_circ;
47
48#[cfg(feature = "hs-common")]
54#[derive(Debug, Clone, Copy, Eq, PartialEq)]
55#[non_exhaustive]
56pub enum HsCircKind {
57 SvcHsDir,
59 SvcIntro,
61 SvcRend,
63 ClientHsDir,
65 ClientIntro,
67 ClientRend,
69}
70
71impl HsCircKind {
72 fn stem_kind(&self) -> HsCircStemKind {
74 match self {
75 HsCircKind::SvcIntro => HsCircStemKind::Naive,
76 HsCircKind::SvcHsDir => {
77 HsCircStemKind::Naive
79 }
80 HsCircKind::ClientRend => {
81 HsCircStemKind::Guarded
88 }
89 HsCircKind::SvcRend | HsCircKind::ClientHsDir | HsCircKind::ClientIntro => {
90 HsCircStemKind::Guarded
91 }
92 }
93 }
94}
95
96pub(crate) struct HsCircStem<C: AbstractTunnel> {
102 pub(crate) circ: C,
104 pub(crate) kind: HsCircStemKind,
106}
107
108impl<C: AbstractTunnel> HsCircStem<C> {
109 fn satisfies_prefs(&self, prefs: &HsCircPrefs) -> bool {
113 let HsCircPrefs { kind_prefs } = prefs;
114
115 match kind_prefs {
116 Some(kind) => *kind == self.kind,
117 None => true,
118 }
119 }
120}
121
122impl<C: AbstractTunnel> Deref for HsCircStem<C> {
123 type Target = C;
124
125 fn deref(&self) -> &Self::Target {
126 &self.circ
127 }
128}
129
130impl<C: AbstractTunnel> HsCircStem<C> {
131 pub(crate) fn can_become(&self, other: HsCircStemKind) -> bool {
138 use HsCircStemKind::*;
139
140 match (self.kind, other) {
141 (Naive, Naive) | (Guarded, Guarded) | (Naive, Guarded) => true,
142 (Guarded, Naive) => false,
143 }
144 }
145}
146
147#[allow(rustdoc::private_intra_doc_links)]
148#[derive(Copy, Clone, Debug, PartialEq, derive_more::Display)]
172#[non_exhaustive]
173pub(crate) enum HsCircStemKind {
174 #[display("NAIVE")]
179 Naive,
180 #[display("GUARDED")]
185 Guarded,
186}
187
188impl HsCircStemKind {
189 pub(crate) fn num_hops(&self, mode: VanguardMode) -> StdResult<usize, Bug> {
192 use HsCircStemKind::*;
193 use VanguardMode::*;
194
195 let len = match (mode, self) {
196 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
197 (Lite, _) => 3,
198 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
199 (Full, Naive) => 3,
200 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
201 (Full, Guarded) => 4,
202 (Disabled, _) => 3,
203 (_, _) => {
204 return Err(internal!("Unsupported vanguard mode {mode}"));
205 }
206 };
207
208 Ok(len)
209 }
210}
211
212pub struct HsCircPool<R: Runtime>(Arc<HsCircPoolInner<TunnelBuilder<R>, R>>);
214
215impl<R: Runtime> HsCircPool<R> {
216 pub fn new(circmgr: &Arc<CircMgr<R>>) -> Self {
220 Self(Arc::new(HsCircPoolInner::new(circmgr)))
221 }
222
223 #[instrument(level = "trace", skip_all)]
227 pub async fn get_or_launch_client_dir<T>(
228 &self,
229 netdir: &NetDir,
230 target: T,
231 ) -> Result<ClientOnionServiceDirTunnel>
232 where
233 T: CircTarget + Sync,
234 {
235 let tunnel = self
236 .0
237 .get_or_launch_specific(netdir, HsCircKind::ClientHsDir, target)
238 .await?;
239 Ok(tunnel.into())
240 }
241
242 #[instrument(level = "trace", skip_all)]
246 pub async fn get_or_launch_client_intro<T>(
247 &self,
248 netdir: &NetDir,
249 target: T,
250 ) -> Result<ClientOnionServiceIntroTunnel>
251 where
252 T: CircTarget + Sync,
253 {
254 let tunnel = self
255 .0
256 .get_or_launch_specific(netdir, HsCircKind::ClientIntro, target)
257 .await?;
258 Ok(tunnel.into())
259 }
260
261 #[instrument(level = "trace", skip_all)]
265 pub async fn get_or_launch_svc_dir<T>(
266 &self,
267 netdir: &NetDir,
268 target: T,
269 ) -> Result<ServiceOnionServiceDirTunnel>
270 where
271 T: CircTarget + Sync,
272 {
273 let tunnel = self
274 .0
275 .get_or_launch_specific(netdir, HsCircKind::SvcHsDir, target)
276 .await?;
277 Ok(tunnel.into())
278 }
279
280 #[instrument(level = "trace", skip_all)]
284 pub async fn get_or_launch_svc_intro<T>(
285 &self,
286 netdir: &NetDir,
287 target: T,
288 ) -> Result<ServiceOnionServiceIntroTunnel>
289 where
290 T: CircTarget + Sync,
291 {
292 let tunnel = self
293 .0
294 .get_or_launch_specific(netdir, HsCircKind::SvcIntro, target)
295 .await?;
296 Ok(tunnel.into())
297 }
298
299 #[instrument(level = "trace", skip_all)]
303 pub async fn get_or_launch_svc_rend<T>(
304 &self,
305 netdir: &NetDir,
306 target: T,
307 ) -> Result<ServiceOnionServiceDataTunnel>
308 where
309 T: CircTarget + Sync,
310 {
311 let tunnel = self
312 .0
313 .get_or_launch_specific(netdir, HsCircKind::SvcRend, target)
314 .await?;
315 Ok(tunnel.into())
316 }
317
318 #[instrument(level = "trace", skip_all)]
324 pub async fn get_or_launch_client_rend<'a>(
325 &self,
326 netdir: &'a NetDir,
327 ) -> Result<(ClientOnionServiceDataTunnel, Relay<'a>)> {
328 let (tunnel, relay) = self.0.get_or_launch_client_rend(netdir).await?;
329 Ok((tunnel.into(), relay))
330 }
331
332 pub fn estimate_timeout(&self, timeout_action: &timeouts::Action) -> std::time::Duration {
349 self.0.estimate_timeout(timeout_action)
350 }
351
352 pub fn launch_background_tasks(
356 self: &Arc<Self>,
357 runtime: &R,
358 netdir_provider: &Arc<dyn NetDirProvider + 'static>,
359 ) -> Result<Vec<TaskHandle>> {
360 HsCircPoolInner::launch_background_tasks(&self.0.clone(), runtime, netdir_provider)
361 }
362
363 pub fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
369 self.0.retire_all_circuits()
370 }
371
372 pub fn now(&self) -> Instant {
377 self.0.circmgr.mgr.peek_runtime().now()
378 }
379
380 pub fn wallclock(&self) -> SystemTime {
382 self.0.circmgr.mgr.peek_runtime().wallclock()
383 }
384}
385
386pub(crate) struct HsCircPoolInner<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> {
388 circmgr: Arc<CircMgrInner<B, R>>,
390 launcher_handle: OnceCell<TaskHandle>,
398 inner: Mutex<Inner<B::Tunnel>>,
400}
401
402struct Inner<C: AbstractTunnel> {
404 pool: pool::Pool<C>,
406}
407
408impl<R: Runtime> HsCircPoolInner<TunnelBuilder<R>, R> {
409 pub(crate) fn new(circmgr: &CircMgr<R>) -> Self {
411 Self::new_internal(&circmgr.0)
412 }
413}
414
415impl<B: AbstractTunnelBuilder<R> + 'static, R: Runtime> HsCircPoolInner<B, R> {
416 pub(crate) fn new_internal(circmgr: &Arc<CircMgrInner<B, R>>) -> Self {
418 let circmgr = Arc::clone(circmgr);
419 let pool = pool::Pool::default();
420 Self {
421 circmgr,
422 launcher_handle: OnceCell::new(),
423 inner: Mutex::new(Inner { pool }),
424 }
425 }
426
427 #[instrument(level = "trace", skip_all)]
429 pub(crate) fn launch_background_tasks(
430 self: &Arc<Self>,
431 runtime: &R,
432 netdir_provider: &Arc<dyn NetDirProvider + 'static>,
433 ) -> Result<Vec<TaskHandle>> {
434 let handle = self.launcher_handle.get_or_try_init(|| {
435 runtime
436 .spawn(remove_unusable_circuits(
437 Arc::downgrade(self),
438 Arc::downgrade(netdir_provider),
439 ))
440 .map_err(|e| Error::from_spawn("preemptive onion circuit expiration task", e))?;
441
442 let (schedule, handle) = TaskSchedule::new(runtime.clone());
443 runtime
444 .spawn(launch_hs_circuits_as_needed(
445 Arc::downgrade(self),
446 Arc::downgrade(netdir_provider),
447 schedule,
448 ))
449 .map_err(|e| Error::from_spawn("preemptive onion circuit builder task", e))?;
450
451 Result::<TaskHandle>::Ok(handle)
452 })?;
453
454 Ok(vec![handle.clone()])
455 }
456
457 #[instrument(level = "trace", skip_all)]
459 pub(crate) async fn get_or_launch_client_rend<'a>(
460 &self,
461 netdir: &'a NetDir,
462 ) -> Result<(B::Tunnel, Relay<'a>)> {
463 let circ = self
471 .take_or_launch_stem_circuit::<OwnedCircTarget>(netdir, None, HsCircKind::ClientRend)
472 .await?;
473
474 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
475 if matches!(
476 self.vanguard_mode(),
477 VanguardMode::Full | VanguardMode::Lite
478 ) && circ.kind != HsCircStemKind::Guarded
479 {
480 return Err(internal!("wanted a GUARDED circuit, but got NAIVE?!").into());
481 }
482
483 let path = circ.single_path().map_err(|error| Error::Protocol {
484 action: "launching a client rend circuit",
485 peer: None, unique_id: Some(circ.unique_id()),
487 error,
488 })?;
489
490 match path.hops().last() {
491 Some(ent) => {
492 let Some(ct) = ent.as_chan_target() else {
493 return Err(
494 internal!("HsPool gave us a circuit with a virtual last hop!?").into(),
495 );
496 };
497 match netdir.by_ids(ct) {
498 Some(relay) => Ok((circ.circ, relay)),
499 None => Err(internal!("Got circuit with unknown last hop!?").into()),
506 }
507 }
508 None => Err(internal!("Circuit with an empty path!?").into()),
509 }
510 }
511
512 #[instrument(level = "trace", skip_all)]
515 pub(crate) async fn get_or_launch_specific<T>(
516 &self,
517 netdir: &NetDir,
518 kind: HsCircKind,
519 target: T,
520 ) -> Result<B::Tunnel>
521 where
522 T: CircTarget + Sync,
523 {
524 if kind == HsCircKind::ClientRend {
525 return Err(bad_api_usage!("get_or_launch_specific with ClientRend circuit!?").into());
526 }
527
528 let wanted_kind = kind.stem_kind();
529
530 let circ = self
539 .take_or_launch_stem_circuit(netdir, Some(&target), kind)
540 .await?;
541
542 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
543 if matches!(
544 self.vanguard_mode(),
545 VanguardMode::Full | VanguardMode::Lite
546 ) && circ.kind != wanted_kind
547 {
548 return Err(internal!(
549 "take_or_launch_stem_circuit() returned {:?}, but we need {wanted_kind:?}",
550 circ.kind
551 )
552 .into());
553 }
554
555 let mut params = onion_circparams_from_netparams(netdir.params())?;
556
557 params.n_incoming_cells_permitted = match kind {
560 HsCircKind::ClientHsDir => Some(netdir.params().hsdir_dl_max_reply_cells.into()),
561 HsCircKind::SvcHsDir => Some(netdir.params().hsdir_ul_max_reply_cells.into()),
562 HsCircKind::SvcIntro
563 | HsCircKind::SvcRend
564 | HsCircKind::ClientIntro
565 | HsCircKind::ClientRend => None,
566 };
567 self.extend_circ(circ, params, target).await
568 }
569
570 async fn extend_circ<T>(
572 &self,
573 circ: HsCircStem<B::Tunnel>,
574 params: CircParameters,
575 target: T,
576 ) -> Result<B::Tunnel>
577 where
578 T: CircTarget + Sync,
579 {
580 let protocol_err = |error| Error::Protocol {
581 action: "extending to chosen HS hop",
582 peer: None, unique_id: Some(circ.unique_id()),
584 error,
585 };
586
587 let n_hops = circ.n_hops().map_err(protocol_err)?;
590 let (extend_timeout, _) = self.circmgr.mgr.peek_builder().estimator().timeouts(
591 &crate::timeouts::Action::ExtendCircuit {
592 initial_length: n_hops,
593 final_length: n_hops + 1,
594 },
595 );
596
597 let extend_future = circ.extend(&target, params).map_err(protocol_err);
599
600 self.circmgr
602 .mgr
603 .peek_runtime()
604 .timeout(extend_timeout, extend_future)
605 .await
606 .map_err(|_| Error::CircTimeout(Some(circ.unique_id())))??;
607
608 Ok(circ.circ)
610 }
611
612 pub(crate) fn retire_all_circuits(&self) -> StdResult<(), tor_config::ReconfigureError> {
614 self.inner
615 .lock()
616 .expect("poisoned lock")
617 .pool
618 .retire_all_circuits()?;
619
620 Ok(())
621 }
622
623 #[instrument(level = "trace", skip_all)]
632 async fn take_or_launch_stem_circuit<T>(
633 &self,
634 netdir: &NetDir,
635 avoid_target: Option<&T>,
636 kind: HsCircKind,
637 ) -> Result<HsCircStem<B::Tunnel>>
638 where
639 T: CircTarget + Sync,
642 {
643 let stem_kind = kind.stem_kind();
644 let vanguard_mode = self.vanguard_mode();
645 trace!(
646 vanguards=%vanguard_mode,
647 kind=%stem_kind,
648 "selecting HS circuit stem"
649 );
650
651 let target_exclusion = {
654 let path_cfg = self.circmgr.builder().path_config();
655 let cfg = path_cfg.relay_selection_config();
656 match avoid_target {
657 Some(ct) => RelayExclusion::exclude_channel_target_family(&cfg, ct, netdir),
660 None => RelayExclusion::no_relays_excluded(),
661 }
662 };
663
664 let found_usable_circ = {
665 let mut inner = self.inner.lock().expect("lock poisoned");
666
667 let restrictions = |circ: &HsCircStem<B::Tunnel>| {
668 match vanguard_mode {
672 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
673 VanguardMode::Lite | VanguardMode::Full => {
674 vanguards_circuit_compatible_with_target(
675 netdir,
676 circ,
677 stem_kind,
678 kind,
679 avoid_target,
680 )
681 }
682 VanguardMode::Disabled => {
683 circuit_compatible_with_target(netdir, circ, kind, &target_exclusion)
684 }
685 _ => {
686 warn!("unknown vanguard mode {vanguard_mode}");
687 false
688 }
689 }
690 };
691
692 let mut prefs = HsCircPrefs::default();
693
694 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
695 if matches!(vanguard_mode, VanguardMode::Full | VanguardMode::Lite) {
696 prefs.preferred_stem_kind(stem_kind);
697 }
698
699 let found_usable_circ =
700 inner
701 .pool
702 .take_one_where(&mut rand::rng(), restrictions, &prefs);
703
704 if inner.pool.very_low() || found_usable_circ.is_none() {
707 let handle = self.launcher_handle.get().ok_or_else(|| {
708 Error::from(bad_api_usage!("The circuit launcher wasn't initialized"))
709 })?;
710 handle.fire();
711 }
712 found_usable_circ
713 };
714 if let Some(circuit) = found_usable_circ {
716 let circuit = self
717 .maybe_extend_stem_circuit(netdir, circuit, avoid_target, stem_kind, kind)
718 .await?;
719 self.ensure_suitable_circuit(&circuit, avoid_target, stem_kind)?;
720 return Ok(circuit);
721 }
722
723 let circ = self
730 .circmgr
731 .launch_hs_unmanaged(avoid_target, netdir, stem_kind, Some(kind))
732 .await?;
733
734 self.ensure_suitable_circuit(&circ, avoid_target, stem_kind)?;
735
736 Ok(HsCircStem {
737 circ,
738 kind: stem_kind,
739 })
740 }
741
742 async fn maybe_extend_stem_circuit<T>(
744 &self,
745 netdir: &NetDir,
746 circuit: HsCircStem<B::Tunnel>,
747 avoid_target: Option<&T>,
748 stem_kind: HsCircStemKind,
749 circ_kind: HsCircKind,
750 ) -> Result<HsCircStem<B::Tunnel>>
751 where
752 T: CircTarget + Sync,
753 {
754 match self.vanguard_mode() {
755 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
756 VanguardMode::Full => {
757 self.extend_full_vanguards_circuit(
760 netdir,
761 circuit,
762 avoid_target,
763 stem_kind,
764 circ_kind,
765 )
766 .await
767 }
768 _ => {
769 let HsCircStem { circ, kind: _ } = circuit;
770
771 Ok(HsCircStem {
772 circ,
773 kind: stem_kind,
774 })
775 }
776 }
777 }
778
779 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
781 async fn extend_full_vanguards_circuit<T>(
782 &self,
783 netdir: &NetDir,
784 circuit: HsCircStem<B::Tunnel>,
785 avoid_target: Option<&T>,
786 stem_kind: HsCircStemKind,
787 circ_kind: HsCircKind,
788 ) -> Result<HsCircStem<B::Tunnel>>
789 where
790 T: CircTarget + Sync,
791 {
792 use crate::path::hspath::hs_stem_terminal_hop_usage;
793 use tor_relay_selection::RelaySelector;
794
795 match (circuit.kind, stem_kind) {
796 (HsCircStemKind::Naive, HsCircStemKind::Guarded) => {
797 debug!("Wanted GUARDED circuit, but got NAIVE; extending by 1 hop...");
798 let params = crate::build::onion_circparams_from_netparams(netdir.params())?;
799 let circ_path = circuit
800 .circ
801 .single_path()
802 .map_err(|error| Error::Protocol {
803 action: "extending full vanguards circuit",
804 peer: None, unique_id: Some(circuit.unique_id()),
806 error,
807 })?;
808
809 debug_assert_eq!(circ_path.hops().len(), 3);
811
812 let target_exclusion = if let Some(target) = &avoid_target {
813 RelayExclusion::exclude_identities(
814 target.identities().map(|id| id.to_owned()).collect(),
815 )
816 } else {
817 RelayExclusion::no_relays_excluded()
818 };
819 let selector = RelaySelector::new(
820 hs_stem_terminal_hop_usage(Some(circ_kind)),
821 target_exclusion,
822 );
823 let hops = circ_path
824 .iter()
825 .flat_map(|hop| hop.as_chan_target())
826 .map(IntoOwnedChanTarget::to_owned)
827 .collect::<Vec<OwnedChanTarget>>();
828
829 let extra_hop =
830 select_middle_for_vanguard_circ(&hops, netdir, &selector, &mut rand::rng())?;
831
832 let circ = self.extend_circ(circuit, params, extra_hop).await?;
835
836 Ok(HsCircStem {
837 circ,
838 kind: stem_kind,
839 })
840 }
841 (HsCircStemKind::Guarded, HsCircStemKind::Naive) => {
842 Err(internal!("wanted a NAIVE circuit, but got GUARDED?!").into())
843 }
844 _ => {
845 trace!("Wanted {stem_kind} circuit, got {}", circuit.kind);
846 Ok(circuit)
848 }
849 }
850 }
851
852 fn ensure_suitable_circuit<T>(
854 &self,
855 circ: &B::Tunnel,
856 target: Option<&T>,
857 kind: HsCircStemKind,
858 ) -> Result<()>
859 where
860 T: CircTarget + Sync,
861 {
862 Self::ensure_circuit_can_extend_to_target(circ, target)?;
863 self.ensure_circuit_length_valid(circ, kind)?;
864
865 Ok(())
866 }
867
868 fn ensure_circuit_length_valid(&self, tunnel: &B::Tunnel, kind: HsCircStemKind) -> Result<()> {
870 let circ_path_len = tunnel.n_hops().map_err(|error| Error::Protocol {
871 action: "validating circuit length",
872 peer: None, unique_id: Some(tunnel.unique_id()),
874 error,
875 })?;
876
877 let mode = self.vanguard_mode();
878
879 let expected_len = kind.num_hops(mode)?;
881
882 if circ_path_len != expected_len {
883 return Err(internal!(
884 "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
885 kind,
886 expected_len,
887 circ_path_len
888 )
889 .into());
890 }
891
892 Ok(())
893 }
894
895 fn ensure_circuit_can_extend_to_target<T>(tunnel: &B::Tunnel, target: Option<&T>) -> Result<()>
902 where
903 T: CircTarget + Sync,
904 {
905 if let Some(target) = target {
906 let take_n = 2;
907 if let Some(hop) = tunnel
908 .single_path()
909 .map_err(|error| Error::Protocol {
910 action: "validating circuit compatibility with target",
911 peer: None, unique_id: Some(tunnel.unique_id()),
913 error,
914 })?
915 .hops()
916 .iter()
917 .rev()
918 .take(take_n)
919 .flat_map(|hop| hop.as_chan_target())
920 .find(|hop| hop.has_any_relay_id_from(target))
921 {
922 return Err(internal!(
923 "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
924 target.display_relay_ids(),
925 hop.display_relay_ids()
926 ).into());
927 }
928 }
929
930 Ok(())
931 }
932
933 fn remove_closed(&self) {
935 let mut inner = self.inner.lock().expect("lock poisoned");
936 inner.pool.retain(|circ| !circ.is_closing());
937 }
938
939 fn remove_unlisted(&self, netdir: &NetDir) {
942 let mut inner = self.inner.lock().expect("lock poisoned");
943 inner
944 .pool
945 .retain(|circ| circuit_still_useable(netdir, circ, |_relay| true, |_last_hop| true));
946 }
947
948 fn vanguard_mode(&self) -> VanguardMode {
950 cfg_if::cfg_if! {
951 if #[cfg(all(feature = "vanguards", feature = "hs-common"))] {
952 self
953 .circmgr
954 .mgr
955 .peek_builder()
956 .vanguardmgr()
957 .mode()
958 } else {
959 VanguardMode::Disabled
960 }
961 }
962 }
963
964 pub(crate) fn estimate_timeout(
966 &self,
967 timeout_action: &timeouts::Action,
968 ) -> std::time::Duration {
969 self.circmgr.estimate_timeout(timeout_action)
970 }
971}
972
973fn circuit_compatible_with_target<C: AbstractTunnel>(
979 netdir: &NetDir,
980 circ: &HsCircStem<C>,
981 circ_kind: HsCircKind,
982 exclude_target: &RelayExclusion,
983) -> bool {
984 let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
985
986 circuit_still_useable(
995 netdir,
996 circ,
997 |relay| exclude_target.low_level_predicate_permits_relay(relay),
998 |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
999 )
1000}
1001
1002fn vanguards_circuit_compatible_with_target<C: AbstractTunnel, T>(
1008 netdir: &NetDir,
1009 circ: &HsCircStem<C>,
1010 kind: HsCircStemKind,
1011 circ_kind: HsCircKind,
1012 avoid_target: Option<&T>,
1013) -> bool
1014where
1015 T: CircTarget + Sync,
1016{
1017 if let Some(target) = avoid_target {
1018 let Ok(circ_path) = circ.circ.single_path() else {
1019 return false;
1021 };
1022 let take_n = 2;
1026 if circ_path
1027 .hops()
1028 .iter()
1029 .rev()
1030 .take(take_n)
1031 .flat_map(|hop| hop.as_chan_target())
1032 .any(|hop| hop.has_any_relay_id_from(target))
1033 {
1034 return false;
1035 }
1036 }
1037
1038 let last_hop_usage = hs_stem_terminal_hop_usage(Some(circ_kind));
1040
1041 circ.can_become(kind)
1042 && circuit_still_useable(
1043 netdir,
1044 circ,
1045 |_relay| true,
1046 |last_hop| last_hop_usage.low_level_predicate_permits_relay(last_hop),
1047 )
1048}
1049
1050fn circuit_still_useable<C, F1, F2>(
1056 netdir: &NetDir,
1057 circ: &HsCircStem<C>,
1058 relay_okay: F1,
1059 last_hop_ok: F2,
1060) -> bool
1061where
1062 C: AbstractTunnel,
1063 F1: Fn(&Relay<'_>) -> bool,
1064 F2: Fn(&Relay<'_>) -> bool,
1065{
1066 let circ = &circ.circ;
1067 if circ.is_closing() {
1068 return false;
1069 }
1070
1071 let Ok(path) = circ.single_path() else {
1072 return false;
1074 };
1075 let last_hop = path.hops().last().expect("No hops in circuit?!");
1076 match relay_for_path_ent(netdir, last_hop) {
1077 Err(NoRelayForPathEnt::HopWasVirtual) => {}
1078 Err(NoRelayForPathEnt::NoSuchRelay) => {
1079 return false;
1080 }
1081 Ok(r) => {
1082 if !last_hop_ok(&r) {
1083 return false;
1084 }
1085 }
1086 };
1087
1088 path.iter().all(|ent: &circuit::PathEntry| {
1089 match relay_for_path_ent(netdir, ent) {
1090 Err(NoRelayForPathEnt::HopWasVirtual) => {
1091 true
1093 }
1094 Err(NoRelayForPathEnt::NoSuchRelay) => {
1095 false
1098 }
1099 Ok(r) => {
1100 relay_okay(&r)
1102 }
1103 }
1104 })
1105}
1106
1107#[derive(Clone, Debug)]
1111enum NoRelayForPathEnt {
1112 HopWasVirtual,
1114 NoSuchRelay,
1116}
1117
1118fn relay_for_path_ent<'a>(
1120 netdir: &'a NetDir,
1121 ent: &circuit::PathEntry,
1122) -> StdResult<Relay<'a>, NoRelayForPathEnt> {
1123 let Some(c) = ent.as_chan_target() else {
1124 return Err(NoRelayForPathEnt::HopWasVirtual);
1125 };
1126 let Some(relay) = netdir.by_ids(c) else {
1127 return Err(NoRelayForPathEnt::NoSuchRelay);
1128 };
1129 Ok(relay)
1130}
1131
1132#[allow(clippy::cognitive_complexity)] #[instrument(level = "trace", skip_all)]
1135async fn launch_hs_circuits_as_needed<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
1136 pool: Weak<HsCircPoolInner<B, R>>,
1137 netdir_provider: Weak<dyn NetDirProvider + 'static>,
1138 mut schedule: TaskSchedule<R>,
1139) {
1140 const DELAY: Duration = Duration::from_secs(30);
1142
1143 while schedule.next().await.is_some() {
1144 let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1145 (Some(x), Some(y)) => (x, y),
1146 _ => {
1147 break;
1148 }
1149 };
1150 let now = pool.circmgr.mgr.peek_runtime().now();
1151 pool.remove_closed();
1152 let mut circs_to_launch = {
1153 let mut inner = pool.inner.lock().expect("poisioned_lock");
1154 inner.pool.update_target_size(now);
1155 inner.pool.circs_to_launch()
1156 };
1157 let n_to_launch = circs_to_launch.n_to_launch();
1158 let mut max_attempts = n_to_launch * 2;
1159
1160 if n_to_launch > 0 {
1161 debug!(
1162 "launching {} NAIVE and {} GUARDED circuits",
1163 circs_to_launch.stem(),
1164 circs_to_launch.guarded_stem()
1165 );
1166 }
1167
1168 'inner: while circs_to_launch.n_to_launch() > 0 {
1170 max_attempts -= 1;
1171 if max_attempts == 0 {
1172 warn!("Too many preemptive onion service circuits failed; waiting a while.");
1175 break 'inner;
1176 }
1177 if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1178 let no_target: Option<&OwnedCircTarget> = None;
1185 let for_launch = circs_to_launch.for_launch();
1186
1187 match pool
1189 .circmgr
1190 .launch_hs_unmanaged(no_target, &netdir, for_launch.kind(), None)
1191 .await
1192 {
1193 Ok(circ) => {
1194 let kind = for_launch.kind();
1195 let circ = HsCircStem { circ, kind };
1196 pool.inner.lock().expect("poisoned lock").pool.insert(circ);
1197 trace!("successfully launched {kind} circuit");
1198 for_launch.note_circ_launched();
1199 }
1200 Err(err) => {
1201 debug_report!(err, "Unable to build preemptive circuit for onion services");
1202 }
1203 }
1204 } else {
1205 break 'inner;
1211 }
1212 }
1213
1214 schedule.fire_in(DELAY);
1216 }
1217}
1218
1219async fn remove_unusable_circuits<B: AbstractTunnelBuilder<R> + 'static, R: Runtime>(
1221 pool: Weak<HsCircPoolInner<B, R>>,
1222 netdir_provider: Weak<dyn NetDirProvider + 'static>,
1223) {
1224 let mut event_stream = match netdir_provider.upgrade() {
1225 Some(nd) => nd.events(),
1226 None => return,
1227 };
1228
1229 while event_stream.next().await.is_some() {
1235 let (pool, provider) = match (pool.upgrade(), netdir_provider.upgrade()) {
1236 (Some(x), Some(y)) => (x, y),
1237 _ => {
1238 break;
1239 }
1240 };
1241 pool.remove_closed();
1242 if let Ok(netdir) = provider.netdir(tor_netdir::Timeliness::Timely) {
1243 pool.remove_unlisted(&netdir);
1244 }
1245 }
1246}
1247
1248#[cfg(test)]
1249mod test {
1250 #![allow(clippy::bool_assert_comparison)]
1252 #![allow(clippy::clone_on_copy)]
1253 #![allow(clippy::dbg_macro)]
1254 #![allow(clippy::mixed_attributes_style)]
1255 #![allow(clippy::print_stderr)]
1256 #![allow(clippy::print_stdout)]
1257 #![allow(clippy::single_char_pattern)]
1258 #![allow(clippy::unwrap_used)]
1259 #![allow(clippy::unchecked_time_subtraction)]
1260 #![allow(clippy::useless_vec)]
1261 #![allow(clippy::needless_pass_by_value)]
1262 #![allow(clippy::cognitive_complexity)]
1264
1265 use tor_config::ExplicitOrAuto;
1266 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1267 use tor_guardmgr::VanguardConfigBuilder;
1268 use tor_guardmgr::VanguardMode;
1269 use tor_memquota::ArcMemoryQuotaTrackerExt as _;
1270 use tor_proto::memquota::ToplevelAccount;
1271 use tor_rtmock::MockRuntime;
1272
1273 use super::*;
1274 use crate::{CircMgrInner, TestConfig};
1275
1276 fn circmgr_with_vanguards<R: Runtime>(
1278 runtime: R,
1279 mode: VanguardMode,
1280 ) -> Arc<CircMgrInner<crate::build::TunnelBuilder<R>, R>> {
1281 let chanmgr = tor_chanmgr::ChanMgr::new(
1282 runtime.clone(),
1283 Default::default(),
1284 tor_chanmgr::Dormancy::Dormant,
1285 &Default::default(),
1286 ToplevelAccount::new_noop(),
1287 )
1288 .unwrap();
1289 let guardmgr = tor_guardmgr::GuardMgr::new(
1290 runtime.clone(),
1291 tor_persist::TestingStateMgr::new(),
1292 &tor_guardmgr::TestConfig::default(),
1293 )
1294 .unwrap();
1295
1296 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1297 let vanguard_config = VanguardConfigBuilder::default()
1298 .mode(ExplicitOrAuto::Explicit(mode))
1299 .build()
1300 .unwrap();
1301
1302 let config = TestConfig {
1303 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1304 vanguard_config,
1305 ..Default::default()
1306 };
1307
1308 CircMgrInner::new(
1309 &config,
1310 tor_persist::TestingStateMgr::new(),
1311 &runtime,
1312 Arc::new(chanmgr),
1313 &guardmgr,
1314 )
1315 .unwrap()
1316 .into()
1317 }
1318
1319 #[test]
1321 fn pool_with_vanguards_disabled() {
1322 MockRuntime::test_with_various(|runtime| async move {
1323 let circmgr = circmgr_with_vanguards(runtime, VanguardMode::Disabled);
1324 let circpool = HsCircPoolInner::new_internal(&circmgr);
1325 assert!(circpool.vanguard_mode() == VanguardMode::Disabled);
1326 });
1327 }
1328
1329 #[test]
1330 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
1331 fn pool_with_vanguards_enabled() {
1332 MockRuntime::test_with_various(|runtime| async move {
1333 for mode in [VanguardMode::Lite, VanguardMode::Full] {
1334 let circmgr = circmgr_with_vanguards(runtime.clone(), mode);
1335 let circpool = HsCircPoolInner::new_internal(&circmgr);
1336 assert!(circpool.vanguard_mode() == mode);
1337 }
1338 });
1339 }
1340}