[LP fix] Registration client with fallback (#6419)
* don't start mixnet client for lp reg, with fallback * tweaks * add logging
This commit is contained in:
@@ -17,8 +17,8 @@ use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::error::RegistrationClientError;
|
||||
use crate::{LpRegistrationConfig, config::RegistrationMode};
|
||||
use crate::{config::RegistrationClientConfig, error::RegistrationClientError};
|
||||
|
||||
const VPN_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15);
|
||||
const MIXNET_CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -31,19 +31,29 @@ pub struct NymNodeWithKeys {
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct BuilderConfig {
|
||||
// For future reference
|
||||
// Common options
|
||||
pub entry_node: NymNodeWithKeys,
|
||||
pub exit_node: NymNodeWithKeys,
|
||||
pub data_path: Option<PathBuf>,
|
||||
pub mode: RegistrationMode,
|
||||
pub cancel_token: CancellationToken,
|
||||
|
||||
// Toggle
|
||||
#[builder(default)]
|
||||
pub enable_lp_regitration: bool,
|
||||
|
||||
// Mixnet based only option
|
||||
pub mixnet_client_config: MixnetClientConfig,
|
||||
#[builder(default = MIXNET_CLIENT_STARTUP_TIMEOUT)]
|
||||
pub mixnet_client_startup_timeout: Duration,
|
||||
pub mode: RegistrationMode,
|
||||
pub user_agent: UserAgent,
|
||||
pub custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
|
||||
pub network_env: NymNetworkDetails,
|
||||
pub cancel_token: CancellationToken,
|
||||
#[cfg(unix)]
|
||||
pub connection_fd_callback: Arc<dyn Fn(RawFd) + Send + Sync>,
|
||||
|
||||
// LP based only option
|
||||
#[builder(default)]
|
||||
pub lp_registration_config: LpRegistrationConfig,
|
||||
}
|
||||
@@ -77,29 +87,36 @@ impl BuilderConfig {
|
||||
// Mixnet mode uses 5-hop configuration
|
||||
RegistrationMode::Mixnet => mixnet_debug_config(&self.mixnet_client_config),
|
||||
// Wireguard and LP both use 2-hop configuration
|
||||
RegistrationMode::Wireguard | RegistrationMode::Lp => {
|
||||
two_hop_debug_config(&self.mixnet_client_config)
|
||||
}
|
||||
RegistrationMode::Wireguard => two_hop_debug_config(&self.mixnet_client_config),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_storage(
|
||||
pub fn registration_client_config(&self) -> RegistrationClientConfig {
|
||||
RegistrationClientConfig {
|
||||
entry: self.entry_node.clone(),
|
||||
exit: self.exit_node.clone(),
|
||||
mode: self.mode,
|
||||
lp_registration_config: self.lp_registration_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_mixnet_client_storage(
|
||||
&self,
|
||||
) -> Result<Option<(OnDiskPersistent, PersistentStorage)>, RegistrationClientError> {
|
||||
if let Some(path) = &self.data_path {
|
||||
tracing::debug!("Using custom key storage path: {}", path.display());
|
||||
|
||||
let storage_paths = StoragePaths::new_from_dir(path)
|
||||
.map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?;
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
let mixnet_client_storage = storage_paths
|
||||
.initialise_persistent_storage(&self.mixnet_client_debug_config())
|
||||
.await
|
||||
.map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?;
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
let credential_storage = storage_paths
|
||||
.persistent_credential_storage()
|
||||
.await
|
||||
.map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?;
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
Ok(Some((mixnet_client_storage, credential_storage)))
|
||||
} else {
|
||||
@@ -107,6 +124,26 @@ impl BuilderConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_credential_storage(
|
||||
&self,
|
||||
) -> Result<Option<PersistentStorage>, RegistrationClientError> {
|
||||
if let Some(path) = &self.data_path {
|
||||
tracing::debug!("Using custom credential storage path: {}", path.display());
|
||||
|
||||
let storage_paths = StoragePaths::new_from_dir(path)
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
let credential_storage = storage_paths
|
||||
.persistent_credential_storage()
|
||||
.await
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
Ok(Some(credential_storage))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_and_connect_mixnet_client<S>(
|
||||
self,
|
||||
builder: MixnetClientBuilder<S>,
|
||||
@@ -123,7 +160,7 @@ impl BuilderConfig {
|
||||
let debug_config = self.mixnet_client_debug_config();
|
||||
let remember_me = match self.mode {
|
||||
RegistrationMode::Mixnet => RememberMe::new_mixnet(),
|
||||
RegistrationMode::Wireguard | RegistrationMode::Lp => RememberMe::new_vpn(),
|
||||
RegistrationMode::Wireguard => RememberMe::new_vpn(),
|
||||
};
|
||||
|
||||
let identity = self.entry_node.node.identity.to_string();
|
||||
|
||||
@@ -13,7 +13,12 @@ use nym_validator_client::{
|
||||
nyxd::{Config as NyxdClientConfig, NyxdClient},
|
||||
};
|
||||
|
||||
use crate::{RegistrationClient, config::RegistrationClientConfig, error::RegistrationClientError};
|
||||
use crate::{
|
||||
RegistrationClient,
|
||||
clients::{LpBasedRegistrationClient, MixnetBasedRegistrationClient},
|
||||
config::RegistrationMode,
|
||||
error::RegistrationClientError,
|
||||
};
|
||||
use config::BuilderConfig;
|
||||
|
||||
pub(crate) mod config;
|
||||
@@ -27,14 +32,38 @@ impl RegistrationClientBuilder {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn use_lp(&self) -> bool {
|
||||
let lp_enabled = self.config.enable_lp_regitration;
|
||||
let lp_info_available = self.config.entry_node.node.lp_data.is_some()
|
||||
&& self.config.exit_node.node.lp_data.is_some();
|
||||
// To remove when LP supports Mixnet registration
|
||||
let wireguard_mode = self.config.mode == RegistrationMode::Wireguard;
|
||||
let use_lp = lp_enabled && lp_info_available && wireguard_mode;
|
||||
if !use_lp && lp_enabled {
|
||||
tracing::warn!(
|
||||
"LP is enabled but can't be used: Missing LP information: {lp_info_available}, wireguard mode: {wireguard_mode}"
|
||||
);
|
||||
}
|
||||
use_lp
|
||||
}
|
||||
|
||||
pub async fn build(self) -> Result<RegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_storage().await?;
|
||||
let config = RegistrationClientConfig {
|
||||
entry: self.config.entry_node.clone(),
|
||||
exit: self.config.exit_node.clone(),
|
||||
mode: self.config.mode,
|
||||
lp_registration_config: self.config.lp_registration_config,
|
||||
};
|
||||
if self.use_lp() {
|
||||
tracing::debug!("Using LP for registration");
|
||||
Ok(RegistrationClient::Lp(Box::new(self.build_lp().await?)))
|
||||
} else {
|
||||
tracing::debug!("Using Mixnet for registration");
|
||||
Ok(RegistrationClient::Mixnet(Box::new(
|
||||
self.build_mixnet().await?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_mixnet(
|
||||
self,
|
||||
) -> Result<MixnetBasedRegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_mixnet_client_storage().await?;
|
||||
let config = self.config.registration_client_config();
|
||||
let cancel_token = self.config.cancel_token.clone();
|
||||
let (event_tx, event_rx) = mpsc::unbounded();
|
||||
|
||||
@@ -84,7 +113,7 @@ impl RegistrationClientBuilder {
|
||||
};
|
||||
let mixnet_client_address = *mixnet_client.nym_address();
|
||||
|
||||
Ok(RegistrationClient {
|
||||
Ok(MixnetBasedRegistrationClient {
|
||||
mixnet_client,
|
||||
config,
|
||||
cancel_token,
|
||||
@@ -93,6 +122,30 @@ impl RegistrationClientBuilder {
|
||||
event_rx,
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_lp(self) -> Result<LpBasedRegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_credential_storage().await?;
|
||||
let config = self.config.registration_client_config();
|
||||
|
||||
let nyxd_client = get_nyxd_client(&self.config.network_env)?;
|
||||
|
||||
let bandwidth_controller: Box<dyn BandwidthTicketProvider> =
|
||||
if let Some(credential_storage) = storage {
|
||||
Box::new(BandwidthController::new(credential_storage, nyxd_client))
|
||||
} else {
|
||||
Box::new(BandwidthController::new(
|
||||
EphemeralCredentialStorage::default(),
|
||||
nyxd_client,
|
||||
))
|
||||
};
|
||||
|
||||
Ok(LpBasedRegistrationClient {
|
||||
config,
|
||||
bandwidth_controller,
|
||||
cancel_token: self.config.cancel_token.clone(),
|
||||
fallback_client_builder: Some(self),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// temporary while we use the legacy bandwidth-controller
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::builder::RegistrationClientBuilder;
|
||||
use crate::config::RegistrationClientConfig;
|
||||
use crate::config::RegistrationMode;
|
||||
use crate::error::RegistrationClientError;
|
||||
use crate::lp_client::helpers::to_lp_remote_peer;
|
||||
use crate::lp_client::{LpRegistrationClient, NestedLpSession};
|
||||
use crate::types::{LpRegistrationResult, RegistrationResult};
|
||||
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct LpBasedRegistrationClient {
|
||||
pub(crate) config: RegistrationClientConfig,
|
||||
pub(crate) bandwidth_controller: Box<dyn BandwidthTicketProvider>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
// While we allow a fallback, we need to be able to build it
|
||||
pub(crate) fallback_client_builder: Option<RegistrationClientBuilder>,
|
||||
}
|
||||
|
||||
impl LpBasedRegistrationClient {
|
||||
// create dedicated method taking RNG instance for tests
|
||||
async fn register_wg_with_rng<R>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> Result<RegistrationResult, RegistrationClientError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
// Extract and validate LP data
|
||||
let entry_lp_data = self.config.entry.node.lp_data.ok_or(
|
||||
RegistrationClientError::LpRegistrationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let exit_lp_data = self.config.exit.node.lp_data.ok_or(
|
||||
RegistrationClientError::LpRegistrationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
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;
|
||||
|
||||
tracing::debug!("Entry gateway LP address: {entry_address}");
|
||||
tracing::debug!("Exit gateway LP address: {exit_address}");
|
||||
|
||||
// Generate fresh Ed25519 keypairs for LP registration
|
||||
// These are ephemeral and used only for the LP handshake protocol
|
||||
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
|
||||
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
|
||||
|
||||
let entry_peer = to_lp_remote_peer(self.config.entry.node.identity, entry_lp_data);
|
||||
let exit_peer = to_lp_remote_peer(self.config.exit.node.identity, exit_lp_data);
|
||||
|
||||
// STEP 1: Establish outer session with entry gateway
|
||||
// This creates the LP session that will be used to forward packets to exit.
|
||||
// Uses packet-per-connection model: each handshake packet on new TCP connection.
|
||||
tracing::info!("Establishing outer session with entry gateway");
|
||||
let mut entry_client = LpRegistrationClient::new(
|
||||
entry_lp_keypair.clone(),
|
||||
entry_peer,
|
||||
entry_address,
|
||||
entry_lp_protocol,
|
||||
self.config.lp_registration_config,
|
||||
);
|
||||
|
||||
// Perform handshake with entry gateway (outer session now established)
|
||||
entry_client.perform_handshake().await.map_err(|source| {
|
||||
RegistrationClientError::EntryGatewayRegisterLp {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
lp_address: entry_address,
|
||||
source: Box::new(source),
|
||||
}
|
||||
})?;
|
||||
|
||||
tracing::info!("Outer session with entry gateway established");
|
||||
|
||||
// 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, 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
|
||||
.handshake_and_register_dvpn::<TcpStream, _>(
|
||||
&mut entry_client,
|
||||
rng,
|
||||
&self.config.exit.keys,
|
||||
&self.config.exit.node.identity,
|
||||
&*self.bandwidth_controller,
|
||||
TicketType::V1WireguardExit,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
|
||||
gateway_id: self.config.exit.node.identity.to_base58_string(),
|
||||
lp_address: exit_address,
|
||||
source: Box::new(source),
|
||||
})?;
|
||||
|
||||
tracing::info!("Exit gateway registration completed via forwarding");
|
||||
|
||||
// STEP 3: Register with entry gateway (packet-per-connection)
|
||||
tracing::info!("Registering with entry gateway");
|
||||
let entry_gateway_data = entry_client
|
||||
.register_dvpn(
|
||||
rng,
|
||||
&self.config.entry.keys,
|
||||
&self.config.entry.node.identity,
|
||||
&*self.bandwidth_controller,
|
||||
TicketType::V1WireguardEntry,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
lp_address: entry_address,
|
||||
source: Box::new(source),
|
||||
})?;
|
||||
|
||||
tracing::info!("Entry gateway registration successful");
|
||||
|
||||
tracing::info!("LP registration successful for both gateways");
|
||||
|
||||
// LP is registration-only (packet-per-connection model).
|
||||
// All data flows through WireGuard after this point.
|
||||
// Each LP packet used its own TCP connection which was closed after the exchange.
|
||||
// Exit registration was completed via forwarding through entry gateway.
|
||||
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
|
||||
entry_gateway_data,
|
||||
exit_gateway_data,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn register_wg(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
|
||||
self.register_wg_with_rng(&mut rng).await
|
||||
}
|
||||
|
||||
pub(crate) async fn register(mut self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let fallback = self.fallback_client_builder.take();
|
||||
match &self.config.mode {
|
||||
RegistrationMode::Mixnet => {
|
||||
if let Some(fallback) = fallback {
|
||||
register_with_fallback(fallback).await
|
||||
} else {
|
||||
Err(RegistrationClientError::UnsupportedMode)
|
||||
}
|
||||
}
|
||||
RegistrationMode::Wireguard => {
|
||||
let lp_registration_result = self
|
||||
.cancel_token
|
||||
.clone()
|
||||
.run_until_cancelled(self.register_wg())
|
||||
.await;
|
||||
match lp_registration_result {
|
||||
// Everything went fine
|
||||
Some(Ok(res)) => Ok(res),
|
||||
|
||||
// LP reg failed, try fallback if we have one
|
||||
Some(Err(e)) => {
|
||||
tracing::error!("LP registration failed : {e}");
|
||||
if let Some(fallback) = fallback {
|
||||
tracing::info!("Registering with fallback");
|
||||
register_with_fallback(fallback).await
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Cancelled registration
|
||||
None => Err(RegistrationClientError::Cancelled),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_with_fallback(
|
||||
client_builder: RegistrationClientBuilder,
|
||||
) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
// This is forcefully building a mixnet based client
|
||||
let fallback_client = client_builder.build_mixnet().await?;
|
||||
fallback_client.register().await
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::RegistrationClientConfig;
|
||||
use crate::config::RegistrationMode;
|
||||
use crate::error::RegistrationClientError;
|
||||
use crate::types::{MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult};
|
||||
use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
use nym_registration_common::AssignedAddresses;
|
||||
use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct MixnetBasedRegistrationClient {
|
||||
pub(crate) mixnet_client: MixnetClient,
|
||||
pub(crate) config: RegistrationClientConfig,
|
||||
pub(crate) mixnet_client_address: Recipient,
|
||||
pub(crate) bandwidth_controller: Box<dyn BandwidthTicketProvider>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
pub(crate) event_rx: EventReceiver,
|
||||
}
|
||||
|
||||
impl MixnetBasedRegistrationClient {
|
||||
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let entry_mixnet_gateway_ip = self.config.entry.node.ip_address;
|
||||
|
||||
let exit_mixnet_gateway_ip = self.config.exit.node.ip_address;
|
||||
|
||||
let ipr_address = self.config.exit.node.ipr_address.ok_or(
|
||||
RegistrationClientError::NoIpPacketRouterAddress {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
let mut ipr_client = IprClientConnect::new(self.mixnet_client, self.cancel_token.clone());
|
||||
let interface_addresses = ipr_client
|
||||
.connect(ipr_address)
|
||||
.await
|
||||
.map_err(RegistrationClientError::ConnectToIpPacketRouter)?;
|
||||
|
||||
Ok(RegistrationResult::Mixnet(Box::new(
|
||||
MixnetRegistrationResult {
|
||||
mixnet_client: ipr_client.into_mixnet_client(),
|
||||
assigned_addresses: AssignedAddresses {
|
||||
interface_addresses,
|
||||
exit_mix_address: ipr_address,
|
||||
mixnet_client_address: self.mixnet_client_address,
|
||||
entry_mixnet_gateway_ip,
|
||||
exit_mixnet_gateway_ip,
|
||||
},
|
||||
event_rx: self.event_rx,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn register_wg(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let entry_auth_address = self.config.entry.node.authenticator_address.ok_or(
|
||||
RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let exit_auth_address = self.config.exit.node.authenticator_address.ok_or(
|
||||
RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let entry_version = self.config.entry.node.version;
|
||||
tracing::debug!("Entry gateway version: {entry_version}");
|
||||
let exit_version = self.config.exit.node.version;
|
||||
tracing::debug!("Exit gateway version: {exit_version}");
|
||||
|
||||
// Start the auth client mixnet listener, which will listen for incoming messages from the
|
||||
// mixnet and rebroadcast them to the auth clients.
|
||||
let mixnet_listener =
|
||||
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.clone()).start();
|
||||
|
||||
let mut entry_auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
entry_auth_address,
|
||||
entry_version,
|
||||
self.config.entry.keys,
|
||||
self.config.entry.node.ip_address,
|
||||
);
|
||||
|
||||
let mut exit_auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
exit_auth_address,
|
||||
exit_version,
|
||||
self.config.exit.keys,
|
||||
self.config.exit.node.ip_address,
|
||||
);
|
||||
|
||||
let entry_fut = entry_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardEntry);
|
||||
let exit_fut = exit_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit);
|
||||
|
||||
let (entry, exit) = Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await;
|
||||
|
||||
let entry = entry.map_err(|source| {
|
||||
RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.entry.node.identity.to_base58_string(),
|
||||
entry_auth_address,
|
||||
true, // is entry
|
||||
)
|
||||
})?;
|
||||
let exit = exit.map_err(|source| {
|
||||
RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.exit.node.identity.to_base58_string(),
|
||||
exit_auth_address,
|
||||
false, // is exit (not entry)
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(RegistrationResult::Wireguard(Box::new(
|
||||
WireguardRegistrationResult {
|
||||
entry_gateway_client: entry_auth_client,
|
||||
exit_gateway_client: exit_auth_client,
|
||||
entry_gateway_data: entry,
|
||||
exit_gateway_data: exit,
|
||||
authenticator_listener_handle: mixnet_listener,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn register(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
self.cancel_token
|
||||
.clone()
|
||||
.run_until_cancelled(async {
|
||||
match self.config.mode {
|
||||
RegistrationMode::Mixnet => self.register_mix_exit().await,
|
||||
RegistrationMode::Wireguard => self.register_wg().await,
|
||||
}
|
||||
})
|
||||
.await
|
||||
.ok_or(RegistrationClientError::Cancelled)?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod lp;
|
||||
pub(crate) mod mixnet;
|
||||
|
||||
pub use lp::LpBasedRegistrationClient;
|
||||
pub use mixnet::MixnetBasedRegistrationClient;
|
||||
@@ -10,22 +10,8 @@ use crate::builder::config::NymNodeWithKeys;
|
||||
pub enum RegistrationMode {
|
||||
/// 5-hop mixnet with IPR (IP Packet Router)
|
||||
Mixnet,
|
||||
/// 2-hop WireGuard with authenticator
|
||||
/// 2-hop WireGuard
|
||||
Wireguard,
|
||||
/// 2-hop WireGuard with LP (Lewes Protocol)
|
||||
Lp,
|
||||
}
|
||||
|
||||
impl RegistrationMode {
|
||||
/// Legacy method for backward compatibility
|
||||
#[deprecated(note = "use explicit enum variant instead")]
|
||||
pub fn legacy_two_hop(use_two_hop: bool) -> RegistrationMode {
|
||||
if use_two_hop {
|
||||
Self::Wireguard
|
||||
} else {
|
||||
Self::Mixnet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegistrationClientConfig {
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RegistrationClientError {
|
||||
#[error("trying to register in mixnet mode with LP and no fallback. This shouldn't happen")]
|
||||
UnsupportedMode,
|
||||
|
||||
#[error("failed to build mixnet client")]
|
||||
BuildMixnetClient(#[source] Box<nym_sdk::Error>),
|
||||
|
||||
#[error("failed to initialize storage")]
|
||||
StorageInitialization(#[source] Box<nym_sdk::Error>),
|
||||
|
||||
#[error("failed to connect to mixnet")]
|
||||
ConnectToMixnet(#[source] Box<nym_sdk::Error>),
|
||||
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::RegistrationClientConfig;
|
||||
use crate::lp_client::helpers::to_lp_remote_peer;
|
||||
use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
use nym_registration_common::AssignedAddresses;
|
||||
use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub use builder::RegistrationClientBuilder;
|
||||
pub use builder::config::{
|
||||
BuilderConfig as RegistrationClientBuilderConfig, MixnetClientConfig,
|
||||
@@ -25,272 +11,27 @@ pub use error::RegistrationClientError;
|
||||
pub use lp_client::{
|
||||
LpRegistrationClient, LpRegistrationConfig, NestedLpSession, error::LpClientError,
|
||||
};
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
|
||||
pub use types::{
|
||||
LpRegistrationResult, MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult,
|
||||
};
|
||||
|
||||
mod builder;
|
||||
mod clients;
|
||||
mod config;
|
||||
mod error;
|
||||
mod lp_client;
|
||||
mod types;
|
||||
|
||||
pub struct RegistrationClient {
|
||||
mixnet_client: MixnetClient,
|
||||
config: RegistrationClientConfig,
|
||||
mixnet_client_address: Recipient,
|
||||
bandwidth_controller: Box<dyn BandwidthTicketProvider>,
|
||||
cancel_token: CancellationToken,
|
||||
event_rx: EventReceiver,
|
||||
pub enum RegistrationClient {
|
||||
Mixnet(Box<clients::MixnetBasedRegistrationClient>),
|
||||
Lp(Box<clients::LpBasedRegistrationClient>),
|
||||
}
|
||||
|
||||
impl RegistrationClient {
|
||||
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let entry_mixnet_gateway_ip = self.config.entry.node.ip_address;
|
||||
|
||||
let exit_mixnet_gateway_ip = self.config.exit.node.ip_address;
|
||||
|
||||
let ipr_address = self.config.exit.node.ipr_address.ok_or(
|
||||
RegistrationClientError::NoIpPacketRouterAddress {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
let mut ipr_client = IprClientConnect::new(self.mixnet_client, self.cancel_token.clone());
|
||||
let interface_addresses = ipr_client
|
||||
.connect(ipr_address)
|
||||
.await
|
||||
.map_err(RegistrationClientError::ConnectToIpPacketRouter)?;
|
||||
|
||||
Ok(RegistrationResult::Mixnet(Box::new(
|
||||
MixnetRegistrationResult {
|
||||
mixnet_client: ipr_client.into_mixnet_client(),
|
||||
assigned_addresses: AssignedAddresses {
|
||||
interface_addresses,
|
||||
exit_mix_address: ipr_address,
|
||||
mixnet_client_address: self.mixnet_client_address,
|
||||
entry_mixnet_gateway_ip,
|
||||
exit_mixnet_gateway_ip,
|
||||
},
|
||||
event_rx: self.event_rx,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn register_wg(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let entry_auth_address = self.config.entry.node.authenticator_address.ok_or(
|
||||
RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let exit_auth_address = self.config.exit.node.authenticator_address.ok_or(
|
||||
RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let entry_version = self.config.entry.node.version;
|
||||
tracing::debug!("Entry gateway version: {entry_version}");
|
||||
let exit_version = self.config.exit.node.version;
|
||||
tracing::debug!("Exit gateway version: {exit_version}");
|
||||
|
||||
// Start the auth client mixnet listener, which will listen for incoming messages from the
|
||||
// mixnet and rebroadcast them to the auth clients.
|
||||
let mixnet_listener =
|
||||
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.clone()).start();
|
||||
|
||||
let mut entry_auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
entry_auth_address,
|
||||
entry_version,
|
||||
self.config.entry.keys,
|
||||
self.config.entry.node.ip_address,
|
||||
);
|
||||
|
||||
let mut exit_auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
exit_auth_address,
|
||||
exit_version,
|
||||
self.config.exit.keys,
|
||||
self.config.exit.node.ip_address,
|
||||
);
|
||||
|
||||
let entry_fut = entry_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardEntry);
|
||||
let exit_fut = exit_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit);
|
||||
|
||||
let (entry, exit) = Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await;
|
||||
|
||||
let entry = entry.map_err(|source| {
|
||||
RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.entry.node.identity.to_base58_string(),
|
||||
entry_auth_address,
|
||||
true, // is entry
|
||||
)
|
||||
})?;
|
||||
let exit = exit.map_err(|source| {
|
||||
RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.exit.node.identity.to_base58_string(),
|
||||
exit_auth_address,
|
||||
false, // is exit (not entry)
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(RegistrationResult::Wireguard(Box::new(
|
||||
WireguardRegistrationResult {
|
||||
entry_gateway_client: entry_auth_client,
|
||||
exit_gateway_client: exit_auth_client,
|
||||
entry_gateway_data: entry,
|
||||
exit_gateway_data: exit,
|
||||
authenticator_listener_handle: mixnet_listener,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
// create dedicated method taking RNG instance for tests
|
||||
async fn register_lp_with_rng<R>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> Result<RegistrationResult, RegistrationClientError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
// Extract and validate LP data
|
||||
let entry_lp_data = self.config.entry.node.lp_data.ok_or(
|
||||
RegistrationClientError::LpRegistrationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let exit_lp_data = self.config.exit.node.lp_data.ok_or(
|
||||
RegistrationClientError::LpRegistrationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
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;
|
||||
|
||||
tracing::debug!("Entry gateway LP address: {entry_address}");
|
||||
tracing::debug!("Exit gateway LP address: {exit_address}");
|
||||
|
||||
// Generate fresh Ed25519 keypairs for LP registration
|
||||
// These are ephemeral and used only for the LP handshake protocol
|
||||
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
|
||||
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
|
||||
|
||||
let entry_peer = to_lp_remote_peer(self.config.entry.node.identity, entry_lp_data);
|
||||
let exit_peer = to_lp_remote_peer(self.config.exit.node.identity, exit_lp_data);
|
||||
|
||||
// STEP 1: Establish outer session with entry gateway
|
||||
// This creates the LP session that will be used to forward packets to exit.
|
||||
// Uses packet-per-connection model: each handshake packet on new TCP connection.
|
||||
tracing::info!("Establishing outer session with entry gateway");
|
||||
let mut entry_client = LpRegistrationClient::new(
|
||||
entry_lp_keypair.clone(),
|
||||
entry_peer,
|
||||
entry_address,
|
||||
entry_lp_protocol,
|
||||
self.config.lp_registration_config,
|
||||
);
|
||||
|
||||
// Perform handshake with entry gateway (outer session now established)
|
||||
entry_client.perform_handshake().await.map_err(|source| {
|
||||
RegistrationClientError::EntryGatewayRegisterLp {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
lp_address: entry_address,
|
||||
source: Box::new(source),
|
||||
}
|
||||
})?;
|
||||
|
||||
tracing::info!("Outer session with entry gateway established");
|
||||
|
||||
// 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, 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
|
||||
.handshake_and_register_dvpn::<TcpStream, _>(
|
||||
&mut entry_client,
|
||||
rng,
|
||||
&self.config.exit.keys,
|
||||
&self.config.exit.node.identity,
|
||||
&*self.bandwidth_controller,
|
||||
TicketType::V1WireguardExit,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
|
||||
gateway_id: self.config.exit.node.identity.to_base58_string(),
|
||||
lp_address: exit_address,
|
||||
source: Box::new(source),
|
||||
})?;
|
||||
|
||||
tracing::info!("Exit gateway registration completed via forwarding");
|
||||
|
||||
// STEP 3: Register with entry gateway (packet-per-connection)
|
||||
tracing::info!("Registering with entry gateway");
|
||||
let entry_gateway_data = entry_client
|
||||
.register_dvpn(
|
||||
rng,
|
||||
&self.config.entry.keys,
|
||||
&self.config.entry.node.identity,
|
||||
&*self.bandwidth_controller,
|
||||
TicketType::V1WireguardEntry,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
lp_address: entry_address,
|
||||
source: Box::new(source),
|
||||
})?;
|
||||
|
||||
tracing::info!("Entry gateway registration successful");
|
||||
|
||||
tracing::info!("LP registration successful for both gateways");
|
||||
|
||||
// LP is registration-only (packet-per-connection model).
|
||||
// All data flows through WireGuard after this point.
|
||||
// Each LP packet used its own TCP connection which was closed after the exchange.
|
||||
// Exit registration was completed via forwarding through entry gateway.
|
||||
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
|
||||
entry_gateway_data,
|
||||
exit_gateway_data,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn register_lp(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
|
||||
self.register_lp_with_rng(&mut rng).await
|
||||
}
|
||||
|
||||
pub async fn register(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
self.cancel_token
|
||||
.clone()
|
||||
.run_until_cancelled(async {
|
||||
match self.config.mode {
|
||||
RegistrationMode::Mixnet => self.register_mix_exit().await,
|
||||
RegistrationMode::Wireguard => self.register_wg().await,
|
||||
RegistrationMode::Lp => self.register_lp().await,
|
||||
}
|
||||
})
|
||||
.await
|
||||
.ok_or(RegistrationClientError::Cancelled)?
|
||||
match self {
|
||||
RegistrationClient::Mixnet(client) => client.register().await,
|
||||
RegistrationClient::Lp(client) => client.register().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user