1use super::*;
2
3pub(in crate::node) enum TransitNextHopPlan {
4 Route(NodeAddr),
5 Loop(NodeAddr),
6 NoRoute,
7}
8
9impl Node {
10 pub(in crate::node) fn cache_current_root_coords(
13 &mut self,
14 node_addr: NodeAddr,
15 coords: crate::tree::TreeCoordinate,
16 now_ms: u64,
17 ) -> bool {
18 if coords.node_addr() != &node_addr
19 || coords.root_id() != self.tree_state.my_coords().root_id()
20 {
21 return false;
22 }
23 self.coord_cache.insert(node_addr, coords, now_ms);
24 true
25 }
26
27 pub(crate) fn is_tree_peer(&self, peer_addr: &NodeAddr) -> bool {
32 if !self.tree_state.is_root() && self.tree_state.my_declaration().parent_id() == peer_addr {
34 return true;
35 }
36 if let Some(decl) = self.tree_state.peer_declaration(peer_addr)
38 && decl.parent_id() == self.node_addr()
39 {
40 return true;
41 }
42 false
43 }
44
45 pub fn find_next_hop(&mut self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
72 if dest_node_addr == self.node_addr() {
74 return None;
75 }
76 let now_ms = Self::now_ms();
77 let direct_path_hard_degraded =
78 self.session_direct_path_is_degraded(dest_node_addr, now_ms);
79 let direct_path_soft_degraded = !direct_path_hard_degraded
80 && self.session_direct_discovered_endpoint_trust_expired(dest_node_addr, now_ms);
81 let fallback_peer_available = self.has_sendable_fallback_lookup_peer(dest_node_addr);
82 let direct_session_degraded =
83 fallback_peer_available && (direct_path_hard_degraded || direct_path_soft_degraded);
84 let direct_session_untrusted = !direct_session_degraded
85 && self.session_direct_path_exclusive_trust_expired(dest_node_addr, now_ms);
86 let stale_traversal_direct_route = self
87 .peers
88 .get(dest_node_addr)
89 .filter(|peer| {
90 !direct_path_hard_degraded
91 && !direct_session_degraded
92 && !peer.is_healthy()
93 && peer.can_send()
94 })
95 .and_then(|_| {
96 self.configured_peer(dest_node_addr)
97 .and_then(|peer_config| {
98 (peer_config.is_auto_connect()
99 && self.active_peer_uses_traversal_path(dest_node_addr, peer_config))
100 .then_some(*dest_node_addr)
101 })
102 });
103
104 let healthy_direct_route = self
105 .peers
106 .get(dest_node_addr)
107 .filter(|peer| peer.is_healthy() && !direct_session_degraded)
108 .map(|_| *dest_node_addr);
109 let direct_session_has_recent_data_return =
110 self.session_direct_path_has_recent_data_return(dest_node_addr, now_ms);
111 if let Some(direct_addr) = healthy_direct_route
112 && direct_session_has_recent_data_return
113 {
114 return self.peers.get(&direct_addr);
115 }
116 if let Some(direct_addr) = healthy_direct_route
117 && !direct_session_untrusted
118 && self.dataplane_fmp_link_cost(&direct_addr)
119 <= 1.0 + ROUTING_FALLBACK_MIN_COST_ADVANTAGE
120 {
121 return self.peers.get(&direct_addr);
122 }
123 let direct_payload_eligible = healthy_direct_route.is_some();
124 let payload_candidate_can_send = |addr: &NodeAddr, peer: &ActivePeer| {
125 if addr == dest_node_addr {
126 direct_payload_eligible
127 } else {
128 peer.is_healthy()
129 }
130 };
131
132 let fallback_beats_direct = |node: &Self, fallback_addr: NodeAddr| {
137 if direct_session_untrusted {
138 return healthy_direct_route != Some(fallback_addr)
139 && node
140 .peers
141 .get(&fallback_addr)
142 .is_some_and(|peer| peer.is_healthy());
143 }
144 node.route_candidate_beats_direct(healthy_direct_route, fallback_addr)
145 };
146
147 let sendable_learned_peers = if self.config.node.routing.mode == RoutingMode::ReplyLearned {
148 Some(
149 self.peers
150 .iter()
151 .filter(|(addr, peer)| payload_candidate_can_send(addr, peer))
152 .map(|(addr, _)| *addr)
153 .collect::<HashSet<_>>(),
154 )
155 } else {
156 None
157 };
158
159 let explore_fallback = sendable_learned_peers.as_ref().is_some_and(|sendable| {
160 self.learned_routes.should_explore_fallback(
161 dest_node_addr,
162 now_ms,
163 self.config.node.routing.learned_fallback_explore_interval,
164 |addr| sendable.contains(addr),
165 )
166 });
167 if let Some(next_hop_addr) = sendable_learned_peers.as_ref().and_then(|sendable| {
168 let activity = self.dataplane.fsp_owner_activity(dest_node_addr)?;
169 let next_hop_addr = activity.last_outbound_next_hop()?;
170 if next_hop_addr == *dest_node_addr
171 || !activity.has_recent_outbound_activity(
172 now_ms,
173 self.session_direct_path_exclusive_trust_timeout_ms(),
174 )
175 || !sendable.contains(&next_hop_addr)
176 || !fallback_beats_direct(self, next_hop_addr)
177 {
178 return None;
179 }
180 Some(next_hop_addr)
181 }) {
182 self.learned_routes
183 .record_selected(dest_node_addr, &next_hop_addr, now_ms);
184 return self.peers.get(&next_hop_addr);
185 }
186
187 if let Some(sendable) = &sendable_learned_peers
194 && !explore_fallback
195 {
196 let eligible = sendable
197 .iter()
198 .copied()
199 .filter(|addr| fallback_beats_direct(self, *addr))
200 .collect::<HashSet<_>>();
201 if !eligible.is_empty()
202 && let Some(next_hop_addr) =
203 self.learned_routes
204 .select_next_hop(dest_node_addr, now_ms, |addr| eligible.contains(addr))
205 {
206 return self.peers.get(&next_hop_addr);
207 }
208 }
209
210 let Some(dest_coords) = self
212 .coord_cache
213 .get_and_touch(dest_node_addr, now_ms)
214 .cloned()
215 else {
216 if (healthy_direct_route.is_none() || explore_fallback)
217 && let Some(sendable) = &sendable_learned_peers
218 && let Some(next_hop_addr) =
219 self.learned_routes
220 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
221 {
222 return self.peers.get(&next_hop_addr);
223 }
224 if let Some(direct_addr) = healthy_direct_route {
225 return self.peers.get(&direct_addr);
226 }
227 if let Some(direct_addr) = stale_traversal_direct_route {
228 return self.peers.get(&direct_addr);
229 }
230 return None;
231 };
232
233 let coordinate_route_addr = {
236 let candidates: Vec<&ActivePeer> = self
237 .peers
238 .iter()
239 .filter(|(addr, peer)| {
240 payload_candidate_can_send(addr, peer) && peer.may_reach(dest_node_addr)
241 })
242 .map(|(_, peer)| peer)
243 .collect();
244 if !candidates.is_empty() {
245 self.select_best_candidate(&candidates, &dest_coords)
246 .map(|peer| *peer.node_addr())
247 } else {
248 None
249 }
250 };
251 if let Some(next_hop_addr) = coordinate_route_addr
252 && fallback_beats_direct(self, next_hop_addr)
253 {
254 return self.peers.get(&next_hop_addr);
255 }
256
257 let tree_route_addr = self.select_tree_payload_candidate(
259 &dest_coords,
260 dest_node_addr,
261 direct_payload_eligible,
262 );
263 if let Some(next_hop_addr) = tree_route_addr
264 && fallback_beats_direct(self, next_hop_addr)
265 {
266 return self.peers.get(&next_hop_addr);
267 }
268
269 if explore_fallback
270 && let Some(peer) = sendable_learned_peers.as_ref().and_then(|sendable| {
271 self.learned_routes
272 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
273 .and_then(|next_hop_addr| self.peers.get(&next_hop_addr))
274 })
275 {
276 return Some(peer);
277 }
278
279 if let Some(direct_addr) = healthy_direct_route {
280 return self.peers.get(&direct_addr);
281 }
282
283 if let Some(sendable) = &sendable_learned_peers
284 && let Some(next_hop_addr) =
285 self.learned_routes
286 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
287 {
288 return self.peers.get(&next_hop_addr);
289 }
290
291 if let Some(direct_addr) = stale_traversal_direct_route {
292 return self.peers.get(&direct_addr);
293 }
294
295 None
296 }
297
298 pub(in crate::node) fn plan_transit_next_hop(
299 &mut self,
300 dest_node_addr: &NodeAddr,
301 previous_hop: &NodeAddr,
302 ) -> TransitNextHopPlan {
303 if dest_node_addr == self.node_addr() {
304 return TransitNextHopPlan::NoRoute;
305 }
306
307 if dest_node_addr != previous_hop
308 && self
309 .peers
310 .get(dest_node_addr)
311 .is_some_and(|peer| peer.is_healthy())
312 {
313 return TransitNextHopPlan::Route(*dest_node_addr);
314 }
315
316 if self.config.node.routing.mode == RoutingMode::ReplyLearned {
322 let sendable = self
323 .peers
324 .iter()
325 .filter(|(addr, peer)| *addr != previous_hop && peer.is_healthy())
326 .map(|(addr, _)| *addr)
327 .collect::<HashSet<_>>();
328 if let Some(next_hop_addr) =
329 self.learned_routes
330 .select_handshake_route(dest_node_addr, Self::now_ms(), |addr| {
331 sendable.contains(addr)
332 })
333 {
334 return TransitNextHopPlan::Route(next_hop_addr);
335 }
336 if let Some(next_hop_addr) =
337 self.learned_routes
338 .select_next_hop(dest_node_addr, Self::now_ms(), |addr| {
339 sendable.contains(addr)
340 })
341 {
342 return TransitNextHopPlan::Route(next_hop_addr);
343 }
344 }
345
346 let Some(next_hop_addr) = self
347 .find_next_hop(dest_node_addr)
348 .map(|peer| *peer.node_addr())
349 else {
350 return TransitNextHopPlan::NoRoute;
351 };
352 if next_hop_addr == *dest_node_addr && &next_hop_addr != previous_hop {
353 return TransitNextHopPlan::Route(next_hop_addr);
354 }
355
356 let now_ms = Self::now_ms();
357 let dest_coords = self
358 .coord_cache
359 .get_and_touch(dest_node_addr, now_ms)
360 .cloned();
361 let selected_strictly_progresses = dest_coords.as_ref().is_some_and(|dest_coords| {
362 self.tree_state.my_coords().root_id() == dest_coords.root_id()
363 && self
364 .tree_state
365 .peer_coords(&next_hop_addr)
366 .is_some_and(|peer_coords| {
367 peer_coords.distance_to(dest_coords)
368 < self.tree_state.my_coords().distance_to(dest_coords)
369 })
370 });
371
372 if &next_hop_addr != previous_hop && (dest_coords.is_none() || selected_strictly_progresses)
373 {
374 return TransitNextHopPlan::Route(next_hop_addr);
375 }
376
377 let coordinate_fallback = dest_coords.and_then(|dest_coords| {
378 self.select_tree_payload_candidate_avoiding(
379 &dest_coords,
380 dest_node_addr,
381 false,
382 Some(previous_hop),
383 )
384 });
385 if let Some(next_hop_addr) = coordinate_fallback {
386 return TransitNextHopPlan::Route(next_hop_addr);
387 }
388 TransitNextHopPlan::Loop(next_hop_addr)
389 }
390
391 #[cfg(test)]
392 pub(in crate::node) fn find_transit_next_hop(
393 &mut self,
394 dest_node_addr: &NodeAddr,
395 previous_hop: &NodeAddr,
396 ) -> Option<NodeAddr> {
397 match self.plan_transit_next_hop(dest_node_addr, previous_hop) {
398 TransitNextHopPlan::Route(next_hop_addr) => Some(next_hop_addr),
399 TransitNextHopPlan::Loop(next_hop_addr) => {
400 self.record_route_failure(*dest_node_addr, next_hop_addr);
401 None
402 }
403 TransitNextHopPlan::NoRoute => None,
404 }
405 }
406
407 pub(super) fn route_candidate_beats_direct(
408 &self,
409 healthy_direct_route: Option<NodeAddr>,
410 candidate_addr: NodeAddr,
411 ) -> bool {
412 let Some(direct_addr) = healthy_direct_route else {
413 return true;
414 };
415 if candidate_addr == direct_addr {
416 return false;
417 }
418
419 if !self.peers.contains_key(&direct_addr) {
420 return true;
421 }
422 if self.active_peer_uses_configured_static_udp_path(&direct_addr) {
423 return false;
424 }
425 let Some(candidate) = self.peers.get(&candidate_addr) else {
426 return false;
427 };
428 if !candidate.is_healthy() {
429 return false;
430 }
431
432 let direct_cost = self.dataplane_fmp_link_cost(&direct_addr);
433 let candidate_cost = self.dataplane_fmp_link_cost(&candidate_addr);
434 candidate_cost + ROUTING_FALLBACK_MIN_COST_ADVANTAGE < direct_cost
435 }
436
437 pub(super) fn select_tree_payload_candidate(
438 &self,
439 dest_coords: &crate::tree::TreeCoordinate,
440 direct_dest: &NodeAddr,
441 direct_payload_eligible: bool,
442 ) -> Option<NodeAddr> {
443 self.select_tree_payload_candidate_avoiding(
444 dest_coords,
445 direct_dest,
446 direct_payload_eligible,
447 None,
448 )
449 }
450
451 fn select_tree_payload_candidate_avoiding(
452 &self,
453 dest_coords: &crate::tree::TreeCoordinate,
454 direct_dest: &NodeAddr,
455 direct_payload_eligible: bool,
456 excluded: Option<&NodeAddr>,
457 ) -> Option<NodeAddr> {
458 if self.tree_state.my_coords().root_id() != dest_coords.root_id() {
459 return None;
460 }
461
462 let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
463 let mut best: Option<(NodeAddr, usize)> = None;
464
465 for (peer_addr, peer) in &self.peers {
466 if excluded == Some(peer_addr) {
467 continue;
468 }
469 if peer_addr == direct_dest {
470 if !direct_payload_eligible {
471 continue;
472 }
473 } else if !peer.is_healthy() {
474 continue;
475 }
476
477 let Some(peer_coords) = self.tree_state.peer_coords(peer_addr) else {
478 continue;
479 };
480 let distance = peer_coords.distance_to(dest_coords);
481 if distance >= my_distance {
482 continue;
483 }
484
485 let dominated = match &best {
486 None => true,
487 Some((best_id, best_dist)) => {
488 distance < *best_dist || (distance == *best_dist && peer_addr < best_id)
489 }
490 };
491 if dominated {
492 best = Some((*peer_addr, distance));
493 }
494 }
495
496 best.map(|(peer_addr, _)| peer_addr)
497 }
498
499 pub(in crate::node) fn session_direct_path_is_degraded(
500 &mut self,
501 dest: &NodeAddr,
502 now_ms: u64,
503 ) -> bool {
504 self.session_direct_degradation.is_degraded(dest, now_ms)
505 }
506
507 pub(in crate::node) fn session_direct_path_degradation_active(
508 &self,
509 dest: &NodeAddr,
510 now_ms: u64,
511 ) -> bool {
512 self.session_direct_degradation.is_degraded_at(dest, now_ms)
513 }
514
515 pub(in crate::node) fn session_direct_path_blocks_direct_payload(
516 &mut self,
517 dest: &NodeAddr,
518 now_ms: u64,
519 ) -> bool {
520 self.session_direct_path_is_degraded(dest, now_ms)
521 || self.session_direct_discovered_endpoint_trust_expired(dest, now_ms)
522 }
523
524 pub(in crate::node) fn session_direct_path_exclusive_trust_timeout_ms(&self) -> u64 {
525 self.config
526 .node
527 .heartbeat_interval_secs
528 .saturating_mul(1000)
529 .saturating_add(1_500)
530 .max(SESSION_DIRECT_MIN_EXCLUSIVE_TRUST_MS)
531 }
532
533 pub(in crate::node) fn session_direct_path_exclusive_trust_expired(
534 &self,
535 dest: &NodeAddr,
536 now_ms: u64,
537 ) -> bool {
538 if !self
539 .peers
540 .get(dest)
541 .is_some_and(|peer| peer.is_healthy() && peer.can_send())
542 {
543 return false;
544 }
545 let Some(activity) = self.dataplane.fsp_owner_activity(dest) else {
546 return false;
547 };
548 activity.has_recent_outbound_without_data_return_from(
549 dest,
550 now_ms,
551 self.session_direct_path_exclusive_trust_timeout_ms(),
552 )
553 }
554
555 pub(in crate::node) fn session_direct_path_has_recent_data_return(
556 &self,
557 dest: &NodeAddr,
558 now_ms: u64,
559 ) -> bool {
560 self.dataplane
561 .fsp_owner_activity(dest)
562 .is_some_and(|activity| {
563 activity.has_recent_data_return_from(
564 dest,
565 now_ms,
566 self.session_direct_path_exclusive_trust_timeout_ms(),
567 )
568 })
569 }
570
571 fn session_direct_discovered_endpoint_trust_expired(
572 &self,
573 dest: &NodeAddr,
574 now_ms: u64,
575 ) -> bool {
576 self.session_direct_path_exclusive_trust_expired(dest, now_ms)
577 && self.configured_peer(dest).is_some_and(|peer_config| {
578 peer_config.is_auto_connect()
579 && self.active_peer_uses_traversal_path(dest, peer_config)
580 })
581 }
582
583 pub(in crate::node) fn mark_session_direct_path_degraded(
584 &mut self,
585 dest: NodeAddr,
586 now_ms: u64,
587 ) -> bool {
588 let changed = self.session_direct_degradation.mark_degraded(
589 dest,
590 now_ms,
591 SESSION_DIRECT_DEGRADED_HOLD_MS,
592 );
593 if changed {
594 let _ = self.refresh_dataplane_fsp_owner_routes(&dest);
595 }
596 changed
597 }
598
599 pub(in crate::node) fn clear_session_direct_path_degraded(&mut self, dest: &NodeAddr) -> bool {
600 let changed = self.session_direct_degradation.clear(dest);
601 if changed {
602 let _ = self.refresh_dataplane_fsp_owner_routes(dest);
603 }
604 changed
605 }
606
607 pub(in crate::node) fn clear_session_direct_path_degraded_after_promotion(
608 &mut self,
609 dest: &NodeAddr,
610 now_ms: u64,
611 ) {
612 let keep_degraded = self.session_direct_path_blocks_direct_payload(dest, now_ms);
613 if !keep_degraded {
614 self.clear_session_direct_path_degraded(dest);
615 } else if self.promoted_path_matches_configured_static_peer(dest) {
616 debug!(
617 peer = %self.peer_display_name(dest),
618 "Clearing direct payload degradation after configured direct-path promotion"
619 );
620 self.clear_session_direct_path_degraded(dest);
621 } else {
622 debug!(
623 peer = %self.peer_display_name(dest),
624 "Keeping direct payload degraded after direct-path promotion"
625 );
626 }
627 }
628
629 fn promoted_path_matches_configured_static_peer(&self, peer_node_addr: &NodeAddr) -> bool {
630 self.config
631 .auto_connect_peers()
632 .filter(|peer_config| {
633 PeerIdentity::from_npub(&peer_config.npub)
634 .ok()
635 .is_some_and(|identity| identity.node_addr() == peer_node_addr)
636 })
637 .any(|peer_config| {
638 self.static_peer_addresses(peer_config)
639 .iter()
640 .any(|candidate| self.active_peer_matches_candidate(peer_node_addr, candidate))
641 })
642 }
643
644 pub(in crate::node) fn learn_reverse_route(
645 &mut self,
646 destination: NodeAddr,
647 next_hop: NodeAddr,
648 ) {
649 if self.config.node.routing.mode != RoutingMode::ReplyLearned
650 || destination == *self.node_addr()
651 {
652 return;
653 }
654 let now_ms = Self::now_ms();
655 self.learned_routes.learn(
656 destination,
657 next_hop,
658 now_ms,
659 self.config.node.routing.learned_ttl_secs,
660 self.config.node.routing.max_learned_routes_per_dest,
661 );
662 let _ = self.refresh_dataplane_fsp_owner_routes_retaining_current(&destination);
667 }
668
669 pub(in crate::node) fn pin_handshake_reverse_route(
670 &mut self,
671 destination: NodeAddr,
672 next_hop: NodeAddr,
673 ) {
674 if self.config.node.routing.mode != RoutingMode::ReplyLearned
675 || destination == *self.node_addr()
676 {
677 return;
678 }
679 self.learned_routes.pin_handshake_route(
680 destination,
681 next_hop,
682 Self::now_ms(),
683 self.config.node.routing.learned_ttl_secs,
684 self.config.node.routing.max_learned_routes_per_dest,
685 );
686 }
687
688 pub(in crate::node) fn routing_error_matches_active_path(
689 &mut self,
690 destination: &NodeAddr,
691 previous_hop: &NodeAddr,
692 reporter: &NodeAddr,
693 ) -> bool {
694 if self.config.node.routing.mode != RoutingMode::ReplyLearned {
695 return true;
696 }
697 let Some(pinned_hop) = self
698 .learned_routes
699 .active_handshake_route(destination, Self::now_ms())
700 else {
701 return true;
702 };
703 pinned_hop == *previous_hop && pinned_hop == *reporter
712 }
713
714 pub(in crate::node) fn record_route_failure(
715 &mut self,
716 destination: NodeAddr,
717 next_hop: NodeAddr,
718 ) {
719 if self.config.node.routing.mode != RoutingMode::ReplyLearned {
720 return;
721 }
722 self.learned_routes.record_failure(&destination, &next_hop);
723 let _ = self.refresh_dataplane_fsp_owner_routes(&destination);
724 }
725
726 pub(crate) fn learned_route_table_snapshot(&self, now_ms: u64) -> LearnedRouteTableSnapshot {
727 self.learned_routes.snapshot(now_ms)
728 }
729
730 pub(in crate::node) fn purge_learned_routes(&mut self, now_ms: u64) {
731 self.learned_routes.purge_expired(now_ms);
732 }
733
734 pub(super) fn select_best_candidate<'a>(
743 &'a self,
744 candidates: &[&'a ActivePeer],
745 dest_coords: &crate::tree::TreeCoordinate,
746 ) -> Option<&'a ActivePeer> {
747 let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
748
749 let mut best: Option<(&ActivePeer, f64, usize)> = None;
750
751 for &candidate in candidates {
752 if !candidate.can_send() {
753 continue;
754 }
755
756 let cost = self.dataplane_fmp_link_cost(candidate.node_addr());
757
758 let dist = self
759 .tree_state
760 .peer_coords(candidate.node_addr())
761 .map(|pc| pc.distance_to(dest_coords))
762 .unwrap_or(usize::MAX);
763
764 if dist >= my_distance {
767 continue;
768 }
769
770 let dominated = match &best {
771 None => true,
772 Some((_, best_cost, best_dist)) => {
773 cost < *best_cost
774 || (cost == *best_cost && dist < *best_dist)
775 || (cost == *best_cost
776 && dist == *best_dist
777 && candidate.node_addr() < best.as_ref().unwrap().0.node_addr())
778 }
779 };
780
781 if dominated {
782 best = Some((candidate, cost, dist));
783 }
784 }
785
786 best.map(|(peer, _, _)| peer)
787 }
788
789 pub fn destination_in_filters(&self, dest: &NodeAddr) -> Vec<&ActivePeer> {
791 self.peers.values().filter(|p| p.may_reach(dest)).collect()
792 }
793}