[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:
Simon Wicky
2026-02-05 10:51:37 +01:00
committed by GitHub
parent d4882ca276
commit 88a4633bc4
8 changed files with 481 additions and 302 deletions
+48 -11
View File
@@ -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();
+62 -9
View File
@@ -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
+200
View File
@@ -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;
+1 -15
View File
@@ -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 {
+6
View File
@@ -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>),
+8 -267
View File
@@ -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,
}
}
}