From d4882ca276733b04dabd07ab877d067902556d05 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Wed, 4 Feb 2026 09:35:44 +0100 Subject: [PATCH] [LP-fix] expose wg psk for the vpn-client (#6411) * expose wg psk for the vpn-client : store socket addr of nested session as socketaddr * probe fix * nits --- common/nym-lp/src/codec.rs | 49 ++++++++++- common/nym-lp/src/message.rs | 83 ++++++++++++------- common/registration/src/lib.rs | 1 + common/registration/src/lp_messages.rs | 14 +--- gateway/src/node/lp_listener/handler.rs | 5 +- gateway/src/node/lp_listener/registration.rs | 2 + integration-tests/src/lp_registration.rs | 2 +- nym-authenticator-client/src/lib.rs | 1 + nym-gateway-probe/src/common/probe_tests.rs | 14 ++-- nym-registration-client/src/lib.rs | 8 +- .../src/lp_client/client.rs | 24 ++++-- .../src/lp_client/nested_session.rs | 44 ++++++---- 12 files changed, 164 insertions(+), 83 deletions(-) diff --git a/common/nym-lp/src/codec.rs b/common/nym-lp/src/codec.rs index 9c41cacc69..d8eee41844 100644 --- a/common/nym-lp/src/codec.rs +++ b/common/nym-lp/src/codec.rs @@ -757,12 +757,12 @@ mod tests { } #[test] - fn test_forward_packet_encode_decode_roundtrip() { + fn test_forward_packet_encode_decode_roundtrip_v4() { let mut dst = BytesMut::new(); let forward_data = crate::message::ForwardPacketData { target_gateway_identity: [77u8; 32], - target_lp_address: "1.2.3.4:41264".to_string(), + target_lp_address: "1.2.3.4:41264".parse().unwrap(), inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd], }; @@ -789,7 +789,50 @@ mod tests { if let LpMessage::ForwardPacket(data) = decoded.message { assert_eq!(data.target_gateway_identity, [77u8; 32]); - assert_eq!(data.target_lp_address, "1.2.3.4:41264"); + assert_eq!(data.target_lp_address, "1.2.3.4:41264".parse().unwrap()); + assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]); + } else { + panic!("Expected ForwardPacket message"); + } + } + + #[test] + fn test_forward_packet_encode_decode_roundtrip_v6() { + let mut dst = BytesMut::new(); + + let forward_data = crate::message::ForwardPacketData { + target_gateway_identity: [77u8; 32], + target_lp_address: "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap(), + inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd], + }; + + let packet = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: 999, + counter: 555, + }, + message: LpMessage::ForwardPacket(forward_data), + trailer: [0xff; TRAILER_LEN], + }; + + // Serialize + serialize_lp_packet(&packet, &mut dst, None).unwrap(); + + // Parse back + let decoded = parse_lp_packet(&dst, None).unwrap(); + + // Verify LP protocol handling works correctly + assert_eq!(decoded.header.receiver_idx, 999); + assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket)); + + if let LpMessage::ForwardPacket(data) = decoded.message { + assert_eq!(data.target_gateway_identity, [77u8; 32]); + assert_eq!( + data.target_lp_address, + "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap() + ); assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]); } else { panic!("Expected ForwardPacket message"); diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs index fb836e63f5..0796d0ffcc 100644 --- a/common/nym-lp/src/message.rs +++ b/common/nym-lp/src/message.rs @@ -9,6 +9,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use rand::RngCore; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; /// Data structure for the ClientHello message #[derive(Debug, Clone, Serialize, Deserialize)] @@ -229,9 +230,8 @@ pub struct ForwardPacketData { /// Target gateway's Ed25519 identity (32 bytes) pub target_gateway_identity: [u8; 32], - // TODO: replace it with `SocketAddr` /// Target gateway's LP address (IP:port string) - pub target_lp_address: String, + pub target_lp_address: SocketAddr, /// Complete inner LP packet bytes (serialized LpPacket) /// This is the CLIENT→EXIT gateway packet, encrypted for exit @@ -241,7 +241,7 @@ pub struct ForwardPacketData { impl ForwardPacketData { pub fn new( target_gateway_identity: ed25519::PublicKey, - target_lp_address: String, + target_lp_address: SocketAddr, inner_packet_bytes: Vec, ) -> Self { ForwardPacketData { @@ -254,20 +254,31 @@ impl ForwardPacketData { fn len(&self) -> usize { // 32 bytes target gateway identity // + - // 4 bytes length of target lp address + // 1 byte length of target lp address type // + - // target_lp_address.len() + // {4,16} target_lp_address IPv{4,6} + // + + // 2 bytes target_lp_address port // + // 4 bytes of length of inner packet bytes // + // inner_packet_bytes.len() - 32 + 4 + self.target_lp_address.len() + 4 + self.inner_packet_bytes.len() + match self.target_lp_address { + SocketAddr::V4(_) => 32 + 1 + 4 + 2 + 4 + self.inner_packet_bytes.len(), + SocketAddr::V6(_) => 32 + 1 + 16 + 2 + 4 + self.inner_packet_bytes.len(), + } } fn encode(&self, dst: &mut BytesMut) { + let (is_ipv6, ip_bytes) = match &self.target_lp_address { + SocketAddr::V4(address) => (false, address.ip().octets().to_vec()), + SocketAddr::V6(address) => (true, address.ip().octets().to_vec()), + }; + dst.put_slice(&self.target_gateway_identity); - dst.put_u16_le(self.target_lp_address.len() as u16); - dst.put_slice(self.target_lp_address.as_bytes()); + dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4 + dst.put_slice(&ip_bytes); // IP bytes + dst.put_u16_le(self.target_lp_address.port()); // Port dst.put_u32_le(self.inner_packet_bytes.len() as u32); dst.put_slice(&self.inner_packet_bytes); } @@ -279,42 +290,56 @@ impl ForwardPacketData { } pub fn decode(bytes: &[u8]) -> Result { - // smallest possible packet with empty address and empty data - if bytes.len() < 38 { + // smallest possible packet with ipv4 and empty data + if bytes.len() < 43 { + // 32 + 1 + 4 + 2 + 4 + 0 return Err(LpError::DeserializationError(format!( - "Too few bytes to deserialise ForwardPacketData[1]. got {}", + "Too few bytes to deserialise ForwardPacketData. got {}", bytes.len() ))); } // SAFETY: we ensured we have sufficient data #[allow(clippy::unwrap_used)] let target_gateway_identity = bytes[0..32].try_into().unwrap(); - let target_lp_address_len = u16::from_le_bytes([bytes[32], bytes[33]]); + let target_lp_address_is_ipv6 = bytes[32] != 0; - // smallest possible packet with empty data - if bytes[34..].len() < 4 + target_lp_address_len as usize { - return Err(LpError::DeserializationError(format!( - "Too few bytes to deserialise ForwardPacketData[2]. got {}", - bytes.len() - ))); - } + let (target_lp_address, next_index) = if target_lp_address_is_ipv6 { + // IPv6, first check we have actually enough bytes + // smallest possible packet with ipv6 and empty data + if bytes.len() < 55 { + // 32 + 1 + 16 + 2 + 4 + 0 + return Err(LpError::DeserializationError(format!( + "Too few bytes to deserialise ipv6 ForwardPacketData. got {}", + bytes.len() + ))); + } + // SAFETY: we ensured we have sufficient data, and the length is correct for casting + #[allow(clippy::unwrap_used)] + let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap())); + let port = u16::from_le_bytes([bytes[49], bytes[50]]); + (SocketAddr::new(ipv6, port), 51) + } else { + // IPv4. Length check done at the start + // SAFETY: we ensured we have sufficient data, and the length is correct for casting + #[allow(clippy::unwrap_used)] + let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap())); + let port = u16::from_le_bytes([bytes[37], bytes[38]]); + (SocketAddr::new(ipv4, port), 39) + }; - let target_lp_address = - String::from_utf8_lossy(&bytes[34..34 + target_lp_address_len as usize]).to_string(); let inner_packet_bytes_len = u32::from_le_bytes([ - bytes[34 + target_lp_address_len as usize], - bytes[34 + target_lp_address_len as usize + 1], - bytes[34 + target_lp_address_len as usize + 2], - bytes[34 + target_lp_address_len as usize + 3], + bytes[next_index], + bytes[next_index + 1], + bytes[next_index + 2], + bytes[next_index + 3], ]); - if bytes[34 + target_lp_address_len as usize + 4..].len() != inner_packet_bytes_len as usize - { + if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize { return Err(LpError::DeserializationError(format!( "Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}", - bytes[34 + target_lp_address_len as usize + 4..].len() + bytes[next_index + 4..].len() ))); } - let inner_packet_bytes = bytes[34 + target_lp_address_len as usize + 4..].to_vec(); + let inner_packet_bytes = bytes[next_index + 4..].to_vec(); Ok(ForwardPacketData { target_gateway_identity, diff --git a/common/registration/src/lib.rs b/common/registration/src/lib.rs index 777e97fed2..2beb0632ed 100644 --- a/common/registration/src/lib.rs +++ b/common/registration/src/lib.rs @@ -31,6 +31,7 @@ pub struct NymNodeInformation { pub struct WireguardConfiguration { #[serde(with = "bs58_x25519_pubkey")] pub public_key: x25519::PublicKey, + pub psk: Option<[u8; 32]>, pub endpoint: SocketAddr, pub private_ipv4: Ipv4Addr, pub private_ipv6: Ipv6Addr, diff --git a/common/registration/src/lp_messages.rs b/common/registration/src/lp_messages.rs index 9a7bcc59fb..ff64fc3746 100644 --- a/common/registration/src/lp_messages.rs +++ b/common/registration/src/lp_messages.rs @@ -17,7 +17,6 @@ use crate::mixnet::{ use crate::serialisation::{BincodeError, BincodeOptions, lp_bincode_serializer}; use nym_authenticator_requests::models::BandwidthClaim; use nym_credentials_interface::TicketType; -use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use tracing::error; @@ -133,17 +132,11 @@ impl LpRegistrationRequest { } /// Create new dVPN registration initialisation request - pub fn new_initial_dvpn( - rng: &mut R, + pub fn new_initial_dvpn( wg_public_key: nym_wireguard_types::PeerPublicKey, + psk: [u8; 32], ticket_type: TicketType, - ) -> Self - where - R: RngCore + CryptoRng, - { - let mut psk = [0u8; 32]; - rng.fill_bytes(&mut psk); - + ) -> Self { Self::new(LpRegistrationRequestData::Dvpn { data: Box::new(LpDvpnRegistrationRequestMessage { content: LpDvpnRegistrationRequestMessageContent::InitialRequest( @@ -458,6 +451,7 @@ mod tests { public_key: nym_crypto::asymmetric::x25519::PublicKey::from( nym_sphinx::PublicKey::from([1u8; 32]), ), + psk: Some([42u8; 32]), private_ipv4: Ipv4Addr::new(10, 0, 0, 1), private_ipv6: Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1), endpoint: "192.168.1.1:8080".parse().expect("Valid test endpoint"), diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index db9e77bdf3..599e3796ed 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -1002,10 +1002,7 @@ where let start = std::time::Instant::now(); // Parse target gateway address - let target_addr: SocketAddr = forward_data.target_lp_address.parse().map_err(|e| { - inc!("lp_forward_failed"); - GatewayError::LpProtocolError(format!("Invalid target address: {}", e)) - })?; + let target_addr = forward_data.target_lp_address; // Check if we need to open a new connection let need_new_connection = match &self.exit_stream { diff --git a/gateway/src/node/lp_listener/registration.rs b/gateway/src/node/lp_listener/registration.rs index 577ab9a460..421dc14260 100644 --- a/gateway/src/node/lp_listener/registration.rs +++ b/gateway/src/node/lp_listener/registration.rs @@ -117,6 +117,7 @@ impl LpHandlerState { LpRegistrationResponse::success_dvpn( WireguardConfiguration { public_key: *self.keypair().public_key(), + psk: None, // TODO: according to @SW this is most likely very wrong endpoint: self.wireguard_config().bind_address, private_ipv4: peer_private_ipv4, @@ -486,6 +487,7 @@ impl LpHandlerState { ticket_type, wireguard_config: WireguardConfiguration { public_key: gateway_pubkey, + psk: None, endpoint: gateway_endpoint, private_ipv4: client_ipv4, private_ipv6: client_ipv6, diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index 80afb54004..81112582ed 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -705,7 +705,7 @@ mod tests { // technically we should use different ephemeral keys than we had for the entry // but crypto is going to work the same let mut nested_session = NestedLpSession::new( - exit.base.socket_addr.to_string(), + exit.base.socket_addr, client_data.base.peer.ed25519().clone(), exit.base.peer.as_remote(), exit.base.lp_version, diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index b214264cd4..3b36c647e6 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -350,6 +350,7 @@ impl AuthenticatorClient { let gateway_data = WireguardConfiguration { public_key: registered_data.pub_key().inner().into(), + psk: None, // Mixnet-based regsitration does not have psk endpoint: SocketAddr::new(self.ip_addr, registered_data.wg_port()), private_ipv4: registered_data.private_ips().ipv4, private_ipv6: registered_data.private_ips().ipv6, diff --git a/nym-gateway-probe/src/common/probe_tests.rs b/nym-gateway-probe/src/common/probe_tests.rs index ddeab981fd..fa0a9aa2c3 100644 --- a/nym-gateway-probe/src/common/probe_tests.rs +++ b/nym-gateway-probe/src/common/probe_tests.rs @@ -244,6 +244,12 @@ pub async fn lp_registration_probe( info!("LP registration successful! Received gateway data:"); info!(" - Gateway public key: {:?}", gateway_data.public_key); + info!( + " - PSK: {:?}", + gateway_data + .psk + .map(|k| general_purpose::STANDARD.encode(k)) + ); info!(" - Private IPv4: {}", gateway_data.private_ipv4); info!(" - Private IPv6: {}", gateway_data.private_ipv6); info!(" - Endpoint: {}", gateway_data.endpoint); @@ -332,12 +338,8 @@ 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, - exit_lp_version, - ); + let mut nested_session = + NestedLpSession::new(exit_address, exit_lp_keypair, exit_peer, exit_lp_version); let exit_gateway_pubkey = exit_gateway.identity; diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs index 3ff496d7ff..dc7b81fdaf 100644 --- a/nym-registration-client/src/lib.rs +++ b/nym-registration-client/src/lib.rs @@ -220,12 +220,8 @@ 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, - exit_lp_protocol, - ); + let mut nested_session = + NestedLpSession::new(exit_address, 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 490a7437ed..99a6d08a1f 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -858,7 +858,9 @@ where { // 1. Build registration request let wg_public_key = PeerPublicKey::from(*wg_keypair.public_key()); - let request = LpRegistrationRequest::new_initial_dvpn(rng, wg_public_key, ticket_type); + let mut psk = [0u8; 32]; + rng.fill_bytes(&mut psk); + let request = LpRegistrationRequest::new_initial_dvpn(wg_public_key, psk, ticket_type); tracing::trace!("Built dVPN registration request: {request:?}"); @@ -887,12 +889,12 @@ where }; // 7. check response to the initial request - match dvpn_response.content { + let final_response = match dvpn_response.content { LpDvpnRegistrationResponseMessageContent::RegistrationFailure(res) => { let reason = res.error; // the registration has failed tracing::warn!("Gateway rejected registration: {reason}"); - Err(LpClientError::RegistrationRejected { reason }) + return Err(LpClientError::RegistrationRejected { reason }); } LpDvpnRegistrationResponseMessageContent::CompletedRegistration(res) => { // we have already registered with this gateway before, the gateway has updated the psk and sent us the config @@ -900,7 +902,7 @@ where "LP registration successful! Allocated bandwidth: {} bytes", res.available_bandwidth ); - Ok(res.config) + res.config } LpDvpnRegistrationResponseMessageContent::RequiresCredential(_) => { // we're registering for the first time with this gateway - we need to attach a credential @@ -911,9 +913,17 @@ where bandwidth_controller, ticket_type, ) - .await + .await? } - } + }; + + Ok(WireguardConfiguration { + public_key: final_response.public_key, + psk: Some(psk), + endpoint: SocketAddr::new(self.gateway_lp_address.ip(), final_response.endpoint.port()), + private_ipv4: final_response.private_ipv4, + private_ipv6: final_response.private_ipv6, + }) } /// Register with automatic retry on network failure. @@ -1054,7 +1064,7 @@ where &mut self, forward_data: ForwardPacketData, ) -> Result> { - let target_address = forward_data.target_lp_address.clone(); + let target_address = forward_data.target_lp_address; tracing::debug!( "Sending ForwardPacket to {target_address} ({} inner bytes, persistent connection)", diff --git a/nym-registration-client/src/lp_client/nested_session.rs b/nym-registration-client/src/lp_client/nested_session.rs index a8e7a1be9b..887338f2e0 100644 --- a/nym-registration-client/src/lp_client/nested_session.rs +++ b/nym-registration-client/src/lp_client/nested_session.rs @@ -41,6 +41,7 @@ use nym_registration_common::{ }; use nym_wireguard_types::PeerPublicKey; use rand::{CryptoRng, RngCore}; +use std::net::SocketAddr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tracing::warn; @@ -67,7 +68,7 @@ use tracing::warn; /// ``` pub struct NestedLpSession { /// Exit gateway's LP address (e.g., "2.2.2.2:41264") - exit_address: String, + exit_address: SocketAddr, /// Encapsulates all the client keys needed for the Lewes Protocol. lp_local_peer: LpLocalPeer, @@ -92,7 +93,7 @@ impl NestedLpSession { /// * `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, + exit_address: SocketAddr, client_keypair: Arc, gateway_lp_peer: LpRemotePeer, gateway_supported_lp_protocol_version: u8, @@ -145,16 +146,13 @@ impl NestedLpSession { /// Attempt to wrap the provided `LpData` into a `ForwardPacketData` /// using the inner state machine. fn prepare_forward_packet(&mut self, data: LpData) -> Result { - let target_gateway_identity = self.gateway_lp_peer.ed25519(); - let target_lp_address = self.exit_address.clone(); - let state_machine = self.state_machine_mut()?; let inner_packet_bytes = prepare_serialised_send_packet(data, state_machine)?; - Ok(ForwardPacketData { - target_gateway_identity: target_gateway_identity.to_bytes(), - target_lp_address, + Ok(ForwardPacketData::new( + self.gateway_lp_peer.ed25519(), + self.exit_address, inner_packet_bytes, - }) + )) } /// Attempt to recover received `LpData` from the received `LpPacket` @@ -224,7 +222,7 @@ impl NestedLpSession { let client_hello_bytes = serialize_packet(&client_hello_packet, None)?; let forward_packet_data = ForwardPacketData::new( self.gateway_lp_peer.ed25519(), - self.exit_address.clone(), + self.exit_address, client_hello_bytes, ); @@ -498,7 +496,10 @@ impl NestedLpSession { // Step 2: Build registration request let wg_public_key = PeerPublicKey::from(*wg_keypair.public_key()); - let request = LpRegistrationRequest::new_initial_dvpn(rng, wg_public_key, ticket_type); + let mut psk = [0u8; 32]; + rng.fill_bytes(&mut psk); + + let request = LpRegistrationRequest::new_initial_dvpn(wg_public_key, psk, ticket_type); // Step 3: Serialize the request let send_data = request.to_lp_data()?; @@ -528,12 +529,12 @@ impl NestedLpSession { }; // Step 9: check response to the initial request - match dvpn_response.content { + let final_response = match dvpn_response.content { LpDvpnRegistrationResponseMessageContent::RegistrationFailure(res) => { let reason = res.error; // the registration has failed tracing::warn!("Gateway rejected registration: {reason}"); - Err(LpClientError::RegistrationRejected { reason }) + return Err(LpClientError::RegistrationRejected { reason }); } LpDvpnRegistrationResponseMessageContent::CompletedRegistration(res) => { // we have already registered with this gateway before, the gateway has updated the psk and sent us the config @@ -541,7 +542,7 @@ impl NestedLpSession { "LP registration successful! Allocated bandwidth: {} bytes", res.available_bandwidth ); - Ok(res.config) + res.config } LpDvpnRegistrationResponseMessageContent::RequiresCredential(_) => { // we're registering for the first time with this gateway - we need to attach a credential @@ -553,9 +554,18 @@ impl NestedLpSession { bandwidth_controller, ticket_type, ) - .await + .await? } - } + }; + + // JS/SW TODO Adapt this to new gateway response + Ok(WireguardConfiguration { + public_key: final_response.public_key, + psk: Some(psk), + endpoint: SocketAddr::new(self.exit_address.ip(), final_response.endpoint.port()), + private_ipv4: final_response.private_ipv4, + private_ipv6: final_response.private_ipv6, + }) } /// Performs handshake and registration with the exit gateway via forwarding, @@ -683,7 +693,7 @@ impl NestedLpSession { let packet_bytes = serialize_packet(packet, send_key.as_ref())?; let forward_data = ForwardPacketData::new( self.gateway_lp_peer.ed25519(), - self.exit_address.clone(), + self.exit_address, packet_bytes, ); let response_bytes = outer_client