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 failed_learned_routes = self.learned_routes.failed_next_hops(dest_node_addr, now_ms);
78 let direct_path_hard_degraded =
79 self.session_direct_path_is_degraded(dest_node_addr, now_ms);
80 let direct_path_soft_degraded = !direct_path_hard_degraded
81 && self.session_direct_discovered_endpoint_trust_expired(dest_node_addr, now_ms);
82 let fallback_peer_available = self.has_sendable_fallback_lookup_peer(dest_node_addr);
83 let direct_session_degraded =
84 fallback_peer_available && (direct_path_hard_degraded || direct_path_soft_degraded);
85 let direct_session_untrusted = !direct_session_degraded
86 && self.session_direct_path_exclusive_trust_expired(dest_node_addr, now_ms);
87 let stale_traversal_direct_route = self
88 .peers
89 .get(dest_node_addr)
90 .filter(|peer| {
91 !direct_path_hard_degraded
92 && !direct_session_degraded
93 && !peer.is_healthy()
94 && peer.can_send()
95 })
96 .and_then(|_| {
97 self.configured_peer(dest_node_addr)
98 .and_then(|peer_config| {
99 (peer_config.is_auto_connect()
100 && self.active_peer_uses_traversal_path(dest_node_addr, peer_config))
101 .then_some(*dest_node_addr)
102 })
103 });
104
105 let healthy_direct_route = self
106 .peers
107 .get(dest_node_addr)
108 .filter(|peer| peer.is_healthy() && !direct_session_degraded)
109 .map(|_| *dest_node_addr);
110 let direct_payload_untried = self.sessions.get(dest_node_addr).is_some()
111 && self
112 .dataplane
113 .fsp_owner_activity(dest_node_addr)
114 .is_none_or(|activity| activity.last_outbound_next_hop().is_none());
115
116 if let Some(direct_addr) = healthy_direct_route
125 && direct_payload_untried
126 && !direct_session_untrusted
127 {
128 return self.peers.get(&direct_addr);
129 }
130 let authenticated_direct_handshake = self.config.node.routing.mode
131 == RoutingMode::ReplyLearned
132 && self
133 .learned_routes
134 .active_handshake_route(dest_node_addr, now_ms)
135 == Some(*dest_node_addr);
136
137 if let Some(direct_addr) = healthy_direct_route
144 && authenticated_direct_handshake
145 && !direct_session_untrusted
146 {
147 return self.peers.get(&direct_addr);
148 }
149 let active_fallback_affinity = (self.config.node.routing.mode == RoutingMode::ReplyLearned)
150 .then(|| {
151 let activity = self.dataplane.fsp_owner_activity(dest_node_addr)?;
152 let next_hop = activity.last_outbound_next_hop()?;
153 (next_hop != *dest_node_addr
154 && activity.has_recent_outbound_activity(
155 now_ms,
156 self.session_direct_path_exclusive_trust_timeout_ms(),
157 )
158 && !failed_learned_routes.contains(&next_hop)
159 && self
160 .peers
161 .get(&next_hop)
162 .is_some_and(|peer| peer.is_healthy() && peer.can_send()))
163 .then_some(next_hop)
164 })
165 .flatten();
166 if let Some(next_hop_addr) = active_fallback_affinity {
167 self.learned_routes
168 .record_selected(dest_node_addr, &next_hop_addr, now_ms);
169 return self.peers.get(&next_hop_addr);
170 }
171 let direct_session_has_recent_data_return =
172 self.session_direct_path_has_recent_data_return(dest_node_addr, now_ms);
173 if let Some(direct_addr) = healthy_direct_route
174 && direct_session_has_recent_data_return
175 {
176 return self.peers.get(&direct_addr);
177 }
178 if let Some(direct_addr) = healthy_direct_route
179 && !direct_session_untrusted
180 && self.dataplane_fmp_link_cost(&direct_addr)
181 <= 1.0 + ROUTING_FALLBACK_MIN_COST_ADVANTAGE
182 {
183 return self.peers.get(&direct_addr);
184 }
185 let direct_payload_eligible = healthy_direct_route.is_some();
186 let payload_candidate_can_send = |addr: &NodeAddr, peer: &ActivePeer| {
187 if addr == dest_node_addr {
188 direct_payload_eligible
189 } else {
190 peer.is_healthy() && !failed_learned_routes.contains(addr)
191 }
192 };
193
194 let fallback_beats_direct = |node: &Self, fallback_addr: NodeAddr| {
199 if direct_session_untrusted {
200 return healthy_direct_route != Some(fallback_addr)
201 && node
202 .peers
203 .get(&fallback_addr)
204 .is_some_and(|peer| peer.is_healthy());
205 }
206 node.route_candidate_beats_direct(healthy_direct_route, fallback_addr)
207 };
208
209 let sendable_learned_peers = if self.config.node.routing.mode == RoutingMode::ReplyLearned {
210 Some(
211 self.peers
212 .iter()
213 .filter(|(addr, peer)| payload_candidate_can_send(addr, peer))
214 .map(|(addr, _)| *addr)
215 .collect::<HashSet<_>>(),
216 )
217 } else {
218 None
219 };
220
221 let explore_fallback = sendable_learned_peers.as_ref().is_some_and(|sendable| {
222 self.learned_routes.should_explore_fallback(
223 dest_node_addr,
224 now_ms,
225 self.config.node.routing.learned_fallback_explore_interval,
226 |addr| sendable.contains(addr),
227 )
228 });
229 if let Some(sendable) = &sendable_learned_peers
236 && !explore_fallback
237 {
238 let eligible = sendable
239 .iter()
240 .copied()
241 .filter(|addr| fallback_beats_direct(self, *addr))
242 .collect::<HashSet<_>>();
243 if !eligible.is_empty()
244 && let Some(next_hop_addr) =
245 self.learned_routes
246 .select_next_hop(dest_node_addr, now_ms, |addr| eligible.contains(addr))
247 {
248 return self.peers.get(&next_hop_addr);
249 }
250 }
251
252 let Some(dest_coords) = self
254 .coord_cache
255 .get_and_touch(dest_node_addr, now_ms)
256 .cloned()
257 else {
258 if (healthy_direct_route.is_none() || explore_fallback)
259 && let Some(sendable) = &sendable_learned_peers
260 && let Some(next_hop_addr) =
261 self.learned_routes
262 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
263 {
264 return self.peers.get(&next_hop_addr);
265 }
266 if let Some(direct_addr) = healthy_direct_route {
267 return self.peers.get(&direct_addr);
268 }
269 if let Some(direct_addr) = stale_traversal_direct_route {
270 return self.peers.get(&direct_addr);
271 }
272 return None;
273 };
274
275 let coordinate_route_addr = {
278 let candidates: Vec<&ActivePeer> = self
279 .peers
280 .iter()
281 .filter(|(addr, peer)| {
282 payload_candidate_can_send(addr, peer) && peer.may_reach(dest_node_addr)
283 })
284 .map(|(_, peer)| peer)
285 .collect();
286 if !candidates.is_empty() {
287 self.select_best_candidate(&candidates, &dest_coords)
288 .map(|peer| *peer.node_addr())
289 } else {
290 None
291 }
292 };
293 if let Some(next_hop_addr) = coordinate_route_addr
294 && fallback_beats_direct(self, next_hop_addr)
295 {
296 return self.peers.get(&next_hop_addr);
297 }
298
299 let tree_route_addr = self.select_tree_payload_candidate(
301 &dest_coords,
302 dest_node_addr,
303 direct_payload_eligible,
304 );
305 if let Some(next_hop_addr) = tree_route_addr
306 && fallback_beats_direct(self, next_hop_addr)
307 {
308 return self.peers.get(&next_hop_addr);
309 }
310
311 if explore_fallback
312 && let Some(peer) = sendable_learned_peers.as_ref().and_then(|sendable| {
313 self.learned_routes
314 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
315 .and_then(|next_hop_addr| self.peers.get(&next_hop_addr))
316 })
317 {
318 return Some(peer);
319 }
320
321 if let Some(direct_addr) = healthy_direct_route {
322 return self.peers.get(&direct_addr);
323 }
324
325 if let Some(sendable) = &sendable_learned_peers
326 && let Some(next_hop_addr) =
327 self.learned_routes
328 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
329 {
330 return self.peers.get(&next_hop_addr);
331 }
332
333 if let Some(direct_addr) = stale_traversal_direct_route {
334 return self.peers.get(&direct_addr);
335 }
336
337 None
338 }
339
340 pub(in crate::node) fn plan_transit_next_hop(
341 &mut self,
342 dest_node_addr: &NodeAddr,
343 previous_hop: &NodeAddr,
344 ) -> TransitNextHopPlan {
345 if dest_node_addr == self.node_addr() {
346 return TransitNextHopPlan::NoRoute;
347 }
348
349 if dest_node_addr != previous_hop
350 && self
351 .peers
352 .get(dest_node_addr)
353 .is_some_and(|peer| peer.is_healthy())
354 {
355 return TransitNextHopPlan::Route(*dest_node_addr);
356 }
357
358 if self.config.node.routing.mode == RoutingMode::ReplyLearned {
364 let sendable = self
365 .peers
366 .iter()
367 .filter(|(addr, peer)| *addr != previous_hop && peer.is_healthy())
368 .map(|(addr, _)| *addr)
369 .collect::<HashSet<_>>();
370 if let Some(next_hop_addr) =
371 self.learned_routes
372 .select_handshake_route(dest_node_addr, Self::now_ms(), |addr| {
373 sendable.contains(addr)
374 })
375 {
376 return TransitNextHopPlan::Route(next_hop_addr);
377 }
378 }
379
380 let Some(next_hop_addr) = self
381 .find_next_hop(dest_node_addr)
382 .map(|peer| *peer.node_addr())
383 else {
384 return TransitNextHopPlan::NoRoute;
385 };
386 if next_hop_addr == *dest_node_addr && &next_hop_addr != previous_hop {
387 return TransitNextHopPlan::Route(next_hop_addr);
388 }
389
390 let now_ms = Self::now_ms();
391 let dest_coords = self
392 .coord_cache
393 .get_and_touch(dest_node_addr, now_ms)
394 .cloned();
395 let selected_strictly_progresses = dest_coords.as_ref().is_some_and(|dest_coords| {
396 self.tree_state.my_coords().root_id() == dest_coords.root_id()
397 && self
398 .tree_state
399 .peer_coords(&next_hop_addr)
400 .is_some_and(|peer_coords| {
401 peer_coords.distance_to(dest_coords)
402 < self.tree_state.my_coords().distance_to(dest_coords)
403 })
404 });
405
406 if &next_hop_addr != previous_hop && (dest_coords.is_none() || selected_strictly_progresses)
407 {
408 return TransitNextHopPlan::Route(next_hop_addr);
409 }
410
411 let coordinate_fallback = dest_coords.and_then(|dest_coords| {
412 self.select_tree_payload_candidate_avoiding(
413 &dest_coords,
414 dest_node_addr,
415 false,
416 Some(previous_hop),
417 )
418 });
419 if let Some(next_hop_addr) = coordinate_fallback {
420 return TransitNextHopPlan::Route(next_hop_addr);
421 }
422 TransitNextHopPlan::Loop(next_hop_addr)
423 }
424
425 #[cfg(test)]
426 pub(in crate::node) fn find_transit_next_hop(
427 &mut self,
428 dest_node_addr: &NodeAddr,
429 previous_hop: &NodeAddr,
430 ) -> Option<NodeAddr> {
431 match self.plan_transit_next_hop(dest_node_addr, previous_hop) {
432 TransitNextHopPlan::Route(next_hop_addr) => Some(next_hop_addr),
433 TransitNextHopPlan::Loop(next_hop_addr) => {
434 self.record_route_failure(*dest_node_addr, next_hop_addr);
435 None
436 }
437 TransitNextHopPlan::NoRoute => None,
438 }
439 }
440
441 pub(super) fn route_candidate_beats_direct(
442 &self,
443 healthy_direct_route: Option<NodeAddr>,
444 candidate_addr: NodeAddr,
445 ) -> bool {
446 let Some(direct_addr) = healthy_direct_route else {
447 return true;
448 };
449 if candidate_addr == direct_addr {
450 return false;
451 }
452
453 if !self.peers.contains_key(&direct_addr) {
454 return true;
455 }
456 if self.active_peer_uses_configured_static_udp_path(&direct_addr) {
457 return false;
458 }
459 let Some(candidate) = self.peers.get(&candidate_addr) else {
460 return false;
461 };
462 if !candidate.is_healthy() {
463 return false;
464 }
465
466 let direct_cost = self.dataplane_fmp_link_cost(&direct_addr);
467 let candidate_cost = self.dataplane_fmp_link_cost(&candidate_addr);
468 candidate_cost + ROUTING_FALLBACK_MIN_COST_ADVANTAGE < direct_cost
469 }
470
471 pub(super) fn select_tree_payload_candidate(
472 &self,
473 dest_coords: &crate::tree::TreeCoordinate,
474 direct_dest: &NodeAddr,
475 direct_payload_eligible: bool,
476 ) -> Option<NodeAddr> {
477 self.select_tree_payload_candidate_avoiding(
478 dest_coords,
479 direct_dest,
480 direct_payload_eligible,
481 None,
482 )
483 }
484
485 fn select_tree_payload_candidate_avoiding(
486 &self,
487 dest_coords: &crate::tree::TreeCoordinate,
488 direct_dest: &NodeAddr,
489 direct_payload_eligible: bool,
490 excluded: Option<&NodeAddr>,
491 ) -> Option<NodeAddr> {
492 if self.tree_state.my_coords().root_id() != dest_coords.root_id() {
493 return None;
494 }
495
496 let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
497 let mut best: Option<(NodeAddr, usize)> = None;
498
499 for (peer_addr, peer) in &self.peers {
500 if excluded == Some(peer_addr) {
501 continue;
502 }
503 if peer_addr == direct_dest {
504 if !direct_payload_eligible {
505 continue;
506 }
507 } else if !peer.is_healthy() {
508 continue;
509 }
510
511 let Some(peer_coords) = self.tree_state.peer_coords(peer_addr) else {
512 continue;
513 };
514 let distance = peer_coords.distance_to(dest_coords);
515 if distance >= my_distance {
516 continue;
517 }
518
519 let dominated = match &best {
520 None => true,
521 Some((best_id, best_dist)) => {
522 distance < *best_dist || (distance == *best_dist && peer_addr < best_id)
523 }
524 };
525 if dominated {
526 best = Some((*peer_addr, distance));
527 }
528 }
529
530 best.map(|(peer_addr, _)| peer_addr)
531 }
532
533 pub(in crate::node) fn session_direct_path_is_degraded(
534 &mut self,
535 dest: &NodeAddr,
536 now_ms: u64,
537 ) -> bool {
538 self.session_direct_degradation.is_degraded(dest, now_ms)
539 }
540
541 pub(in crate::node) fn session_direct_path_degradation_active(
542 &self,
543 dest: &NodeAddr,
544 now_ms: u64,
545 ) -> bool {
546 self.session_direct_degradation.is_degraded_at(dest, now_ms)
547 }
548
549 pub(in crate::node) fn session_direct_path_blocks_direct_payload(
550 &mut self,
551 dest: &NodeAddr,
552 now_ms: u64,
553 ) -> bool {
554 self.session_direct_path_is_degraded(dest, now_ms)
555 || self.session_direct_discovered_endpoint_trust_expired(dest, now_ms)
556 }
557
558 pub(in crate::node) fn session_direct_path_exclusive_trust_timeout_ms(&self) -> u64 {
559 self.config
560 .node
561 .heartbeat_interval_secs
562 .saturating_mul(1000)
563 .saturating_add(1_500)
564 .max(SESSION_DIRECT_MIN_EXCLUSIVE_TRUST_MS)
565 }
566
567 pub(in crate::node) fn session_direct_path_exclusive_trust_expired(
568 &self,
569 dest: &NodeAddr,
570 now_ms: u64,
571 ) -> bool {
572 if !self
573 .peers
574 .get(dest)
575 .is_some_and(|peer| peer.is_healthy() && peer.can_send())
576 {
577 return false;
578 }
579 let Some(activity) = self.dataplane.fsp_owner_activity(dest) else {
580 return false;
581 };
582 activity.has_recent_outbound_without_data_return_from(
583 dest,
584 now_ms,
585 self.session_direct_path_exclusive_trust_timeout_ms(),
586 )
587 }
588
589 pub(in crate::node) fn session_direct_path_has_recent_data_return(
590 &self,
591 dest: &NodeAddr,
592 now_ms: u64,
593 ) -> bool {
594 self.dataplane
595 .fsp_owner_activity(dest)
596 .is_some_and(|activity| {
597 activity.has_recent_data_return_from(
598 dest,
599 now_ms,
600 self.session_direct_path_exclusive_trust_timeout_ms(),
601 )
602 })
603 }
604
605 fn session_direct_discovered_endpoint_trust_expired(
606 &self,
607 dest: &NodeAddr,
608 now_ms: u64,
609 ) -> bool {
610 self.session_direct_path_exclusive_trust_expired(dest, now_ms)
611 && self.configured_peer(dest).is_some_and(|peer_config| {
612 peer_config.is_auto_connect()
613 && self.active_peer_uses_traversal_path(dest, peer_config)
614 })
615 }
616
617 pub(in crate::node) fn mark_session_direct_path_degraded(
618 &mut self,
619 dest: NodeAddr,
620 now_ms: u64,
621 ) -> bool {
622 let changed = self.session_direct_degradation.mark_degraded(
623 dest,
624 now_ms,
625 SESSION_DIRECT_DEGRADED_HOLD_MS,
626 );
627 if changed {
628 let _ = self.refresh_dataplane_fsp_owner_routes(&dest);
629 }
630 changed
631 }
632
633 pub(in crate::node) fn clear_session_direct_path_degraded(&mut self, dest: &NodeAddr) -> bool {
634 let changed = self.session_direct_degradation.clear(dest);
635 if changed {
636 let _ = self.refresh_dataplane_fsp_owner_routes(dest);
637 }
638 changed
639 }
640
641 pub(in crate::node) fn clear_session_direct_path_degraded_after_promotion(
642 &mut self,
643 dest: &NodeAddr,
644 now_ms: u64,
645 ) {
646 let direct_was_degraded = self.session_direct_path_degradation_active(dest, now_ms);
647 let active_fallback_next_hop = self
648 .dataplane
649 .fsp_owner_activity(dest)
650 .and_then(|activity| activity.last_outbound_next_hop())
651 .filter(|next_hop| next_hop != dest);
652 if direct_was_degraded || active_fallback_next_hop.is_some() {
653 if let Some(fallback_next_hop) = active_fallback_next_hop {
654 let _ = self
655 .dataplane
656 .forget_fsp_data_route(*dest, fallback_next_hop);
657 }
658 debug!(
659 peer = %self.peer_display_name(dest),
660 direct_was_degraded,
661 released_fallback_affinity = active_fallback_next_hop.is_some(),
662 "Authenticated direct-path promotion restored payload eligibility"
663 );
664 if !self.clear_session_direct_path_degraded(dest) {
665 let _ = self.refresh_dataplane_fsp_owner_routes(dest);
666 }
667 return;
668 }
669
670 let keep_degraded = self.session_direct_path_blocks_direct_payload(dest, now_ms);
671 if !keep_degraded {
672 self.clear_session_direct_path_degraded(dest);
673 } else if self.promoted_path_matches_configured_static_peer(dest) {
674 debug!(
675 peer = %self.peer_display_name(dest),
676 "Clearing direct payload degradation after configured direct-path promotion"
677 );
678 self.clear_session_direct_path_degraded(dest);
679 } else {
680 debug!(
681 peer = %self.peer_display_name(dest),
682 "Keeping direct payload degraded after direct-path promotion"
683 );
684 }
685 }
686
687 pub(in crate::node) fn retain_direct_payload_validation_after_fmp_rekey(
688 &mut self,
689 dest: &NodeAddr,
690 ) {
691 if !self.promoted_path_matches_configured_static_peer(dest) {
697 return;
698 }
699
700 let fallback_next_hop = self
701 .dataplane
702 .fsp_owner_activity(dest)
703 .and_then(|activity| activity.last_outbound_next_hop())
704 .filter(|next_hop| next_hop != dest);
705 if let Some(next_hop) = fallback_next_hop {
706 let _ = self.dataplane.forget_fsp_data_route(*dest, next_hop);
707 }
708 let _ = self
709 .session_direct_degradation
710 .release_hold_for_validation(dest, Self::now_ms());
711 let refreshed = self.refresh_dataplane_fsp_owner_routes(dest);
712 let pending_payload_validation =
713 self.session_direct_degradation.has_pending_validation(dest);
714 debug!(
715 peer = %self.peer_display_name(dest),
716 released_fallback_affinity = fallback_next_hop.is_some(),
717 refreshed,
718 pending_payload_validation,
719 "Authenticated FMP rekey made direct FSP payload eligible for validation"
720 );
721 if !pending_payload_validation {
722 self.clear_retry_unless_direct_refresh_needed(dest);
723 }
724 }
725
726 fn promoted_path_matches_configured_static_peer(&self, peer_node_addr: &NodeAddr) -> bool {
727 self.config
728 .auto_connect_peers()
729 .filter(|peer_config| {
730 PeerIdentity::from_npub(&peer_config.npub)
731 .ok()
732 .is_some_and(|identity| identity.node_addr() == peer_node_addr)
733 })
734 .any(|peer_config| {
735 self.static_peer_addresses(peer_config)
736 .iter()
737 .any(|candidate| self.active_peer_matches_candidate(peer_node_addr, candidate))
738 })
739 }
740
741 pub(in crate::node) fn learn_reverse_route(
742 &mut self,
743 destination: NodeAddr,
744 next_hop: NodeAddr,
745 ) {
746 if self.config.node.routing.mode != RoutingMode::ReplyLearned
747 || destination == *self.node_addr()
748 {
749 return;
750 }
751 let now_ms = Self::now_ms();
752 self.learned_routes.learn(
753 destination,
754 next_hop,
755 now_ms,
756 self.config.node.routing.learned_ttl_secs,
757 self.config.node.routing.max_learned_routes_per_dest,
758 );
759 let _ = self.refresh_dataplane_fsp_owner_routes_retaining_current(&destination);
764 }
765
766 pub(in crate::node) fn pin_handshake_reverse_route(
767 &mut self,
768 destination: NodeAddr,
769 next_hop: NodeAddr,
770 ) {
771 if self.config.node.routing.mode != RoutingMode::ReplyLearned
772 || destination == *self.node_addr()
773 {
774 return;
775 }
776 self.learned_routes.pin_handshake_route(
777 destination,
778 next_hop,
779 Self::now_ms(),
780 self.config.node.routing.learned_ttl_secs,
781 self.config.node.routing.max_learned_routes_per_dest,
782 );
783 }
784
785 pub(in crate::node) fn pin_duplicate_handshake_reverse_route(
786 &mut self,
787 destination: NodeAddr,
788 next_hop: NodeAddr,
789 ) {
790 if self.config.node.routing.mode != RoutingMode::ReplyLearned
791 || destination == *self.node_addr()
792 {
793 return;
794 }
795 let now_ms = Self::now_ms();
796 if self
797 .learned_routes
798 .active_handshake_route(&destination, now_ms)
799 .is_some_and(|pinned_hop| pinned_hop != next_hop)
800 {
801 return;
802 }
803 self.learned_routes.pin_handshake_route(
804 destination,
805 next_hop,
806 now_ms,
807 self.config.node.routing.learned_ttl_secs,
808 self.config.node.routing.max_learned_routes_per_dest,
809 );
810 }
811
812 pub(in crate::node) fn routing_error_matches_active_path(
813 &mut self,
814 destination: &NodeAddr,
815 previous_hop: &NodeAddr,
816 ) -> bool {
817 if self.config.node.routing.mode != RoutingMode::ReplyLearned {
818 return true;
819 }
820
821 if let Some(last_outbound_next_hop) = self
827 .dataplane
828 .fsp_owner_activity(destination)
829 .and_then(|activity| activity.last_outbound_next_hop())
830 {
831 return last_outbound_next_hop == *previous_hop;
832 }
833
834 if let Some(pinned_hop) = self
835 .learned_routes
836 .active_handshake_route(destination, Self::now_ms())
837 {
838 return pinned_hop == *previous_hop;
842 }
843
844 self.dataplane
853 .fsp_owner_next_hop(destination)
854 .is_none_or(|next_hop| next_hop == *previous_hop)
855 }
856
857 pub(in crate::node) fn record_route_failure(
858 &mut self,
859 destination: NodeAddr,
860 next_hop: NodeAddr,
861 ) {
862 if self.config.node.routing.mode != RoutingMode::ReplyLearned {
863 return;
864 }
865 let _ = self.dataplane.forget_fsp_data_route(destination, next_hop);
866 self.learned_routes.record_failure(&destination, &next_hop);
867 let _ = self.refresh_dataplane_fsp_owner_routes(&destination);
868 }
869
870 pub(crate) fn learned_route_table_snapshot(&self, now_ms: u64) -> LearnedRouteTableSnapshot {
871 self.learned_routes.snapshot(now_ms)
872 }
873
874 pub(in crate::node) fn purge_learned_routes(&mut self, now_ms: u64) {
875 self.learned_routes.purge_expired(now_ms);
876 }
877
878 pub(super) fn select_best_candidate<'a>(
887 &'a self,
888 candidates: &[&'a ActivePeer],
889 dest_coords: &crate::tree::TreeCoordinate,
890 ) -> Option<&'a ActivePeer> {
891 let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
892
893 let mut best: Option<(&ActivePeer, f64, usize)> = None;
894
895 for &candidate in candidates {
896 if !candidate.can_send() {
897 continue;
898 }
899
900 let cost = self.dataplane_fmp_link_cost(candidate.node_addr());
901
902 let dist = self
903 .tree_state
904 .peer_coords(candidate.node_addr())
905 .map(|pc| pc.distance_to(dest_coords))
906 .unwrap_or(usize::MAX);
907
908 if dist >= my_distance {
911 continue;
912 }
913
914 let dominated = match &best {
915 None => true,
916 Some((_, best_cost, best_dist)) => {
917 cost < *best_cost
918 || (cost == *best_cost && dist < *best_dist)
919 || (cost == *best_cost
920 && dist == *best_dist
921 && candidate.node_addr() < best.as_ref().unwrap().0.node_addr())
922 }
923 };
924
925 if dominated {
926 best = Some((candidate, cost, dist));
927 }
928 }
929
930 best.map(|(peer, _, _)| peer)
931 }
932
933 pub fn destination_in_filters(&self, dest: &NodeAddr) -> Vec<&ActivePeer> {
935 self.peers.values().filter(|p| p.may_reach(dest)).collect()
936 }
937}