1#[cfg(feature = "vanguards")]
64mod vanguards;
65
66use rand::Rng;
67use tor_error::internal;
68use tor_linkspec::{HasRelayIds, OwnedChanTarget};
69use tor_netdir::{NetDir, Relay};
70use tor_relay_selection::{RelayExclusion, RelaySelectionConfig, RelaySelector, RelayUsage};
71use tracing::instrument;
72
73use crate::{Error, Result, hspool::HsCircKind, hspool::HsCircStemKind};
74
75use super::AnonymousPathBuilder;
76
77use {
78 crate::path::{TorPath, pick_path},
79 crate::{DirInfo, PathConfig},
80 std::time::SystemTime,
81 tor_guardmgr::{GuardMgr, GuardMonitor, GuardUsable},
82 tor_rtcompat::Runtime,
83};
84
85#[cfg(feature = "vanguards")]
86use {
87 crate::path::{MaybeOwnedRelay, select_guard},
88 tor_error::bad_api_usage,
89 tor_guardmgr::VanguardMode,
90 tor_guardmgr::vanguards::Layer,
91 tor_guardmgr::vanguards::VanguardMgr,
92};
93
94#[cfg(feature = "vanguards")]
95pub(crate) use vanguards::select_middle_for_vanguard_circ;
96
97pub(crate) struct HsPathBuilder {
101 compatible_with: Option<OwnedChanTarget>,
105 #[cfg_attr(not(feature = "vanguards"), allow(dead_code))]
109 stem_kind: HsCircStemKind,
110
111 circ_kind: Option<HsCircKind>,
114}
115
116impl HsPathBuilder {
117 pub(crate) fn new(
125 compatible_with: Option<OwnedChanTarget>,
126 stem_kind: HsCircStemKind,
127 circ_kind: Option<HsCircKind>,
128 ) -> Self {
129 Self {
130 compatible_with,
131 stem_kind,
132 circ_kind,
133 }
134 }
135
136 #[cfg_attr(feature = "vanguards", allow(unused))]
138 #[instrument(skip_all, level = "trace")]
139 pub(crate) fn pick_path<'a, R: Rng, RT: Runtime>(
140 &self,
141 rng: &mut R,
142 netdir: DirInfo<'a>,
143 guards: &GuardMgr<RT>,
144 config: &PathConfig,
145 now: SystemTime,
146 ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
147 pick_path(self, rng, netdir, guards, config, now)
148 }
149
150 #[cfg(feature = "vanguards")]
155 #[cfg_attr(not(feature = "vanguards"), allow(unused))]
156 #[instrument(skip_all, level = "trace")]
157 pub(crate) fn pick_path_with_vanguards<'a, R: Rng, RT: Runtime>(
158 &self,
159 rng: &mut R,
160 netdir: DirInfo<'a>,
161 guards: &GuardMgr<RT>,
162 vanguards: &VanguardMgr<RT>,
163 config: &PathConfig,
164 now: SystemTime,
165 ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
166 let mode = vanguards.mode();
167 if mode == VanguardMode::Disabled {
168 return pick_path(self, rng, netdir, guards, config, now);
169 }
170
171 let vanguard_path_builder = VanguardHsPathBuilder {
172 stem_kind: self.stem_kind,
173 circ_kind: self.circ_kind,
174 compatible_with: self.compatible_with.clone(),
175 };
176
177 vanguard_path_builder.pick_path(rng, netdir, guards, vanguards)
178 }
179}
180
181impl AnonymousPathBuilder for HsPathBuilder {
182 fn compatible_with(&self) -> Option<&OwnedChanTarget> {
183 self.compatible_with.as_ref()
184 }
185
186 fn path_kind(&self) -> &'static str {
187 "onion-service circuit"
188 }
189
190 fn pick_exit<'a, R: Rng>(
191 &self,
192 rng: &mut R,
193 netdir: &'a NetDir,
194 guard_exclusion: RelayExclusion<'a>,
195 _rs_cfg: &RelaySelectionConfig<'_>,
196 ) -> Result<(Relay<'a>, RelayUsage)> {
197 let selector =
198 RelaySelector::new(hs_stem_terminal_hop_usage(self.circ_kind), guard_exclusion);
199
200 let (relay, info) = selector.select_relay(rng, netdir);
201 let relay = relay.ok_or_else(|| Error::NoRelay {
202 path_kind: self.path_kind(),
203 role: "final hop",
204 problem: info.to_string(),
205 })?;
206 Ok((relay, RelayUsage::middle_relay(Some(selector.usage()))))
207 }
208}
209
210#[cfg(feature = "vanguards")]
216struct VanguardHsPathBuilder {
217 stem_kind: HsCircStemKind,
219 circ_kind: Option<HsCircKind>,
222 compatible_with: Option<OwnedChanTarget>,
224}
225
226#[cfg(feature = "vanguards")]
227impl VanguardHsPathBuilder {
228 #[instrument(skip_all, level = "trace")]
230 fn pick_path<'a, R: Rng, RT: Runtime>(
231 &self,
232 rng: &mut R,
233 netdir: DirInfo<'a>,
234 guards: &GuardMgr<RT>,
235 vanguards: &VanguardMgr<RT>,
236 ) -> Result<(TorPath<'a>, GuardMonitor, GuardUsable)> {
237 let netdir = match netdir {
238 DirInfo::Directory(d) => d,
239 _ => {
240 return Err(bad_api_usage!(
241 "Tried to build a multihop path without a network directory"
242 )
243 .into());
244 }
245 };
246
247 let (l1_guard, mon, usable) = select_guard(netdir, guards, None)?;
250
251 let target_exclusion = if let Some(target) = self.compatible_with.as_ref() {
252 RelayExclusion::exclude_identities(
253 target.identities().map(|id| id.to_owned()).collect(),
254 )
255 } else {
256 RelayExclusion::no_relays_excluded()
257 };
258
259 let mode = vanguards.mode();
260 let path = match mode {
261 VanguardMode::Lite => {
262 self.pick_lite_vanguard_path(rng, netdir, vanguards, l1_guard, &target_exclusion)?
263 }
264 VanguardMode::Full => {
265 self.pick_full_vanguard_path(rng, netdir, vanguards, l1_guard, &target_exclusion)?
266 }
267 VanguardMode::Disabled => {
268 return Err(internal!(
269 "VanguardHsPathBuilder::pick_path called, but vanguards are disabled?!"
270 )
271 .into());
272 }
273 _ => {
274 return Err(internal!("unrecognized vanguard mode {mode}").into());
275 }
276 };
277
278 let actual_len = path.len();
279 let expected_len = self.stem_kind.num_hops(mode)?;
280 if actual_len != expected_len {
281 return Err(internal!(
282 "invalid path length for {} {mode}-vanguard circuit (expected {} hops, got {})",
283 self.stem_kind,
284 expected_len,
285 actual_len
286 )
287 .into());
288 }
289
290 Ok((path, mon, usable))
291 }
292
293 fn pick_full_vanguard_path<'n, R: Rng, RT: Runtime>(
295 &self,
296 rng: &mut R,
297 netdir: &'n NetDir,
298 vanguards: &VanguardMgr<RT>,
299 l1_guard: MaybeOwnedRelay<'n>,
300 target_exclusion: &RelayExclusion<'n>,
301 ) -> Result<TorPath<'n>> {
302 let l2_target_exclusion = match self.stem_kind {
307 HsCircStemKind::Guarded => RelayExclusion::no_relays_excluded(),
308 HsCircStemKind::Naive => target_exclusion.clone(),
309 };
310 let l3_usage = match self.stem_kind {
312 HsCircStemKind::Naive => hs_stem_terminal_hop_usage(self.circ_kind),
313 HsCircStemKind::Guarded => hs_intermediate_hop_usage(),
314 };
315 let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), l2_target_exclusion);
316 let l3_selector = RelaySelector::new(l3_usage, target_exclusion.clone());
317
318 let path = vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard);
319
320 let path = path
321 .add_vanguard(&l2_selector, Layer::Layer2)?
322 .add_vanguard(&l3_selector, Layer::Layer3)?;
323
324 match self.stem_kind {
325 HsCircStemKind::Guarded => {
326 let mid_selector = RelaySelector::new(
331 hs_stem_terminal_hop_usage(self.circ_kind),
332 target_exclusion.clone(),
333 );
334 path.add_middle(&mid_selector)?.build()
335 }
336 HsCircStemKind::Naive => path.build(),
337 }
338 }
339
340 fn pick_lite_vanguard_path<'n, R: Rng, RT: Runtime>(
342 &self,
343 rng: &mut R,
344 netdir: &'n NetDir,
345 vanguards: &VanguardMgr<RT>,
346 l1_guard: MaybeOwnedRelay<'n>,
347 target_exclusion: &RelayExclusion<'n>,
348 ) -> Result<TorPath<'n>> {
349 let l2_selector = RelaySelector::new(hs_intermediate_hop_usage(), target_exclusion.clone());
350 let mid_selector = RelaySelector::new(
351 hs_stem_terminal_hop_usage(self.circ_kind),
352 target_exclusion.clone(),
353 );
354
355 vanguards::PathBuilder::new(rng, netdir, vanguards, l1_guard)
356 .add_vanguard(&l2_selector, Layer::Layer2)?
357 .add_middle(&mid_selector)?
358 .build()
359 }
360}
361
362pub(crate) fn hs_intermediate_hop_usage() -> RelayUsage {
368 RelayUsage::middle_relay(Some(&RelayUsage::new_intro_point()))
377}
378
379pub(crate) fn hs_stem_terminal_hop_usage(kind: Option<HsCircKind>) -> RelayUsage {
384 let Some(kind) = kind else {
385 return hs_intermediate_hop_usage();
388 };
389 match kind {
390 HsCircKind::ClientRend => {
391 RelayUsage::new_rend_point()
394 }
395 HsCircKind::SvcHsDir
396 | HsCircKind::SvcIntro
397 | HsCircKind::SvcRend
398 | HsCircKind::ClientHsDir
399 | HsCircKind::ClientIntro => {
400 hs_intermediate_hop_usage()
403 }
404 }
405}
406
407#[cfg(test)]
408mod test {
409 #![allow(clippy::bool_assert_comparison)]
411 #![allow(clippy::clone_on_copy)]
412 #![allow(clippy::dbg_macro)]
413 #![allow(clippy::mixed_attributes_style)]
414 #![allow(clippy::print_stderr)]
415 #![allow(clippy::print_stdout)]
416 #![allow(clippy::single_char_pattern)]
417 #![allow(clippy::unwrap_used)]
418 #![allow(clippy::unchecked_time_subtraction)]
419 #![allow(clippy::useless_vec)]
420 #![allow(clippy::needless_pass_by_value)]
421 use std::sync::Arc;
424
425 use super::*;
426
427 use tor_linkspec::{ChannelMethod, OwnedCircTarget};
428 use tor_netdir::{NetDirProvider, testnet::NodeBuilders, testprovider::TestNetDirProvider};
429 use tor_netdoc::doc::netstatus::RelayWeight;
430 use tor_netdoc::types::relay_flags::RelayFlag;
431 use tor_rtcompat::SleepProvider as _;
432 use tor_rtmock::MockRuntime;
433
434 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
435 use {
436 crate::path::OwnedPath, tor_basic_utils::test_rng::testing_rng,
437 tor_guardmgr::VanguardMgrError, tor_netdir::testnet::construct_custom_netdir,
438 };
439
440 const MAX_NET_SIZE: usize = 40;
442
443 fn construct_test_network<F>(size: usize, mut set_family: F) -> NetDir
445 where
446 F: FnMut(usize, &mut NodeBuilders),
447 {
448 assert!(
449 size <= MAX_NET_SIZE,
450 "the test network supports at most {MAX_NET_SIZE} relays"
451 );
452 let netdir = construct_custom_netdir(|pos, nb, _| {
453 nb.omit_rs = pos >= size;
454 if !nb.omit_rs {
455 let f = RelayFlag::Running
456 | RelayFlag::Valid
457 | RelayFlag::V2Dir
458 | RelayFlag::Fast
459 | RelayFlag::Stable;
460 nb.rs.set_flags(f | RelayFlag::Guard);
461 nb.rs.weight(RelayWeight::Measured(10_000));
462
463 set_family(pos, nb);
464 }
465 })
466 .unwrap()
467 .unwrap_if_sufficient()
468 .unwrap();
469
470 assert_eq!(netdir.all_relays().count(), size);
471
472 netdir
473 }
474
475 fn same_family_test_network(size: usize) -> NetDir {
477 construct_test_network(size, |_pos, nb| {
478 let family = (0..MAX_NET_SIZE)
480 .map(|i| hex::encode([i as u8; 20]))
481 .collect::<Vec<_>>()
482 .join(" ");
483
484 nb.md.family(family.parse().unwrap());
485 })
486 }
487
488 fn path_hops(path: &TorPath) -> Vec<OwnedCircTarget> {
490 let path: OwnedPath = path.try_into().unwrap();
491 match path {
492 OwnedPath::ChannelOnly(_) => {
493 panic!("expected OwnedPath::Normal, got OwnedPath::ChannelOnly")
494 }
495 OwnedPath::Normal(ref v) => v.clone(),
496 }
497 }
498
499 fn assert_duplicate_hops(path: &TorPath, expect_dupes: bool) {
504 let hops = path_hops(path);
505 let has_dupes = hops.iter().enumerate().any(|(i, hop)| {
506 hops.iter()
507 .skip(i + 1)
508 .any(|h| h.has_any_relay_id_from(hop))
509 });
510 let msg = if expect_dupes { "have" } else { "not have any" };
511
512 assert_eq!(
513 has_dupes, expect_dupes,
514 "expected path to {msg} duplicate hops: {:?}",
515 hops
516 );
517 }
518
519 #[cfg(feature = "vanguards")]
521 fn assert_vanguard_path_ok(
522 path: &TorPath,
523 stem_kind: HsCircStemKind,
524 mode: VanguardMode,
525 target: Option<&OwnedChanTarget>,
526 ) {
527 use itertools::Itertools;
528
529 assert_eq!(
530 path.len(),
531 stem_kind.num_hops(mode).unwrap(),
532 "invalid path length for {stem_kind} {mode}-vanguards circuit"
533 );
534
535 let hops = path_hops(path);
536 for (hop1, hop2, hop3) in hops.iter().tuple_windows() {
537 if hop1.has_any_relay_id_from(hop2)
538 || hop1.has_any_relay_id_from(hop3)
539 || hop2.has_any_relay_id_from(hop3)
540 {
541 panic!(
542 "neighboring hops should be distinct: [{}], [{}], [{}]",
543 hop1.display_relay_ids(),
544 hop2.display_relay_ids(),
545 hop3.display_relay_ids(),
546 );
547 }
548 }
549
550 if let Some(target) = target {
552 for hop in hops.iter().rev().take(2) {
553 if hop.has_any_relay_id_from(target) {
554 panic!(
555 "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
556 hop.display_relay_ids(),
557 target.display_relay_ids(),
558 );
559 }
560 }
561 }
562 }
563
564 fn assert_hs_path_ok(path: &TorPath, target: Option<&OwnedChanTarget>) {
566 assert_eq!(path.len(), 3);
567 assert_duplicate_hops(path, false);
568 if let Some(target) = target {
569 for hop in path_hops(path) {
570 if hop.has_any_relay_id_from(target) {
571 panic!(
572 "invalid path: hop {} is the same relay as the circuit target {}",
573 hop.display_relay_ids(),
574 target.display_relay_ids()
575 )
576 }
577 }
578 }
579 }
580
581 async fn pick_vanguard_path<'a>(
583 runtime: &MockRuntime,
584 netdir: &'a NetDir,
585 stem_kind: HsCircStemKind,
586 circ_kind: Option<HsCircKind>,
587 mode: VanguardMode,
588 target: Option<&OwnedChanTarget>,
589 ) -> Result<TorPath<'a>> {
590 let vanguardmgr = VanguardMgr::new_testing(runtime, mode).unwrap();
591 let _provider = vanguardmgr.init_vanguard_sets(netdir).await.unwrap();
592
593 let mut rng = testing_rng();
594 let guards = tor_guardmgr::GuardMgr::new(
595 runtime.clone(),
596 tor_persist::TestingStateMgr::new(),
597 &tor_guardmgr::TestConfig::default(),
598 )
599 .unwrap();
600 let netdir_provider = Arc::new(TestNetDirProvider::new());
601 netdir_provider.set_netdir(netdir.clone());
602 let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
603 guards.install_netdir_provider(&netdir_provider).unwrap();
604 let config = PathConfig::default();
605 let now = runtime.wallclock();
606 let dirinfo = (netdir).into();
607 HsPathBuilder::new(target.cloned(), stem_kind, circ_kind)
608 .pick_path_with_vanguards(&mut rng, dirinfo, &guards, &vanguardmgr, &config, now)
609 .map(|res| res.0)
610 }
611
612 fn pick_hs_path_no_vanguards<'a>(
614 netdir: &'a NetDir,
615 target: Option<&OwnedChanTarget>,
616 circ_kind: Option<HsCircKind>,
617 ) -> Result<TorPath<'a>> {
618 let mut rng = testing_rng();
619 let config = PathConfig::default();
620 let runtime = MockRuntime::new();
621 let now = runtime.wallclock();
622 let dirinfo = (netdir).into();
623 let guards = tor_guardmgr::GuardMgr::new(
624 runtime,
625 tor_persist::TestingStateMgr::new(),
626 &tor_guardmgr::TestConfig::default(),
627 )
628 .unwrap();
629 let netdir_provider = Arc::new(TestNetDirProvider::new());
630 netdir_provider.set_netdir(netdir.clone());
631 let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
632 guards.install_netdir_provider(&netdir_provider).unwrap();
633 HsPathBuilder::new(target.cloned(), HsCircStemKind::Naive, circ_kind)
634 .pick_path(&mut rng, dirinfo, &guards, &config, now)
635 .map(|res| res.0)
636 }
637
638 fn test_target() -> OwnedChanTarget {
644 OwnedChanTarget::builder()
646 .addrs(vec!["127.0.0.3:9001".parse().unwrap()])
647 .ed_identity([0xAA; 32].into())
648 .rsa_identity([0x00; 20].into())
649 .method(ChannelMethod::Direct(vec!["0.0.0.3:9001".parse().unwrap()]))
650 .build()
651 .unwrap()
652 }
653
654 #[test]
660 fn hs_path_no_vanguards_incompatible_target() {
661 let target = test_target();
663
664 let netdir = construct_test_network(3, |pos, nb| {
665 if pos == 0 {
668 let family = (0..MAX_NET_SIZE)
669 .map(|i| hex::encode([i as u8; 20]))
670 .collect::<Vec<_>>()
671 .join(" ");
672
673 nb.md.family(family.parse().unwrap());
674 } else {
675 nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
676 }
677 });
678 let err = pick_hs_path_no_vanguards(&netdir, Some(&target), None)
681 .map(|_| ())
682 .unwrap_err();
683
684 assert!(
685 matches!(
686 err,
687 Error::NoRelay {
688 ref problem,
689 ..
690 } if problem == "Failed: rejected 3/3 as in same family as already selected"
691 ),
692 "{err:?}"
693 );
694 }
695
696 #[test]
697 fn hs_path_no_vanguards_reject_same_family() {
698 let netdir = same_family_test_network(MAX_NET_SIZE);
701 let err = match pick_hs_path_no_vanguards(&netdir, None, None) {
702 Ok(path) => panic!(
703 "expected error, but got valid path: {:?})",
704 OwnedPath::try_from(&path).unwrap()
705 ),
706 Err(e) => e,
707 };
708
709 assert!(
710 matches!(
711 err,
712 Error::NoRelay {
713 ref problem,
714 ..
715 } if problem == "Failed: rejected 40/40 as in same family as already selected"
716 ),
717 "{err:?}"
718 );
719 }
720
721 #[test]
722 fn hs_path_no_vanguards() {
723 let netdir = construct_test_network(20, |pos, nb| {
724 nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
725 });
726 let target = test_target();
728 for _ in 0..100 {
729 for target in [None, Some(target.clone())] {
730 let path = pick_hs_path_no_vanguards(&netdir, target.as_ref(), None).unwrap();
731 assert_hs_path_ok(&path, target.as_ref());
732 }
733 }
734 }
735
736 #[test]
737 #[cfg(feature = "vanguards")]
738 fn lite_vanguard_path_insufficient_relays() {
739 MockRuntime::test_with_various(|runtime| async move {
740 let netdir = same_family_test_network(2);
741 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
742 let err = pick_vanguard_path(
743 &runtime,
744 &netdir,
745 stem_kind,
746 None,
747 VanguardMode::Lite,
748 None,
749 )
750 .await
751 .map(|_| ())
752 .unwrap_err();
753
754 assert!(
756 matches!(
757 err,
758 Error::NoRelay {
759 ref problem,
760 ..
761 } if problem == "Failed: rejected 2/2 as already selected",
762 ),
763 "{err:?}"
764 );
765 }
766 });
767 }
768
769 #[test]
771 #[cfg(feature = "vanguards")]
772 fn lite_vanguard_path() {
773 MockRuntime::test_with_various(|runtime| async move {
774 let target = OwnedChanTarget::builder()
776 .rsa_identity([0x00; 20].into())
777 .build()
778 .unwrap();
779 let netdir = same_family_test_network(10);
780 let mode = VanguardMode::Lite;
781
782 for target in [None, Some(target)] {
783 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
784 let path = pick_vanguard_path(
785 &runtime,
786 &netdir,
787 stem_kind,
788 None,
789 mode,
790 target.as_ref(),
791 )
792 .await
793 .unwrap();
794 assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
795 }
796 }
797 });
798 }
799
800 #[test]
801 #[cfg(feature = "vanguards")]
802 fn full_vanguard_path() {
803 MockRuntime::test_with_various(|runtime| async move {
804 let netdir = same_family_test_network(MAX_NET_SIZE);
805 let mode = VanguardMode::Full;
806
807 let target = OwnedChanTarget::builder()
809 .rsa_identity([0x00; 20].into())
810 .build()
811 .unwrap();
812
813 for target in [None, Some(target)] {
814 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
815 let path = pick_vanguard_path(
816 &runtime,
817 &netdir,
818 stem_kind,
819 None,
820 mode,
821 target.as_ref(),
822 )
823 .await
824 .unwrap();
825 assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
826 }
827 }
828 });
829 }
830
831 #[test]
832 #[cfg(feature = "vanguards")]
833 fn full_vanguard_path_insufficient_relays() {
834 MockRuntime::test_with_various(|runtime| async move {
835 let netdir = same_family_test_network(2);
836
837 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
838 let err = pick_vanguard_path(
839 &runtime,
840 &netdir,
841 stem_kind,
842 None,
843 VanguardMode::Full,
844 None,
845 )
846 .await
847 .map(|_| ())
848 .unwrap_err();
849 assert!(
850 matches!(
851 err,
852 Error::VanguardMgrInit(VanguardMgrError::NoSuitableRelay(Layer::Layer3)),
853 ),
854 "{err:?}"
855 );
856 }
857
858 let netdir = same_family_test_network(3);
861 let mode = VanguardMode::Full;
862
863 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
864 let path = pick_vanguard_path(&runtime, &netdir, stem_kind, None, mode, None)
865 .await
866 .unwrap();
867 assert_vanguard_path_ok(&path, stem_kind, mode, None);
868 match stem_kind {
869 HsCircStemKind::Naive => {
870 assert_duplicate_hops(&path, false);
886 }
887 HsCircStemKind::Guarded => {
888 assert_duplicate_hops(&path, true);
891 }
892 }
893 }
894 });
895 }
896}