//! Peer Connection (Handshake Phase) //! //! Represents an in-progress connection before authentication completes. //! PeerConnection tracks the Noise XX handshake state and transitions to //! ActivePeer upon successful authentication. use crate::utils::index::SessionIndex; use crate::noise::{self, NoiseError, NoiseSession}; use crate::transport::{LinkDirection, LinkId, LinkStats, TransportAddr, TransportId}; use crate::PeerIdentity; use secp256k1::Keypair; use std::fmt; /// Handshake protocol state machine. /// /// For Noise XX pattern: /// - Initiator: Initial → SentMsg1 → Complete (after processing msg2 + sending msg3) /// - Responder: Initial → ReceivedMsg1 → Complete (after processing msg3) #[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) } } /// A connection in the handshake phase, before authentication completes. /// /// For outbound connections, we know the expected peer identity from config. /// For inbound connections, we learn the identity during the Noise handshake. pub struct PeerConnection { // === Link Reference === /// The link carrying this connection. link_id: LinkId, /// 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, /// Noise handshake state (consumes on completion). noise_handshake: Option, /// Completed Noise session (available after handshake complete). noise_session: Option, // === Timing === /// When the connection attempt started (Unix milliseconds). started_at: u64, /// When the last handshake message was sent/received. last_activity: u64, // === Statistics === /// Link statistics during handshake. link_stats: LinkStats, // === Wire Protocol Index Tracking === /// Our sender_idx for this handshake (chosen by us). /// For outbound: included in msg1, used as receiver_idx in msg2 echo. /// For inbound: chosen after processing msg1, included in msg2. our_index: Option, /// Their sender_idx (learned from their messages). /// For outbound: learned from msg2. /// For inbound: learned from msg1. their_index: Option, /// Transport ID (for index namespace). transport_id: Option, /// Current source address (updated on packet receipt). source_addr: Option, // === Epoch (Restart Detection) === /// Remote peer's startup epoch (learned from handshake). remote_epoch: Option<[u8; 8]>, // === Handshake Resend === /// Wire-format msg1 bytes for resend (initiator only). handshake_msg1: Option>, /// Wire-format msg2 bytes for resend (responder only). handshake_msg2: Option>, /// Number of resends performed so far. resend_count: u32, /// When the next resend should fire (Unix ms). 0 = no resend scheduled. next_resend_at_ms: u64, } impl PeerConnection { /// Create a new outbound connection (we are initiating). /// /// For outbound, we know who we're trying to reach from configuration. /// The Noise handshake will be initialized when `start_handshake` is called. pub fn outbound( link_id: LinkId, expected_identity: PeerIdentity, current_time_ms: u64, ) -> Self { Self { link_id, direction: LinkDirection::Outbound, handshake_state: HandshakeState::Initial, expected_identity: Some(expected_identity), noise_handshake: None, noise_session: None, started_at: current_time_ms, last_activity: current_time_ms, link_stats: LinkStats::new(), our_index: None, their_index: None, transport_id: None, source_addr: None, remote_epoch: None, handshake_msg1: None, handshake_msg2: None, resend_count: 0, next_resend_at_ms: 0, } } /// Create a new inbound connection (they are initiating). /// /// For inbound, we don't know who they are until we decrypt their /// identity from Noise message 1. pub fn inbound(link_id: LinkId, current_time_ms: u64) -> Self { Self { link_id, direction: LinkDirection::Inbound, handshake_state: HandshakeState::Initial, expected_identity: None, noise_handshake: None, noise_session: None, started_at: current_time_ms, last_activity: current_time_ms, link_stats: LinkStats::new(), our_index: None, their_index: None, transport_id: None, source_addr: None, remote_epoch: None, handshake_msg1: None, handshake_msg2: None, resend_count: 0, next_resend_at_ms: 0, } } /// Create a new inbound connection with transport information. /// /// Used when processing msg1 where we know the transport and source address. pub fn inbound_with_transport( link_id: LinkId, transport_id: TransportId, source_addr: TransportAddr, current_time_ms: u64, ) -> Self { Self { link_id, direction: LinkDirection::Inbound, handshake_state: HandshakeState::Initial, expected_identity: None, noise_handshake: None, noise_session: None, started_at: current_time_ms, last_activity: current_time_ms, link_stats: LinkStats::new(), our_index: None, their_index: None, transport_id: Some(transport_id), source_addr: Some(source_addr), remote_epoch: None, handshake_msg1: None, handshake_msg2: None, resend_count: 0, next_resend_at_ms: 0, } } // === Accessors === /// Get the link ID. pub fn link_id(&self) -> LinkId { self.link_id } /// Get the connection direction. pub fn direction(&self) -> LinkDirection { 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() } /// Check if this is an outbound connection. pub fn is_outbound(&self) -> bool { self.direction == LinkDirection::Outbound } /// Check if this is an inbound connection. pub fn is_inbound(&self) -> bool { 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 } /// When the last activity occurred. pub fn last_activity(&self) -> u64 { self.last_activity } /// Connection duration so far. pub fn duration(&self, current_time_ms: u64) -> u64 { current_time_ms.saturating_sub(self.started_at) } /// Time since last activity. pub fn idle_time(&self, current_time_ms: u64) -> u64 { current_time_ms.saturating_sub(self.last_activity) } /// Get link statistics. pub fn link_stats(&self) -> &LinkStats { &self.link_stats } /// Get mutable link statistics. pub fn link_stats_mut(&mut self) -> &mut LinkStats { &mut self.link_stats } // === Index Accessors === /// Get our session index (if set). pub fn our_index(&self) -> Option { self.our_index } /// Set our session index. pub fn set_our_index(&mut self, index: SessionIndex) { self.our_index = Some(index); } /// Get their session index (if known). pub fn their_index(&self) -> Option { self.their_index } /// Set their session index. pub fn set_their_index(&mut self, index: SessionIndex) { self.their_index = Some(index); } /// Get the transport ID (if set). pub fn transport_id(&self) -> Option { self.transport_id } /// Set the transport ID. pub fn set_transport_id(&mut self, id: TransportId) { self.transport_id = Some(id); } /// Get the source address (if known). pub fn source_addr(&self) -> Option<&TransportAddr> { self.source_addr.as_ref() } /// Set the source address. pub fn set_source_addr(&mut self, addr: TransportAddr) { self.source_addr = Some(addr); } // === Epoch Accessors === /// Get the remote peer's startup epoch (available after handshake). pub fn remote_epoch(&self) -> Option<[u8; 8]> { self.remote_epoch } // === Handshake Resend === /// Store the wire-format msg1 bytes for resend and schedule the first resend. pub fn set_handshake_msg1(&mut self, msg1: Vec, first_resend_at_ms: u64) { self.handshake_msg1 = Some(msg1); self.resend_count = 0; self.next_resend_at_ms = first_resend_at_ms; } /// Store the wire-format msg2 bytes for resend on duplicate msg1. pub fn set_handshake_msg2(&mut self, msg2: Vec) { self.handshake_msg2 = Some(msg2); } /// Get the stored msg1 bytes (if any). pub fn handshake_msg1(&self) -> Option<&[u8]> { self.handshake_msg1.as_deref() } /// Get the stored msg2 bytes (if any). pub fn handshake_msg2(&self) -> Option<&[u8]> { self.handshake_msg2.as_deref() } /// Number of resends performed. pub fn resend_count(&self) -> u32 { self.resend_count } /// When the next resend is scheduled (Unix ms). pub fn next_resend_at_ms(&self) -> u64 { self.next_resend_at_ms } /// Record a resend and schedule the next one. pub fn record_resend(&mut self, next_resend_at_ms: u64) { self.resend_count += 1; self.next_resend_at_ms = next_resend_at_ms; } // === Noise Handshake Operations === /// Start the handshake as initiator and generate message 1. /// /// For outbound connections only. Returns the Noise XX msg1 bytes. /// XX msg1 is ephemeral-only (33 bytes) — no identity or epoch. pub fn start_handshake( &mut self, our_keypair: Keypair, epoch: [u8; 8], current_time_ms: u64, ) -> Result, NoiseError> { if self.direction != LinkDirection::Outbound { return Err(NoiseError::WrongState { expected: "outbound connection".to_string(), got: "inbound connection".to_string(), }); } if self.handshake_state != HandshakeState::Initial { return Err(NoiseError::WrongState { expected: "initial state".to_string(), got: self.handshake_state.to_string(), }); } // XX initiator: no remote static needed upfront let mut hs = noise::HandshakeState::new_xx_initiator(our_keypair); hs.set_local_epoch(epoch); let msg1 = hs.write_xx_message_1()?; self.noise_handshake = Some(hs); self.handshake_state = HandshakeState::SentMsg1; self.last_activity = current_time_ms; Ok(msg1) } /// Initialize responder and process incoming message 1. /// /// For inbound connections only. Returns the Noise XX msg2 bytes. /// XX: identity is NOT learned from msg1 (only ephemeral exchange). /// The responder learns the initiator's identity from msg3. /// The handshake remains in ReceivedMsg1 state (not Complete). /// /// If `negotiation_payload` is provided, it is encrypted and appended /// to the returned msg2 bytes. pub fn receive_handshake_init( &mut self, our_keypair: Keypair, epoch: [u8; 8], message: &[u8], negotiation_payload: Option<&[u8]>, current_time_ms: u64, ) -> Result, NoiseError> { if self.direction != LinkDirection::Inbound { return Err(NoiseError::WrongState { expected: "inbound connection".to_string(), got: "outbound connection".to_string(), }); } if self.handshake_state != HandshakeState::Initial { return Err(NoiseError::WrongState { expected: "initial state".to_string(), got: self.handshake_state.to_string(), }); } let mut hs = noise::HandshakeState::new_xx_responder(our_keypair); hs.set_local_epoch(epoch); // Process XX message 1 (ephemeral only — no identity learned) hs.read_xx_message_1(message)?; // Generate XX message 2 (sends our static + epoch) let mut msg2 = hs.write_xx_message_2()?; // Append encrypted negotiation payload if provided if let Some(payload) = negotiation_payload { let encrypted = hs.encrypt_payload(payload)?; msg2.extend_from_slice(&encrypted); } // XX: handshake NOT complete yet — need msg3. // Keep the handshake state for complete_handshake_msg3(). self.noise_handshake = Some(hs); self.handshake_state = HandshakeState::ReceivedMsg1; self.last_activity = current_time_ms; Ok(msg2) } /// Complete the handshake by processing message 2 and generating message 3. /// /// For outbound connections only (initiator). Processes the responder's /// msg2 (learning their identity and epoch), then generates msg3. /// Returns the Noise XX msg3 bytes to send. /// /// If `negotiation_payload` is provided, it is encrypted and appended /// to the returned msg3 bytes. If the received msg2 contains a negotiation /// payload (bytes beyond the base XX msg2), it is decrypted and returned. pub fn complete_handshake( &mut self, message: &[u8], negotiation_payload: Option<&[u8]>, current_time_ms: u64, ) -> Result<(Vec, Option>), NoiseError> { if self.handshake_state != HandshakeState::SentMsg1 { return Err(NoiseError::WrongState { expected: "sent_msg1 state".to_string(), got: self.handshake_state.to_string(), }); } let mut hs = self .noise_handshake .take() .expect("noise handshake must exist in SentMsg1 state"); // Split msg2 into base XX part and optional negotiation let base_size = noise::XX_HANDSHAKE_MSG2_SIZE; let (base_msg2, extra) = if message.len() > base_size { (&message[..base_size], Some(&message[base_size..])) } else { (message, None) }; // Process XX msg2 (learns responder identity + epoch) hs.read_xx_message_2(base_msg2)?; // Decrypt negotiation payload from msg2 if present let received_negotiation = if let Some(encrypted) = extra { Some(hs.decrypt_payload(encrypted)?) } else { None }; // Learn responder identity from msg2 let remote_static = *hs .remote_static() .expect("remote static available after XX msg2"); self.expected_identity = Some(PeerIdentity::from_pubkey_full(remote_static)); // Capture remote epoch from msg2 self.remote_epoch = hs.remote_epoch(); // Generate XX msg3 let mut msg3 = hs.write_xx_message_3()?; // Append encrypted negotiation payload if provided if let Some(payload) = negotiation_payload { let encrypted = hs.encrypt_payload(payload)?; msg3.extend_from_slice(&encrypted); } // Handshake complete for initiator let session = hs.into_session()?; self.noise_session = Some(session); self.handshake_state = HandshakeState::Complete; self.last_activity = current_time_ms; Ok((msg3, received_negotiation)) } /// Complete the responder handshake by processing message 3. /// /// For inbound connections only (responder). Processes the initiator's /// msg3, learning their identity and epoch. /// /// If the msg3 contains a negotiation payload (bytes beyond base XX msg3), /// it is decrypted and returned. pub fn complete_handshake_msg3( &mut self, message: &[u8], current_time_ms: u64, ) -> Result>, NoiseError> { if self.handshake_state != HandshakeState::ReceivedMsg1 { return Err(NoiseError::WrongState { expected: "received_msg1 state".to_string(), got: self.handshake_state.to_string(), }); } let mut hs = self .noise_handshake .take() .expect("noise handshake must exist in ReceivedMsg1 state"); // Split msg3 into base XX part and optional negotiation let base_size = noise::XX_HANDSHAKE_MSG3_SIZE; let (base_msg3, extra) = if message.len() > base_size { (&message[..base_size], Some(&message[base_size..])) } else { (message, None) }; // Process XX msg3 (learns initiator identity + epoch) hs.read_xx_message_3(base_msg3)?; // Decrypt negotiation payload from msg3 if present let received_negotiation = if let Some(encrypted) = extra { Some(hs.decrypt_payload(encrypted)?) } else { None }; // Learn initiator identity from msg3 let remote_static = *hs .remote_static() .expect("remote static available after XX msg3"); self.expected_identity = Some(PeerIdentity::from_pubkey_full(remote_static)); // Capture remote epoch from msg3 self.remote_epoch = hs.remote_epoch(); // Handshake complete for responder let session = hs.into_session()?; self.noise_session = Some(session); self.handshake_state = HandshakeState::Complete; self.last_activity = current_time_ms; Ok(received_negotiation) } /// Take the completed Noise session. /// /// Returns the NoiseSession for use in ActivePeer. Can only be called /// once after handshake completes. pub fn take_session(&mut self) -> Option { if self.handshake_state == HandshakeState::Complete { self.noise_session.take() } else { None } } /// Check if we have a completed session ready to take. pub fn has_session(&self) -> bool { self.handshake_state == HandshakeState::Complete && self.noise_session.is_some() } // === State Transitions (for manual control if needed) === /// Mark handshake as failed. pub fn mark_failed(&mut self) { self.handshake_state = HandshakeState::Failed; self.noise_handshake = None; } /// Update last activity timestamp. pub fn touch(&mut self, current_time_ms: u64) { self.last_activity = current_time_ms; } // === Validation === /// Check if the connection has timed out. pub fn is_timed_out(&self, current_time_ms: u64, timeout_ms: u64) -> bool { self.idle_time(current_time_ms) > timeout_ms } } impl fmt::Debug for PeerConnection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PeerConnection") .field("link_id", &self.link_id) .field("direction", &self.direction) .field("handshake_state", &self.handshake_state) .field("expected_identity", &self.expected_identity) .field("has_noise_handshake", &self.noise_handshake.is_some()) .field("has_noise_session", &self.noise_session.is_some()) .field("our_index", &self.our_index) .field("their_index", &self.their_index) .field("transport_id", &self.transport_id) .field("started_at", &self.started_at) .field("last_activity", &self.last_activity) .finish() } } #[cfg(test)] mod tests { use super::*; use crate::Identity; use rand::Rng; fn make_peer_identity() -> PeerIdentity { let identity = Identity::generate(); PeerIdentity::from_pubkey(identity.pubkey()) } fn make_keypair() -> Keypair { let identity = Identity::generate(); identity.keypair() } fn make_epoch() -> [u8; 8] { let mut epoch = [0u8; 8]; rand::rng().fill_bytes(&mut epoch); 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(); let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000); assert!(conn.is_outbound()); assert!(!conn.is_inbound()); assert_eq!(conn.handshake_state(), HandshakeState::Initial); assert!(conn.expected_identity().is_some()); assert_eq!(conn.started_at(), 1000); } #[test] fn test_inbound_connection() { let conn = PeerConnection::inbound(LinkId::new(2), 2000); assert!(conn.is_inbound()); assert!(!conn.is_outbound()); assert_eq!(conn.handshake_state(), HandshakeState::Initial); assert!(conn.expected_identity().is_none()); assert_eq!(conn.started_at(), 2000); } #[test] fn test_full_handshake_flow() { // Create identities let initiator_identity = Identity::generate(); let responder_identity = Identity::generate(); let initiator_keypair = initiator_identity.keypair(); let responder_keypair = responder_identity.keypair(); let initiator_epoch = make_epoch(); let responder_epoch = make_epoch(); // Use from_pubkey_full to preserve parity for ECDH let responder_peer_id = PeerIdentity::from_pubkey_full(responder_identity.pubkey_full()); // Create connections let mut initiator_conn = PeerConnection::outbound(LinkId::new(1), responder_peer_id, 1000); let mut responder_conn = PeerConnection::inbound(LinkId::new(2), 1000); // Initiator starts XX handshake let msg1 = initiator_conn.start_handshake(initiator_keypair, initiator_epoch, 1100).unwrap(); assert_eq!(initiator_conn.handshake_state(), HandshakeState::SentMsg1); // Responder processes msg1 and sends msg2 (XX: does NOT complete yet) let msg2 = responder_conn .receive_handshake_init(responder_keypair, responder_epoch, &msg1, None, 1200) .unwrap(); assert_eq!(responder_conn.handshake_state(), HandshakeState::ReceivedMsg1); // Responder does NOT know initiator's identity yet (XX property) assert!(responder_conn.expected_identity().is_none()); // Initiator processes msg2 and generates msg3 let (msg3, _neg) = initiator_conn.complete_handshake(&msg2, None, 1300).unwrap(); assert_eq!(initiator_conn.handshake_state(), HandshakeState::Complete); // Initiator learned responder's identity from msg2 let discovered = initiator_conn.expected_identity().unwrap(); assert_eq!(discovered.pubkey(), responder_identity.pubkey()); assert_eq!(initiator_conn.remote_epoch(), Some(responder_epoch)); // Responder processes msg3 (completes handshake) let _neg = responder_conn.complete_handshake_msg3(&msg3, 1400).unwrap(); assert_eq!(responder_conn.handshake_state(), HandshakeState::Complete); // Responder learned initiator's identity from msg3 let discovered = responder_conn.expected_identity().unwrap(); assert_eq!(discovered.pubkey(), initiator_identity.pubkey()); assert_eq!(responder_conn.remote_epoch(), Some(initiator_epoch)); // Both have sessions assert!(initiator_conn.has_session()); assert!(responder_conn.has_session()); // Take and verify sessions work let mut init_session = initiator_conn.take_session().unwrap(); let mut resp_session = responder_conn.take_session().unwrap(); // Encrypt/decrypt test let plaintext = b"test message"; let ciphertext = init_session.encrypt(plaintext).unwrap(); let decrypted = resp_session.decrypt(&ciphertext).unwrap(); assert_eq!(decrypted, plaintext); } #[test] fn test_connection_timing() { let identity = make_peer_identity(); let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000); assert_eq!(conn.duration(1500), 500); assert_eq!(conn.idle_time(1500), 500); assert!(!conn.is_timed_out(1500, 1000)); assert!(conn.is_timed_out(2500, 1000)); } #[test] fn test_connection_failure() { let identity = make_peer_identity(); let mut conn = PeerConnection::outbound(LinkId::new(1), identity, 1000); conn.mark_failed(); assert!(conn.is_failed()); assert!(!conn.is_in_progress()); assert!(!conn.is_complete()); } #[test] fn test_wrong_direction_errors() { let identity = make_peer_identity(); let keypair = make_keypair(); // Outbound can't receive_handshake_init let mut outbound = PeerConnection::outbound(LinkId::new(1), identity, 1000); assert!(outbound .receive_handshake_init(keypair, make_epoch(), &[0u8; 33], None, 1100) .is_err()); // Inbound can't start_handshake let mut inbound = PeerConnection::inbound(LinkId::new(2), 1000); assert!(inbound.start_handshake(keypair, make_epoch(), 1100).is_err()); } }