1use std::collections::BTreeMap;
4use std::collections::BTreeSet;
5
6use rings_core::dht::Did;
7use rings_core::ecc::PublicKey;
8use rings_core::measure::PeerQuality;
9use rings_core::message::DhtProtocolMode;
10
11use super::circuit::MAX_ONION_CIRCUIT_HOPS;
12use super::OnionExitDescriptor;
13use super::OnionRouteError;
14use super::OnionServiceName;
15use super::ONION_RELAY_CAPABILITY;
16use crate::error::Error;
17use crate::error::Result;
18use crate::online::OnlineNodeDescriptor;
19
20pub const DEFAULT_ONION_ROUTE_HOPS: usize = 3;
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct OnionRouteRequest {
26 pub service: OnionServiceName,
28 pub hop_count: usize,
30 pub allow_short_paths: bool,
32}
33
34impl OnionRouteRequest {
35 pub fn new(
37 service: impl AsRef<str>,
38 hop_count: usize,
39 allow_short_paths: bool,
40 ) -> Result<Self> {
41 Ok(Self::from_service_name(
42 parse_route_service(service)?,
43 hop_count,
44 allow_short_paths,
45 ))
46 }
47
48 pub fn from_service_name(
50 service: OnionServiceName,
51 hop_count: usize,
52 allow_short_paths: bool,
53 ) -> Self {
54 Self {
55 service,
56 hop_count,
57 allow_short_paths,
58 }
59 }
60
61 pub fn service(&self) -> &str {
63 self.service.as_str()
64 }
65
66 pub(crate) fn service_name(&self) -> &OnionServiceName {
67 &self.service
68 }
69
70 fn target_hop_count(&self) -> usize {
71 if self.hop_count == 0 {
72 DEFAULT_ONION_ROUTE_HOPS
73 } else {
74 self.hop_count
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub struct OnionRouteHop {
82 pub did: Did,
84 pub session_public_key: PublicKey<33>,
86}
87
88impl OnionRouteHop {
89 pub const fn new(did: Did, session_public_key: PublicKey<33>) -> Self {
91 Self {
92 did,
93 session_public_key,
94 }
95 }
96}
97
98#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct OnionRoute {
101 service: OnionServiceName,
103 hops: Vec<Did>,
105 encryption_hops: Vec<OnionRouteHop>,
107 exit: OnionExitDescriptor,
109}
110
111impl OnionRoute {
112 pub(crate) fn new(
120 service: OnionServiceName,
121 encryption_hops: Vec<OnionRouteHop>,
122 exit: OnionExitDescriptor,
123 ) -> Result<Self> {
124 validate_route_hops(&service, &encryption_hops, &exit)?;
125 let hops = encryption_hops
126 .iter()
127 .map(|hop| hop.did)
128 .collect::<Vec<_>>();
129 Ok(Self {
130 service,
131 hops,
132 encryption_hops,
133 exit,
134 })
135 }
136
137 pub fn service(&self) -> &str {
139 self.service.as_str()
140 }
141
142 pub fn service_name(&self) -> &OnionServiceName {
144 &self.service
145 }
146
147 pub fn hops(&self) -> &[Did] {
149 self.hops.as_slice()
150 }
151
152 pub(crate) fn encryption_hops(&self) -> &[OnionRouteHop] {
154 self.encryption_hops.as_slice()
155 }
156
157 pub fn exit(&self) -> &OnionExitDescriptor {
159 &self.exit
160 }
161
162 pub fn exit_did(&self) -> Did {
164 self.exit.did
165 }
166}
167
168pub(crate) trait RouteEntropy {
169 fn next_u64(&mut self) -> u64;
170}
171
172pub(crate) struct SystemRouteEntropy;
173
174impl SystemRouteEntropy {
175 pub(crate) const fn new() -> Self {
176 Self
177 }
178}
179
180impl RouteEntropy for SystemRouteEntropy {
181 fn next_u64(&mut self) -> u64 {
182 rand::random()
183 }
184}
185
186#[derive(Clone, Debug)]
187pub(crate) struct OnionRouteCandidates {
188 pub(in crate::onion) relays: Vec<OnionRouteHop>,
189 pub(in crate::onion) exits: Vec<OnionExitDescriptor>,
190}
191
192impl OnionRouteCandidates {
193 pub(crate) fn from_validated_descriptors(
194 local: Did,
195 dht_protocol: DhtProtocolMode,
196 now_ms: u128,
197 service: &OnionServiceName,
198 online_nodes: impl IntoIterator<Item = OnlineNodeDescriptor>,
199 exits: impl IntoIterator<Item = OnionExitDescriptor>,
200 ) -> Self {
201 let relays = eligible_relay_dids(dht_protocol, now_ms, local, online_nodes);
202 let exits = eligible_exits(dht_protocol.network_id, now_ms, service, exits)
203 .into_iter()
204 .filter(|descriptor| descriptor.did != local)
205 .collect();
206
207 Self { relays, exits }
208 }
209}
210
211pub fn select_onion_route(
216 local: Did,
217 dht_protocol: DhtProtocolMode,
218 now_ms: u128,
219 request: &OnionRouteRequest,
220 online_nodes: impl IntoIterator<Item = OnlineNodeDescriptor>,
221 exits: impl IntoIterator<Item = OnionExitDescriptor>,
222 qualities: impl IntoIterator<Item = (Did, PeerQuality)>,
223) -> Result<OnionRoute> {
224 let candidates = OnionRouteCandidates {
225 relays: eligible_relay_dids(dht_protocol, now_ms, local, online_nodes)
226 .into_iter()
227 .collect(),
228 exits: eligible_exits(
229 dht_protocol.network_id,
230 now_ms,
231 request.service_name(),
232 exits,
233 )
234 .into_iter()
235 .filter(|descriptor| descriptor.did != local)
236 .collect(),
237 };
238 select_onion_route_from_candidates(
239 request,
240 candidates,
241 qualities,
242 &mut SystemRouteEntropy::new(),
243 )
244}
245
246pub(crate) fn select_onion_route_from_candidates(
247 request: &OnionRouteRequest,
248 candidates: OnionRouteCandidates,
249 qualities: impl IntoIterator<Item = (Did, PeerQuality)>,
250 entropy: &mut impl RouteEntropy,
251) -> Result<OnionRoute> {
252 select_onion_route_from_candidates_with_first_hop(
253 request,
254 candidates,
255 qualities,
256 entropy,
257 |_| true,
258 )
259}
260
261pub(crate) fn select_onion_route_from_candidates_with_first_hop(
262 request: &OnionRouteRequest,
263 candidates: OnionRouteCandidates,
264 qualities: impl IntoIterator<Item = (Did, PeerQuality)>,
265 entropy: &mut impl RouteEntropy,
266 first_hop_permitted: impl Fn(Did) -> bool,
267) -> Result<OnionRoute> {
268 let target_hop_count = request.target_hop_count();
269 if target_hop_count == 0 || target_hop_count > usize::from(MAX_ONION_CIRCUIT_HOPS) {
270 return Err(Error::OnionRouteError(
271 OnionRouteError::HopCountOutOfBounds {
272 hop_count: target_hop_count,
273 max_hops: MAX_ONION_CIRCUIT_HOPS,
274 },
275 ));
276 }
277
278 let quality_by_did = qualities.into_iter().collect::<BTreeMap<_, _>>();
279 let mut exit_candidates = candidates.exits;
280 let first_hop_permitted = &first_hop_permitted;
281 let first_hop_exit_only = target_hop_count == 1;
282 if exit_candidates.is_empty() {
283 return Err(Error::OnionRouteError(OnionRouteError::NoLiveExit {
284 service: request.service().to_string(),
285 }));
286 }
287 if first_hop_exit_only {
288 return select_direct_exit_route(
289 request,
290 exit_candidates,
291 &quality_by_did,
292 entropy,
293 first_hop_permitted,
294 );
295 }
296
297 let mut relay_candidates = candidates.relays.into_iter().collect::<Vec<_>>();
298 let relay_hops_needed = target_hop_count.saturating_sub(1);
299 let mut selected_relays = Vec::with_capacity(relay_hops_needed);
300 if relay_hops_needed > 0 {
301 let has_relay_candidates = !relay_candidates.is_empty();
302 let Some(first_index) =
303 pick_weighted_hop_index_where(&relay_candidates, &quality_by_did, entropy, |did| {
304 first_hop_permitted(did)
305 && route_can_still_select_exit(&selected_relays, did, &exit_candidates)
306 })
307 else {
308 if request.allow_short_paths {
309 return select_direct_exit_route(
310 request,
311 exit_candidates,
312 &quality_by_did,
313 entropy,
314 first_hop_permitted,
315 );
316 }
317 let error = if has_relay_candidates {
318 OnionRouteError::NoPermittedFirstHop
319 } else {
320 OnionRouteError::NotEnoughRelays {
321 hop_count: target_hop_count,
322 }
323 };
324 return Err(Error::OnionRouteError(error));
325 };
326 selected_relays.push(relay_candidates.remove(first_index));
327 while selected_relays.len() < relay_hops_needed {
328 let Some(next_index) =
329 pick_weighted_hop_index_where(&relay_candidates, &quality_by_did, entropy, |did| {
330 route_can_still_select_exit(&selected_relays, did, &exit_candidates)
331 })
332 else {
333 break;
334 };
335 selected_relays.push(relay_candidates.remove(next_index));
336 }
337 }
338
339 if selected_relays.len() < relay_hops_needed && !request.allow_short_paths {
340 return Err(Error::OnionRouteError(OnionRouteError::NotEnoughRelays {
341 hop_count: target_hop_count,
342 }));
343 }
344
345 let exit_index =
346 pick_weighted_exit_index_where(&exit_candidates, &quality_by_did, entropy, |did| {
347 !route_already_contains_did(&selected_relays, did)
348 })
349 .ok_or_else(|| {
350 Error::OnionRouteError(OnionRouteError::NoLiveExit {
351 service: request.service().to_string(),
352 })
353 })?;
354 let exit = exit_candidates.remove(exit_index);
355 let exit_did = exit.did;
356 let mut encryption_hops = selected_relays;
357 encryption_hops.push(OnionRouteHop::new(exit_did, exit.session_public_key));
358 OnionRoute::new(request.service.clone(), encryption_hops, exit)
359}
360
361fn select_direct_exit_route(
362 request: &OnionRouteRequest,
363 mut exits: Vec<OnionExitDescriptor>,
364 quality_by_did: &BTreeMap<Did, PeerQuality>,
365 entropy: &mut impl RouteEntropy,
366 first_hop_permitted: &impl Fn(Did) -> bool,
367) -> Result<OnionRoute> {
368 let exit_index =
369 pick_weighted_exit_index_where(&exits, quality_by_did, entropy, first_hop_permitted)
370 .ok_or(Error::OnionRouteError(OnionRouteError::NoPermittedFirstHop))?;
371 let exit = exits.remove(exit_index);
372 let encryption_hops = vec![OnionRouteHop::new(exit.did, exit.session_public_key)];
373 OnionRoute::new(request.service.clone(), encryption_hops, exit)
374}
375
376fn route_can_still_select_exit(
377 selected_relays: &[OnionRouteHop],
378 candidate_relay: Did,
379 exits: &[OnionExitDescriptor],
380) -> bool {
381 exits.iter().any(|exit| {
382 exit.did != candidate_relay && !route_already_contains_did(selected_relays, exit.did)
383 })
384}
385
386fn route_already_contains_did(selected_relays: &[OnionRouteHop], did: Did) -> bool {
387 selected_relays.iter().any(|hop| hop.did == did)
388}
389
390fn pick_weighted_hop_index_where(
391 hops: &[OnionRouteHop],
392 quality_by_did: &BTreeMap<Did, PeerQuality>,
393 entropy: &mut impl RouteEntropy,
394 permitted: impl Fn(Did) -> bool,
395) -> Option<usize> {
396 let eligible = hops
397 .iter()
398 .enumerate()
399 .filter_map(|(index, hop)| permitted(hop.did).then_some((index, hop.did)))
400 .collect::<Vec<_>>();
401 pick_weighted_candidate_index(eligible, quality_by_did, entropy)
402}
403
404fn pick_weighted_exit_index_where(
405 exits: &[OnionExitDescriptor],
406 quality_by_did: &BTreeMap<Did, PeerQuality>,
407 entropy: &mut impl RouteEntropy,
408 permitted: impl Fn(Did) -> bool,
409) -> Option<usize> {
410 let eligible = exits
411 .iter()
412 .enumerate()
413 .filter_map(|(index, descriptor)| {
414 permitted(descriptor.did).then_some((index, descriptor.did))
415 })
416 .collect::<Vec<_>>();
417 pick_weighted_candidate_index(eligible, quality_by_did, entropy)
418}
419
420fn pick_weighted_candidate_index(
421 eligible: Vec<(usize, Did)>,
422 quality_by_did: &BTreeMap<Did, PeerQuality>,
423 entropy: &mut impl RouteEntropy,
424) -> Option<usize> {
425 let dids = eligible.iter().map(|(_, did)| *did).collect::<Vec<_>>();
426 let selected = pick_weighted_index(&dids, quality_by_did, entropy)?;
427 eligible.into_iter().nth(selected).map(|(index, _)| index)
428}
429
430fn pick_weighted_index(
431 dids: &[Did],
432 quality_by_did: &BTreeMap<Did, PeerQuality>,
433 entropy: &mut impl RouteEntropy,
434) -> Option<usize> {
435 let total_weight = dids
436 .iter()
437 .map(|did| quality_weight(quality_by_did.get(did).copied()))
438 .sum::<u64>();
439 if total_weight == 0 {
440 return None;
441 }
442
443 let mut roll = entropy.next_u64() % total_weight;
444 for (index, did) in dids.iter().enumerate() {
445 let weight = quality_weight(quality_by_did.get(did).copied());
446 if roll < weight {
447 return Some(index);
448 }
449 roll -= weight;
450 }
451 None
452}
453
454fn quality_weight(quality: Option<PeerQuality>) -> u64 {
455 match quality {
456 Some(PeerQuality::Healthy) => 8,
457 Some(PeerQuality::Unknown) | None => 4,
458 Some(PeerQuality::Degraded) => 1,
459 }
460}
461
462fn eligible_exits(
463 network_id: u32,
464 now_ms: u128,
465 service: &OnionServiceName,
466 exits: impl IntoIterator<Item = OnionExitDescriptor>,
467) -> Vec<OnionExitDescriptor> {
468 OnionExitDescriptor::latest_valid_by_service_did(exits, now_ms, false)
469 .into_iter()
470 .filter(|descriptor| descriptor.matches_network(network_id))
471 .filter(|descriptor| descriptor.offers_service(service.as_str()))
472 .collect()
473}
474
475fn eligible_relay_dids(
476 dht_protocol: DhtProtocolMode,
477 now_ms: u128,
478 local: Did,
479 online_nodes: impl IntoIterator<Item = OnlineNodeDescriptor>,
480) -> Vec<OnionRouteHop> {
481 OnlineNodeDescriptor::latest_valid_by_did(online_nodes, now_ms, false)
482 .into_iter()
483 .filter(|descriptor| descriptor.matches_dht_protocol(dht_protocol))
484 .filter(has_onion_relay_capability)
485 .map(|descriptor| OnionRouteHop::new(descriptor.did, descriptor.session_public_key))
486 .filter(|hop| hop.did != local)
487 .map(|hop| (hop.did, hop))
488 .collect::<BTreeMap<_, _>>()
489 .into_values()
490 .collect()
491}
492
493fn has_onion_relay_capability(descriptor: &OnlineNodeDescriptor) -> bool {
494 descriptor
495 .capabilities
496 .iter()
497 .any(|capability| capability == ONION_RELAY_CAPABILITY)
498}
499
500fn has_duplicate_dids(hops: &[Did]) -> bool {
501 let mut seen = BTreeSet::new();
502 hops.iter().any(|did| !seen.insert(*did))
503}
504
505fn validate_route_hops(
506 service: &OnionServiceName,
507 encryption_hops: &[OnionRouteHop],
508 exit: &OnionExitDescriptor,
509) -> Result<()> {
510 if encryption_hops.is_empty() || encryption_hops.len() > usize::from(MAX_ONION_CIRCUIT_HOPS) {
511 return Err(Error::OnionRouteError(
512 OnionRouteError::HopCountOutOfBounds {
513 hop_count: encryption_hops.len(),
514 max_hops: MAX_ONION_CIRCUIT_HOPS,
515 },
516 ));
517 }
518 let Some(last) = encryption_hops.last() else {
519 return Err(Error::OnionRouteError(OnionRouteError::RouteHasNoHops));
520 };
521 if last.did != exit.did || last.session_public_key != exit.session_public_key {
522 return Err(Error::OnionRouteError(OnionRouteError::ExitHopMismatch));
523 }
524 let hops = encryption_hops
525 .iter()
526 .map(|hop| hop.did)
527 .collect::<Vec<_>>();
528 if has_duplicate_dids(&hops) {
529 return Err(Error::OnionRouteError(OnionRouteError::DuplicateRouteHops));
530 }
531 if !exit.offers_service(service.as_str()) {
532 return Err(Error::OnionRouteError(OnionRouteError::ExitServiceMismatch));
533 }
534 Ok(())
535}
536
537fn parse_route_service(service: impl AsRef<str>) -> Result<OnionServiceName> {
538 let service = service.as_ref();
539 if service.trim().is_empty() {
540 return Err(Error::OnionRouteError(OnionRouteError::EmptyRouteService));
541 }
542 OnionServiceName::parse(service)
543}
544
545#[cfg(test)]
546mod tests;