Merge pull request #6513 from nymtech/bugfix/lp-psqv2-review-comments

addressing LP PR comments
This commit is contained in:
Jędrzej Stuczyński
2026-03-04 13:41:51 +00:00
committed by GitHub
28 changed files with 173 additions and 262 deletions
Generated
+1 -1
View File
@@ -6875,7 +6875,7 @@ dependencies = [
[[package]]
name = "nym-lp"
version = "0.1.0"
version = "1.20.4"
dependencies = [
"anyhow",
"bs58",
+3 -1
View File
@@ -202,7 +202,7 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2024"
license = "Apache-2.0"
rust-version = "1.85"
rust-version = "1.87.0"
readme = "README.md"
version = "1.20.4"
@@ -448,8 +448,10 @@ nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", def
nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
+2 -4
View File
@@ -511,14 +511,12 @@ mod tests {
#[test]
fn test_key_conversion() {
let dalek_kp = super::KeyPair::new(&mut rand::thread_rng());
let dalek_kp = KeyPair::new(&mut rand::thread_rng());
let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned();
libcrux_curve25519::clamp(&mut dalek_private_key_bytes);
let libcrux_private_key =
libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&dalek_private_key_bytes)
.unwrap();
let libcrux_private_key = DHPrivateKey::from_bytes(&dalek_private_key_bytes).unwrap();
let libcrux_public_key = libcrux_private_key.to_public();
assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes());
+2 -2
View File
@@ -12,9 +12,9 @@ num_enum = { workspace = true }
strum = { workspace = true }
# internal
nym-crypto = { path = "../crypto", features = ["hashing"] }
nym-crypto = { workspace = true, features = ["hashing"] }
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
nym-kkt-context = { path = "../nym-kkt-context" }
nym-kkt-context = { workspace = true }
nym-pemstore = { workspace = true }
libcrux-kem = { workspace = true }
+3 -2
View File
@@ -10,6 +10,7 @@ use crate::error::KKTError;
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX";
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX";
const CARRIER_KKT_AAD: &[u8] = b"kkt-carrier-v1";
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Carrier {
@@ -107,7 +108,7 @@ impl Carrier {
&self.tx_key,
plaintext,
&mut output_buffer,
b"kkt-carrier-v1",
CARRIER_KKT_AAD,
&as_nonce_bytes(self.tx_counter),
)?;
@@ -126,7 +127,7 @@ impl Carrier {
&self.rx_key,
&mut output_buffer,
ciphertext,
b"kkt-carrier-v1",
CARRIER_KKT_AAD,
&as_nonce_bytes(self.rx_counter),
)?;
+11 -11
View File
@@ -16,40 +16,40 @@ pub enum KKTError {
#[error(transparent)]
MaskedByteError(#[from] MaskedByteError),
#[error("KEM mapping failure: {}", info)]
#[error("KEM mapping failure: {info}")]
KEMMapping { info: &'static str },
#[error("Insecure Encapsulation Key Hash Length")]
InsecureHashLen,
#[error("KKT Frame Decoding Error: {}", info)]
#[error("KKT Frame Decoding Error: {info}")]
FrameDecodingError { info: String },
#[error("KKT Frame Encoding Error: {}", info)]
#[error("KKT Frame Encoding Error: {info}")]
FrameEncodingError { info: String },
#[error("KKT Incompatibility Error: {}", info)]
#[error("KKT Incompatibility Error: {info}")]
IncompatibilityError { info: &'static str },
#[error("KKT Responder Flagged Error: {}", status)]
#[error("KKT Responder Flagged Error: {status}")]
ResponderFlaggedError { status: KKTStatus },
#[error("PSQ KEM Error: {}", info)]
#[error("PSQ KEM Error: {info}")]
KEMError { info: &'static str },
#[error("Local Function Input Error: {}", info)]
#[error("Local Function Input Error: {info}")]
FunctionInputError { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
X25519Error { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
AEADError { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
DecodingError { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
UnsupportedAlgorithm { info: &'static str },
#[error("Generic libcrux error")]
+2 -2
View File
@@ -114,14 +114,14 @@ impl KKTRequestPlaintext {
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN);
let mut out = Vec::with_capacity(Self::SIZE);
out.extend_from_slice(self.dh_pubkey.as_ref());
out.extend_from_slice(self.masked_version_bytes.as_slice());
out
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
if b.len() != x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN {
if b.len() != Self::SIZE {
return Err(KKTError::FrameDecodingError {
info: "the KKTRequest frame has invalid length".to_string(),
});
+3 -5
View File
@@ -4,15 +4,13 @@
//! Post-Quantum Re-Key Protocol
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
/// We currently support MlKem768 and XWing.
/// We currently support MlKem768.
///
/// This protocol is safe if it runs under a trusted secure channel.
///
/// Bandwidth costs:
/// Request (MlKem768): 1216 bytes
/// Response (MlKem768): 1088 bytes
/// Request (XWing): 1248 bytes
/// Response (XWing): 1120 bytes
use libcrux_kem::*;
use nym_crypto::hkdf::blake3::derive_key_blake3;
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
@@ -60,7 +58,7 @@ impl RekeyInitiator {
///
/// Inputs:
/// rng: something that implements CryptoRng + RngCore
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
/// kem: a KEM algorithm (we currently support MlKem768 only)
///
/// Outputs:
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
@@ -171,7 +169,7 @@ where
Some(num) => match num {
// If message length is 1216 (32 + 1184) then the algorithm should be MlKem768
ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768,
// If message length is 1248 (32 + 1216) then the algorithm should be MlKem768
// If message length is 1248 (32 + 1216) then the algorithm should be xwing
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
// We don't support McEliece because the keys are massive.
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
+1 -1
View File
@@ -146,7 +146,7 @@ impl<'a> KKTResponder<'a> {
};
// for now the response payload is empty
let response_payload = Vec::with_capacity(0);
let response_payload = Vec::new();
let frame = KKTFrame::new(local_context, kem_key, response_payload);
+13 -7
View File
@@ -1,8 +1,14 @@
[package]
name = "nym-lp"
version = "0.1.0"
edition = { workspace = true }
license = { workspace = true }
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
publish = false
[dependencies]
@@ -11,11 +17,11 @@ bs58 = { workspace = true }
bytes = { workspace = true }
tracing = { workspace = true }
rand09 = { workspace = true }
tls_codec = { workspace = true }
tls_codec = { workspace = true }
tokio = { workspace = true, features = ["net", "io-util"] }
nym-crypto = { path = "../crypto", features = ["hashing"] }
nym-kkt = { path = "../nym-kkt" }
nym-crypto = { workspace = true, features = ["hashing"] }
nym-kkt = { workspace = true }
nym-kkt-ciphersuite = { workspace = true }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
@@ -28,7 +34,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
nym-test-utils = { workspace = true, optional = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { workspace = true, features = ["html_reports"] }
nym-test-utils = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
-79
View File
@@ -1,79 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Configuration for LP protocol.
//!
//! LP security stack = KKT (key fetch) → PSQ (PQ PSK) → Noise (transport).
//! KEM algorithm selection affects only PSQ layer. Noise always uses X25519 DH.
//! Migration to PQ KEMs (MlKem768, XWing) requires only config change.
use nym_kkt::ciphersuite::KEM;
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Default PSK time-to-live (1 hour, matches psk.rs implementation).
pub const DEFAULT_PSK_TTL_SECS: u64 = 3600;
/// Configuration for LP protocol.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpConfig {
/// KEM algorithm for PSQ key encapsulation.
/// Supported KEMs: MlKem768, McEliece
#[serde(with = "kem_serde")]
pub kem_algorithm: KEM,
/// PSK time-to-live in seconds.
pub psk_ttl_secs: u64,
/// Enable KKT for authenticated key distribution.
pub enable_kkt: bool,
}
impl Default for LpConfig {
fn default() -> Self {
Self {
kem_algorithm: KEM::MlKem768,
psk_ttl_secs: DEFAULT_PSK_TTL_SECS,
enable_kkt: true,
}
}
}
impl LpConfig {
/// Returns PSK TTL as Duration.
pub fn psk_ttl(&self) -> Duration {
Duration::from_secs(self.psk_ttl_secs)
}
}
mod kem_serde {
use nym_kkt::ciphersuite::KEM;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(kem: &KEM, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match kem {
KEM::MlKem768 => "MlKem768",
KEM::McEliece => "McEliece",
KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")),
KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")),
}
.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<KEM, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"MlKem768" => Ok(KEM::MlKem768),
"McEliece" => Ok(KEM::McEliece),
"X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")),
"XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")),
_ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))),
}
}
}
+4 -1
View File
@@ -82,7 +82,10 @@ impl Debug for LpLocalPeer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LpLocalPeer")
.field("ciphersuite", &self.ciphersuite)
.field("x25519", &self.x25519.pk)
.field(
"x25519",
&bs58::encode(self.x25519.pk.as_ref()).into_string(),
)
.field("kem_keypairs", &self.kem_keypairs)
.finish()
}
+22 -23
View File
@@ -29,7 +29,7 @@ pub struct LpPeerConfig {
// Determine the hop id.
// Should be 0 if node_initiator is true
// Should be > 1 if is_exit is true
// Should be > 1 && < 16 if is_exit is true
hop_id: u8,
// Determine if the recipient should be an exit node
@@ -198,37 +198,36 @@ impl LpPeerConfig {
}
pub fn serialize(&self) -> [u8; LP_PEER_CONFIG_SIZE] {
let mut output_bytes: [u8; LP_PEER_CONFIG_SIZE] = [0u8; LP_PEER_CONFIG_SIZE];
output_bytes[0..4].copy_from_slice(self.pack_config().as_slice());
let mut output_bytes = [0u8; LP_PEER_CONFIG_SIZE];
output_bytes[0..4].copy_from_slice(&self.pack_config());
output_bytes[4..].copy_from_slice(&self.seed);
output_bytes
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, LpError> {
if bytes.len() != LP_PEER_CONFIG_SIZE {
Err(LpError::DeserializationError(format!(
return Err(LpError::DeserializationError(format!(
"Invalid Lp Config Length ({}), expected ({})",
bytes.len(),
LP_PEER_CONFIG_SIZE
)))
} else {
let (hop_id, is_exit, node_initiator, censorship_resistance) =
Self::unpack_first_byte(bytes[0]);
let mut filler: [u8; FILLER_LEN] = [0u8; FILLER_LEN];
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
let mut seed: [u8; SEED_LEN] = [0u8; SEED_LEN];
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
Self::build_checked(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
seed,
filler,
)
)));
}
let (hop_id, is_exit, node_initiator, censorship_resistance) =
Self::unpack_first_byte(bytes[0]);
let mut filler = [0u8; FILLER_LEN];
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
let mut seed = [0u8; SEED_LEN];
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
Self::build_checked(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
seed,
filler,
)
}
fn pack_config(&self) -> [u8; 4] {
+1 -1
View File
@@ -44,7 +44,7 @@ pub enum LpAction {
pub type SessionId = [u8; 32];
/// A session in the Lewes Protocol, handling connection state with Noise.
/// A session in the Lewes Protocol..
///
/// Sessions manage connection state, including LP replay protection.
/// Each session has a unique receiving index and sending index for connection identification.
+1 -6
View File
@@ -17,17 +17,12 @@ use crate::session::{LpAction, LpInput};
/// Manages the lifecycle of Lewes Protocol sessions.
///
/// The SessionManager is responsible for creating, storing, and retrieving sessions
#[derive(Default)]
pub struct SessionManager {
/// Manages state machines directly, keyed by lp_id
sessions: HashMap<LpReceiverIndex, LpTransportSession>,
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
impl SessionManager {
/// Creates a new session manager with empty session storage.
pub fn new() -> Self {
+2 -1
View File
@@ -79,10 +79,11 @@ async fn read_n_bytes_async_read<R>(reader: &mut R, n: usize) -> Result<Vec<u8>,
where
R: AsyncRead + Unpin,
{
let mut buf = vec![0u8; n];
if n > MAX_HANDSHAKE_PACKET_SIZE {
return Err(LpTransportError::PacketTooBig { size: n });
}
let mut buf = vec![0u8; n];
reader
.read_exact(&mut buf)
.await
+2 -2
View File
@@ -105,9 +105,9 @@ pub struct GatewayTasksBuilder {
shutdown_tracker: ShutdownTracker,
// populated and cached as necessary
use_mock_ecash: bool,
// populated and cached as necessary
ecash_manager:
Option<Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>>,
@@ -226,7 +226,7 @@ impl GatewayTasksBuilder {
> {
// Check if we should use mock ecash for testing
if self.use_mock_ecash {
warn!("Using MockEcashManager for LP testing (credentials NOT verified)");
warn!("Using MockEcashManager for testing (credentials NOT verified)");
let mock_manager = MockEcashManager::new(Box::new(self.storage.clone()));
return Ok(Arc::new(mock_manager)
as Arc<
+1 -1
View File
@@ -127,7 +127,7 @@ impl DailyMerkleTree {
pub(crate) fn maybe_rebuild(&mut self) {
// every 1000 leaves, rebuild the tree to purge the history
// (I wish the API of the library allowed to do it without having to go through those extra steps...)
if !self.inserted_leaves.is_empty() && self.inserted_leaves.len() % 1000 == 0 {
if !self.inserted_leaves.is_empty() && self.inserted_leaves.len().is_multiple_of(1000) {
self.rebuild_without_history();
}
}
+1 -1
View File
@@ -803,7 +803,7 @@ impl EcashState {
merkle_entry.maybe_rebuild();
// toss a coin to check if we should clean memory of old merkle trees
if thread_rng().next_u32() % 10000 == 0 {
if thread_rng().next_u32().is_multiple_of(10000) {
let mut values_to_clean = Vec::new();
let cutoff = self.config.ticketbook_retention_cutoff();
info!("attempting to remove old issued ticketbooks. the cutoff is set to {cutoff}");
+3 -8
View File
@@ -172,13 +172,11 @@ pub async fn lp_registration_probe(
let mut lp_outcome = LpProbeResults::default();
// Generate Ed25519 keypair for this connection (X25519 will be derived internally by LP)
// Generate X25519 keypair for this connection
let mut rng09 = rand09::rngs::StdRng::from_os_rng();
let client_x25519_keypair = Arc::new(DHKeyPair::new(&mut rng09));
// Step 0: Derive X25519 keys from Ed25519 for the gateways
// Create LP registration client (uses Ed25519 keys directly, derives X25519 internally)
// Create LP registration client
let mut client = LpRegistrationClient::<TcpStream>::new_with_default_config(
client_x25519_keypair,
peer,
@@ -212,16 +210,13 @@ pub async fn lp_registration_probe(
let mut rng = rand::thread_rng();
let wg_keypair = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
// Convert gateway identity to ed25519 public key
let gateway_ed25519_pubkey = gateway_identity;
// Register using the new packet-per-connection API (returns GatewayData directly)
let ticket_type = TicketType::V1WireguardEntry;
let gateway_data = match client
.register_dvpn(
&mut rng09,
&wg_keypair,
&gateway_ed25519_pubkey,
&gateway_identity,
bandwidth_controller,
ticket_type,
)
+1 -3
View File
@@ -13,8 +13,6 @@ license = "GPL-3.0"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
path = "src/lib.rs"
[dependencies]
async-trait = { workspace = true }
@@ -117,7 +115,7 @@ nym-network-requester = { path = "../service-providers/network-requester" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
# LP dependencies
nym-lp = { path = "../common/nym-lp" }
nym-lp = { workspace = true }
nym-registration-common = { path = "../common/registration" }
bincode = { workspace = true }
+1 -3
View File
@@ -12,7 +12,7 @@ use nym_lp::session::{LpAction, LpInput};
use nym_lp::transport::LpHandshakeChannel;
use nym_lp::transport::traits::LpTransportChannel;
use nym_lp::{LpTransportSession, packet::message::ExpectedResponseSize};
use nym_metrics::{add_histogram_obs, inc};
use nym_metrics::{add_histogram_obs, inc, inc_by};
use nym_registration_common::{LpRegistrationRequest, RegistrationStatus};
use std::net::SocketAddr;
use std::time::Duration;
@@ -625,8 +625,6 @@ where
/// Emit connection lifecycle metrics
fn emit_lifecycle_metrics(&self, graceful: bool) {
use nym_metrics::inc_by;
// Track connection duration
let duration = self.stats.start_time.elapsed().as_secs_f64();
add_histogram_obs!(
+3 -3
View File
@@ -453,16 +453,16 @@ impl NymNode {
&config.storage_paths.keys.x25519_noise_storage_paths(),
)?;
trace!("attempting to x25519 lp keypair");
trace!("attempting to store x25519 lp keypair");
store_x25519_lp_keypair(
&x25519_lp_keys,
&config.storage_paths.keys.x25519_lp_key_paths(),
)?;
trace!("attempting to mlkem768 keypair");
trace!("attempting to store mlkem768 keypair");
store_mlkem768_keypair(&mlkem, &config.storage_paths.keys.mlkem768_key_paths())?;
trace!("attempting to mceliece keypair");
trace!("attempting to store mceliece keypair");
store_mceliece_keypair(&mceliece, &config.storage_paths.keys.mceliece_key_paths())?;
trace!("creating description file");
+3 -2
View File
@@ -64,7 +64,6 @@ impl LpBasedRegistrationClient {
tracing::debug!("Exit gateway LP address: {exit_address}");
// Generate fresh x25519 keypairs for LP registration
// TODO: persist them for the duration of the sessions
let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
@@ -100,7 +99,7 @@ impl LpBasedRegistrationClient {
tracing::info!("Registering with exit gateway via entry forwarding");
let mut nested_session = NestedLpSession::new(
exit_address,
exit_lp_keypair,
exit_lp_keypair.clone(),
exit_peer,
exit_ciphersuite,
exit_lp_protocol,
@@ -153,6 +152,8 @@ impl LpBasedRegistrationClient {
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
entry_gateway_data,
exit_gateway_data,
entry_lp_keypair,
exit_lp_keypair,
bw_controller: self.bandwidth_controller,
})))
}
+27 -43
View File
@@ -5,7 +5,7 @@
use super::config::LpRegistrationConfig;
use super::error::{LpClientError, Result};
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt};
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt, exponential_backoff_with_jitter};
use crate::lp_client::nested_session::connection::NestedConnection;
use crate::lp_client::session_helpers::{extract_forwarded_response, prepare_send_packet};
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
@@ -23,12 +23,12 @@ use nym_registration_common::{
WireguardRegistrationData,
};
use nym_wireguard_types::PeerPublicKey;
use rand09::{CryptoRng, Rng, RngCore};
use rand09::{CryptoRng, RngCore};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use tracing::warn;
use tracing::{debug, warn};
/// LP (Lewes Protocol) registration client for direct gateway connections.
///
@@ -391,7 +391,6 @@ where
let protocol_version = self.gateway_supported_lp_protocol_version;
let connection = self.stream_mut()?;
// TODO:
let session = LpTransportSession::psq_handshake_initiator(
connection,
local_peer,
@@ -504,7 +503,7 @@ where
/// sends the registration request, and receives the response
/// on the same underlying connection.
/// Do note that this method does **not** perform retries on network failures,
/// for that please use [`Self::register_with_retry`] instead
/// for that please use [`Self::handshake_and_register_with_retry`] instead
///
/// # Arguments
/// * `rng` - RNG instance for generating PSK
@@ -632,7 +631,7 @@ where
/// # Note
/// Unlike `register()`, this method handles the full flow including handshake.
/// Do NOT call `perform_handshake()` before this method.
pub async fn register_with_retry<R>(
pub async fn handshake_and_register_with_retry<R>(
&mut self,
rng: &mut R,
wg_keypair: &x25519::KeyPair,
@@ -646,59 +645,44 @@ where
{
tracing::debug!("Starting resilient registration (max_retries={max_retries})",);
// attempt to perform handshake with retries
let mut last_error = None;
for attempt in 0..=max_retries {
let attempt_display = attempt + 1;
debug!("registration attempt {attempt_display}");
if attempt > 0 {
// Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped)
let base_delay_ms = 100u64 * (1 << attempt.min(4));
let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1));
let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms);
tracing::info!("Retrying registration (attempt {attempt_display}) after {delay:?}");
tokio::time::sleep(delay).await;
}
// Ensure fresh connection and handshake for each attempt
// (On retry, the old connection/session may be dead)
if self.stream.is_none() || attempt > 0 {
// Clear any stale state before re-handshaking
self.close();
self.transport_session = None;
self.close();
if let Err(e) = self.perform_handshake().await {
tracing::warn!("Handshake failed on attempt {attempt_display}: {e}");
last_error = Some(e);
continue;
}
exponential_backoff_with_jitter(attempt).await
}
match self
.register_dvpn(
rng,
wg_keypair,
gateway_identity,
bandwidth_controller,
ticket_type,
)
.await
{
Ok(data) => {
if attempt > 0 {
tracing::info!("Registration succeeded on retry attempt {attempt_display}");
}
return Ok(data);
}
match self.perform_handshake().await {
Ok(_) => break,
Err(e) => {
tracing::warn!("Registration attempt {attempt_display} failed: {e}");
tracing::warn!("Handshake failed on attempt {attempt_display}: {e}");
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
message: "Registration failed after all retries".to_string(),
}))
if self.transport_session.is_none() {
return Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
message: "Registration failed after all retries".to_string(),
}));
}
self.register_dvpn(
rng,
wg_keypair,
gateway_identity,
bandwidth_controller,
ticket_type,
)
.await
.inspect_err(|e| tracing::warn!("Registration failed: {e}"))
}
/// Get the LP session ID (receiver_idx) for this client.
@@ -11,6 +11,7 @@ use nym_lp::session::{LpAction, LpInput};
use nym_registration_common::{
LpRegistrationRequest, LpRegistrationResponse, NymNodeLPInformation,
};
use rand09::Rng;
pub(crate) trait LpDataSendExt {
fn to_lp_data(&self) -> Result<LpMessage, LpClientError>;
@@ -83,3 +84,12 @@ pub(crate) fn try_convert_forward_response(action: LpAction) -> Result<Vec<u8>,
pub(crate) fn to_lp_remote_peer(data: NymNodeLPInformation) -> LpRemotePeer {
LpRemotePeer::new(data.x25519).with_key_digests(data.expected_kem_key_hashes)
}
pub(crate) async fn exponential_backoff_with_jitter(attempt: u32) {
// Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped)
let base_delay_ms = 100u64 * (1 << attempt.min(4));
let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1));
let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms);
tracing::info!("Retrying registration after the following delay {delay:?}");
tokio::time::sleep(delay).await;
}
@@ -20,7 +20,7 @@
use super::client::LpRegistrationClient;
use super::error::{LpClientError, Result};
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt};
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt, exponential_backoff_with_jitter};
use crate::lp_client::session_helpers::{extract_forwarded_response, prepare_send_packet};
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
use nym_credentials_interface::TicketType;
@@ -37,7 +37,7 @@ use nym_registration_common::{
WireguardRegistrationData,
};
use nym_wireguard_types::PeerPublicKey;
use rand09::{CryptoRng, Rng, RngCore};
use rand09::{CryptoRng, RngCore};
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::{debug, warn};
@@ -284,7 +284,7 @@ impl NestedLpSession {
LpDvpnRegistrationResponseMessageContent::RegistrationFailure(res) => {
let reason = res.error;
// the registration has failed
tracing::warn!("Gateway rejected registration: {reason}");
warn!("Gateway rejected registration: {reason}");
Err(LpClientError::RegistrationRejected { reason })
}
LpDvpnRegistrationResponseMessageContent::CompletedRegistration(res) => Ok(res.config),
@@ -379,7 +379,7 @@ impl NestedLpSession {
LpDvpnRegistrationResponseMessageContent::RegistrationFailure(res) => {
let reason = res.error;
// the registration has failed
tracing::warn!("Gateway rejected registration: {reason}");
warn!("Gateway rejected registration: {reason}");
return Err(LpClientError::RegistrationRejected { reason });
}
LpDvpnRegistrationResponseMessageContent::CompletedRegistration(res) => res.config,
@@ -434,7 +434,7 @@ impl NestedLpSession {
/// - Forwarding through entry gateway fails
/// - Response decryption/deserialization fails
/// - Gateway rejects the registration
pub(crate) async fn handshake_and_register_dvpn<S, R>(
pub async fn handshake_and_register_dvpn<S, R>(
&mut self,
outer_client: &mut LpRegistrationClient<S>,
rng: &mut R,
@@ -509,60 +509,49 @@ impl NestedLpSession {
max_retries
);
// attempt to perform handshake with retries
let mut last_error = None;
for attempt in 0..=max_retries {
if attempt > 0 {
// Verify outer session is still usable before retry
if !outer_client.is_handshake_complete() {
return Err(LpClientError::Other(
"Outer session lost during retry - caller must re-establish entry gateway connection".to_string()
));
}
let attempt_display = attempt + 1;
debug!("registration attempt {attempt_display}");
// Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped)
let base_delay_ms = 100u64 * (1 << attempt.min(4));
let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1));
let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms);
tracing::info!(
"Retrying exit registration (attempt {}) after {:?}",
attempt + 1,
delay
);
tokio::time::sleep(delay).await;
// Clear state machine before retry - handshake needs fresh start
self.transport_session = None;
// Verify outer session is still usable before retry
if !outer_client.is_handshake_complete() {
return Err(LpClientError::Other(
"Outer session lost during retry - caller must re-establish entry gateway connection".to_string()
));
}
match self
.handshake_and_register_dvpn(
outer_client,
rng,
wg_keypair,
gateway_identity,
bandwidth_controller,
ticket_type,
)
.await
{
Ok(data) => {
if attempt > 0 {
tracing::info!(
"Exit registration succeeded on retry attempt {}",
attempt + 1
);
}
return Ok(data);
}
if attempt > 0 {
// Clear state machine before retry - handshake needs fresh start
self.transport_session = None;
exponential_backoff_with_jitter(attempt).await
}
match self.perform_handshake(outer_client).await {
Ok(_) => break,
Err(e) => {
tracing::warn!("Exit registration attempt {} failed: {}", attempt + 1, e);
warn!("Handshake failed on attempt {attempt_display}: {e}");
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
message: "Exit Registration failed after all retries".to_string(),
}))
if self.transport_session.is_none() {
return Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
message: "Exit Registration failed after all retries".to_string(),
}));
}
self.register_dvpn(
outer_client,
rng,
wg_keypair,
gateway_identity,
bandwidth_controller,
ticket_type,
)
.await
.inspect_err(|e| warn!("Exit Registration failed: {e}"))
}
}
+12
View File
@@ -3,8 +3,10 @@
use nym_authenticator_client::{AuthClientMixnetListenerHandle, AuthenticatorClient};
use nym_bandwidth_controller::BandwidthTicketProvider;
use nym_lp::peer::DHKeyPair;
use nym_registration_common::{AssignedAddresses, WireguardConfiguration};
use nym_sdk::mixnet::{EventReceiver, MixnetClient};
use std::sync::Arc;
pub enum RegistrationResult {
Mixnet(Box<MixnetRegistrationResult>),
@@ -36,6 +38,8 @@ pub struct WireguardRegistrationResult {
/// # Fields
/// * `entry_gateway_data` - WireGuard configuration from entry gateway
/// * `exit_gateway_data` - WireGuard configuration from exit gateway
/// * `entry_lp_keypair` - x25519 keypair used on the entry LP channel (persist to resume a pre-established session)
/// * `exit_lp_keypair` - x25519 keypair used on the exit LP channel (persist to resume a pre-established session)
/// * `bw_controller` - Bandwidth ticket provider for credential management
pub struct LpRegistrationResult {
/// Gateway configuration data from entry gateway
@@ -44,6 +48,14 @@ pub struct LpRegistrationResult {
/// Gateway configuration data from exit gateway
pub exit_gateway_data: WireguardConfiguration,
/// x25519 keypair used on the entry channel.
/// the purpose of persisting those keys is to be able to resume the pre-established session
pub entry_lp_keypair: Arc<DHKeyPair>,
/// x25519 keypair used on the exit channel
/// the purpose of persisting those keys is to be able to resume the pre-established session
pub exit_lp_keypair: Arc<DHKeyPair>,
/// Bandwidth controller for credential management
pub bw_controller: Box<dyn BandwidthTicketProvider>,
}