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