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