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