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