Skip to main content

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