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 #![allow(clippy::string_slice)] use std::sync::Arc;
425
426 use super::*;
427
428 use tor_linkspec::{ChannelMethod, OwnedCircTarget};
429 use tor_netdir::{NetDirProvider, testnet::NodeBuilders, testprovider::TestNetDirProvider};
430 use tor_netdoc::doc::netstatus::RelayWeight;
431 use tor_netdoc::types::relay_flags::RelayFlag;
432 use tor_rtcompat::SleepProvider as _;
433 use tor_rtmock::MockRuntime;
434
435 #[cfg(all(feature = "vanguards", feature = "hs-common"))]
436 use {
437 crate::path::OwnedPath, tor_basic_utils::test_rng::testing_rng,
438 tor_guardmgr::VanguardMgrError, tor_netdir::testnet::construct_custom_netdir,
439 };
440
441 const MAX_NET_SIZE: usize = 40;
443
444 fn construct_test_network<F>(size: usize, mut set_family: F) -> NetDir
446 where
447 F: FnMut(usize, &mut NodeBuilders),
448 {
449 assert!(
450 size <= MAX_NET_SIZE,
451 "the test network supports at most {MAX_NET_SIZE} relays"
452 );
453 let netdir = construct_custom_netdir(|pos, nb, _| {
454 nb.omit_rs = pos >= size;
455 if !nb.omit_rs {
456 let f = RelayFlag::Running
457 | RelayFlag::Valid
458 | RelayFlag::V2Dir
459 | RelayFlag::Fast
460 | RelayFlag::Stable;
461 nb.rs.set_flags(f | RelayFlag::Guard);
462 nb.rs.weight(RelayWeight::Measured(10_000));
463
464 set_family(pos, nb);
465 }
466 })
467 .unwrap()
468 .unwrap_if_sufficient()
469 .unwrap();
470
471 assert_eq!(netdir.all_relays().count(), size);
472
473 netdir
474 }
475
476 fn same_family_test_network(size: usize) -> NetDir {
478 construct_test_network(size, |_pos, nb| {
479 let family = (0..MAX_NET_SIZE)
481 .map(|i| hex::encode([i as u8; 20]))
482 .collect::<Vec<_>>()
483 .join(" ");
484
485 nb.md.family(family.parse().unwrap());
486 })
487 }
488
489 fn path_hops(path: &TorPath) -> Vec<OwnedCircTarget> {
491 let path: OwnedPath = path.try_into().unwrap();
492 match path {
493 OwnedPath::ChannelOnly(_) => {
494 panic!("expected OwnedPath::Normal, got OwnedPath::ChannelOnly")
495 }
496 OwnedPath::Normal(ref v) => v.clone(),
497 }
498 }
499
500 fn assert_duplicate_hops(path: &TorPath, expect_dupes: bool) {
505 let hops = path_hops(path);
506 let has_dupes = hops.iter().enumerate().any(|(i, hop)| {
507 hops.iter()
508 .skip(i + 1)
509 .any(|h| h.has_any_relay_id_from(hop))
510 });
511 let msg = if expect_dupes { "have" } else { "not have any" };
512
513 assert_eq!(
514 has_dupes, expect_dupes,
515 "expected path to {msg} duplicate hops: {:?}",
516 hops
517 );
518 }
519
520 #[cfg(feature = "vanguards")]
522 fn assert_vanguard_path_ok(
523 path: &TorPath,
524 stem_kind: HsCircStemKind,
525 mode: VanguardMode,
526 target: Option<&OwnedChanTarget>,
527 ) {
528 use itertools::Itertools;
529
530 assert_eq!(
531 path.len(),
532 stem_kind.num_hops(mode).unwrap(),
533 "invalid path length for {stem_kind} {mode}-vanguards circuit"
534 );
535
536 let hops = path_hops(path);
537 for (hop1, hop2, hop3) in hops.iter().tuple_windows() {
538 if hop1.has_any_relay_id_from(hop2)
539 || hop1.has_any_relay_id_from(hop3)
540 || hop2.has_any_relay_id_from(hop3)
541 {
542 panic!(
543 "neighboring hops should be distinct: [{}], [{}], [{}]",
544 hop1.display_relay_ids(),
545 hop2.display_relay_ids(),
546 hop3.display_relay_ids(),
547 );
548 }
549 }
550
551 if let Some(target) = target {
553 for hop in hops.iter().rev().take(2) {
554 if hop.has_any_relay_id_from(target) {
555 panic!(
556 "invalid path: circuit target {} appears as one of the last 2 hops (matches hop {})",
557 hop.display_relay_ids(),
558 target.display_relay_ids(),
559 );
560 }
561 }
562 }
563 }
564
565 fn assert_hs_path_ok(path: &TorPath, target: Option<&OwnedChanTarget>) {
567 assert_eq!(path.len(), 3);
568 assert_duplicate_hops(path, false);
569 if let Some(target) = target {
570 for hop in path_hops(path) {
571 if hop.has_any_relay_id_from(target) {
572 panic!(
573 "invalid path: hop {} is the same relay as the circuit target {}",
574 hop.display_relay_ids(),
575 target.display_relay_ids()
576 )
577 }
578 }
579 }
580 }
581
582 async fn pick_vanguard_path<'a>(
584 runtime: &MockRuntime,
585 netdir: &'a NetDir,
586 stem_kind: HsCircStemKind,
587 circ_kind: Option<HsCircKind>,
588 mode: VanguardMode,
589 target: Option<&OwnedChanTarget>,
590 ) -> Result<TorPath<'a>> {
591 let vanguardmgr = VanguardMgr::new_testing(runtime, mode).unwrap();
592 let _provider = vanguardmgr.init_vanguard_sets(netdir).await.unwrap();
593
594 let mut rng = testing_rng();
595 let guards = tor_guardmgr::GuardMgr::new(
596 runtime.clone(),
597 tor_persist::TestingStateMgr::new(),
598 &tor_guardmgr::TestConfig::default(),
599 )
600 .unwrap();
601 let netdir_provider = Arc::new(TestNetDirProvider::new());
602 netdir_provider.set_netdir(netdir.clone());
603 let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
604 guards.install_netdir_provider(&netdir_provider).unwrap();
605 let config = PathConfig::default();
606 let now = runtime.wallclock();
607 let dirinfo = (netdir).into();
608 HsPathBuilder::new(target.cloned(), stem_kind, circ_kind)
609 .pick_path_with_vanguards(&mut rng, dirinfo, &guards, &vanguardmgr, &config, now)
610 .map(|res| res.0)
611 }
612
613 fn pick_hs_path_no_vanguards<'a>(
615 netdir: &'a NetDir,
616 target: Option<&OwnedChanTarget>,
617 circ_kind: Option<HsCircKind>,
618 ) -> Result<TorPath<'a>> {
619 let mut rng = testing_rng();
620 let config = PathConfig::default();
621 let runtime = MockRuntime::new();
622 let now = runtime.wallclock();
623 let dirinfo = (netdir).into();
624 let guards = tor_guardmgr::GuardMgr::new(
625 runtime,
626 tor_persist::TestingStateMgr::new(),
627 &tor_guardmgr::TestConfig::default(),
628 )
629 .unwrap();
630 let netdir_provider = Arc::new(TestNetDirProvider::new());
631 netdir_provider.set_netdir(netdir.clone());
632 let netdir_provider: Arc<dyn NetDirProvider> = netdir_provider;
633 guards.install_netdir_provider(&netdir_provider).unwrap();
634 HsPathBuilder::new(target.cloned(), HsCircStemKind::Naive, circ_kind)
635 .pick_path(&mut rng, dirinfo, &guards, &config, now)
636 .map(|res| res.0)
637 }
638
639 fn test_target() -> OwnedChanTarget {
645 OwnedChanTarget::builder()
647 .addrs(vec!["127.0.0.3:9001".parse().unwrap()])
648 .ed_identity([0xAA; 32].into())
649 .rsa_identity([0x00; 20].into())
650 .method(ChannelMethod::Direct(vec!["0.0.0.3:9001".parse().unwrap()]))
651 .build()
652 .unwrap()
653 }
654
655 #[test]
661 fn hs_path_no_vanguards_incompatible_target() {
662 let target = test_target();
664
665 let netdir = construct_test_network(3, |pos, nb| {
666 if pos == 0 {
669 let family = (0..MAX_NET_SIZE)
670 .map(|i| hex::encode([i as u8; 20]))
671 .collect::<Vec<_>>()
672 .join(" ");
673
674 nb.md.family(family.parse().unwrap());
675 } else {
676 nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
677 }
678 });
679 let err = pick_hs_path_no_vanguards(&netdir, Some(&target), None)
682 .map(|_| ())
683 .unwrap_err();
684
685 assert!(
686 matches!(
687 err,
688 Error::NoRelay {
689 ref problem,
690 ..
691 } if problem == "Failed: rejected 3/3 as in same family as already selected"
692 ),
693 "{err:?}"
694 );
695 }
696
697 #[test]
698 fn hs_path_no_vanguards_reject_same_family() {
699 let netdir = same_family_test_network(MAX_NET_SIZE);
702 let err = match pick_hs_path_no_vanguards(&netdir, None, None) {
703 Ok(path) => panic!(
704 "expected error, but got valid path: {:?})",
705 OwnedPath::try_from(&path).unwrap()
706 ),
707 Err(e) => e,
708 };
709
710 assert!(
711 matches!(
712 err,
713 Error::NoRelay {
714 ref problem,
715 ..
716 } if problem == "Failed: rejected 40/40 as in same family as already selected"
717 ),
718 "{err:?}"
719 );
720 }
721
722 #[test]
723 fn hs_path_no_vanguards() {
724 let netdir = construct_test_network(20, |pos, nb| {
725 nb.md.family(hex::encode([pos as u8; 20]).parse().unwrap());
726 });
727 let target = test_target();
729 for _ in 0..100 {
730 for target in [None, Some(target.clone())] {
731 let path = pick_hs_path_no_vanguards(&netdir, target.as_ref(), None).unwrap();
732 assert_hs_path_ok(&path, target.as_ref());
733 }
734 }
735 }
736
737 #[test]
738 #[cfg(feature = "vanguards")]
739 fn lite_vanguard_path_insufficient_relays() {
740 MockRuntime::test_with_various(|runtime| async move {
741 let netdir = same_family_test_network(2);
742 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
743 let err = pick_vanguard_path(
744 &runtime,
745 &netdir,
746 stem_kind,
747 None,
748 VanguardMode::Lite,
749 None,
750 )
751 .await
752 .map(|_| ())
753 .unwrap_err();
754
755 assert!(
757 matches!(
758 err,
759 Error::NoRelay {
760 ref problem,
761 ..
762 } if problem == "Failed: rejected 2/2 as already selected",
763 ),
764 "{err:?}"
765 );
766 }
767 });
768 }
769
770 #[test]
772 #[cfg(feature = "vanguards")]
773 fn lite_vanguard_path() {
774 MockRuntime::test_with_various(|runtime| async move {
775 let target = OwnedChanTarget::builder()
777 .rsa_identity([0x00; 20].into())
778 .build()
779 .unwrap();
780 let netdir = same_family_test_network(10);
781 let mode = VanguardMode::Lite;
782
783 for target in [None, Some(target)] {
784 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
785 let path = pick_vanguard_path(
786 &runtime,
787 &netdir,
788 stem_kind,
789 None,
790 mode,
791 target.as_ref(),
792 )
793 .await
794 .unwrap();
795 assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
796 }
797 }
798 });
799 }
800
801 #[test]
802 #[cfg(feature = "vanguards")]
803 fn full_vanguard_path() {
804 MockRuntime::test_with_various(|runtime| async move {
805 let netdir = same_family_test_network(MAX_NET_SIZE);
806 let mode = VanguardMode::Full;
807
808 let target = OwnedChanTarget::builder()
810 .rsa_identity([0x00; 20].into())
811 .build()
812 .unwrap();
813
814 for target in [None, Some(target)] {
815 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
816 let path = pick_vanguard_path(
817 &runtime,
818 &netdir,
819 stem_kind,
820 None,
821 mode,
822 target.as_ref(),
823 )
824 .await
825 .unwrap();
826 assert_vanguard_path_ok(&path, stem_kind, mode, target.as_ref());
827 }
828 }
829 });
830 }
831
832 #[test]
833 #[cfg(feature = "vanguards")]
834 fn full_vanguard_path_insufficient_relays() {
835 MockRuntime::test_with_various(|runtime| async move {
836 let netdir = same_family_test_network(2);
837
838 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
839 let err = pick_vanguard_path(
840 &runtime,
841 &netdir,
842 stem_kind,
843 None,
844 VanguardMode::Full,
845 None,
846 )
847 .await
848 .map(|_| ())
849 .unwrap_err();
850 assert!(
851 matches!(
852 err,
853 Error::VanguardMgrInit(VanguardMgrError::NoSuitableRelay(Layer::Layer3)),
854 ),
855 "{err:?}"
856 );
857 }
858
859 let netdir = same_family_test_network(3);
862 let mode = VanguardMode::Full;
863
864 for stem_kind in [HsCircStemKind::Naive, HsCircStemKind::Guarded] {
865 let path = pick_vanguard_path(&runtime, &netdir, stem_kind, None, mode, None)
866 .await
867 .unwrap();
868 assert_vanguard_path_ok(&path, stem_kind, mode, None);
869 match stem_kind {
870 HsCircStemKind::Naive => {
871 assert_duplicate_hops(&path, false);
887 }
888 HsCircStemKind::Guarded => {
889 assert_duplicate_hops(&path, true);
892 }
893 }
894 }
895 });
896 }
897}