mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Add rustfmt.toml with stable defaults and apply cargo fmt to all source files. This establishes a consistent formatting baseline for CI enforcement.
497 lines
17 KiB
Rust
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;
|
|
}
|
|
}
|