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