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