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