fips_core/peer/active.rs
1//! Active Peer (Authenticated Phase)
2//!
3//! Represents a fully authenticated peer after successful Noise handshake.
4//! ActivePeer holds tree state, Bloom filter, and routing information.
5
6use crate::bloom::BloomFilter;
7use crate::mmp::{MmpConfig, MmpPeerState};
8use crate::node::REKEY_JITTER_SECS;
9use crate::noise::{HandshakeState as NoiseHandshakeState, NoiseError, NoiseSession};
10use crate::transport::{LinkId, LinkStats, TransportAddr, TransportId};
11use crate::tree::{ParentDeclaration, TreeCoordinate};
12use crate::utils::index::SessionIndex;
13use crate::{FipsAddress, NodeAddr, PeerIdentity};
14use rand::RngExt;
15use secp256k1::XOnlyPublicKey;
16use std::fmt;
17use std::time::{Duration, Instant};
18
19fn draw_rekey_jitter() -> i64 {
20 rand::rng().random_range(-REKEY_JITTER_SECS..=REKEY_JITTER_SECS)
21}
22
23/// Connectivity state for an active peer.
24///
25/// This is simpler than the full PeerState since authentication is complete.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum ConnectivityState {
28 /// Peer is fully connected and responsive.
29 Connected,
30 /// Peer hasn't been heard from recently (potential timeout).
31 Stale,
32 /// Connection lost, attempting to reconnect.
33 Reconnecting,
34 /// Peer has been explicitly disconnected.
35 Disconnected,
36}
37
38impl ConnectivityState {
39 /// Check if the peer is usable for sending traffic.
40 pub fn can_send(&self) -> bool {
41 matches!(
42 self,
43 ConnectivityState::Connected | ConnectivityState::Stale
44 )
45 }
46
47 /// Check if this is a terminal state requiring cleanup.
48 pub fn is_terminal(&self) -> bool {
49 matches!(self, ConnectivityState::Disconnected)
50 }
51
52 /// Check if peer is fully healthy.
53 pub fn is_healthy(&self) -> bool {
54 matches!(self, ConnectivityState::Connected)
55 }
56}
57
58impl fmt::Display for ConnectivityState {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 let s = match self {
61 ConnectivityState::Connected => "connected",
62 ConnectivityState::Stale => "stale",
63 ConnectivityState::Reconnecting => "reconnecting",
64 ConnectivityState::Disconnected => "disconnected",
65 };
66 write!(f, "{}", s)
67 }
68}
69
70/// A fully authenticated remote FIPS node.
71///
72/// Created only after successful Noise KK handshake. The identity is
73/// cryptographically verified at this point.
74///
75/// Note: ActivePeer intentionally does not implement Clone because it
76/// contains NoiseSession, which cannot be safely cloned (cloning would
77/// risk nonce reuse, a catastrophic security failure).
78#[derive(Debug)]
79pub struct ActivePeer {
80 // === Identity (Verified) ===
81 /// Cryptographic identity (verified via handshake).
82 identity: PeerIdentity,
83
84 // === Connection ===
85 /// Link used to reach this peer.
86 link_id: LinkId,
87 /// Current connectivity state.
88 connectivity: ConnectivityState,
89
90 // === Session (Wire Protocol) ===
91 /// Noise session for encryption/decryption (None if legacy peer).
92 noise_session: Option<NoiseSession>,
93 /// Our session index (they include this when sending TO us).
94 our_index: Option<SessionIndex>,
95 /// Their session index (we include this when sending TO them).
96 their_index: Option<SessionIndex>,
97 /// Transport ID for this peer's link.
98 transport_id: Option<TransportId>,
99 /// Current transport address (for roaming support).
100 current_addr: Option<TransportAddr>,
101
102 // === Spanning Tree ===
103 /// Their latest parent declaration.
104 declaration: Option<ParentDeclaration>,
105 /// Their path to root.
106 ancestry: Option<TreeCoordinate>,
107
108 // === Tree Announce Rate Limiting ===
109 /// Minimum interval between TreeAnnounce messages (milliseconds).
110 tree_announce_min_interval_ms: u64,
111 /// Last time we sent a TreeAnnounce to this peer (Unix milliseconds).
112 last_tree_announce_sent_ms: u64,
113 /// Whether a tree announce is pending (deferred due to rate limit).
114 pending_tree_announce: bool,
115
116 // === Bloom Filter ===
117 /// What's reachable through them (inbound filter).
118 inbound_filter: Option<BloomFilter>,
119 /// Their filter's sequence number.
120 filter_sequence: u64,
121 /// When we received their last filter (Unix milliseconds).
122 filter_received_at: u64,
123 /// Whether we owe them a filter update.
124 pending_filter_update: bool,
125
126 // === Timing ===
127 /// Session start time for computing session-relative timestamps.
128 /// Used as the epoch for the 4-byte inner header timestamp field.
129 session_start: Instant,
130
131 // === Statistics ===
132 /// Link statistics.
133 link_stats: LinkStats,
134 /// When this peer was authenticated (Unix milliseconds).
135 authenticated_at: u64,
136 /// When this peer was last seen (any activity, Unix milliseconds).
137 last_seen: u64,
138
139 // === Epoch (Restart Detection) ===
140 /// Remote peer's startup epoch (from handshake). Used to detect restarts.
141 remote_epoch: Option<[u8; 8]>,
142
143 // === MMP ===
144 /// Per-peer MMP state (None for legacy peers without Noise sessions).
145 mmp: Option<MmpPeerState>,
146
147 // === Heartbeat ===
148 /// When we last sent a heartbeat to this peer.
149 last_heartbeat_sent: Option<Instant>,
150
151 // === Handshake Resend ===
152 /// Wire-format msg2 for resend on duplicate msg1 (responder only).
153 /// Cleared after the handshake timeout window.
154 handshake_msg2: Option<Vec<u8>>,
155
156 // === Replay Detection Suppression ===
157 /// Number of replay detections suppressed since last session reset.
158 replay_suppressed_count: u32,
159 /// Consecutive decryption failures (reset on any successful decrypt).
160 consecutive_decrypt_failures: u32,
161
162 // === Rekey (Key Rotation) ===
163 /// When the current Noise session was established (for rekey timer).
164 session_established_at: Instant,
165 /// Per-session symmetric jitter applied to the rekey timer trigger.
166 rekey_jitter_secs: i64,
167 /// Current K-bit epoch value (alternates each rekey).
168 current_k_bit: bool,
169 /// Previous session kept alive during drain window after cutover.
170 previous_session: Option<NoiseSession>,
171 /// Previous session's our_index (for registry cleanup on drain expiry).
172 previous_our_index: Option<SessionIndex>,
173 /// When the drain window started (None = no drain in progress).
174 drain_started: Option<Instant>,
175 /// Pending new session from completed rekey (before K-bit cutover).
176 pending_new_session: Option<NoiseSession>,
177 /// Pending new session's our_index.
178 pending_our_index: Option<SessionIndex>,
179 /// Pending new session's their_index.
180 pending_their_index: Option<SessionIndex>,
181 /// True when this node initiated the pending FMP rekey.
182 pending_rekey_initiator: bool,
183 /// When the pending FMP rekey completed locally.
184 pending_rekey_completed_at: Option<Instant>,
185 /// Whether a rekey is currently in progress (handshake sent, not yet complete).
186 rekey_in_progress: bool,
187 /// When we last received a rekey msg1 from this peer (dampening).
188 last_peer_rekey: Option<Instant>,
189 /// In-progress rekey: Noise handshake state (initiator only).
190 rekey_handshake: Option<NoiseHandshakeState>,
191 /// In-progress rekey: our new session index.
192 rekey_our_index: Option<SessionIndex>,
193 /// In-progress rekey: wire-format msg1 for resend.
194 rekey_msg1: Option<Vec<u8>>,
195 /// In-progress rekey: next resend timestamp (Unix ms).
196 rekey_msg1_next_resend: u64,
197 /// In-progress rekey: number of msg1 retransmissions performed so far.
198 rekey_msg1_resend_count: u32,
199
200 // === Connected Peer UDP Socket (Unix fast path) ===
201 /// Per-peer `connect()`-ed UDP socket, opened once we have a
202 /// stable kernel `SocketAddr` for the peer (i.e. session
203 /// established + transport address known). When `Some`, the
204 /// encrypt-worker send path can `sendmsg(2)` on this fd without
205 /// per-packet `msg_name` — the kernel-side route + neighbor cache
206 /// is pinned by the `connect()` call. On the receive side, Linux
207 /// and Darwin UDP demux preferentially route inbound packets from
208 /// this peer to this socket (most-specific 5-tuple match via
209 /// `SO_REUSEPORT`), so the paired drain thread must keep it empty.
210 ///
211 /// Closed automatically on Drop. Behind an `Arc` so the
212 /// encrypt-worker's send path can hold a refcount without owning
213 /// the only handle (rekey / address-change may rotate the socket
214 /// while older jobs are still in-flight on the worker channel).
215 #[cfg(any(target_os = "linux", target_os = "macos"))]
216 connected_udp:
217 Option<std::sync::Arc<crate::transport::udp::connected_peer::ConnectedPeerSocket>>,
218
219 /// Per-peer receive-drain thread. Always paired with
220 /// `connected_udp`: while the connected socket is installed, the
221 /// kernel UDP demux preferentially routes inbound packets from
222 /// this peer to it (via SO_REUSEPORT + 5-tuple match), so the
223 /// socket *must* be drained or packets pile up in its kernel
224 /// recv buffer. Drop signals the thread to exit via self-pipe.
225 #[cfg(any(target_os = "linux", target_os = "macos"))]
226 peer_recv_drain: Option<crate::transport::udp::peer_drain::PeerRecvDrain>,
227}
228
229impl ActivePeer {
230 /// Create a new active peer from verified identity.
231 ///
232 /// Called after successful authentication handshake.
233 /// For peers with Noise sessions, use `with_session` instead.
234 pub fn new(identity: PeerIdentity, link_id: LinkId, authenticated_at: u64) -> Self {
235 let now = Instant::now();
236 Self {
237 identity,
238 link_id,
239 connectivity: ConnectivityState::Connected,
240 noise_session: None,
241 our_index: None,
242 their_index: None,
243 transport_id: None,
244 current_addr: None,
245 declaration: None,
246 ancestry: None,
247 tree_announce_min_interval_ms: 500,
248 last_tree_announce_sent_ms: 0,
249 pending_tree_announce: false,
250 inbound_filter: None,
251 filter_sequence: 0,
252 filter_received_at: 0,
253 pending_filter_update: true, // Send filter on new connection
254 session_start: now,
255 link_stats: LinkStats::new(),
256 authenticated_at,
257 last_seen: authenticated_at,
258 remote_epoch: None,
259 mmp: None,
260 last_heartbeat_sent: None,
261 handshake_msg2: None,
262 replay_suppressed_count: 0,
263 consecutive_decrypt_failures: 0,
264 session_established_at: now,
265 rekey_jitter_secs: draw_rekey_jitter(),
266 current_k_bit: false,
267 previous_session: None,
268 previous_our_index: None,
269 drain_started: None,
270 pending_new_session: None,
271 pending_our_index: None,
272 pending_their_index: None,
273 pending_rekey_initiator: false,
274 pending_rekey_completed_at: None,
275 rekey_in_progress: false,
276 last_peer_rekey: None,
277 rekey_handshake: None,
278 rekey_our_index: None,
279 rekey_msg1: None,
280 rekey_msg1_next_resend: 0,
281 rekey_msg1_resend_count: 0,
282 #[cfg(any(target_os = "linux", target_os = "macos"))]
283 connected_udp: None,
284 #[cfg(any(target_os = "linux", target_os = "macos"))]
285 peer_recv_drain: None,
286 }
287 }
288
289 /// Create from verified identity with existing link stats.
290 ///
291 /// Used when promoting from PeerConnection, preserving handshake stats.
292 /// For peers with Noise sessions, use `with_session` instead.
293 pub fn with_stats(
294 identity: PeerIdentity,
295 link_id: LinkId,
296 authenticated_at: u64,
297 link_stats: LinkStats,
298 ) -> Self {
299 let mut peer = Self::new(identity, link_id, authenticated_at);
300 peer.link_stats = link_stats;
301 peer
302 }
303
304 /// Create from verified identity with Noise session and index tracking.
305 ///
306 /// This is the primary constructor for the wire protocol path.
307 /// The NoiseSession provides encryption/decryption and replay protection.
308 #[allow(clippy::too_many_arguments)]
309 pub fn with_session(
310 identity: PeerIdentity,
311 link_id: LinkId,
312 authenticated_at: u64,
313 noise_session: NoiseSession,
314 our_index: SessionIndex,
315 their_index: SessionIndex,
316 transport_id: TransportId,
317 current_addr: TransportAddr,
318 link_stats: LinkStats,
319 is_initiator: bool,
320 mmp_config: &MmpConfig,
321 remote_epoch: Option<[u8; 8]>,
322 ) -> Self {
323 let now = Instant::now();
324 Self {
325 identity,
326 link_id,
327 connectivity: ConnectivityState::Connected,
328 noise_session: Some(noise_session),
329 our_index: Some(our_index),
330 their_index: Some(their_index),
331 transport_id: Some(transport_id),
332 current_addr: Some(current_addr),
333 declaration: None,
334 ancestry: None,
335 tree_announce_min_interval_ms: 500,
336 last_tree_announce_sent_ms: 0,
337 pending_tree_announce: false,
338 inbound_filter: None,
339 filter_sequence: 0,
340 filter_received_at: 0,
341 pending_filter_update: true,
342 session_start: now,
343 link_stats,
344 authenticated_at,
345 last_seen: authenticated_at,
346 remote_epoch,
347 mmp: Some(MmpPeerState::new(mmp_config, is_initiator)),
348 last_heartbeat_sent: None,
349 handshake_msg2: None,
350 replay_suppressed_count: 0,
351 consecutive_decrypt_failures: 0,
352 session_established_at: now,
353 rekey_jitter_secs: draw_rekey_jitter(),
354 current_k_bit: false,
355 previous_session: None,
356 previous_our_index: None,
357 drain_started: None,
358 pending_new_session: None,
359 pending_our_index: None,
360 pending_their_index: None,
361 pending_rekey_initiator: false,
362 pending_rekey_completed_at: None,
363 rekey_in_progress: false,
364 last_peer_rekey: None,
365 rekey_handshake: None,
366 rekey_our_index: None,
367 rekey_msg1: None,
368 rekey_msg1_next_resend: 0,
369 rekey_msg1_resend_count: 0,
370 #[cfg(any(target_os = "linux", target_os = "macos"))]
371 connected_udp: None,
372 #[cfg(any(target_os = "linux", target_os = "macos"))]
373 peer_recv_drain: None,
374 }
375 }
376
377 /// Unix UDP fast path: clone the refcount on the per-peer
378 /// `connect()`-ed UDP socket if one has been installed. Encrypt-
379 /// worker send path uses this to bypass the wildcard listen
380 /// socket's per-packet sockaddr handling.
381 #[cfg(any(target_os = "linux", target_os = "macos"))]
382 pub(crate) fn connected_udp(
383 &self,
384 ) -> Option<std::sync::Arc<crate::transport::udp::connected_peer::ConnectedPeerSocket>> {
385 self.connected_udp.clone()
386 }
387
388 /// Install a per-peer `connect()`-ed UDP socket **with** its
389 /// paired recv drain thread. The two are owned together: the
390 /// drain thread is the only consumer of packets arriving on this
391 /// socket (Linux UDP demux preferentially routes them away from
392 /// the wildcard listen socket via SO_REUSEPORT 5-tuple match),
393 /// so installing one without the other would silently drop
394 /// inbound packets from this peer.
395 ///
396 /// Replacing an existing pair drops the old drain (its self-pipe
397 /// shutdown signal fires; thread exits within one poll
398 /// iteration) and drops the old socket Arc. Any encrypt-worker
399 /// jobs already in-flight holding the old socket Arc stay valid
400 /// until they complete, at which point the kernel fd closes.
401 #[cfg(any(target_os = "linux", target_os = "macos"))]
402 pub(crate) fn set_connected_udp(
403 &mut self,
404 socket: std::sync::Arc<crate::transport::udp::connected_peer::ConnectedPeerSocket>,
405 drain: crate::transport::udp::peer_drain::PeerRecvDrain,
406 ) {
407 // Drop order matters: drop the old drain BEFORE the old
408 // socket so the drain thread's last reference to the kernel
409 // fd is released cleanly.
410 self.peer_recv_drain = None;
411 self.connected_udp = None;
412 self.connected_udp = Some(socket);
413 self.peer_recv_drain = Some(drain);
414 }
415
416 /// Clear the per-peer connected UDP socket + drain thread (e.g.
417 /// on rekey or disconnect). The drain thread exits via self-pipe
418 /// signal; the kernel fd closes when the last `Arc` to the
419 /// socket drops.
420 #[cfg(any(target_os = "linux", target_os = "macos"))]
421 pub(crate) fn clear_connected_udp(&mut self) {
422 self.peer_recv_drain = None;
423 self.connected_udp = None;
424 }
425
426 // === Identity Accessors ===
427
428 /// Get the peer's verified identity.
429 pub fn identity(&self) -> &PeerIdentity {
430 &self.identity
431 }
432
433 /// Get the peer's NodeAddr.
434 pub fn node_addr(&self) -> &NodeAddr {
435 self.identity.node_addr()
436 }
437
438 /// Get the peer's FIPS address.
439 pub fn address(&self) -> &FipsAddress {
440 self.identity.address()
441 }
442
443 /// Get the peer's public key.
444 pub fn pubkey(&self) -> XOnlyPublicKey {
445 self.identity.pubkey()
446 }
447
448 /// Get the peer's npub string.
449 pub fn npub(&self) -> String {
450 self.identity.npub()
451 }
452
453 // === Connection Accessors ===
454
455 /// Get the link ID.
456 pub fn link_id(&self) -> LinkId {
457 self.link_id
458 }
459
460 /// Get the connectivity state.
461 pub fn connectivity(&self) -> ConnectivityState {
462 self.connectivity
463 }
464
465 /// Check if peer can receive traffic.
466 pub fn can_send(&self) -> bool {
467 self.connectivity.can_send()
468 }
469
470 /// Check if peer is fully healthy.
471 pub fn is_healthy(&self) -> bool {
472 self.connectivity.is_healthy()
473 }
474
475 /// Check if peer is disconnected.
476 pub fn is_disconnected(&self) -> bool {
477 self.connectivity.is_terminal()
478 }
479
480 // === Session Accessors ===
481
482 /// Check if this peer has a Noise session.
483 pub fn has_session(&self) -> bool {
484 self.noise_session.is_some()
485 }
486
487 /// Get the Noise session, if present.
488 pub fn noise_session(&self) -> Option<&NoiseSession> {
489 self.noise_session.as_ref()
490 }
491
492 /// Get mutable access to the Noise session.
493 pub fn noise_session_mut(&mut self) -> Option<&mut NoiseSession> {
494 self.noise_session.as_mut()
495 }
496
497 /// Get our session index (they use this to send TO us).
498 pub fn our_index(&self) -> Option<SessionIndex> {
499 self.our_index
500 }
501
502 /// Get their session index (we use this to send TO them).
503 pub fn their_index(&self) -> Option<SessionIndex> {
504 self.their_index
505 }
506
507 /// Update their session index (used during cross-connection resolution
508 /// when the losing node keeps its inbound session but needs the peer's
509 /// outbound index).
510 pub fn set_their_index(&mut self, index: SessionIndex) {
511 self.their_index = Some(index);
512 }
513
514 /// Replace the Noise session and indices during cross-connection resolution.
515 ///
516 /// When both nodes simultaneously initiate, each promotes its inbound
517 /// handshake first. When the peer's msg2 arrives, we learn the correct
518 /// session — the outbound handshake that pairs with the peer's inbound.
519 /// This replaces the entire session so both nodes use matching keys.
520 ///
521 /// Returns the old our_index so the caller can update session-index dispatch.
522 /// Also resets the replay suppression counter since the session changed.
523 pub fn replace_session(
524 &mut self,
525 new_session: NoiseSession,
526 new_our_index: SessionIndex,
527 new_their_index: SessionIndex,
528 ) -> Option<SessionIndex> {
529 self.reset_replay_suppressed();
530 let old_our_index = self.our_index;
531 self.noise_session = Some(new_session);
532 self.our_index = Some(new_our_index);
533 self.their_index = Some(new_their_index);
534 old_our_index
535 }
536
537 /// Get the transport ID for this peer.
538 pub fn transport_id(&self) -> Option<TransportId> {
539 self.transport_id
540 }
541
542 /// Get the current transport address.
543 pub fn current_addr(&self) -> Option<&TransportAddr> {
544 self.current_addr.as_ref()
545 }
546
547 /// Update the current address (for roaming support).
548 ///
549 /// Called when we receive a valid authenticated packet from a new address.
550 /// Short-circuits when neither the transport_id nor the TransportAddr
551 /// bytes changed — at multi-Gbps the same peer's source 4-tuple is
552 /// stable per session and the overwhelming majority of inbound
553 /// packets hit this fast path. Saves both the redundant
554 /// `Option::take` + Vec drop on the cached side and the caller's
555 /// `.clone()` allocation on the input side: the caller can pass
556 /// `&TransportAddr` and we only `.to_owned()` when storing.
557 ///
558 /// Returns `true` iff the stored `(transport_id, current_addr)` pair
559 /// actually changed. The caller uses this signal to invalidate
560 /// derived caches whose validity is bound to the peer's 5-tuple —
561 /// in particular the Linux per-peer `connect()`-ed UDP socket,
562 /// which is pinned to one kernel route + neighbour entry and goes
563 /// stale the moment the peer roams. (Clearing it here would force
564 /// `&mut self` users into the wrong shape: the policy of when to
565 /// rebuild the connected socket lives on `Node`, not on the peer
566 /// state. Returning a bool keeps that policy where it belongs.)
567 pub fn set_current_addr(&mut self, transport_id: TransportId, addr: &TransportAddr) -> bool {
568 if self.transport_id == Some(transport_id) && self.current_addr.as_ref() == Some(addr) {
569 return false;
570 }
571 self.transport_id = Some(transport_id);
572 self.current_addr = Some(addr.clone());
573 true
574 }
575
576 // === Handshake Resend ===
577
578 /// Store wire-format msg2 for resend on duplicate msg1.
579 pub fn set_handshake_msg2(&mut self, msg2: Vec<u8>) {
580 self.handshake_msg2 = Some(msg2);
581 }
582
583 /// Get stored msg2 bytes for resend.
584 pub fn handshake_msg2(&self) -> Option<&[u8]> {
585 self.handshake_msg2.as_deref()
586 }
587
588 /// Clear stored msg2 (no longer needed after handshake window).
589 pub fn clear_handshake_msg2(&mut self) {
590 self.handshake_msg2 = None;
591 }
592
593 // === Replay Detection Suppression ===
594
595 /// Increment replay suppression counter. Returns the new count.
596 pub fn increment_replay_suppressed(&mut self) -> u32 {
597 self.replay_suppressed_count += 1;
598 self.replay_suppressed_count
599 }
600
601 /// Reset replay suppression counter, returning previous count.
602 pub fn reset_replay_suppressed(&mut self) -> u32 {
603 let count = self.replay_suppressed_count;
604 self.replay_suppressed_count = 0;
605 count
606 }
607
608 /// Current replay suppression count.
609 pub fn replay_suppressed_count(&self) -> u32 {
610 self.replay_suppressed_count
611 }
612
613 // === Decryption Failure Tracking ===
614
615 /// Increment consecutive decryption failure counter, returning new count.
616 pub fn increment_decrypt_failures(&mut self) -> u32 {
617 self.consecutive_decrypt_failures += 1;
618 self.consecutive_decrypt_failures
619 }
620
621 /// Reset consecutive decryption failure counter.
622 pub fn reset_decrypt_failures(&mut self) {
623 self.consecutive_decrypt_failures = 0;
624 }
625
626 /// Current consecutive decryption failure count.
627 pub fn consecutive_decrypt_failures(&self) -> u32 {
628 self.consecutive_decrypt_failures
629 }
630
631 // === Epoch Accessors ===
632
633 /// Get the remote peer's startup epoch (from handshake).
634 pub fn remote_epoch(&self) -> Option<[u8; 8]> {
635 self.remote_epoch
636 }
637
638 /// Update the remote peer's startup epoch after a successful in-place
639 /// rekey. Initial handshakes set this through `with_session`, but recovery
640 /// rekeys also exchange epochs and must keep restart detection current.
641 pub(crate) fn set_remote_epoch(&mut self, remote_epoch: Option<[u8; 8]>) {
642 self.remote_epoch = remote_epoch;
643 }
644
645 // === Tree Accessors ===
646
647 /// Get the peer's tree coordinates, if known.
648 pub fn coords(&self) -> Option<&TreeCoordinate> {
649 self.ancestry.as_ref()
650 }
651
652 /// Get the peer's parent declaration, if known.
653 pub fn declaration(&self) -> Option<&ParentDeclaration> {
654 self.declaration.as_ref()
655 }
656
657 /// Check if this peer has a known tree position.
658 pub fn has_tree_position(&self) -> bool {
659 self.declaration.is_some() && self.ancestry.is_some()
660 }
661
662 // === Filter Accessors ===
663
664 /// Get the peer's inbound filter, if known.
665 pub fn inbound_filter(&self) -> Option<&BloomFilter> {
666 self.inbound_filter.as_ref()
667 }
668
669 /// Get the filter sequence number.
670 pub fn filter_sequence(&self) -> u64 {
671 self.filter_sequence
672 }
673
674 /// Check if this peer's filter is stale.
675 pub fn filter_is_stale(&self, current_time_ms: u64, stale_threshold_ms: u64) -> bool {
676 if self.filter_received_at == 0 {
677 return true;
678 }
679 current_time_ms.saturating_sub(self.filter_received_at) > stale_threshold_ms
680 }
681
682 /// Check if a destination might be reachable through this peer.
683 pub fn may_reach(&self, node_addr: &NodeAddr) -> bool {
684 match &self.inbound_filter {
685 Some(filter) => filter.contains(node_addr),
686 None => false,
687 }
688 }
689
690 /// Check if we need to send this peer a filter update.
691 pub fn needs_filter_update(&self) -> bool {
692 self.pending_filter_update
693 }
694
695 // === Statistics Accessors ===
696
697 /// Get link statistics.
698 pub fn link_stats(&self) -> &LinkStats {
699 &self.link_stats
700 }
701
702 /// Get mutable link statistics.
703 pub fn link_stats_mut(&mut self) -> &mut LinkStats {
704 &mut self.link_stats
705 }
706
707 // === MMP Accessors ===
708
709 /// Get MMP state (None for legacy peers without sessions).
710 pub fn mmp(&self) -> Option<&MmpPeerState> {
711 self.mmp.as_ref()
712 }
713
714 /// Get mutable MMP state.
715 pub fn mmp_mut(&mut self) -> Option<&mut MmpPeerState> {
716 self.mmp.as_mut()
717 }
718
719 /// Link cost for routing decisions.
720 ///
721 /// Returns a scalar cost where lower is better (1.0 = ideal).
722 /// Computed as RTT-weighted ETX: `etx * (1.0 + srtt_ms / 100.0)`.
723 ///
724 /// Returns 1.0 (optimistic default) when MMP metrics are not yet
725 /// available, matching depth-only parent selection behavior.
726 pub fn link_cost(&self) -> f64 {
727 match self.mmp() {
728 Some(mmp) => {
729 let etx = mmp.metrics.etx;
730 match mmp.metrics.srtt_ms() {
731 Some(srtt_ms) => etx * (1.0 + srtt_ms / 100.0),
732 None => 1.0,
733 }
734 }
735 None => 1.0,
736 }
737 }
738
739 /// Whether this peer has at least one MMP RTT measurement.
740 pub fn has_srtt(&self) -> bool {
741 self.mmp()
742 .is_some_and(|mmp| mmp.metrics.srtt_ms().is_some())
743 }
744
745 /// When this peer was authenticated.
746 pub fn authenticated_at(&self) -> u64 {
747 self.authenticated_at
748 }
749
750 /// When this peer was last seen.
751 pub fn last_seen(&self) -> u64 {
752 self.last_seen
753 }
754
755 /// Time since last activity.
756 pub fn idle_time(&self, current_time_ms: u64) -> u64 {
757 current_time_ms.saturating_sub(self.last_seen)
758 }
759
760 /// Connection duration since authentication.
761 pub fn connection_duration(&self, current_time_ms: u64) -> u64 {
762 current_time_ms.saturating_sub(self.authenticated_at)
763 }
764
765 /// Session-relative elapsed time in milliseconds (for inner header timestamp).
766 ///
767 /// Returns milliseconds since session establishment, truncated to u32.
768 /// Wraps at ~49.7 days which is acceptable for session-relative timing.
769 pub fn session_elapsed_ms(&self) -> u32 {
770 self.session_start.elapsed().as_millis() as u32
771 }
772
773 /// When this peer's session started (for link-dead fallback timing).
774 pub fn session_start(&self) -> Instant {
775 self.session_start
776 }
777
778 // === Heartbeat ===
779
780 /// When we last sent a heartbeat to this peer.
781 pub fn last_heartbeat_sent(&self) -> Option<Instant> {
782 self.last_heartbeat_sent
783 }
784
785 /// Record that we sent a heartbeat.
786 pub fn mark_heartbeat_sent(&mut self, now: Instant) {
787 self.last_heartbeat_sent = Some(now);
788 }
789
790 // === State Updates ===
791
792 /// Update last seen timestamp.
793 pub fn touch(&mut self, current_time_ms: u64) {
794 self.last_seen = current_time_ms;
795 // Stale links are still sendable, so authenticated traffic refreshes
796 // them. Reconnecting links were declared link-dead and need a fresh
797 // handshake/reprobe before they can carry traffic again.
798 if self.connectivity == ConnectivityState::Stale {
799 self.connectivity = ConnectivityState::Connected;
800 }
801 }
802
803 /// Mark peer as stale (no recent traffic).
804 pub fn mark_stale(&mut self) {
805 if self.connectivity == ConnectivityState::Connected {
806 self.connectivity = ConnectivityState::Stale;
807 }
808 }
809
810 /// Mark peer as reconnecting.
811 pub fn mark_reconnecting(&mut self) {
812 self.connectivity = ConnectivityState::Reconnecting;
813 }
814
815 /// Mark peer as disconnected.
816 pub fn mark_disconnected(&mut self) {
817 self.connectivity = ConnectivityState::Disconnected;
818 }
819
820 /// Mark peer as connected (e.g., after successful reconnect).
821 pub fn mark_connected(&mut self, current_time_ms: u64) {
822 self.connectivity = ConnectivityState::Connected;
823 self.last_seen = current_time_ms;
824 }
825
826 /// Update the link ID (e.g., on reconnect).
827 pub fn set_link_id(&mut self, link_id: LinkId) {
828 self.link_id = link_id;
829 }
830
831 // === Tree Updates ===
832
833 /// Update peer's tree position.
834 pub fn update_tree_position(
835 &mut self,
836 declaration: ParentDeclaration,
837 ancestry: TreeCoordinate,
838 current_time_ms: u64,
839 ) {
840 self.declaration = Some(declaration);
841 self.ancestry = Some(ancestry);
842 self.last_seen = current_time_ms;
843 }
844
845 /// Clear peer's tree position.
846 pub fn clear_tree_position(&mut self) {
847 self.declaration = None;
848 self.ancestry = None;
849 }
850
851 // === Tree Announce Rate Limiting ===
852
853 /// Set the minimum interval between TreeAnnounce messages (milliseconds).
854 pub fn set_tree_announce_min_interval_ms(&mut self, ms: u64) {
855 self.tree_announce_min_interval_ms = ms;
856 }
857
858 /// Get the last tree announce send timestamp (for carrying across reconnection).
859 pub fn last_tree_announce_sent_ms(&self) -> u64 {
860 self.last_tree_announce_sent_ms
861 }
862
863 /// Set the last tree announce send timestamp (to preserve rate limit across reconnection).
864 pub fn set_last_tree_announce_sent_ms(&mut self, ms: u64) {
865 self.last_tree_announce_sent_ms = ms;
866 }
867
868 /// Check if we can send a TreeAnnounce now (rate limiting).
869 pub fn can_send_tree_announce(&self, now_ms: u64) -> bool {
870 now_ms.saturating_sub(self.last_tree_announce_sent_ms) >= self.tree_announce_min_interval_ms
871 }
872
873 /// Record that we sent a TreeAnnounce to this peer.
874 pub fn record_tree_announce_sent(&mut self, now_ms: u64) {
875 self.last_tree_announce_sent_ms = now_ms;
876 self.pending_tree_announce = false;
877 }
878
879 /// Mark that a tree announce is pending (deferred due to rate limit).
880 pub fn mark_tree_announce_pending(&mut self) {
881 self.pending_tree_announce = true;
882 }
883
884 /// Check if a deferred tree announce is waiting to be sent.
885 pub fn has_pending_tree_announce(&self) -> bool {
886 self.pending_tree_announce
887 }
888
889 // === Filter Updates ===
890
891 /// Update peer's inbound filter.
892 pub fn update_filter(&mut self, filter: BloomFilter, sequence: u64, current_time_ms: u64) {
893 self.inbound_filter = Some(filter);
894 self.filter_sequence = sequence;
895 self.filter_received_at = current_time_ms;
896 self.last_seen = current_time_ms;
897 }
898
899 /// Clear peer's inbound filter.
900 pub fn clear_filter(&mut self) {
901 self.inbound_filter = None;
902 self.filter_sequence = 0;
903 self.filter_received_at = 0;
904 }
905
906 /// Mark that we need to send this peer a filter update.
907 pub fn mark_filter_update_needed(&mut self) {
908 self.pending_filter_update = true;
909 }
910
911 /// Clear the pending filter update flag.
912 pub fn clear_filter_update_needed(&mut self) {
913 self.pending_filter_update = false;
914 }
915}
916
917mod rekey;
918
919#[cfg(test)]
920mod tests;