lp: attempt to negotiate (and use) protocol version (#6399)

This commit is contained in:
Jędrzej Stuczyński
2026-01-30 12:38:32 +00:00
committed by GitHub
parent dccdde108c
commit 8916b021a9
17 changed files with 441 additions and 110 deletions
+28 -7
View File
@@ -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 {
+2 -2
View File
@@ -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,
+59 -25
View File
@@ -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<Self, LpError> {
// 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<LpPacket, LpError> {
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());
+58 -7
View File
@@ -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()
);
+51 -6
View File
@@ -172,8 +172,16 @@ impl SessionManager {
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
salt: &[u8; 32],
protocol_version: u8,
) -> Result<u32, LpError> {
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());
+101 -23
View File
@@ -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<Self, LpError> {
// 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
+4
View File
@@ -42,6 +42,10 @@ pub struct NymNodeLPInformation {
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
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)]
+25 -10
View File
@@ -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
+6
View File
@@ -54,6 +54,7 @@ mod tests {
peer: LpLocalPeer,
x25519_wg_keys: Arc<x25519::KeyPair>,
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)
+4 -1
View File
@@ -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<KEM, KEMKeyDigests>,
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
pub x25519: x25519::PublicKey,
pub lp_version: u8,
}
impl TestedNodeDetails {
+12 -2
View File
@@ -163,6 +163,7 @@ pub async fn lp_registration_probe(
bandwidth_controller: &dyn BandwidthTicketProvider,
) -> anyhow::Result<LpProbeResults> {
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())
+3 -1
View File
@@ -317,6 +317,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
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<ProbeResult> {
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))
+10 -2
View File
@@ -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
+51 -24
View File
@@ -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<S = TcpStream> {
/// 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<LpStateMachine>,
@@ -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<ed25519::KeyPair>,
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<ed25519::KeyPair>,
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<ed25519::KeyPair>,
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());
@@ -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<LpStateMachine>,
}
@@ -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<ed25519::KeyPair>,
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
+3
View File
@@ -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();
+1
View File
@@ -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