mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
node: derive handshake state from the peer machine, delete the leg field
The connection leg's HandshakeState field duplicated the peer machine's handshake phase. Delete it and derive the displayed handshake state string from the machine's PeerState, looked up by link id (mirroring the resend count). Move the failure signal onto the machine: a send_failed flag that preserves retransmit eligibility (the machine stays in its handshake phase), alongside the existing Failed state. The leg's crypto self-gates now guard on Noise handle presence instead of the deleted phase field, and mark_failed only drops the handle. Telemetry strings, wire bytes, index allocation, and the stale-connection reaping are byte-identical for all normal paths.
This commit is contained in:
@@ -1307,7 +1307,7 @@ pub fn show_connections(node: &Node) -> Value {
|
||||
let mut conn_json = json!({
|
||||
"link_id": conn.link_id().as_u64(),
|
||||
"direction": format!("{}", conn.direction()),
|
||||
"handshake_state": format!("{}", conn.handshake_state()),
|
||||
"handshake_state": node.connection_handshake_state(conn.link_id()),
|
||||
"started_at_ms": conn.started_at(),
|
||||
"idle_ms": now.saturating_sub(conn.last_activity()),
|
||||
"resend_count": node.connection_resend_count(conn.link_id()),
|
||||
|
||||
@@ -92,7 +92,7 @@ pub use cache::{CacheEntry, CacheError, CacheStats, CoordCache};
|
||||
pub use proto::fmp::{PromotionResult, cross_connection_winner};
|
||||
|
||||
// Re-export peer types
|
||||
pub use peer::{ActivePeer, ConnectivityState, HandshakeState, PeerConnection, PeerError};
|
||||
pub use peer::{ActivePeer, ConnectivityState, PeerConnection, PeerError};
|
||||
|
||||
// Re-export node types
|
||||
pub use node::{Node, NodeError, NodeState, UpdatePeersOutcome};
|
||||
|
||||
@@ -997,7 +997,18 @@ impl Node {
|
||||
error = %e,
|
||||
"Handshake completion failed"
|
||||
);
|
||||
// Drop the leg's Noise handle (byte-identical point) and record
|
||||
// the failure on the control machine as `send_failed` — the
|
||||
// failure state's new home. The machine PHASE stays exactly
|
||||
// where the old leg-carried failure left it (`SentMsg1`): the
|
||||
// stale-connection sweep reclaims the leg unconditionally via
|
||||
// the machine `is_failed()` at the next tick, before any
|
||||
// projection or resend, so the phase in that window is
|
||||
// byte-identical to the pre-collapse machine.
|
||||
conn.mark_failed();
|
||||
if let Some(machine) = self.peer_machines.get_mut(&link_id) {
|
||||
machine.mark_send_failed();
|
||||
}
|
||||
self.stats_mut()
|
||||
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
|
||||
return;
|
||||
|
||||
@@ -19,15 +19,15 @@ impl LifecycleView for Node {
|
||||
// reap.
|
||||
self.peer_machines
|
||||
.iter()
|
||||
.filter_map(|(link_id, machine)| machine.leg().map(|conn| (link_id, conn)))
|
||||
.filter(|(link_id, conn)| {
|
||||
conn.is_failed()
|
||||
.filter_map(|(link_id, machine)| machine.leg().map(|conn| (link_id, machine, conn)))
|
||||
.filter(|(link_id, machine, conn)| {
|
||||
machine.is_failed()
|
||||
|| (conn.is_timed_out(now_ms, timeout_ms)
|
||||
&& !self.peer_timers.get(*link_id).is_some_and(|timers| {
|
||||
timers.contains_key(&TimerKind::HandshakeTimeout)
|
||||
}))
|
||||
})
|
||||
.map(|(link_id, conn)| ConnSnapshot {
|
||||
.map(|(link_id, _machine, conn)| ConnSnapshot {
|
||||
link: *link_id,
|
||||
is_outbound: conn.is_outbound(),
|
||||
retry_addr: conn.expected_identity().map(|id| *id.node_addr()),
|
||||
@@ -70,10 +70,16 @@ impl Node {
|
||||
match action {
|
||||
ConnAction::ScheduleRetry { peer } => self.note_handshake_timeout(peer, now_ms),
|
||||
ConnAction::Teardown { link } => {
|
||||
// Log before cleanup (needs live connection state).
|
||||
// Log before cleanup (needs live connection state). The
|
||||
// failure signal is now read from the control machine; the
|
||||
// leg still carries direction/idle for the log fields.
|
||||
let is_failed = self
|
||||
.peer_machines
|
||||
.get(&link)
|
||||
.is_some_and(|machine| machine.is_failed());
|
||||
if let Some(conn) = self.leg(&link) {
|
||||
let direction = conn.direction();
|
||||
if conn.is_failed() {
|
||||
if is_failed {
|
||||
debug!(
|
||||
link_id = %link,
|
||||
direction = %direction,
|
||||
|
||||
@@ -1981,7 +1981,7 @@ impl Node {
|
||||
.map(|conn| snap::ConnectionRow {
|
||||
link_id: conn.link_id().as_u64(),
|
||||
direction: format!("{}", conn.direction()),
|
||||
handshake_state: format!("{}", conn.handshake_state()),
|
||||
handshake_state: self.connection_handshake_state(conn.link_id()).to_string(),
|
||||
started_at_ms: conn.started_at(),
|
||||
last_activity_ms: conn.last_activity(),
|
||||
resend_count: self.connection_resend_count(conn.link_id()),
|
||||
@@ -2309,6 +2309,17 @@ impl Node {
|
||||
.map_or(0, |machine| machine.resend_count())
|
||||
}
|
||||
|
||||
/// Operator-visible handshake-state string for a pending handshake `link`,
|
||||
/// derived from the per-peer control machine (the phase's home now that the
|
||||
/// leg no longer carries it). Every leg surfaced by `connections()` is
|
||||
/// embedded in a machine, so the lookup resolves; the `"initial"` default is
|
||||
/// unreachable in that view and only guards a missing machine.
|
||||
pub(crate) fn connection_handshake_state(&self, link: LinkId) -> &'static str {
|
||||
self.peer_machines
|
||||
.get(&link)
|
||||
.map_or("initial", |machine| machine.displayed_handshake_state())
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_bootstrap_transport_if_unused(&mut self, transport_id: TransportId) {
|
||||
if !self
|
||||
.supervisor
|
||||
|
||||
@@ -751,7 +751,6 @@ async fn test_failed_connection_cleanup() {
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
conn.mark_failed(); // Simulate send failure
|
||||
|
||||
let link = Link::connectionless(
|
||||
link_id,
|
||||
@@ -767,6 +766,24 @@ async fn test_failed_connection_cleanup() {
|
||||
node.pending_outbound
|
||||
.insert((transport_id, our_index.as_u32()), link_id);
|
||||
|
||||
// Simulate a stored-handshake send failure through the control machine —
|
||||
// the failure carrier the stale-connection sweep now reads (the leg no
|
||||
// longer carries a failed phase of its own).
|
||||
{
|
||||
let machine = node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.expect("machine seeded by add_connection");
|
||||
let alloc = &mut node.index_allocator;
|
||||
let actions = machine.step(
|
||||
crate::peer::machine::PeerEvent::HandshakeSendFailed,
|
||||
now_ms,
|
||||
alloc,
|
||||
);
|
||||
assert!(actions.is_empty());
|
||||
assert!(machine.is_failed());
|
||||
}
|
||||
|
||||
assert_eq!(node.connection_count(), 1);
|
||||
|
||||
// Failed connections should be cleaned up immediately regardless of age
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Peer Connection (Handshake Phase)
|
||||
//!
|
||||
//! Represents an in-progress connection before authentication completes.
|
||||
//! PeerConnection tracks the Noise IK handshake state and transitions to
|
||||
//! ActivePeer upon successful authentication.
|
||||
//! PeerConnection tracks the Noise IK handshake and transitions to
|
||||
//! ActivePeer upon successful authentication. The handshake *phase* (initial /
|
||||
//! sent_msg1 / complete / failed) is no longer tracked here — it lives on the
|
||||
//! per-peer control machine; the leg's crypto methods gate on the presence of
|
||||
//! their Noise handles (`noise_handshake` / `noise_session`) directly.
|
||||
|
||||
use crate::PeerIdentity;
|
||||
use crate::noise::{self, NoiseError, NoiseSession};
|
||||
@@ -12,12 +15,6 @@ use crate::utils::index::SessionIndex;
|
||||
use secp256k1::Keypair;
|
||||
use std::fmt;
|
||||
|
||||
// The pure handshake-phase bookkeeping (`ConnectionState`) and its
|
||||
// `HandshakeState` phase enum now live in `proto::fmp::state`. Re-export
|
||||
// `HandshakeState` here so its original public path
|
||||
// (`crate::peer::HandshakeState`) is preserved for existing call sites.
|
||||
pub use crate::proto::fmp::HandshakeState;
|
||||
|
||||
/// A connection in the handshake phase, before authentication completes.
|
||||
///
|
||||
/// For outbound connections, we know the expected peer identity from config.
|
||||
@@ -101,11 +98,6 @@ impl PeerConnection {
|
||||
self.state.direction()
|
||||
}
|
||||
|
||||
/// Get the handshake state.
|
||||
pub fn handshake_state(&self) -> HandshakeState {
|
||||
self.state.handshake_state()
|
||||
}
|
||||
|
||||
/// Get the expected/learned peer identity, if known.
|
||||
pub fn expected_identity(&self) -> Option<&PeerIdentity> {
|
||||
self.state.expected_identity()
|
||||
@@ -121,21 +113,6 @@ impl PeerConnection {
|
||||
self.state.is_inbound()
|
||||
}
|
||||
|
||||
/// Check if handshake is in progress.
|
||||
pub fn is_in_progress(&self) -> bool {
|
||||
self.state.is_in_progress()
|
||||
}
|
||||
|
||||
/// Check if handshake completed.
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.state.is_complete()
|
||||
}
|
||||
|
||||
/// Check if handshake failed.
|
||||
pub fn is_failed(&self) -> bool {
|
||||
self.state.is_failed()
|
||||
}
|
||||
|
||||
/// When the connection started.
|
||||
pub fn started_at(&self) -> u64 {
|
||||
self.state.started_at()
|
||||
@@ -256,13 +233,6 @@ impl PeerConnection {
|
||||
});
|
||||
}
|
||||
|
||||
if self.state.handshake_state() != HandshakeState::Initial {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "initial state".to_string(),
|
||||
got: self.state.handshake_state().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let remote_static = self
|
||||
.state
|
||||
.expected_identity()
|
||||
@@ -274,7 +244,6 @@ impl PeerConnection {
|
||||
let msg1 = hs.write_message_1()?;
|
||||
|
||||
self.noise_handshake = Some(hs);
|
||||
self.state.set_handshake_state(HandshakeState::SentMsg1);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(msg1)
|
||||
@@ -298,13 +267,6 @@ impl PeerConnection {
|
||||
});
|
||||
}
|
||||
|
||||
if self.state.handshake_state() != HandshakeState::Initial {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "initial state".to_string(),
|
||||
got: self.state.handshake_state().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = noise::HandshakeState::new_responder(our_keypair);
|
||||
hs.set_local_epoch(epoch);
|
||||
|
||||
@@ -328,7 +290,6 @@ impl PeerConnection {
|
||||
// Handshake is complete for responder
|
||||
let session = hs.into_session()?;
|
||||
self.noise_session = Some(session);
|
||||
self.state.set_handshake_state(HandshakeState::Complete);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(msg2)
|
||||
@@ -342,10 +303,14 @@ impl PeerConnection {
|
||||
message: &[u8],
|
||||
current_time_ms: u64,
|
||||
) -> Result<(), NoiseError> {
|
||||
if self.state.handshake_state() != HandshakeState::SentMsg1 {
|
||||
// The leg is at `SentMsg1` iff its Noise handshake handle is present
|
||||
// (set by `start_handshake`, taken here on completion). Gate on the
|
||||
// handle directly now that the phase enum is gone — byte-equivalent to
|
||||
// the old `!= SentMsg1` guard for every reachable transition.
|
||||
if self.noise_handshake.is_none() {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "sent_msg1 state".to_string(),
|
||||
got: self.state.handshake_state().to_string(),
|
||||
got: "no active handshake".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,7 +326,6 @@ impl PeerConnection {
|
||||
|
||||
let session = hs.into_session()?;
|
||||
self.noise_session = Some(session);
|
||||
self.state.set_handshake_state(HandshakeState::Complete);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(())
|
||||
@@ -372,24 +336,23 @@ impl PeerConnection {
|
||||
/// Returns the NoiseSession for use in ActivePeer. Can only be called
|
||||
/// once after handshake completes.
|
||||
pub fn take_session(&mut self) -> Option<NoiseSession> {
|
||||
if self.state.handshake_state() == HandshakeState::Complete {
|
||||
self.noise_session.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
// The session exists iff the handshake reached `Complete`, so taking it
|
||||
// unconditionally is byte-equivalent to the old `== Complete` gate.
|
||||
self.noise_session.take()
|
||||
}
|
||||
|
||||
/// Check if we have a completed session ready to take.
|
||||
pub fn has_session(&self) -> bool {
|
||||
self.state.handshake_state() == HandshakeState::Complete && self.noise_session.is_some()
|
||||
self.noise_session.is_some()
|
||||
}
|
||||
|
||||
// === State Transitions (for manual control if needed) ===
|
||||
|
||||
/// Mark handshake as failed. Sets the pure lifecycle state and drops the
|
||||
/// shell-owned crypto handshake handle.
|
||||
/// Drop the shell-owned crypto handshake handle. The failure *state* now
|
||||
/// lives on the control machine (`PeerMachine`); this only releases the
|
||||
/// leg's Noise handle at the identical point it was released before, so a
|
||||
/// subsequent `complete_handshake` on this leg still reports `WrongState`.
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.state.mark_failed();
|
||||
self.noise_handshake = None;
|
||||
}
|
||||
|
||||
@@ -411,7 +374,6 @@ impl fmt::Debug for PeerConnection {
|
||||
f.debug_struct("PeerConnection")
|
||||
.field("link_id", &self.state.link_id())
|
||||
.field("direction", &self.state.direction())
|
||||
.field("handshake_state", &self.state.handshake_state())
|
||||
.field("expected_identity", &self.state.expected_identity())
|
||||
.field("has_noise_handshake", &self.noise_handshake.is_some())
|
||||
.field("has_noise_session", &self.noise_session.is_some())
|
||||
@@ -446,18 +408,6 @@ mod tests {
|
||||
epoch
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handshake_state_properties() {
|
||||
assert!(HandshakeState::Initial.is_in_progress());
|
||||
assert!(HandshakeState::SentMsg1.is_in_progress());
|
||||
assert!(HandshakeState::ReceivedMsg1.is_in_progress());
|
||||
assert!(!HandshakeState::Complete.is_in_progress());
|
||||
assert!(!HandshakeState::Failed.is_in_progress());
|
||||
|
||||
assert!(HandshakeState::Complete.is_complete());
|
||||
assert!(HandshakeState::Failed.is_failed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outbound_connection() {
|
||||
let identity = make_peer_identity();
|
||||
@@ -465,7 +415,7 @@ mod tests {
|
||||
|
||||
assert!(conn.is_outbound());
|
||||
assert!(!conn.is_inbound());
|
||||
assert_eq!(conn.handshake_state(), HandshakeState::Initial);
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.expected_identity().is_some());
|
||||
assert_eq!(conn.started_at(), 1000);
|
||||
}
|
||||
@@ -476,7 +426,7 @@ mod tests {
|
||||
|
||||
assert!(conn.is_inbound());
|
||||
assert!(!conn.is_outbound());
|
||||
assert_eq!(conn.handshake_state(), HandshakeState::Initial);
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.expected_identity().is_none());
|
||||
assert_eq!(conn.started_at(), 2000);
|
||||
}
|
||||
@@ -503,13 +453,15 @@ mod tests {
|
||||
let msg1 = initiator_conn
|
||||
.start_handshake(initiator_keypair, initiator_epoch, 1100)
|
||||
.unwrap();
|
||||
assert_eq!(initiator_conn.handshake_state(), HandshakeState::SentMsg1);
|
||||
// Post-msg1 the initiator holds an in-flight handshake, not yet a session.
|
||||
assert!(!initiator_conn.has_session());
|
||||
|
||||
// Responder processes msg1 and sends msg2
|
||||
let msg2 = responder_conn
|
||||
.receive_handshake_init(responder_keypair, responder_epoch, &msg1, 1200)
|
||||
.unwrap();
|
||||
assert_eq!(responder_conn.handshake_state(), HandshakeState::Complete);
|
||||
// The IK responder completes in one step: it now holds a session.
|
||||
assert!(responder_conn.has_session());
|
||||
|
||||
// Responder learned initiator's identity
|
||||
let discovered = responder_conn.expected_identity().unwrap();
|
||||
@@ -520,7 +472,7 @@ mod tests {
|
||||
|
||||
// Initiator completes handshake
|
||||
initiator_conn.complete_handshake(&msg2, 1300).unwrap();
|
||||
assert_eq!(initiator_conn.handshake_state(), HandshakeState::Complete);
|
||||
assert!(initiator_conn.has_session());
|
||||
|
||||
// Initiator learned responder's epoch
|
||||
assert_eq!(initiator_conn.remote_epoch(), Some(responder_epoch));
|
||||
@@ -553,13 +505,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_connection_failure() {
|
||||
// `mark_failed` releases the leg's Noise handshake handle. The failure
|
||||
// *state* now lives on the control machine, but the leg-local effect is
|
||||
// still observable: a completion attempt afterward reports `WrongState`
|
||||
// (the handle-presence gate) and no session is produced.
|
||||
let identity = make_peer_identity();
|
||||
let keypair = make_keypair();
|
||||
let mut conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
|
||||
conn.start_handshake(keypair, make_epoch(), 1100).unwrap();
|
||||
|
||||
conn.mark_failed();
|
||||
assert!(conn.is_failed());
|
||||
assert!(!conn.is_in_progress());
|
||||
assert!(!conn.is_complete());
|
||||
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.complete_handshake(&[0u8; 96], 1200).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -124,7 +124,7 @@ pub(crate) enum PeerState {
|
||||
Closed { backoff_deadline_ms: u64 },
|
||||
}
|
||||
|
||||
/// Handshake phase (mirrors `proto::fmp::HandshakeState`'s in-progress arms).
|
||||
/// Handshake phase (the in-progress arms of the peer lifecycle).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum HandshakePhase {
|
||||
Initial,
|
||||
@@ -132,6 +132,37 @@ pub(crate) enum HandshakePhase {
|
||||
ReceivedMsg1,
|
||||
}
|
||||
|
||||
/// Map a lifecycle state to the operator-visible pending-connection handshake
|
||||
/// string. Total over `PeerState`; byte-identical to the strings the deleted
|
||||
/// leg `HandshakeState` `Display` produced. Only the `Handshaking{SentMsg1}`
|
||||
/// arm (and, via the `send_failed` flag handled by the caller, `"failed"`) is
|
||||
/// production-reachable in the IK pending-connection view — the rest are kept
|
||||
/// total for a complete mapping and the next-branch inbound window.
|
||||
fn handshake_state_str(state: PeerState) -> &'static str {
|
||||
match state {
|
||||
PeerState::Handshaking {
|
||||
phase: HandshakePhase::Initial,
|
||||
..
|
||||
} => "initial",
|
||||
PeerState::Handshaking {
|
||||
phase: HandshakePhase::SentMsg1,
|
||||
..
|
||||
} => "sent_msg1",
|
||||
PeerState::Handshaking {
|
||||
phase: HandshakePhase::ReceivedMsg1,
|
||||
..
|
||||
} => "received_msg1",
|
||||
PeerState::Established { .. }
|
||||
| PeerState::Active { .. }
|
||||
| PeerState::Maintaining { .. }
|
||||
| PeerState::Closing { .. } => "complete",
|
||||
PeerState::Failed { .. } => "failed",
|
||||
PeerState::Discovered | PeerState::Connecting { .. } | PeerState::Closed { .. } => {
|
||||
"initial"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which maintenance sub-machine `Maintaining` is running.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum MaintainKind {
|
||||
@@ -397,6 +428,13 @@ pub(crate) struct PeerMachine {
|
||||
/// payload stashed in Phase 1 (`InboundMsg1`) and emitted in Phase 2
|
||||
/// (`on_authorized`), so a rejected/unauthorized msg1 allocates no index.
|
||||
pending_msg2_payload: Option<Vec<u8>>,
|
||||
/// A stored-handshake send failure was observed on this leg. The failure is
|
||||
/// carried as a flag (not a `PeerState::Failed` transition) so retransmit
|
||||
/// eligibility (`is_handshaking_sent_msg1`) survives until the
|
||||
/// stale-connection sweep reclaims the leg. It drives both `is_failed`
|
||||
/// (reaping) and the displayed handshake state (`"failed"`), reproducing the
|
||||
/// pre-collapse leg `is_failed`/display signal byte-for-byte.
|
||||
send_failed: bool,
|
||||
|
||||
// --- rekey negotiation sub-state (control tier; NOT the pending send slot) ---
|
||||
rekey_in_progress: bool,
|
||||
@@ -437,6 +475,7 @@ impl PeerMachine {
|
||||
conn: ConnectionState::outbound(link, identity, now),
|
||||
remote_epoch: None,
|
||||
pending_msg2_payload: None,
|
||||
send_failed: false,
|
||||
rekey_in_progress: false,
|
||||
rekey_our_index: None,
|
||||
rekey_msg1: None,
|
||||
@@ -464,6 +503,7 @@ impl PeerMachine {
|
||||
conn: ConnectionState::inbound(link, now),
|
||||
remote_epoch: None,
|
||||
pending_msg2_payload: None,
|
||||
send_failed: false,
|
||||
rekey_in_progress: false,
|
||||
rekey_our_index: None,
|
||||
rekey_msg1: None,
|
||||
@@ -543,6 +583,41 @@ impl PeerMachine {
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this peer's handshake has failed. The sole failure carrier now
|
||||
/// that the leg's phase enum is gone: a terminal `PeerState::Failed`
|
||||
/// (hard crypto/transport/ACL failures) OR the `send_failed` flag (a stored
|
||||
/// handshake-initiation send failure that deliberately keeps the machine at
|
||||
/// `Handshaking{SentMsg1}`). The stale-connection sweep reads this to reclaim
|
||||
/// the leg, exactly as it read the leg's `is_failed` before.
|
||||
pub(crate) fn is_failed(&self) -> bool {
|
||||
matches!(self.state, PeerState::Failed { .. }) || self.send_failed
|
||||
}
|
||||
|
||||
/// The operator-visible handshake-state string for the pending-connection
|
||||
/// view, derived from the machine phase. Byte-identical to the strings the
|
||||
/// leg's `HandshakeState` `Display` produced before the phase collapsed onto
|
||||
/// the machine. A `send_failed` leg renders `"failed"` while its phase stays
|
||||
/// `SentMsg1`, matching the pre-collapse leg display.
|
||||
pub(crate) fn displayed_handshake_state(&self) -> &'static str {
|
||||
if self.send_failed {
|
||||
return "failed";
|
||||
}
|
||||
handshake_state_str(self.state)
|
||||
}
|
||||
|
||||
/// Record a handshake failure that the shell observed on the leg (e.g. a
|
||||
/// `complete_handshake` that rejected msg2), WITHOUT leaving the current
|
||||
/// handshake phase. Mirrors the `HandshakeSendFailed` carve-out: the failure
|
||||
/// is carried as `send_failed`, so the machine PHASE is unchanged (matching
|
||||
/// the pre-collapse behavior, where the shell marked only the leg failed and
|
||||
/// left the machine in place) while `is_failed`/display report the failure.
|
||||
/// The stale-connection sweep reclaims the leg via
|
||||
/// [`is_failed`](Self::is_failed) at the next tick, before any projection or
|
||||
/// resend.
|
||||
pub(crate) fn mark_send_failed(&mut self) {
|
||||
self.send_failed = true;
|
||||
}
|
||||
|
||||
/// The crystallized node address, if identity is known.
|
||||
fn addr(&self) -> Option<NodeAddr> {
|
||||
self.identity.map(|id| *id.node_addr())
|
||||
@@ -685,8 +760,11 @@ impl PeerMachine {
|
||||
/// actions are emitted.
|
||||
fn on_handshake_send_failed(&mut self) -> Vec<PeerAction> {
|
||||
if let Some(leg) = self.leg.as_mut() {
|
||||
// Drop the leg's Noise handshake handle at the identical point as
|
||||
// before; the failure *state* is recorded on the machine.
|
||||
leg.mark_failed();
|
||||
}
|
||||
self.send_failed = true;
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
@@ -2467,7 +2545,8 @@ mod tests {
|
||||
);
|
||||
m.set_leg(PeerConnection::outbound(LinkId::new(1), peer, 100));
|
||||
assert!(m.is_handshaking_sent_msg1());
|
||||
assert!(!m.leg().expect("leg embedded").is_failed());
|
||||
assert!(!m.is_failed());
|
||||
assert_eq!(m.displayed_handshake_state(), "sent_msg1");
|
||||
|
||||
let actions = m.step(PeerEvent::HandshakeSendFailed, 200, &mut alloc);
|
||||
assert_eq!(actions, Vec::new(), "HandshakeSendFailed emits no actions");
|
||||
@@ -2476,11 +2555,17 @@ mod tests {
|
||||
"retransmit eligibility survives a send failure"
|
||||
);
|
||||
assert!(
|
||||
m.leg().expect("leg retained").is_failed(),
|
||||
"the leg carries the failed mark the sweep reads"
|
||||
m.is_failed(),
|
||||
"the machine carries the failed mark the sweep reads"
|
||||
);
|
||||
assert_eq!(
|
||||
m.displayed_handshake_state(),
|
||||
"failed",
|
||||
"the send-failed leg still displays as failed"
|
||||
);
|
||||
|
||||
// With no leg (e.g. after take_leg) the event is a defensive no-op.
|
||||
// With no leg (e.g. after take_leg) the event stays a defensive no-op
|
||||
// for the leg handle and keeps the state retransmit-eligible.
|
||||
let _ = m.take_leg();
|
||||
let actions = m.step(PeerEvent::HandshakeSendFailed, 300, &mut alloc);
|
||||
assert_eq!(actions, Vec::new());
|
||||
@@ -2488,6 +2573,76 @@ mod tests {
|
||||
assert!(m.leg().is_none());
|
||||
}
|
||||
|
||||
// ---- Test 7f: handshake_state_str is a total, byte-identical mapping ---
|
||||
// Pins the displayed-string derivation for every `PeerState` arm against the
|
||||
// strings the deleted leg `HandshakeState` `Display` produced.
|
||||
#[test]
|
||||
fn handshake_state_str_total_mapping() {
|
||||
let link = LinkId::new(1);
|
||||
let addr = *peer_identity().node_addr();
|
||||
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Handshaking {
|
||||
link,
|
||||
phase: HandshakePhase::Initial,
|
||||
}),
|
||||
"initial"
|
||||
);
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Handshaking {
|
||||
link,
|
||||
phase: HandshakePhase::SentMsg1,
|
||||
}),
|
||||
"sent_msg1"
|
||||
);
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Handshaking {
|
||||
link,
|
||||
phase: HandshakePhase::ReceivedMsg1,
|
||||
}),
|
||||
"received_msg1"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Established { addr }),
|
||||
"complete"
|
||||
);
|
||||
assert_eq!(handshake_state_str(PeerState::Active { addr }), "complete");
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Maintaining {
|
||||
addr,
|
||||
kind: MaintainKind::Mtu,
|
||||
}),
|
||||
"complete"
|
||||
);
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Closing {
|
||||
addr,
|
||||
reason: CloseReason::Requested,
|
||||
}),
|
||||
"complete"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Failed {
|
||||
reason: FailReason::HandshakeFailed,
|
||||
}),
|
||||
"failed"
|
||||
);
|
||||
|
||||
assert_eq!(handshake_state_str(PeerState::Discovered), "initial");
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Connecting { link }),
|
||||
"initial"
|
||||
);
|
||||
assert_eq!(
|
||||
handshake_state_str(PeerState::Closed {
|
||||
backoff_deadline_ms: 0,
|
||||
}),
|
||||
"initial"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Test 8: liveness -> LinkDeadSuspected -> ReportLost --------------
|
||||
#[test]
|
||||
fn liveness_to_link_dead() {
|
||||
|
||||
@@ -11,7 +11,7 @@ mod connection;
|
||||
pub(crate) mod machine;
|
||||
|
||||
pub use active::{ActivePeer, ConnectivityState};
|
||||
pub use connection::{HandshakeState, PeerConnection};
|
||||
pub use connection::PeerConnection;
|
||||
|
||||
use crate::NodeAddr;
|
||||
use crate::transport::LinkId;
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
//! - `limits.rs` — the pure connection-retry backoff math.
|
||||
//! - `state.rs` — [`ConnectionState`], the pure handshake-phase connection
|
||||
//! bookkeeping (owned by the shell `PeerConnection` beside its Noise crypto
|
||||
//! handles) and its [`HandshakeState`] phase enum, plus [`Fmp`], the
|
||||
//! (stateless) lifecycle anchor owned by `Node`.
|
||||
//! handles), plus [`Fmp`], the (stateless) lifecycle anchor owned by `Node`.
|
||||
//! The handshake phase itself lives on the per-peer control machine.
|
||||
//! - `wire.rs` — the FMP link-framing codec: handshake message types,
|
||||
//! disconnect reasons, and the orderly disconnect message. Also carries the
|
||||
//! FMP link wire framing relocated from `node/wire.rs` — the common prefix,
|
||||
@@ -40,7 +40,6 @@ pub(crate) use core::{
|
||||
};
|
||||
pub use core::{PromotionResult, cross_connection_winner};
|
||||
pub(crate) use limits::backoff_ms;
|
||||
pub use state::HandshakeState;
|
||||
pub(crate) use state::{ConnectionState, Fmp};
|
||||
pub use wire::HandshakeMessageType;
|
||||
pub(crate) use wire::{Disconnect, DisconnectReason};
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
//! Sans-IO FMP connection-lifecycle state.
|
||||
//!
|
||||
//! The pure, runtime-agnostic bookkeeping for an in-progress FMP peer
|
||||
//! connection — link/direction identity, the handshake-phase enum, learned
|
||||
//! peer identity and epoch, index/transport/address tracking, handshake-resend
|
||||
//! scheduling, and link statistics — extracted out of the async node shell.
|
||||
//! connection — link/direction identity, learned peer identity and epoch,
|
||||
//! index/transport/address tracking, handshake-resend scheduling, and link
|
||||
//! statistics — extracted out of the async node shell. The handshake phase
|
||||
//! itself lives solely on the per-peer control machine
|
||||
//! ([`PeerMachine`](crate::peer::machine::PeerMachine)), not here.
|
||||
//!
|
||||
//! [`ConnectionState`] owns every **pure** field of the handshake-phase
|
||||
//! connection. The Noise crypto handles (`noise::HandshakeState`,
|
||||
//! `NoiseSession`) stay shell-owned in
|
||||
//! [`PeerConnection`](crate::peer::PeerConnection), which holds a
|
||||
//! `ConnectionState` alongside them and drives the two halves side by side. The
|
||||
//! shell's XX transition methods validate against the pure phase, drive the
|
||||
//! Noise objects, then write learned results back through the pure setters here
|
||||
//! (`set_handshake_state`, `set_expected_identity`, `set_remote_epoch`,
|
||||
//! `touch`).
|
||||
//! shell's XX transition methods drive the Noise objects, then write learned
|
||||
//! results back through the pure setters here (`set_expected_identity`,
|
||||
//! `set_remote_epoch`, `touch`).
|
||||
//!
|
||||
//! This state is `no_std`+`alloc`-clean with respect to transport: the
|
||||
//! identifier/address/statistics value types are the plain-data `transport`
|
||||
@@ -30,59 +31,6 @@
|
||||
use crate::PeerIdentity;
|
||||
use crate::transport::{LinkDirection, LinkId, LinkStats, TransportAddr, TransportId};
|
||||
use crate::utils::index::SessionIndex;
|
||||
use core::fmt;
|
||||
|
||||
/// Handshake protocol state machine.
|
||||
///
|
||||
/// For Noise IK pattern:
|
||||
/// - Initiator: Initial → SentMsg1 → Complete
|
||||
/// - Responder: Initial → ReceivedMsg1 → Complete
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum HandshakeState {
|
||||
/// Initial state, ready to start handshake.
|
||||
Initial,
|
||||
/// Initiator: Sent message 1, awaiting message 2.
|
||||
SentMsg1,
|
||||
/// Responder: Received message 1, ready to send message 2.
|
||||
ReceivedMsg1,
|
||||
/// Handshake completed successfully.
|
||||
Complete,
|
||||
/// Handshake failed.
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl HandshakeState {
|
||||
/// Check if handshake is still in progress.
|
||||
pub fn is_in_progress(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
HandshakeState::Initial | HandshakeState::SentMsg1 | HandshakeState::ReceivedMsg1
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if handshake completed successfully.
|
||||
pub fn is_complete(&self) -> bool {
|
||||
matches!(self, HandshakeState::Complete)
|
||||
}
|
||||
|
||||
/// Check if handshake failed.
|
||||
pub fn is_failed(&self) -> bool {
|
||||
matches!(self, HandshakeState::Failed)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HandshakeState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
HandshakeState::Initial => "initial",
|
||||
HandshakeState::SentMsg1 => "sent_msg1",
|
||||
HandshakeState::ReceivedMsg1 => "received_msg1",
|
||||
HandshakeState::Complete => "complete",
|
||||
HandshakeState::Failed => "failed",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure, runtime-agnostic bookkeeping for a connection in the handshake phase.
|
||||
///
|
||||
@@ -100,10 +48,6 @@ pub struct ConnectionState {
|
||||
/// Connection direction (we initiated or they initiated).
|
||||
direction: LinkDirection,
|
||||
|
||||
// === Handshake State ===
|
||||
/// Current handshake state.
|
||||
handshake_state: HandshakeState,
|
||||
|
||||
/// Expected peer identity (known for outbound, learned for inbound).
|
||||
/// Updated after receiving their static key in the handshake.
|
||||
expected_identity: Option<PeerIdentity>,
|
||||
@@ -166,7 +110,6 @@ impl ConnectionState {
|
||||
Self {
|
||||
link_id,
|
||||
direction: LinkDirection::Outbound,
|
||||
handshake_state: HandshakeState::Initial,
|
||||
expected_identity: Some(expected_identity),
|
||||
started_at: current_time_ms,
|
||||
last_activity: current_time_ms,
|
||||
@@ -191,7 +134,6 @@ impl ConnectionState {
|
||||
Self {
|
||||
link_id,
|
||||
direction: LinkDirection::Inbound,
|
||||
handshake_state: HandshakeState::Initial,
|
||||
expected_identity: None,
|
||||
started_at: current_time_ms,
|
||||
last_activity: current_time_ms,
|
||||
@@ -220,7 +162,6 @@ impl ConnectionState {
|
||||
Self {
|
||||
link_id,
|
||||
direction: LinkDirection::Inbound,
|
||||
handshake_state: HandshakeState::Initial,
|
||||
expected_identity: None,
|
||||
started_at: current_time_ms,
|
||||
last_activity: current_time_ms,
|
||||
@@ -249,11 +190,6 @@ impl ConnectionState {
|
||||
self.direction
|
||||
}
|
||||
|
||||
/// Get the handshake state.
|
||||
pub fn handshake_state(&self) -> HandshakeState {
|
||||
self.handshake_state
|
||||
}
|
||||
|
||||
/// Get the expected/learned peer identity, if known.
|
||||
pub fn expected_identity(&self) -> Option<&PeerIdentity> {
|
||||
self.expected_identity.as_ref()
|
||||
@@ -269,21 +205,6 @@ impl ConnectionState {
|
||||
self.direction == LinkDirection::Inbound
|
||||
}
|
||||
|
||||
/// Check if handshake is in progress.
|
||||
pub fn is_in_progress(&self) -> bool {
|
||||
self.handshake_state.is_in_progress()
|
||||
}
|
||||
|
||||
/// Check if handshake completed.
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.handshake_state.is_complete()
|
||||
}
|
||||
|
||||
/// Check if handshake failed.
|
||||
pub fn is_failed(&self) -> bool {
|
||||
self.handshake_state.is_failed()
|
||||
}
|
||||
|
||||
/// When the connection started.
|
||||
pub fn started_at(&self) -> u64 {
|
||||
self.started_at
|
||||
@@ -377,20 +298,6 @@ impl ConnectionState {
|
||||
self.expected_identity = Some(identity);
|
||||
}
|
||||
|
||||
// === Handshake Phase Advance ===
|
||||
|
||||
/// Advance the pure handshake phase. Driven by the shell after it has
|
||||
/// stepped the Noise objects.
|
||||
pub fn set_handshake_state(&mut self, state: HandshakeState) {
|
||||
self.handshake_state = state;
|
||||
}
|
||||
|
||||
/// Mark the pure handshake phase failed. The shell drops the crypto handle
|
||||
/// separately.
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.handshake_state = HandshakeState::Failed;
|
||||
}
|
||||
|
||||
// === Handshake Resend ===
|
||||
|
||||
/// Store the wire-format msg1 bytes for resend and schedule the first resend.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Unit tests for the pure FMP connection state ([`ConnectionState`]) and its
|
||||
//! [`HandshakeState`] phase enum. These exercise the extracted bookkeeping
|
||||
//! directly, with no crypto involved; the crypto-driving transition behavior is
|
||||
//! covered by the shell `peer::connection` suite.
|
||||
//! Unit tests for the pure FMP connection state ([`ConnectionState`]). These
|
||||
//! exercise the extracted bookkeeping directly, with no crypto involved; the
|
||||
//! crypto-driving transition behavior is covered by the shell `peer::connection`
|
||||
//! suite, and the handshake phase itself lives on the control machine.
|
||||
|
||||
use crate::proto::fmp::{ConnectionState, HandshakeState};
|
||||
use crate::proto::fmp::ConnectionState;
|
||||
use crate::transport::{LinkId, TransportAddr, TransportId};
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::{Identity, PeerIdentity};
|
||||
@@ -12,21 +12,6 @@ fn make_peer_identity() -> PeerIdentity {
|
||||
PeerIdentity::from_pubkey(Identity::generate().pubkey())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_state_predicates() {
|
||||
assert!(HandshakeState::Initial.is_in_progress());
|
||||
assert!(HandshakeState::SentMsg1.is_in_progress());
|
||||
assert!(HandshakeState::ReceivedMsg1.is_in_progress());
|
||||
assert!(!HandshakeState::Complete.is_in_progress());
|
||||
assert!(!HandshakeState::Failed.is_in_progress());
|
||||
|
||||
assert!(HandshakeState::Complete.is_complete());
|
||||
assert!(!HandshakeState::Initial.is_complete());
|
||||
|
||||
assert!(HandshakeState::Failed.is_failed());
|
||||
assert!(!HandshakeState::Complete.is_failed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outbound_initializes_pure_fields() {
|
||||
let identity = make_peer_identity();
|
||||
@@ -34,8 +19,6 @@ fn outbound_initializes_pure_fields() {
|
||||
|
||||
assert!(state.is_outbound());
|
||||
assert!(!state.is_inbound());
|
||||
assert_eq!(state.handshake_state(), HandshakeState::Initial);
|
||||
assert!(state.is_in_progress());
|
||||
assert!(state.expected_identity().is_some());
|
||||
assert_eq!(state.link_id(), LinkId::new(1));
|
||||
assert_eq!(state.started_at(), 1000);
|
||||
@@ -53,7 +36,6 @@ fn inbound_initializes_pure_fields() {
|
||||
|
||||
assert!(state.is_inbound());
|
||||
assert!(!state.is_outbound());
|
||||
assert_eq!(state.handshake_state(), HandshakeState::Initial);
|
||||
assert!(state.expected_identity().is_none());
|
||||
assert_eq!(state.started_at(), 2000);
|
||||
}
|
||||
@@ -111,27 +93,6 @@ fn identity_and_epoch_setters() {
|
||||
assert_eq!(state.remote_epoch(), Some([9u8; 8]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handshake_state_advance_and_fail() {
|
||||
let mut state = ConnectionState::outbound(LinkId::new(1), make_peer_identity(), 0);
|
||||
assert!(state.is_in_progress());
|
||||
|
||||
state.set_handshake_state(HandshakeState::SentMsg1);
|
||||
assert_eq!(state.handshake_state(), HandshakeState::SentMsg1);
|
||||
assert!(state.is_in_progress());
|
||||
assert!(!state.is_complete());
|
||||
|
||||
state.set_handshake_state(HandshakeState::Complete);
|
||||
assert!(state.is_complete());
|
||||
assert!(!state.is_in_progress());
|
||||
|
||||
state.mark_failed();
|
||||
assert!(state.is_failed());
|
||||
assert!(!state.is_in_progress());
|
||||
assert!(!state.is_complete());
|
||||
assert_eq!(state.handshake_state(), HandshakeState::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resend_bookkeeping() {
|
||||
let mut state = ConnectionState::outbound(LinkId::new(1), make_peer_identity(), 0);
|
||||
|
||||
Reference in New Issue
Block a user