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