Files
fips/src/node/session.rs
T
Johnathan Corgan 13c0b70dc3 Add rustfmt formatting policy and reformat codebase
Add rustfmt.toml with stable defaults and apply cargo fmt to all
source files. This establishes a consistent formatting baseline
for CI enforcement.
2026-04-10 08:27:07 +00:00

497 lines
17 KiB
Rust

//! 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<EndToEndState>,
/// 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<MmpSessionState>,
// === 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<Vec<u8>>,
/// 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<NoiseSession>,
/// When drain window started (Unix ms). 0 = no drain.
drain_started_ms: u64,
/// In-progress rekey state (runs alongside Established session).
rekey_state: Option<HandshakeState>,
/// Pending completed session awaiting K-bit cutover.
pending_new_session: Option<NoiseSession>,
/// 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<EndToEndState> {
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<u8>, 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<HandshakeState> {
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;
}
}