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>>,
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    /// When we last received a rekey msg1 from this peer (dampening).
211    last_peer_rekey: 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_peer_rekey: 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_peer_rekey: 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>) {
546        self.handshake_msg2 = Some(msg2);
547    }
548
549    /// Get stored msg2 bytes for resend.
550    pub fn handshake_msg2(&self) -> Option<&[u8]> {
551        self.handshake_msg2.as_deref()
552    }
553
554    /// Clear stored msg2 (no longer needed after handshake window).
555    pub fn clear_handshake_msg2(&mut self) {
556        self.handshake_msg2 = None;
557    }
558
559    // === Replay Detection Suppression ===
560
561    /// Increment replay suppression counter. Returns the new count.
562    pub fn increment_replay_suppressed(&mut self) -> u32 {
563        self.replay_suppressed_count += 1;
564        self.replay_suppressed_count
565    }
566
567    /// Reset replay suppression counter, returning previous count.
568    pub fn reset_replay_suppressed(&mut self) -> u32 {
569        let count = self.replay_suppressed_count;
570        self.replay_suppressed_count = 0;
571        count
572    }
573
574    /// Current replay suppression count.
575    pub fn replay_suppressed_count(&self) -> u32 {
576        self.replay_suppressed_count
577    }
578
579    // === Decryption Failure Tracking ===
580
581    /// Increment consecutive decryption failure counter, returning new count.
582    pub fn increment_decrypt_failures(&mut self) -> u32 {
583        self.consecutive_decrypt_failures += 1;
584        self.consecutive_decrypt_failures
585    }
586
587    /// Reset consecutive decryption failure counter.
588    pub fn reset_decrypt_failures(&mut self) {
589        self.consecutive_decrypt_failures = 0;
590    }
591
592    /// Current consecutive decryption failure count.
593    pub fn consecutive_decrypt_failures(&self) -> u32 {
594        self.consecutive_decrypt_failures
595    }
596
597    // === Epoch Accessors ===
598
599    /// Get the remote peer's startup epoch (from handshake).
600    pub fn remote_epoch(&self) -> Option<[u8; 8]> {
601        self.remote_epoch
602    }
603
604    /// Update the remote peer's startup epoch after a successful in-place
605    /// rekey. Initial handshakes set this through `with_session`, but recovery
606    /// rekeys also exchange epochs and must keep restart detection current.
607    pub(crate) fn set_remote_epoch(&mut self, remote_epoch: Option<[u8; 8]>) {
608        self.remote_epoch = remote_epoch;
609    }
610
611    // === Tree Accessors ===
612
613    /// Get the peer's tree coordinates, if known.
614    pub fn coords(&self) -> Option<&TreeCoordinate> {
615        self.ancestry.as_ref()
616    }
617
618    /// Get the peer's parent declaration, if known.
619    pub fn declaration(&self) -> Option<&ParentDeclaration> {
620        self.declaration.as_ref()
621    }
622
623    /// Check if this peer has a known tree position.
624    pub fn has_tree_position(&self) -> bool {
625        self.declaration.is_some() && self.ancestry.is_some()
626    }
627
628    // === Filter Accessors ===
629
630    /// Get the peer's inbound filter, if known.
631    pub fn inbound_filter(&self) -> Option<&BloomFilter> {
632        self.inbound_filter.as_ref()
633    }
634
635    /// Get the filter sequence number.
636    pub fn filter_sequence(&self) -> u64 {
637        self.filter_sequence
638    }
639
640    /// Check if this peer's filter is stale.
641    pub fn filter_is_stale(&self, current_time_ms: u64, stale_threshold_ms: u64) -> bool {
642        if self.filter_received_at == 0 {
643            return true;
644        }
645        current_time_ms.saturating_sub(self.filter_received_at) > stale_threshold_ms
646    }
647
648    /// Check if a destination might be reachable through this peer.
649    pub fn may_reach(&self, node_addr: &NodeAddr) -> bool {
650        match &self.inbound_filter {
651            Some(filter) => filter.contains(node_addr),
652            None => false,
653        }
654    }
655
656    /// Check if we need to send this peer a filter update.
657    pub fn needs_filter_update(&self) -> bool {
658        self.pending_filter_update
659    }
660
661    // === Statistics Accessors ===
662
663    /// Get link statistics.
664    pub fn link_stats(&self) -> &LinkStats {
665        &self.link_stats
666    }
667
668    /// Get mutable link statistics.
669    pub fn link_stats_mut(&mut self) -> &mut LinkStats {
670        &mut self.link_stats
671    }
672
673    /// Whether this node initiated the adjacent FMP session.
674    pub fn fmp_mmp_is_initiator(&self) -> bool {
675        self.fmp_mmp_is_initiator
676    }
677
678    pub(crate) fn set_fmp_mmp_is_initiator(&mut self, is_initiator: bool) {
679        self.fmp_mmp_is_initiator = is_initiator;
680    }
681
682    /// When this peer was authenticated.
683    pub fn authenticated_at(&self) -> u64 {
684        self.authenticated_at
685    }
686
687    /// When this peer was last seen.
688    pub fn last_seen(&self) -> u64 {
689        self.last_seen
690    }
691
692    /// Time since last activity.
693    pub fn idle_time(&self, current_time_ms: u64) -> u64 {
694        current_time_ms.saturating_sub(self.last_seen)
695    }
696
697    /// Connection duration since authentication.
698    pub fn connection_duration(&self, current_time_ms: u64) -> u64 {
699        current_time_ms.saturating_sub(self.authenticated_at)
700    }
701
702    /// Session-relative elapsed time in milliseconds (for inner header timestamp).
703    ///
704    /// Returns milliseconds since session establishment, truncated to u32.
705    /// Wraps at ~49.7 days which is acceptable for session-relative timing.
706    pub fn session_elapsed_ms(&self) -> u32 {
707        self.session_start.elapsed().as_millis() as u32
708    }
709
710    /// Local dataplane generation for the current Noise session.
711    pub fn session_generation(&self) -> u64 {
712        self.session_generation
713    }
714
715    /// When this peer's session started (for link-dead fallback timing).
716    pub fn session_start(&self) -> Instant {
717        self.session_start
718    }
719
720    // === Heartbeat ===
721
722    /// When we last sent a heartbeat to this peer.
723    pub fn last_heartbeat_sent(&self) -> Option<Instant> {
724        self.last_heartbeat_sent
725    }
726
727    /// Record that we sent a heartbeat.
728    pub fn mark_heartbeat_sent(&mut self, now: Instant) {
729        self.last_heartbeat_sent = Some(now);
730    }
731
732    // === State Updates ===
733
734    /// Update last seen timestamp.
735    pub fn touch(&mut self, current_time_ms: u64) {
736        self.last_seen = current_time_ms;
737        // Stale links are still sendable, so authenticated traffic refreshes
738        // them. Reconnecting links were declared link-dead and need a fresh
739        // handshake/reprobe before they can carry traffic again.
740        if self.connectivity == ConnectivityState::Stale {
741            self.connectivity = ConnectivityState::Connected;
742        }
743    }
744
745    /// Mark peer as stale (no recent traffic).
746    pub fn mark_stale(&mut self) {
747        if self.connectivity == ConnectivityState::Connected {
748            self.connectivity = ConnectivityState::Stale;
749        }
750    }
751
752    /// Mark peer as reconnecting.
753    pub fn mark_reconnecting(&mut self) {
754        self.connectivity = ConnectivityState::Reconnecting;
755    }
756
757    /// Mark peer as disconnected.
758    pub fn mark_disconnected(&mut self) {
759        self.connectivity = ConnectivityState::Disconnected;
760    }
761
762    /// Mark peer as connected (e.g., after successful reconnect).
763    pub fn mark_connected(&mut self, current_time_ms: u64) {
764        self.connectivity = ConnectivityState::Connected;
765        self.last_seen = current_time_ms;
766    }
767
768    /// Update the link ID (e.g., on reconnect).
769    pub fn set_link_id(&mut self, link_id: LinkId) {
770        self.link_id = link_id;
771    }
772
773    // === Tree Updates ===
774
775    /// Update peer's tree position without refreshing path liveness.
776    ///
777    /// The TreeAnnounce itself may arrive over a fallback/transit FMP path.
778    /// Authenticated receive bookkeeping owns `last_seen`; topology metadata
779    /// must not make a stale direct path look alive again.
780    pub fn update_tree_position(
781        &mut self,
782        declaration: ParentDeclaration,
783        ancestry: TreeCoordinate,
784    ) {
785        self.declaration = Some(declaration);
786        self.ancestry = Some(ancestry);
787    }
788
789    /// Clear peer's tree position.
790    pub fn clear_tree_position(&mut self) {
791        self.declaration = None;
792        self.ancestry = None;
793    }
794
795    // === Tree Announce Rate Limiting ===
796
797    /// Set the minimum interval between TreeAnnounce messages (milliseconds).
798    pub fn set_tree_announce_min_interval_ms(&mut self, ms: u64) {
799        self.tree_announce_min_interval_ms = ms;
800    }
801
802    /// Get the last tree announce send timestamp (for carrying across reconnection).
803    pub fn last_tree_announce_sent_ms(&self) -> u64 {
804        self.last_tree_announce_sent_ms
805    }
806
807    /// Set the last tree announce send timestamp (to preserve rate limit across reconnection).
808    pub fn set_last_tree_announce_sent_ms(&mut self, ms: u64) {
809        self.last_tree_announce_sent_ms = ms;
810    }
811
812    /// Check if we can send a TreeAnnounce now (rate limiting).
813    pub fn can_send_tree_announce(&self, now_ms: u64) -> bool {
814        now_ms.saturating_sub(self.last_tree_announce_sent_ms) >= self.tree_announce_min_interval_ms
815    }
816
817    /// Record that we sent a TreeAnnounce to this peer.
818    pub fn record_tree_announce_sent(&mut self, now_ms: u64) {
819        self.last_tree_announce_sent_ms = now_ms;
820        self.pending_tree_announce = false;
821    }
822
823    /// Mark that a tree announce is pending (deferred due to rate limit).
824    pub fn mark_tree_announce_pending(&mut self) {
825        self.pending_tree_announce = true;
826    }
827
828    /// Check if a deferred tree announce is waiting to be sent.
829    pub fn has_pending_tree_announce(&self) -> bool {
830        self.pending_tree_announce
831    }
832
833    // === Filter Updates ===
834
835    /// Update peer's inbound filter.
836    pub fn update_filter(&mut self, filter: BloomFilter, sequence: u64, current_time_ms: u64) {
837        self.inbound_filter = Some(filter);
838        self.filter_sequence = sequence;
839        self.filter_received_at = current_time_ms;
840        self.last_seen = current_time_ms;
841    }
842
843    /// Clear peer's inbound filter.
844    pub fn clear_filter(&mut self) {
845        self.inbound_filter = None;
846        self.filter_sequence = 0;
847        self.filter_received_at = 0;
848    }
849
850    /// Mark that we need to send this peer a filter update.
851    pub fn mark_filter_update_needed(&mut self) {
852        self.pending_filter_update = true;
853    }
854
855    /// Clear the pending filter update flag.
856    pub fn clear_filter_update_needed(&mut self) {
857        self.pending_filter_update = false;
858    }
859}
860
861mod rekey;
862
863#[cfg(test)]
864mod tests;