1use super::*;
2
3pub(in crate::node) enum TransitNextHopPlan {
4 Route(NodeAddr),
5 Loop(NodeAddr),
6 NoRoute,
7}
8
9impl Node {
10 pub(crate) fn is_tree_peer(&self, peer_addr: &NodeAddr) -> bool {
17 if !self.tree_state.is_root() && self.tree_state.my_declaration().parent_id() == peer_addr {
19 return true;
20 }
21 if let Some(decl) = self.tree_state.peer_declaration(peer_addr)
23 && decl.parent_id() == self.node_addr()
24 {
25 return true;
26 }
27 false
28 }
29
30 pub fn find_next_hop(&mut self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
57 if dest_node_addr == self.node_addr() {
59 return None;
60 }
61 let now_ms = Self::now_ms();
62 let direct_path_hard_degraded =
63 self.session_direct_path_is_degraded(dest_node_addr, now_ms);
64 let direct_path_soft_degraded = !direct_path_hard_degraded
65 && self.session_direct_discovered_endpoint_trust_expired(dest_node_addr, now_ms);
66 let fallback_peer_available = self.has_sendable_fallback_lookup_peer(dest_node_addr);
67 let direct_session_degraded =
68 fallback_peer_available && (direct_path_hard_degraded || direct_path_soft_degraded);
69 let direct_session_untrusted = !direct_session_degraded
70 && self.session_direct_path_exclusive_trust_expired(dest_node_addr, now_ms);
71 let stale_traversal_direct_route = self
72 .peers
73 .get(dest_node_addr)
74 .filter(|peer| {
75 !direct_path_hard_degraded
76 && !direct_session_degraded
77 && !peer.is_healthy()
78 && peer.can_send()
79 })
80 .and_then(|_| {
81 self.configured_peer(dest_node_addr)
82 .and_then(|peer_config| {
83 (peer_config.is_auto_connect()
84 && self.active_peer_uses_traversal_path(dest_node_addr, peer_config))
85 .then_some(*dest_node_addr)
86 })
87 });
88
89 let healthy_direct_route = self
90 .peers
91 .get(dest_node_addr)
92 .filter(|peer| peer.is_healthy() && !direct_session_degraded)
93 .map(|_| *dest_node_addr);
94 let direct_session_has_recent_data_return =
95 self.session_direct_path_has_recent_data_return(dest_node_addr, now_ms);
96 if let Some(direct_addr) = healthy_direct_route
97 && direct_session_has_recent_data_return
98 {
99 return self.peers.get(&direct_addr);
100 }
101 if let Some(direct_addr) = healthy_direct_route
102 && !direct_session_untrusted
103 && self.dataplane_fmp_link_cost(&direct_addr)
104 <= 1.0 + ROUTING_FALLBACK_MIN_COST_ADVANTAGE
105 {
106 return self.peers.get(&direct_addr);
107 }
108 let direct_payload_eligible = healthy_direct_route.is_some();
109 let payload_candidate_can_send = |addr: &NodeAddr, peer: &ActivePeer| {
110 if addr == dest_node_addr {
111 direct_payload_eligible
112 } else {
113 peer.is_healthy()
114 }
115 };
116
117 let fallback_beats_direct = |node: &Self, fallback_addr: NodeAddr| {
122 if direct_session_untrusted {
123 return healthy_direct_route != Some(fallback_addr)
124 && node
125 .peers
126 .get(&fallback_addr)
127 .is_some_and(|peer| peer.is_healthy());
128 }
129 node.route_candidate_beats_direct(healthy_direct_route, fallback_addr)
130 };
131
132 let sendable_learned_peers = if self.config.node.routing.mode == RoutingMode::ReplyLearned {
133 Some(
134 self.peers
135 .iter()
136 .filter(|(addr, peer)| payload_candidate_can_send(addr, peer))
137 .map(|(addr, _)| *addr)
138 .collect::<HashSet<_>>(),
139 )
140 } else {
141 None
142 };
143
144 let explore_fallback = sendable_learned_peers.as_ref().is_some_and(|sendable| {
145 self.learned_routes.should_explore_fallback(
146 dest_node_addr,
147 now_ms,
148 self.config.node.routing.learned_fallback_explore_interval,
149 |addr| sendable.contains(addr),
150 )
151 });
152 if let Some(next_hop_addr) = sendable_learned_peers.as_ref().and_then(|sendable| {
153 let activity = self.dataplane.fsp_owner_activity(dest_node_addr)?;
154 let next_hop_addr = activity.last_outbound_next_hop()?;
155 if next_hop_addr == *dest_node_addr
156 || !activity.has_recent_outbound_activity(
157 now_ms,
158 self.session_direct_path_exclusive_trust_timeout_ms(),
159 )
160 || !sendable.contains(&next_hop_addr)
161 || !fallback_beats_direct(self, next_hop_addr)
162 {
163 return None;
164 }
165 Some(next_hop_addr)
166 }) {
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
172 if let Some(sendable) = &sendable_learned_peers
179 && !explore_fallback
180 {
181 let eligible = sendable
182 .iter()
183 .copied()
184 .filter(|addr| fallback_beats_direct(self, *addr))
185 .collect::<HashSet<_>>();
186 if !eligible.is_empty()
187 && let Some(next_hop_addr) =
188 self.learned_routes
189 .select_next_hop(dest_node_addr, now_ms, |addr| eligible.contains(addr))
190 {
191 return self.peers.get(&next_hop_addr);
192 }
193 }
194
195 let Some(dest_coords) = self
197 .coord_cache
198 .get_and_touch(dest_node_addr, now_ms)
199 .cloned()
200 else {
201 if (healthy_direct_route.is_none() || explore_fallback)
202 && let Some(sendable) = &sendable_learned_peers
203 && let Some(next_hop_addr) =
204 self.learned_routes
205 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
206 {
207 return self.peers.get(&next_hop_addr);
208 }
209 if let Some(direct_addr) = healthy_direct_route {
210 return self.peers.get(&direct_addr);
211 }
212 if let Some(direct_addr) = stale_traversal_direct_route {
213 return self.peers.get(&direct_addr);
214 }
215 return None;
216 };
217
218 let coordinate_route_addr = {
221 let candidates: Vec<&ActivePeer> = self
222 .peers
223 .iter()
224 .filter(|(addr, peer)| {
225 payload_candidate_can_send(addr, peer) && peer.may_reach(dest_node_addr)
226 })
227 .map(|(_, peer)| peer)
228 .collect();
229 if !candidates.is_empty() {
230 self.select_best_candidate(&candidates, &dest_coords)
231 .map(|peer| *peer.node_addr())
232 } else {
233 None
234 }
235 };
236 if let Some(next_hop_addr) = coordinate_route_addr
237 && fallback_beats_direct(self, next_hop_addr)
238 {
239 return self.peers.get(&next_hop_addr);
240 }
241
242 let tree_route_addr = self.select_tree_payload_candidate(
244 &dest_coords,
245 dest_node_addr,
246 direct_payload_eligible,
247 );
248 if let Some(next_hop_addr) = tree_route_addr
249 && fallback_beats_direct(self, next_hop_addr)
250 {
251 return self.peers.get(&next_hop_addr);
252 }
253
254 if explore_fallback
255 && let Some(peer) = sendable_learned_peers.as_ref().and_then(|sendable| {
256 self.learned_routes
257 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
258 .and_then(|next_hop_addr| self.peers.get(&next_hop_addr))
259 })
260 {
261 return Some(peer);
262 }
263
264 if let Some(direct_addr) = healthy_direct_route {
265 return self.peers.get(&direct_addr);
266 }
267
268 if let Some(sendable) = &sendable_learned_peers
269 && let Some(next_hop_addr) =
270 self.learned_routes
271 .select_next_hop(dest_node_addr, now_ms, |addr| sendable.contains(addr))
272 {
273 return self.peers.get(&next_hop_addr);
274 }
275
276 if let Some(direct_addr) = stale_traversal_direct_route {
277 return self.peers.get(&direct_addr);
278 }
279
280 None
281 }
282
283 pub(in crate::node) fn plan_transit_next_hop(
284 &mut self,
285 dest_node_addr: &NodeAddr,
286 previous_hop: &NodeAddr,
287 ) -> TransitNextHopPlan {
288 if dest_node_addr == self.node_addr() {
289 return TransitNextHopPlan::NoRoute;
290 }
291
292 if dest_node_addr != previous_hop
293 && self
294 .peers
295 .get(dest_node_addr)
296 .is_some_and(|peer| peer.is_healthy())
297 {
298 return TransitNextHopPlan::Route(*dest_node_addr);
299 }
300
301 let Some(next_hop_addr) = self
302 .find_next_hop(dest_node_addr)
303 .map(|peer| *peer.node_addr())
304 else {
305 return TransitNextHopPlan::NoRoute;
306 };
307 if &next_hop_addr == previous_hop {
308 return TransitNextHopPlan::Loop(next_hop_addr);
309 }
310 TransitNextHopPlan::Route(next_hop_addr)
311 }
312
313 #[cfg(test)]
314 pub(in crate::node) fn find_transit_next_hop(
315 &mut self,
316 dest_node_addr: &NodeAddr,
317 previous_hop: &NodeAddr,
318 ) -> Option<NodeAddr> {
319 match self.plan_transit_next_hop(dest_node_addr, previous_hop) {
320 TransitNextHopPlan::Route(next_hop_addr) => Some(next_hop_addr),
321 TransitNextHopPlan::Loop(next_hop_addr) => {
322 self.record_route_failure(*dest_node_addr, next_hop_addr);
323 None
324 }
325 TransitNextHopPlan::NoRoute => None,
326 }
327 }
328
329 pub(super) fn route_candidate_beats_direct(
330 &self,
331 healthy_direct_route: Option<NodeAddr>,
332 candidate_addr: NodeAddr,
333 ) -> bool {
334 let Some(direct_addr) = healthy_direct_route else {
335 return true;
336 };
337 if candidate_addr == direct_addr {
338 return false;
339 }
340
341 if !self.peers.contains_key(&direct_addr) {
342 return true;
343 }
344 if self.active_peer_uses_configured_static_udp_path(&direct_addr) {
345 return false;
346 }
347 let Some(candidate) = self.peers.get(&candidate_addr) else {
348 return false;
349 };
350 if !candidate.is_healthy() {
351 return false;
352 }
353
354 let direct_cost = self.dataplane_fmp_link_cost(&direct_addr);
355 let candidate_cost = self.dataplane_fmp_link_cost(&candidate_addr);
356 candidate_cost + ROUTING_FALLBACK_MIN_COST_ADVANTAGE < direct_cost
357 }
358
359 pub(super) fn select_tree_payload_candidate(
360 &self,
361 dest_coords: &crate::tree::TreeCoordinate,
362 direct_dest: &NodeAddr,
363 direct_payload_eligible: bool,
364 ) -> Option<NodeAddr> {
365 if self.tree_state.my_coords().root_id() != dest_coords.root_id() {
366 return None;
367 }
368
369 let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
370 let mut best: Option<(NodeAddr, usize)> = None;
371
372 for (peer_addr, peer) in &self.peers {
373 if peer_addr == direct_dest {
374 if !direct_payload_eligible {
375 continue;
376 }
377 } else if !peer.is_healthy() {
378 continue;
379 }
380
381 let Some(peer_coords) = self.tree_state.peer_coords(peer_addr) else {
382 continue;
383 };
384 let distance = peer_coords.distance_to(dest_coords);
385 if distance >= my_distance {
386 continue;
387 }
388
389 let dominated = match &best {
390 None => true,
391 Some((best_id, best_dist)) => {
392 distance < *best_dist || (distance == *best_dist && peer_addr < best_id)
393 }
394 };
395 if dominated {
396 best = Some((*peer_addr, distance));
397 }
398 }
399
400 best.map(|(peer_addr, _)| peer_addr)
401 }
402
403 pub(in crate::node) fn session_direct_path_is_degraded(
404 &mut self,
405 dest: &NodeAddr,
406 now_ms: u64,
407 ) -> bool {
408 self.session_direct_degradation.is_degraded(dest, now_ms)
409 }
410
411 pub(in crate::node) fn session_direct_path_degradation_active(
412 &self,
413 dest: &NodeAddr,
414 now_ms: u64,
415 ) -> bool {
416 self.session_direct_degradation.is_degraded_at(dest, now_ms)
417 }
418
419 pub(in crate::node) fn session_direct_path_blocks_direct_payload(
420 &mut self,
421 dest: &NodeAddr,
422 now_ms: u64,
423 ) -> bool {
424 self.session_direct_path_is_degraded(dest, now_ms)
425 || self.session_direct_discovered_endpoint_trust_expired(dest, now_ms)
426 }
427
428 pub(in crate::node) fn session_direct_path_exclusive_trust_timeout_ms(&self) -> u64 {
429 self.config
430 .node
431 .heartbeat_interval_secs
432 .saturating_mul(1000)
433 .saturating_add(1_500)
434 .max(SESSION_DIRECT_MIN_EXCLUSIVE_TRUST_MS)
435 }
436
437 pub(in crate::node) fn session_direct_path_exclusive_trust_expired(
438 &self,
439 dest: &NodeAddr,
440 now_ms: u64,
441 ) -> bool {
442 if !self
443 .peers
444 .get(dest)
445 .is_some_and(|peer| peer.is_healthy() && peer.can_send())
446 {
447 return false;
448 }
449 let Some(activity) = self.dataplane.fsp_owner_activity(dest) else {
450 return false;
451 };
452 activity.has_recent_outbound_without_data_return_from(
453 dest,
454 now_ms,
455 self.session_direct_path_exclusive_trust_timeout_ms(),
456 )
457 }
458
459 pub(in crate::node) fn session_direct_path_has_recent_data_return(
460 &self,
461 dest: &NodeAddr,
462 now_ms: u64,
463 ) -> bool {
464 self.dataplane
465 .fsp_owner_activity(dest)
466 .is_some_and(|activity| {
467 activity.has_recent_data_return_from(
468 dest,
469 now_ms,
470 self.session_direct_path_exclusive_trust_timeout_ms(),
471 )
472 })
473 }
474
475 fn session_direct_discovered_endpoint_trust_expired(
476 &self,
477 dest: &NodeAddr,
478 now_ms: u64,
479 ) -> bool {
480 self.session_direct_path_exclusive_trust_expired(dest, now_ms)
481 && self.configured_peer(dest).is_some_and(|peer_config| {
482 peer_config.is_auto_connect()
483 && self.active_peer_uses_traversal_path(dest, peer_config)
484 })
485 }
486
487 pub(in crate::node) fn mark_session_direct_path_degraded(
488 &mut self,
489 dest: NodeAddr,
490 now_ms: u64,
491 ) -> bool {
492 let changed = self.session_direct_degradation.mark_degraded(
493 dest,
494 now_ms,
495 SESSION_DIRECT_DEGRADED_HOLD_MS,
496 );
497 if changed {
498 let _ = self.refresh_dataplane_fsp_owner_routes(&dest);
499 }
500 changed
501 }
502
503 pub(in crate::node) fn clear_session_direct_path_degraded(&mut self, dest: &NodeAddr) -> bool {
504 let changed = self.session_direct_degradation.clear(dest);
505 if changed {
506 let _ = self.refresh_dataplane_fsp_owner_routes(dest);
507 }
508 changed
509 }
510
511 pub(in crate::node) fn clear_session_direct_path_degraded_after_promotion(
512 &mut self,
513 dest: &NodeAddr,
514 now_ms: u64,
515 ) {
516 let keep_degraded = self.session_direct_path_blocks_direct_payload(dest, now_ms);
517 if !keep_degraded {
518 self.clear_session_direct_path_degraded(dest);
519 } else if self.promoted_path_matches_configured_static_peer(dest) {
520 debug!(
521 peer = %self.peer_display_name(dest),
522 "Clearing direct payload degradation after configured direct-path promotion"
523 );
524 self.clear_session_direct_path_degraded(dest);
525 } else {
526 debug!(
527 peer = %self.peer_display_name(dest),
528 "Keeping direct payload degraded after direct-path promotion"
529 );
530 }
531 }
532
533 fn promoted_path_matches_configured_static_peer(&self, peer_node_addr: &NodeAddr) -> bool {
534 self.config
535 .auto_connect_peers()
536 .filter(|peer_config| {
537 PeerIdentity::from_npub(&peer_config.npub)
538 .ok()
539 .is_some_and(|identity| identity.node_addr() == peer_node_addr)
540 })
541 .any(|peer_config| {
542 self.static_peer_addresses(peer_config)
543 .iter()
544 .any(|candidate| self.active_peer_matches_candidate(peer_node_addr, candidate))
545 })
546 }
547
548 pub(in crate::node) fn learn_reverse_route(
549 &mut self,
550 destination: NodeAddr,
551 next_hop: NodeAddr,
552 ) {
553 if self.config.node.routing.mode != RoutingMode::ReplyLearned
554 || destination == *self.node_addr()
555 {
556 return;
557 }
558 let now_ms = Self::now_ms();
559 self.learned_routes.learn(
560 destination,
561 next_hop,
562 now_ms,
563 self.config.node.routing.learned_ttl_secs,
564 self.config.node.routing.max_learned_routes_per_dest,
565 );
566 let _ = self.refresh_dataplane_fsp_owner_routes(&destination);
567 }
568
569 pub(in crate::node) fn record_route_failure(
570 &mut self,
571 destination: NodeAddr,
572 next_hop: NodeAddr,
573 ) {
574 if self.config.node.routing.mode != RoutingMode::ReplyLearned {
575 return;
576 }
577 self.learned_routes.record_failure(&destination, &next_hop);
578 let _ = self.refresh_dataplane_fsp_owner_routes(&destination);
579 }
580
581 pub(crate) fn learned_route_table_snapshot(&self, now_ms: u64) -> LearnedRouteTableSnapshot {
582 self.learned_routes.snapshot(now_ms)
583 }
584
585 pub(in crate::node) fn purge_learned_routes(&mut self, now_ms: u64) {
586 self.learned_routes.purge_expired(now_ms);
587 }
588
589 pub(super) fn select_best_candidate<'a>(
598 &'a self,
599 candidates: &[&'a ActivePeer],
600 dest_coords: &crate::tree::TreeCoordinate,
601 ) -> Option<&'a ActivePeer> {
602 let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
603
604 let mut best: Option<(&ActivePeer, f64, usize)> = None;
605
606 for &candidate in candidates {
607 if !candidate.can_send() {
608 continue;
609 }
610
611 let cost = self.dataplane_fmp_link_cost(candidate.node_addr());
612
613 let dist = self
614 .tree_state
615 .peer_coords(candidate.node_addr())
616 .map(|pc| pc.distance_to(dest_coords))
617 .unwrap_or(usize::MAX);
618
619 if dist >= my_distance {
622 continue;
623 }
624
625 let dominated = match &best {
626 None => true,
627 Some((_, best_cost, best_dist)) => {
628 cost < *best_cost
629 || (cost == *best_cost && dist < *best_dist)
630 || (cost == *best_cost
631 && dist == *best_dist
632 && candidate.node_addr() < best.as_ref().unwrap().0.node_addr())
633 }
634 };
635
636 if dominated {
637 best = Some((candidate, cost, dist));
638 }
639 }
640
641 best.map(|(peer, _, _)| peer)
642 }
643
644 pub fn destination_in_filters(&self, dest: &NodeAddr) -> Vec<&ActivePeer> {
646 self.peers.values().filter(|p| p.may_reach(dest)).collect()
647 }
648}