//! End-to-end session state. //! //! Tracks Noise XK sessions between this node and remote endpoints. //! Sessions are established via a three-message XK handshake //! (SessionSetup/SessionAck/SessionMsg3) carried inside SessionDatagram //! envelopes through the mesh. use std::time::Instant; use crate::NodeAddr; use crate::config::SessionMmpConfig; use crate::mmp::MmpSessionState; use crate::noise::{HandshakeState, NoiseSession}; use secp256k1::PublicKey; /// State machine for an end-to-end session. pub(crate) enum EndToEndState { /// We initiated: sent SessionSetup with Noise XK msg1, awaiting SessionAck. Initiating(HandshakeState), /// XK responder: processed msg1, sent msg2, awaiting msg3. /// Transitions to Established when msg3 arrives. AwaitingMsg3(HandshakeState), /// Handshake complete, NoiseSession available for encrypt/decrypt. Established(NoiseSession), } impl EndToEndState { /// Check if the session is established and ready for data. pub(crate) fn is_established(&self) -> bool { matches!(self, EndToEndState::Established(_)) } /// Check if we are the initiator (waiting for ack). pub(crate) fn is_initiating(&self) -> bool { matches!(self, EndToEndState::Initiating(_)) } /// Check if we are an XK responder awaiting msg3. pub(crate) fn is_awaiting_msg3(&self) -> bool { matches!(self, EndToEndState::AwaitingMsg3(_)) } } /// A single end-to-end session with a remote node. /// /// The state is wrapped in `Option` to allow taking ownership of the /// handshake state during transitions without placeholder values. /// The state is `None` only transiently during handler processing. pub(crate) struct SessionEntry { /// Remote node's address (session table key). #[allow(dead_code)] remote_addr: NodeAddr, /// Remote node's static public key. remote_pubkey: PublicKey, /// Current session state. `None` only during state transitions. state: Option, /// When the session was created (Unix milliseconds). #[cfg_attr(not(test), allow(dead_code))] created_at: u64, /// Last application data activity timestamp (Unix milliseconds). /// Only updated for DataPacket send/receive and session establishment. /// MMP reports do not update this field. Used for idle session timeout. last_activity: u64, /// When the session transitioned to Established (Unix milliseconds). /// Used to compute session-relative timestamps for the FSP inner header. /// Set to 0 until the session is established. session_start_ms: u64, /// Remaining data packets that should include COORDS_PRESENT. /// Initialized from config when session becomes Established; /// reset on CoordsRequired receipt. coords_warmup_remaining: u8, /// Whether this node initiated the Noise handshake. /// Used for spin bit role assignment in session-layer MMP. is_initiator: bool, /// Session-layer MMP state. Initialized on Established transition. mmp: Option, // === Traffic Counters === /// Total data packets sent on this session. packets_sent: u64, /// Total data packets received on this session. packets_recv: u64, /// Total data bytes sent on this session (FSP payload). bytes_sent: u64, /// Total data bytes received on this session (FSP payload). bytes_recv: u64, // === Handshake Resend === /// Encoded session-layer payload for resend (SessionSetup or SessionAck). /// Cleared on Established transition. handshake_payload: Option>, /// Number of resends performed. resend_count: u32, /// When the next resend should fire (Unix ms). 0 = no resend scheduled. next_resend_at_ms: u64, // === Rekey (Key Rotation) === /// Current K-bit epoch value (alternates each rekey). current_k_bit: bool, /// Previous NoiseSession during drain window after cutover. previous_noise_session: Option, /// When drain window started (Unix ms). 0 = no drain. drain_started_ms: u64, /// In-progress rekey state (runs alongside Established session). rekey_state: Option, /// Pending completed session awaiting K-bit cutover. pending_new_session: Option, /// Whether we initiated the current rekey. rekey_initiator: bool, /// Dampening: last time peer sent us a rekey msg1 (Unix ms). last_peer_rekey_ms: u64, /// When the FSP rekey handshake completed (initiator sent msg3, Unix ms). /// Used to defer cutover until msg3 has time to reach the responder. rekey_completed_ms: u64, } impl SessionEntry { /// Create a new session entry. pub(crate) fn new( remote_addr: NodeAddr, remote_pubkey: PublicKey, state: EndToEndState, now_ms: u64, is_initiator: bool, ) -> Self { Self { remote_addr, remote_pubkey, state: Some(state), created_at: now_ms, last_activity: now_ms, session_start_ms: 0, coords_warmup_remaining: 0, is_initiator, mmp: None, packets_sent: 0, packets_recv: 0, bytes_sent: 0, bytes_recv: 0, handshake_payload: None, resend_count: 0, next_resend_at_ms: 0, current_k_bit: false, previous_noise_session: None, drain_started_ms: 0, rekey_state: None, pending_new_session: None, rekey_initiator: false, last_peer_rekey_ms: 0, rekey_completed_ms: 0, } } /// Get the remote node's public key. pub(crate) fn remote_pubkey(&self) -> &PublicKey { &self.remote_pubkey } /// Get the current session state. #[cfg(test)] pub(crate) fn state(&self) -> &EndToEndState { self.state .as_ref() .expect("session state taken but not restored") } /// Get mutable access to the session state. pub(crate) fn state_mut(&mut self) -> &mut EndToEndState { self.state .as_mut() .expect("session state taken but not restored") } /// Replace the session state. pub(crate) fn set_state(&mut self, state: EndToEndState) { self.state = Some(state); } /// Take the state out, leaving `None`. /// /// The caller must call `set_state()` to restore a valid state, /// or discard the entry entirely. pub(crate) fn take_state(&mut self) -> Option { self.state.take() } /// Update the last application data activity timestamp. /// /// Only call for DataPacket send/receive and session establishment, /// not for MMP reports. Used by the idle session timeout. pub(crate) fn touch(&mut self, now_ms: u64) { self.last_activity = now_ms; } /// Check if the session is established. pub(crate) fn is_established(&self) -> bool { self.state.as_ref().is_some_and(|s| s.is_established()) } /// Check if we are the initiator (waiting for ack). pub(crate) fn is_initiating(&self) -> bool { self.state.as_ref().is_some_and(|s| s.is_initiating()) } /// Check if we are an XK responder awaiting msg3. pub(crate) fn is_awaiting_msg3(&self) -> bool { self.state.as_ref().is_some_and(|s| s.is_awaiting_msg3()) } /// Get creation time. #[cfg(test)] pub(crate) fn created_at(&self) -> u64 { self.created_at } /// Get last activity time. pub(crate) fn last_activity(&self) -> u64 { self.last_activity } /// Remaining DataPackets that should include COORDS_PRESENT. pub(crate) fn coords_warmup_remaining(&self) -> u8 { self.coords_warmup_remaining } /// Set the coords warmup counter (used on Established transition /// and CoordsRequired reset). pub(crate) fn set_coords_warmup_remaining(&mut self, value: u8) { self.coords_warmup_remaining = value; } /// Mark the session as started (transition to Established). /// /// Records the current time as the session start for computing /// session-relative timestamps in the FSP inner header. pub(crate) fn mark_established(&mut self, now_ms: u64) { self.session_start_ms = now_ms; } /// Compute a session-relative timestamp for the FSP inner header. /// /// Returns `(now_ms - session_start_ms)` truncated to u32. /// Wraps naturally at ~49.7 days, which is fine for relative timing. pub(crate) fn session_timestamp(&self, now_ms: u64) -> u32 { now_ms.wrapping_sub(self.session_start_ms) as u32 } /// Whether this node initiated the Noise handshake. #[cfg_attr(not(test), allow(dead_code))] pub(crate) fn is_initiator(&self) -> bool { self.is_initiator } /// Get a reference to the session-layer MMP state, if initialized. pub(crate) fn mmp(&self) -> Option<&MmpSessionState> { self.mmp.as_ref() } /// Get a mutable reference to the session-layer MMP state, if initialized. pub(crate) fn mmp_mut(&mut self) -> Option<&mut MmpSessionState> { self.mmp.as_mut() } /// Initialize session-layer MMP state (called on Established transition). pub(crate) fn init_mmp(&mut self, config: &SessionMmpConfig) { self.mmp = Some(MmpSessionState::new(config, self.is_initiator)); } // === Traffic Counters === /// Record a sent data packet. pub(crate) fn record_sent(&mut self, bytes: usize) { self.packets_sent += 1; self.bytes_sent += bytes as u64; } /// Record a received data packet. pub(crate) fn record_recv(&mut self, bytes: usize) { self.packets_recv += 1; self.bytes_recv += bytes as u64; } /// Get traffic counters: (packets_sent, packets_recv, bytes_sent, bytes_recv). pub(crate) fn traffic_counters(&self) -> (u64, u64, u64, u64) { ( self.packets_sent, self.packets_recv, self.bytes_sent, self.bytes_recv, ) } // === Handshake Resend === /// Store the encoded session-layer payload for potential resend. /// /// For initiators, this is the SessionSetup payload bytes. /// For responders, this is the SessionAck payload bytes. /// The payload is re-wrapped in a fresh SessionDatagram on each resend /// so routing can adapt to topology changes. pub(crate) fn set_handshake_payload(&mut self, payload: Vec, next_resend_at_ms: u64) { self.handshake_payload = Some(payload); self.resend_count = 0; self.next_resend_at_ms = next_resend_at_ms; } /// Get the stored handshake payload for resend. pub(crate) fn handshake_payload(&self) -> Option<&[u8]> { self.handshake_payload.as_deref() } /// Clear the stored handshake payload (called on Established transition). pub(crate) fn clear_handshake_payload(&mut self) { self.handshake_payload = None; self.next_resend_at_ms = 0; } /// Number of resends performed so far. pub(crate) fn resend_count(&self) -> u32 { self.resend_count } /// When the next resend should fire (Unix ms). 0 = no resend scheduled. pub(crate) fn next_resend_at_ms(&self) -> u64 { self.next_resend_at_ms } /// Record a resend and schedule the next one. pub(crate) fn record_resend(&mut self, next_resend_at_ms: u64) { self.resend_count += 1; self.next_resend_at_ms = next_resend_at_ms; } // === Rekey (Key Rotation) === /// Current K-bit epoch value. pub(crate) fn current_k_bit(&self) -> bool { self.current_k_bit } /// Whether a rekey is currently in progress. pub(crate) fn has_rekey_in_progress(&self) -> bool { self.rekey_state.is_some() } /// Get the pending new session (completed rekey, not yet cut over). pub(crate) fn pending_new_session(&self) -> Option<&NoiseSession> { self.pending_new_session.as_ref() } /// Get the previous session for decryption fallback during drain. pub(crate) fn previous_noise_session_mut(&mut self) -> Option<&mut NoiseSession> { self.previous_noise_session.as_mut() } /// Whether we initiated the current rekey. pub(crate) fn is_rekey_initiator(&self) -> bool { self.rekey_initiator } /// Check if rekey initiation is dampened. pub(crate) fn is_rekey_dampened(&self, now_ms: u64, dampening_ms: u64) -> bool { if self.last_peer_rekey_ms == 0 { return false; } now_ms.saturating_sub(self.last_peer_rekey_ms) < dampening_ms } /// Record that the peer initiated a rekey (for dampening). pub(crate) fn record_peer_rekey(&mut self, now_ms: u64) { self.last_peer_rekey_ms = now_ms; } /// When the session transitioned to Established (for rekey timer). pub(crate) fn session_start_ms(&self) -> u64 { self.session_start_ms } /// Get the current send counter from the established NoiseSession. pub(crate) fn send_counter(&self) -> u64 { match self.state.as_ref() { Some(EndToEndState::Established(s)) => s.current_send_counter(), _ => 0, } } /// When the FSP rekey handshake completed (initiator sent msg3). pub(crate) fn rekey_completed_ms(&self) -> u64 { self.rekey_completed_ms } /// Record when the FSP rekey handshake completed (initiator side). pub(crate) fn set_rekey_completed_ms(&mut self, ms: u64) { self.rekey_completed_ms = ms; } /// Store a completed rekey session. pub(crate) fn set_pending_session(&mut self, session: NoiseSession) { self.pending_new_session = Some(session); self.rekey_state = None; } /// Set the rekey handshake state (in-progress XK handshake). pub(crate) fn set_rekey_state(&mut self, state: HandshakeState, is_initiator: bool) { self.rekey_state = Some(state); self.rekey_initiator = is_initiator; } /// Take the rekey state for processing. pub(crate) fn take_rekey_state(&mut self) -> Option { self.rekey_state.take() } /// Cut over to the pending new session (initiator side). /// /// Moves current session to previous (for drain), promotes pending to current, /// flips the K-bit. pub(crate) fn cutover_to_new_session(&mut self, now_ms: u64) -> bool { let new_session = match self.pending_new_session.take() { Some(s) => s, None => return false, }; // Demote current to previous for drain if let Some(EndToEndState::Established(old)) = self.state.take() { self.previous_noise_session = Some(old); } self.drain_started_ms = now_ms; // Promote pending to current self.state = Some(EndToEndState::Established(new_session)); self.current_k_bit = !self.current_k_bit; self.session_start_ms = now_ms; self.rekey_state = None; self.rekey_initiator = false; self.rekey_completed_ms = 0; // Reset MMP counters to avoid metric discontinuity let now = Instant::now(); if let Some(mmp) = &mut self.mmp { mmp.reset_for_rekey(now); } true } /// Handle receiving a K-bit flip from the peer (responder side). pub(crate) fn handle_peer_kbit_flip(&mut self, now_ms: u64) -> bool { let new_session = match self.pending_new_session.take() { Some(s) => s, None => return false, }; // Demote current to previous for drain if let Some(EndToEndState::Established(old)) = self.state.take() { self.previous_noise_session = Some(old); } self.drain_started_ms = now_ms; // Promote pending to current self.state = Some(EndToEndState::Established(new_session)); self.current_k_bit = !self.current_k_bit; self.session_start_ms = now_ms; self.rekey_state = None; self.rekey_initiator = false; // Reset MMP counters to avoid metric discontinuity let now = Instant::now(); if let Some(mmp) = &mut self.mmp { mmp.reset_for_rekey(now); } true } /// Check if the drain window has expired. pub(crate) fn drain_expired(&self, now_ms: u64, drain_ms: u64) -> bool { self.drain_started_ms > 0 && now_ms.saturating_sub(self.drain_started_ms) >= drain_ms } /// Whether a drain is in progress. pub(crate) fn is_draining(&self) -> bool { self.drain_started_ms > 0 } /// Complete the drain: drop previous session. pub(crate) fn complete_drain(&mut self) { self.previous_noise_session = None; self.drain_started_ms = 0; } /// Abandon an in-progress rekey. pub(crate) fn abandon_rekey(&mut self) { self.rekey_state = None; self.pending_new_session = None; self.rekey_initiator = false; } }