diff --git a/common/nym-lp/src/lib.rs b/common/nym-lp/src/lib.rs index 0d2afe0b22..e1198e7fdc 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -38,12 +38,25 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) { // Use consistent salt for deterministic tests let salt = [1u8; 32]; - let initiator_session = - LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) - .expect("Test session creation failed"); + let initiator_session = LpSession::new( + receiver_index, + true, + init.clone(), + resp.as_remote(), + &salt, + packet::version::CURRENT, + ) + .expect("Test session creation failed"); - let responder_session = LpSession::new(receiver_index, false, resp, init.as_remote(), &salt) - .expect("Test session creation failed"); + let responder_session = LpSession::new( + receiver_index, + false, + resp, + init.as_remote(), + &salt, + packet::version::CURRENT, + ) + .expect("Test session creation failed"); (initiator_session, responder_session) } @@ -51,7 +64,7 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) { #[cfg(test)] mod tests { use crate::message::LpMessage; - use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; + use crate::packet::{LpHeader, LpPacket, TRAILER_LEN, version}; use crate::session_manager::SessionManager; use crate::{LpError, sessions_for_tests}; use bytes::BytesMut; @@ -182,11 +195,19 @@ mod tests { init.clone(), resp.as_remote(), &salt, + version::CURRENT, ) .unwrap(); let _ = remote_manager - .create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + false, + resp, + init.as_remote(), + &salt, + version::CURRENT, + ) .unwrap(); // === Packet 1 (Counter 0 - Should succeed) === let packet1 = LpPacket { diff --git a/common/nym-lp/src/packet.rs b/common/nym-lp/src/packet.rs index 2873951680..e93edfaad4 100644 --- a/common/nym-lp/src/packet.rs +++ b/common/nym-lp/src/packet.rs @@ -203,9 +203,9 @@ impl LpHeader { } impl LpHeader { - pub fn new(receiver_idx: u32, counter: u64) -> Self { + pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self { Self { - protocol_version: version::CURRENT, + protocol_version, reserved: [0u8; 3], receiver_idx, counter, diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index a6561eb199..0b8cee57a3 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -231,7 +231,7 @@ pub struct LpSession { /// Negotiated protocol version from handshake. /// Set during handshake completion from the ClientHello/ServerHello packet header. /// Used for future version negotiation and compatibility checks. - negotiated_version: std::sync::atomic::AtomicU8, + negotiated_version: u8, } // noiserm @@ -276,18 +276,9 @@ impl LpSession { /// Returns the negotiated protocol version from the handshake. /// - /// Defaults to 1 (current LP version). Set during handshake via - /// `set_negotiated_version()` when ClientHello/ServerHello is processed. + /// Set during `LpSession` creation after sending / receiving `ClientHelloData` pub fn negotiated_version(&self) -> u8 { - self.negotiated_version.load(Ordering::Acquire) - } - - /// Sets the negotiated protocol version from handshake packet header. - /// - /// Should be called during handshake when processing ClientHello (responder) - /// or ServerHello (initiator) to record the agreed protocol version. - pub fn set_negotiated_version(&self, version: u8) { - self.negotiated_version.store(version, Ordering::Release); + self.negotiated_version } /// Returns the local X25519 public key. @@ -364,12 +355,14 @@ impl LpSession { /// * `local_peer` - This side's LP peer's keys /// * `remote_peer` - The remote's LP peer's keys /// * `salt` - Salt for PSK derivation + /// * `protocol_version` - Protocol version to attach in all `LpPacket`s pub fn new( id: u32, is_initiator: bool, local_peer: LpLocalPeer, remote_peer: LpRemotePeer, salt: &[u8; 32], + protocol_version: u8, ) -> Result { // noiserm // if we're LP responder, we **must** set our kem key @@ -444,13 +437,13 @@ impl LpSession { subsession_counter: AtomicU64::new(0), read_only: AtomicBool::new(false), successor_session_id: Mutex::new(None), - negotiated_version: std::sync::atomic::AtomicU8::new(1), // Default to version 1 + negotiated_version: protocol_version, }) } pub fn next_packet(&self, message: LpMessage) -> Result { let counter = self.next_counter(); - let header = LpHeader::new(self.id(), counter); + let header = LpHeader::new(self.id(), counter, self.negotiated_version); let packet = LpPacket::new(header, message); Ok(packet) } @@ -1275,6 +1268,7 @@ impl LpSession { remote_peer: self.remote_peer.clone(), pq_shared_secret: PqSharedSecret::new(pq_secret), subsession_psk, + negotiated_version: self.negotiated_version, }) } } @@ -1314,8 +1308,12 @@ pub struct SubsessionHandshake { /// PQ shared secret inherited from parent (for creating further subsessions) pq_shared_secret: PqSharedSecret, + /// Subsession PSK (for deriving outer AEAD key) subsession_psk: [u8; 32], + + /// Negotiated protocol version from handshake. + negotiated_version: u8, } impl SubsessionHandshake { @@ -1415,7 +1413,7 @@ impl SubsessionHandshake { read_only: AtomicBool::new(false), successor_session_id: Mutex::new(None), // Inherit parent's protocol version - negotiated_version: std::sync::atomic::AtomicU8::new(1), + negotiated_version: self.negotiated_version, }) } } @@ -1423,6 +1421,7 @@ impl SubsessionHandshake { #[cfg(test)] mod tests { use super::*; + use crate::packet::version; use crate::peer::mock_peers; use crate::{replay::ReplayError, sessions_for_tests}; use nym_crypto::asymmetric::ed25519; @@ -1448,8 +1447,15 @@ mod tests { let salt = [0u8; 32]; // Test salt // PSQ will derive the PSK during handshake using X25519 as DHKEM - let session = LpSession::new(receiver_index, is_initiator, local, remote.clone(), &salt) - .expect("Test session creation failed"); + let session = LpSession::new( + receiver_index, + is_initiator, + local, + remote.clone(), + &salt, + version::CURRENT, + ) + .expect("Test session creation failed"); // Initialize KKT state to Completed for tests (bypasses KKT exchange) // This simulates having already received the remote party's KEM key via KKT @@ -2112,14 +2118,28 @@ mod tests { let receiver_index: u32 = 55555; let salt = [0u8; 32]; - let initiator_session = - LpSession::new(receiver_index, true, init.clone(), bad_resp, &salt).unwrap(); + let initiator_session = LpSession::new( + receiver_index, + true, + init.clone(), + bad_resp, + &salt, + version::CURRENT, + ) + .unwrap(); // Initialize KKT state for test initiator_session.set_kkt_completed_for_test(resp.x25519.public_key()); - let responder_session = - LpSession::new(receiver_index, false, resp, bad_init, &salt).unwrap(); + let responder_session = LpSession::new( + receiver_index, + false, + resp, + bad_init, + &salt, + version::CURRENT, + ) + .unwrap(); // Initialize KKT state for test responder_session.set_kkt_completed_for_test(init.x25519.public_key()); @@ -2154,13 +2174,27 @@ mod tests { let receiver_index: u32 = 66666; let salt = [0u8; 32]; - let initiator_session = - LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt).unwrap(); + let initiator_session = LpSession::new( + receiver_index, + true, + init.clone(), + resp.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // Initialize KKT state for test initiator_session.set_kkt_completed_for_test(resp.x25519.public_key()); - let responder_session = - LpSession::new(receiver_index, false, resp, bad_init, &salt).unwrap(); + let responder_session = LpSession::new( + receiver_index, + false, + resp, + bad_init, + &salt, + version::CURRENT, + ) + .unwrap(); // Initialize KKT state for test responder_session.set_kkt_completed_for_test(init.x25519.public_key()); diff --git a/common/nym-lp/src/session_integration/mod.rs b/common/nym-lp/src/session_integration/mod.rs index 7733b5459f..7e52d88b9c 100644 --- a/common/nym-lp/src/session_integration/mod.rs +++ b/common/nym-lp/src/session_integration/mod.rs @@ -58,11 +58,25 @@ mod tests { // 4. Create sessions using the pre-built Noise states let peer_a_sm = session_manager_1 - .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + true, + a.clone(), + b.as_remote(), + &salt, + version::CURRENT, + ) .expect("Failed to create session A"); let peer_b_sm = session_manager_2 - .create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + false, + b.clone(), + a.as_remote(), + &salt, + version::CURRENT, + ) .expect("Failed to create session B"); // Verify session count @@ -481,11 +495,25 @@ mod tests { let salt = [43u8; 32]; let peer_a_sm = session_manager_1 - .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + true, + a.clone(), + b.as_remote(), + &salt, + version::CURRENT, + ) .expect("Failed to create session A"); let peer_b_sm = session_manager_2 - .create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + false, + b.clone(), + a.as_remote(), + &salt, + version::CURRENT, + ) .expect("Failed to create session B"); // Initialize KKT state for both sessions (test bypass) @@ -659,7 +687,14 @@ mod tests { // 2. Create a session (using real noise state) let _session = session_manager - .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + true, + a.clone(), + b.as_remote(), + &salt, + version::CURRENT, + ) .expect("Failed to create session"); // 3. Try to get a non-existent session @@ -682,6 +717,7 @@ mod tests { a.clone(), b.as_remote(), &salt, + version::CURRENT, ) .expect("Failed to create temp session"); @@ -733,6 +769,7 @@ mod tests { } // Remove unused imports if SessionManager methods are no longer direct dependencies // use crate::noise_protocol::{create_noise_state, create_noise_state_responder}; + use crate::packet::version; use crate::peer::mock_peers; use crate::state_machine::LpData; use crate::{ @@ -772,12 +809,26 @@ mod tests { // 3. Create sessions state machines assert!( session_manager_1 - .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt,) // Initiator + .create_session_state_machine( + receiver_index, + true, + a.clone(), + b.as_remote(), + &salt, + version::CURRENT + ) // Initiator .is_ok() ); assert!( session_manager_2 - .create_session_state_machine(receiver_index, false, b, a.as_remote(), &salt,) // Responder + .create_session_state_machine( + receiver_index, + false, + b, + a.as_remote(), + &salt, + version::CURRENT + ) // Responder .is_ok() ); diff --git a/common/nym-lp/src/session_manager.rs b/common/nym-lp/src/session_manager.rs index 41b28da1c7..9988167164 100644 --- a/common/nym-lp/src/session_manager.rs +++ b/common/nym-lp/src/session_manager.rs @@ -172,8 +172,16 @@ impl SessionManager { local_peer: LpLocalPeer, remote_peer: LpRemotePeer, salt: &[u8; 32], + protocol_version: u8, ) -> Result { - let sm = LpStateMachine::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?; + let sm = LpStateMachine::new( + receiver_index, + is_initiator, + local_peer, + remote_peer, + salt, + protocol_version, + )?; self.state_machines.insert(receiver_index, sm); Ok(receiver_index) @@ -204,6 +212,7 @@ impl SessionManager { #[cfg(test)] mod tests { use super::*; + use crate::packet::version; use crate::peer::{mock_peers, random_peer}; use nym_test_utils::helpers::deterministic_rng; @@ -218,7 +227,14 @@ mod tests { let receiver_index: u32 = 1001; let sm_1_id = manager - .create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + true, + local, + peer1.as_remote(), + &salt, + version::CURRENT, + ) .unwrap(); let retrieved = manager.state_machine_exists(sm_1_id); @@ -239,7 +255,14 @@ mod tests { let receiver_index: u32 = 2002; let sm_1_id = manager - .create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt) + .create_session_state_machine( + receiver_index, + true, + local, + peer1.as_remote(), + &salt, + version::CURRENT, + ) .unwrap(); let removed = manager.remove_state_machine(sm_1_id); @@ -262,15 +285,36 @@ mod tests { let salt = [49u8; 32]; let sm_1 = manager - .create_session_state_machine(3001, true, local.clone(), peer1.as_remote(), &salt) + .create_session_state_machine( + 3001, + true, + local.clone(), + peer1.as_remote(), + &salt, + version::CURRENT, + ) .unwrap(); let sm_2 = manager - .create_session_state_machine(3002, true, local.clone(), peer2.as_remote(), &salt) + .create_session_state_machine( + 3002, + true, + local.clone(), + peer2.as_remote(), + &salt, + version::CURRENT, + ) .unwrap(); let sm_3 = manager - .create_session_state_machine(3003, true, local.clone(), peer3.as_remote(), &salt) + .create_session_state_machine( + 3003, + true, + local.clone(), + peer3.as_remote(), + &salt, + version::CURRENT, + ) .unwrap(); assert_eq!(manager.session_count(), 3); @@ -298,6 +342,7 @@ mod tests { init, resp.as_remote(), &salt, + version::CURRENT, ); assert!(sm.is_ok()); diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index 9bf3541c6a..0283f2f244 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -256,16 +256,25 @@ impl LpStateMachine { /// * `local_peer` - This side's LP peer's keys /// * `remote_peer` - The remote's LP peer's keys /// * `salt` - Fresh salt for PSK derivation (must be unique per session) + /// * `protocol_version` - Protocol version to attach in all `LpPacket`s pub fn new( receiver_index: u32, is_initiator: bool, local_peer: LpLocalPeer, remote_peer: LpRemotePeer, salt: &[u8; 32], + protocol_version: u8, ) -> Result { // Create the session with both Ed25519 (for PSQ auth) and derived X25519 keys (for Noise) // receiver_index is client-proposed, passed through directly - let session = LpSession::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?; + let session = LpSession::new( + receiver_index, + is_initiator, + local_peer, + remote_peer, + salt, + protocol_version, + )?; Ok(LpStateMachine { state: LpState::ReadyToHandshake { @@ -1144,6 +1153,7 @@ impl LpStateMachine { #[cfg(test)] mod tests { use super::*; + use crate::packet::version; use crate::peer::mock_peers; #[test] @@ -1155,8 +1165,14 @@ mod tests { let receiver_index: u32 = 77777; - let initiator_sm = - LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt); + let initiator_sm = LpStateMachine::new( + receiver_index, + true, + init.clone(), + resp.as_remote(), + &salt, + version::CURRENT, + ); assert!(initiator_sm.is_ok()); let initiator_sm = initiator_sm.unwrap(); assert!(matches!( @@ -1166,8 +1182,14 @@ mod tests { let init_session = initiator_sm.session().unwrap(); assert!(init_session.is_initiator()); - let responder_sm = - LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt); + let responder_sm = LpStateMachine::new( + receiver_index, + false, + resp, + init.as_remote(), + &salt, + version::CURRENT, + ); assert!(responder_sm.is_ok()); let responder_sm = responder_sm.unwrap(); assert!(matches!( @@ -1196,6 +1218,7 @@ mod tests { init.clone(), resp.as_remote(), &salt, + version::CURRENT, ) .unwrap(); @@ -1205,6 +1228,7 @@ mod tests { resp, init.as_remote(), &salt, + version::CURRENT, ) .unwrap(); @@ -1389,8 +1413,15 @@ mod tests { let receiver_index: u32 = 99901; // Create initiator state machine - let mut initiator = - LpStateMachine::new(receiver_index, true, init, resp.as_remote(), &salt).unwrap(); + let mut initiator = LpStateMachine::new( + receiver_index, + true, + init, + resp.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // Verify initial state assert!(matches!(initiator.state, LpState::ReadyToHandshake { .. })); @@ -1409,8 +1440,15 @@ mod tests { let receiver_index: u32 = 99902; // Create responder state machine - let mut responder = - LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt).unwrap(); + let mut responder = LpStateMachine::new( + receiver_index, + false, + resp, + init.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // Verify initial state assert!(matches!(responder.state, LpState::ReadyToHandshake { .. })); @@ -1430,12 +1468,25 @@ mod tests { let receiver_index: u32 = 99903; // Create both state machines - let mut initiator = - LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) - .unwrap(); + let mut initiator = LpStateMachine::new( + receiver_index, + true, + init.clone(), + resp.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); - let mut responder = - LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt).unwrap(); + let mut responder = LpStateMachine::new( + receiver_index, + false, + resp, + init.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // Step 1: Initiator starts handshake, sends KKT request let init_action = initiator.process_input(LpInput::StartHandshake); @@ -1477,9 +1528,15 @@ mod tests { let receiver_index: u32 = 99904; // Create initiator state machine - let mut initiator = - LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) - .unwrap(); + let mut initiator = LpStateMachine::new( + receiver_index, + true, + init.clone(), + resp.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // Start handshake to enter KKTExchange state initiator.process_input(LpInput::StartHandshake); @@ -1499,9 +1556,15 @@ mod tests { let receiver_index: u32 = 99905; // Create initiator state machine - let mut initiator = - LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) - .unwrap(); + let mut initiator = LpStateMachine::new( + receiver_index, + true, + init.clone(), + resp.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // Start handshake to enter KKTExchange state initiator.process_input(LpInput::StartHandshake); @@ -1534,10 +1597,25 @@ mod tests { let receiver_index: u32 = 111111; // Create state machines - Alice is initiator, Bob is responder - let mut alice = - LpStateMachine::new(receiver_index, true, a.clone(), b.as_remote(), &salt).unwrap(); + let mut alice = LpStateMachine::new( + receiver_index, + true, + a.clone(), + b.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); - let mut bob = LpStateMachine::new(receiver_index, false, b, a.as_remote(), &salt).unwrap(); + let mut bob = LpStateMachine::new( + receiver_index, + false, + b, + a.as_remote(), + &salt, + version::CURRENT, + ) + .unwrap(); // --- Complete KKT Exchange --- // Alice starts handshake diff --git a/common/registration/src/lib.rs b/common/registration/src/lib.rs index ddc46a593f..777e97fed2 100644 --- a/common/registration/src/lib.rs +++ b/common/registration/src/lib.rs @@ -42,6 +42,10 @@ pub struct NymNodeLPInformation { pub expected_kem_key_hashes: HashMap, pub expected_signing_key_hashes: HashMap, pub x25519: x25519::PublicKey, + + /// Supported protocol version of the remote gateway. + /// Included in case we have to downgrade our version. + pub lp_protocol_version: u8, } #[derive(Clone, Copy, Debug)] diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index 9f348bb422..db9e77bdf3 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -292,6 +292,10 @@ where use nym_lp::state_machine::{LpInput, LpStateMachine}; let remote = self.remote_addr; + // if we managed to parse out the header, i.e. we have code supporting whatever + // protocol version has been sent - use that one instead + let protocol_version = packet.header().protocol_version; + // Extract ClientHello data let hello_data = match packet.message() { LpMessage::ClientHello(hello_data) => hello_data, @@ -325,8 +329,10 @@ where // Send Collision response to tell client to retry with new receiver_index // No outer key - this is before PSK derivation // Note: Do NOT set binding on collision - client may retry with new receiver_index - let collision_packet = - LpPacket::new(LpHeader::new(receiver_index, 0), LpMessage::Collision); + let collision_packet = LpPacket::new( + LpHeader::new(receiver_index, 0, protocol_version), + LpMessage::Collision, + ); self.send_lp_packet(collision_packet, None).await?; return Ok(()); @@ -344,6 +350,7 @@ where self.state.local_lp_peer.clone(), hello_data.to_remote_peer(), &hello_data.salt, + protocol_version, ) .map_err(|e| { inc!("lp_client_hello_failed"); @@ -374,7 +381,10 @@ where // Send Ack to confirm ClientHello received // No outer key - this is before PSK derivation - let ack_packet = LpPacket::new(LpHeader::new(receiver_index, 0), LpMessage::Ack); + let ack_packet = LpPacket::new( + LpHeader::new(receiver_index, 0, protocol_version), + LpMessage::Ack, + ); self.send_lp_packet(ack_packet, None).await?; Ok(()) @@ -431,13 +441,15 @@ where self.remote_addr, receiver_idx ); + let session = state_entry.value().state.session().map_err(|err| { + GatewayError::LpHandshakeError(format!("no session available: {err}")) + })?; + // Get outer key for Ack encryption before releasing borrow - let outer_key = state_entry - .value() - .state - .session() - .ok() - .and_then(|s| s.outer_aead_key()); + let outer_key = session.outer_aead_key(); + + // Get previously negotiated protocol version for header creation + let negotiated_version = session.negotiated_version(); // Move state machine to session_states (already in Transport state) // We keep the state machine (not just session) to enable @@ -461,7 +473,10 @@ where inc!("lp_handshakes_success"); // Send Ack to confirm handshake completion to the client - let ack_packet = LpPacket::new(LpHeader::new(receiver_idx, 0), LpMessage::Ack); + let ack_packet = LpPacket::new( + LpHeader::new(receiver_idx, 0, negotiated_version), + LpMessage::Ack, + ); trace!( "Moved session {} to transport mode, sending Ack", receiver_idx diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index 2e171962c4..80afb54004 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -54,6 +54,7 @@ mod tests { peer: LpLocalPeer, x25519_wg_keys: Arc, socket_addr: SocketAddr, + lp_version: u8, } impl Party { @@ -73,6 +74,7 @@ mod tests { .with_kem_psq_key(lp_x25519_keys), x25519_wg_keys, socket_addr: SocketAddr::from((ip, u16::from_le_bytes(port))), + lp_version: 1, } } } @@ -425,6 +427,7 @@ mod tests { client_data.base.peer.ed25519().clone(), entry.base.peer.as_remote(), entry.base.socket_addr, + entry.base.lp_version, ); // 1. establish mock connection between client and gateway and retrieve gateway's handle @@ -524,6 +527,7 @@ mod tests { client_data.base.peer.ed25519().clone(), entry.base.peer.as_remote(), entry.base.socket_addr, + entry.base.lp_version, ); // 1. establish mock connection between client and gateway and retrieve gateway's handle @@ -588,6 +592,7 @@ mod tests { client_data.base.peer.ed25519().clone(), entry.base.peer.as_remote(), entry.base.socket_addr, + entry.base.lp_version, ); // START: ENTRY SETUP @@ -703,6 +708,7 @@ mod tests { exit.base.socket_addr.to_string(), client_data.base.peer.ed25519().clone(), exit.base.peer.as_remote(), + exit.base.lp_version, ); // 13. Perform handshake and registration with exit gateway (all via entry forwarding) diff --git a/nym-gateway-probe/src/common/nodes.rs b/nym-gateway-probe/src/common/nodes.rs index 6950978b14..3e99c02608 100644 --- a/nym-gateway-probe/src/common/nodes.rs +++ b/nym-gateway-probe/src/common/nodes.rs @@ -14,6 +14,7 @@ use nym_crypto::asymmetric::x25519; use nym_http_api_client::UserAgent; use nym_kkt_ciphersuite::SignatureScheme; use nym_kkt_ciphersuite::{KEM, KEMKeyDigests}; +use nym_lp::packet::version; use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; use nym_node_requests::api::client::NymNodeApiClientExt; use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails; @@ -28,7 +29,6 @@ use std::time::Duration; use time::OffsetDateTime; use tracing::{debug, info, warn}; use url::Url; - // in the old behaviour we were getting all skimmed nodes to retrieve performance // that was ultimately unused // should we want to use it again, the code is commented out below @@ -135,6 +135,8 @@ impl DirectoryNode { expected_kem_key_hashes: lp_data.kem_keys()?, expected_signing_key_hashes: lp_data.signing_keys()?, x25519: noise_key.x25519_pubkey, + // \/ TODO: proper derivation from build version + lp_version: version::CURRENT, }), _ => None, }; @@ -480,6 +482,7 @@ pub struct TestedNodeLpDetails { pub expected_kem_key_hashes: HashMap, pub expected_signing_key_hashes: HashMap, pub x25519: x25519::PublicKey, + pub lp_version: u8, } impl TestedNodeDetails { diff --git a/nym-gateway-probe/src/common/probe_tests.rs b/nym-gateway-probe/src/common/probe_tests.rs index 018c414a11..b96564c020 100644 --- a/nym-gateway-probe/src/common/probe_tests.rs +++ b/nym-gateway-probe/src/common/probe_tests.rs @@ -163,6 +163,7 @@ pub async fn lp_registration_probe( bandwidth_controller: &dyn BandwidthTicketProvider, ) -> anyhow::Result { let lp_address = gateway_lp_data.address; + let lp_version = gateway_lp_data.lp_version; let peer = helpers::to_lp_remote_peer(gateway_identity, gateway_lp_data); info!("Starting LP registration probe for gateway at {lp_address}",); @@ -180,6 +181,7 @@ pub async fn lp_registration_probe( client_ed25519_keypair, peer, lp_address, + lp_version, ); // Step 1: Perform handshake (connection is implicit in packet-per-connection model) @@ -289,6 +291,9 @@ pub async fn wg_probe_lp( let entry_address = entry_lp_data.address; let exit_address = exit_lp_data.address; + let entry_lp_version = entry_lp_data.lp_version; + let exit_lp_version = exit_lp_data.lp_version; + let entry_ip = entry_address.ip(); let exit_ip = exit_address.ip(); @@ -316,6 +321,7 @@ pub async fn wg_probe_lp( entry_lp_keypair, entry_peer, entry_address, + entry_lp_version, ); // Perform handshake with entry gateway (connection is implicit) @@ -327,8 +333,12 @@ pub async fn wg_probe_lp( // STEP 2: Use nested session to register with exit gateway via forwarding info!("Registering with exit gateway via entry forwarding..."); - let mut nested_session = - NestedLpSession::new(exit_address.to_string(), exit_lp_keypair, exit_peer); + let mut nested_session = NestedLpSession::new( + exit_address.to_string(), + exit_lp_keypair, + exit_peer, + exit_lp_version, + ); // Convert exit gateway identity to ed25519 public key for registration let exit_gateway_pubkey = ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes()) diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 889617406c..b0cc2e9951 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -317,6 +317,7 @@ pub(crate) async fn run() -> anyhow::Result { expected_kem_key_hashes, expected_signing_key_hashes: todo!(), x25519: x25519_key, + lp_version: todo!(), }; let entry_details = TestedNodeDetails::from_cli(identity, entry_lp_node); @@ -348,8 +349,9 @@ pub(crate) async fn run() -> anyhow::Result { let exit_lp_node = TestedNodeLpDetails { address: exit_lp_addr, expected_kem_key_hashes, - expected_signing_key_hashes: Default::default(), + expected_signing_key_hashes: todo!(), x25519: x25519_key, + lp_version: todo!(), }; Some(TestedNodeDetails::from_cli(identity, exit_lp_node)) diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs index e89143fad6..b175e2e94d 100644 --- a/nym-registration-client/src/lib.rs +++ b/nym-registration-client/src/lib.rs @@ -175,6 +175,9 @@ impl RegistrationClient { }, )?; + let entry_lp_protocol = entry_lp_data.lp_protocol_version; + let exit_lp_protocol = exit_lp_data.lp_protocol_version; + let entry_address = entry_lp_data.address; let exit_address = exit_lp_data.address; @@ -197,6 +200,7 @@ impl RegistrationClient { entry_lp_keypair.clone(), entry_peer, entry_address, + entry_lp_protocol, ); // Perform handshake with entry gateway (outer session now established) @@ -213,8 +217,12 @@ impl RegistrationClient { // STEP 2: Use nested session to register with exit gateway via forwarding // This hides the client's IP address from the exit gateway tracing::info!("Registering with exit gateway via entry forwarding"); - let mut nested_session = - NestedLpSession::new(exit_address.to_string(), exit_lp_keypair, exit_peer); + let mut nested_session = NestedLpSession::new( + exit_address.to_string(), + exit_lp_keypair, + exit_peer, + exit_lp_protocol, + ); // Perform handshake and registration with exit gateway (all via entry forwarding) let exit_gateway_data = nested_session diff --git a/nym-registration-client/src/lp_client/client.rs b/nym-registration-client/src/lp_client/client.rs index 827c98e1b1..c09f2138b9 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -18,6 +18,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_lp::LpPacket; use nym_lp::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet}; use nym_lp::message::ForwardPacketData; +use nym_lp::packet::version; use nym_lp::peer::{LpLocalPeer, LpRemotePeer}; use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine}; use nym_lp_transport::traits::LpTransport; @@ -31,6 +32,7 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::net::TcpStream; +use tracing::warn; /// LP (Lewes Protocol) registration client for direct gateway connections. /// @@ -55,6 +57,10 @@ pub struct LpRegistrationClient { /// Gateway LP listener address (host:port, e.g., "1.1.1.1:41264"). gateway_lp_address: SocketAddr, + /// Supported protocol version of the remote gateway. + /// Included in case we have to downgrade our version. + gateway_supported_lp_protocol_version: u8, + /// LP state machine for managing connection lifecycle. /// Created during handshake initiation. Persists across packet-per-connection calls. state_machine: Option, @@ -77,6 +83,7 @@ where /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol /// * `gateway_lp_address` - Gateway's LP listener socket address + /// * `gateway_supported_lp_protocol_version` - Gateway's LP protocol version /// * `config` - Configuration for timeouts and TCP parameters (use `LpConfig::default()`) /// /// # Note @@ -85,20 +92,58 @@ where local_ed25519_keypair: Arc, gateway_lp_peer: LpRemotePeer, gateway_lp_address: SocketAddr, + gateway_supported_lp_protocol_version: u8, config: LpConfig, ) -> Self { + let lp_protocol = if gateway_supported_lp_protocol_version > version::CURRENT { + warn!( + "suggested LP protocol ({gateway_supported_lp_protocol_version}) is higher than the current known version. attempting to downgrade it to {}", + version::CURRENT + ); + version::CURRENT + } else { + gateway_supported_lp_protocol_version + }; + let local_x25519_keypair = local_ed25519_keypair.to_x25519(); let lp_local_peer = LpLocalPeer::new(local_ed25519_keypair, Arc::new(local_x25519_keypair)); Self { lp_local_peer, gateway_lp_peer, gateway_lp_address, + gateway_supported_lp_protocol_version: lp_protocol, state_machine: None, config, stream: None, } } + /// Creates a new LP registration client with default configuration. + /// + /// # Arguments + /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair + /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol + /// * `gateway_lp_address` - Gateway's LP listener socket address + /// * `gateway_supported_lp_protocol_version` - Gateway's LP protocol version + /// + /// Uses default config (LpConfig::default()) with sane timeout and TCP parameters. + /// PSK is derived automatically during handshake inside the state machine. + /// For custom config, use `new()` directly. + pub fn new_with_default_config( + local_ed25519_keypair: Arc, + gateway_lp_peer: LpRemotePeer, + gateway_lp_address: SocketAddr, + gateway_supported_lp_protocol_version: u8, + ) -> Self { + Self::new( + local_ed25519_keypair, + gateway_lp_peer, + gateway_lp_address, + gateway_supported_lp_protocol_version, + LpConfig::default(), + ) + } + fn state_machine(&self) -> Result<&LpStateMachine> { self.state_machine.as_ref().ok_or_else(|| { LpClientError::transport( @@ -121,30 +166,6 @@ where .ok_or_else(|| LpClientError::transport("Cannot send: not connected")) } - /// Creates a new LP registration client with default configuration. - /// - /// # Arguments - /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair - /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol - /// * `gateway_lp_address` - Gateway's LP listener socket address - /// * `client_ip` - Client IP address for registration - /// - /// Uses default config (LpConfig::default()) with sane timeout and TCP parameters. - /// PSK is derived automatically during handshake inside the state machine. - /// For custom config, use `new()` directly. - pub fn new_with_default_config( - local_ed25519_keypair: Arc, - gateway_lp_peer: LpRemotePeer, - gateway_lp_address: SocketAddr, - ) -> Self { - Self::new( - local_ed25519_keypair, - gateway_lp_peer, - gateway_lp_address, - LpConfig::default(), - ) - } - /// Returns whether the client has completed the handshake and is ready for registration. pub fn is_handshake_complete(&self) -> bool { self.state_machine @@ -418,6 +439,7 @@ where let client_hello_header = nym_lp::packet::LpHeader::new( nym_lp::BOOTSTRAP_RECEIVER_IDX, // session_id not yet established 0, // counter starts at 0 + self.gateway_supported_lp_protocol_version, ); let client_hello_packet = nym_lp::LpPacket::new( client_hello_header, @@ -431,6 +453,8 @@ where let ack_response = self.try_receive_packet_with_key(None).await?; // Verify we received Ack + // this confirms that gateway is fine with our suggested protocol version + // in the future we probably have some fancier negotiation match ack_response.message() { nym_lp::LpMessage::Ack => { tracing::debug!("Received Ack for ClientHello"); @@ -451,6 +475,7 @@ where self.lp_local_peer.clone(), self.gateway_lp_peer.clone(), &salt, + self.gateway_supported_lp_protocol_version, )?; // Step 4: Start handshake - get first packet to send (KKT request) @@ -1201,6 +1226,7 @@ where #[cfg(test)] mod tests { use super::*; + use nym_lp::packet::version; #[test] fn test_client_creation() { @@ -1216,6 +1242,7 @@ mod tests { keypair, gateway_peer, address, + version::CURRENT, ); assert!(!client.is_handshake_complete()); diff --git a/nym-registration-client/src/lp_client/nested_session.rs b/nym-registration-client/src/lp_client/nested_session.rs index ed10f09880..a8e7a1be9b 100644 --- a/nym-registration-client/src/lp_client/nested_session.rs +++ b/nym-registration-client/src/lp_client/nested_session.rs @@ -30,6 +30,7 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_lp::codec::{OuterAeadKey, parse_lp_packet}; use nym_lp::message::ForwardPacketData; +use nym_lp::packet::version; use nym_lp::peer::{LpLocalPeer, LpRemotePeer}; use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine}; use nym_lp::{LpMessage, LpPacket}; @@ -42,6 +43,7 @@ use nym_wireguard_types::PeerPublicKey; use rand::{CryptoRng, RngCore}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::warn; /// Manages a nested LP session where the client establishes a handshake with /// an exit gateway by forwarding packets through an entry gateway. @@ -73,6 +75,10 @@ pub struct NestedLpSession { /// Encapsulates all the exit gateway keys needed for the Lewes Protocol. gateway_lp_peer: LpRemotePeer, + /// Supported protocol version of the remote gateway. + /// Included in case we have to downgrade our version. + gateway_supported_lp_protocol_version: u8, + /// LP state machine for exit gateway session (populated after handshake) state_machine: Option, } @@ -84,18 +90,31 @@ impl NestedLpSession { /// * `exit_address` - Exit gateway's LP address (e.g., "2.2.2.2:41264") /// * `client_keypair` - Client's Ed25519 keypair /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol + /// * `gateway_supported_lp_protocol_version` - Gateway's LP protocol version pub fn new( exit_address: String, client_keypair: Arc, gateway_lp_peer: LpRemotePeer, + gateway_supported_lp_protocol_version: u8, ) -> Self { let local_x25519_keypair = client_keypair.to_x25519(); let lp_local_peer = LpLocalPeer::new(client_keypair, Arc::new(local_x25519_keypair)); + let lp_protocol = if gateway_supported_lp_protocol_version > version::CURRENT { + warn!( + "suggested LP protocol ({gateway_supported_lp_protocol_version}) is higher than the current known version. attempting to downgrade it to {}", + version::CURRENT + ); + version::CURRENT + } else { + gateway_supported_lp_protocol_version + }; + Self { exit_address, lp_local_peer, gateway_lp_peer, + gateway_supported_lp_protocol_version: lp_protocol, state_machine: None, } } @@ -194,6 +213,7 @@ impl NestedLpSession { let client_hello_header = nym_lp::packet::LpHeader::new( nym_lp::BOOTSTRAP_RECEIVER_IDX, // Use constant for bootstrap session 0, // counter starts at 0 + self.gateway_supported_lp_protocol_version, ); let client_hello_packet = nym_lp::LpPacket::new( client_hello_header, @@ -213,6 +233,8 @@ impl NestedLpSession { .await?; // Parse and validate Ack response (cleartext, no outer key before PSK derivation) + // this confirms that gateway is fine with our suggested protocol version + // in the future we probably have some fancier negotiation let ack_response = Self::parse_packet(&response_bytes, None)?; match ack_response.message() { LpMessage::Ack => { @@ -238,6 +260,7 @@ impl NestedLpSession { self.lp_local_peer.clone(), self.gateway_lp_peer.clone(), &salt, + self.gateway_supported_lp_protocol_version, )?; // Step 4: Get initial packet from StartHandshake diff --git a/tools/nym-lp-client/src/client.rs b/tools/nym-lp-client/src/client.rs index cc1e94768b..3be0bc18b2 100644 --- a/tools/nym-lp-client/src/client.rs +++ b/tools/nym-lp-client/src/client.rs @@ -32,6 +32,7 @@ use tracing::{debug, info, trace}; use crate::topology::{GatewayInfo, SpeedtestTopology}; use nym_ip_packet_requests::v8::request::IpPacketRequest; +use nym_lp::packet::version; use nym_lp::peer::LpRemotePeer; use nym_sphinx::forwarding::packet::MixPacket; @@ -127,6 +128,7 @@ impl SpeedtestClient { self.identity_keypair.clone(), gw_peer, self.gateway.lp_address, + self.gateway.lp_version, ); let start = Instant::now(); @@ -173,6 +175,7 @@ impl SpeedtestClient { self.identity_keypair.clone(), gw_peer, self.gateway.lp_address, + self.gateway.lp_version, ); let start = Instant::now(); diff --git a/tools/nym-lp-client/src/topology.rs b/tools/nym-lp-client/src/topology.rs index 5735134317..3e0f76b6ff 100644 --- a/tools/nym-lp-client/src/topology.rs +++ b/tools/nym-lp-client/src/topology.rs @@ -40,6 +40,7 @@ pub struct GatewayInfo { pub lp_address: SocketAddr, /// LP data address (IP:51264) for Sphinx packets wrapped in LP pub lp_data_address: SocketAddr, + pub lp_version: u8, } /// Topology for routing Sphinx packets