diff --git a/Cargo.lock b/Cargo.lock index cd5c5fa981..248993d6af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4121,10 +4121,11 @@ dependencies = [ "nym-credential-verification", "nym-credentials-interface", "nym-crypto", - "nym-gateway", "nym-kkt", "nym-kkt-ciphersuite", + "nym-lp", "nym-lp-transport", + "nym-node", "nym-registration-client", "nym-test-utils", "nym-wireguard", @@ -6392,7 +6393,6 @@ dependencies = [ "bincode", "bip39", "bs58", - "bytes", "dashmap", "defguard_wireguard_rs", "fastrand", @@ -6412,9 +6412,7 @@ dependencies = [ "nym-gateway-storage", "nym-id", "nym-ip-packet-router", - "nym-kcp", "nym-lp", - "nym-lp-transport", "nym-metrics", "nym-mixnet-client", "nym-network-defaults", @@ -7166,6 +7164,7 @@ dependencies = [ "arrayref", "async-trait", "axum 0.7.9", + "bincode", "bip39", "blake2 0.8.1", "bloomfilter", @@ -7180,6 +7179,7 @@ dependencies = [ "criterion", "csv", "cupid", + "dashmap", "futures", "hex", "hkdf", @@ -7199,6 +7199,8 @@ dependencies = [ "nym-http-api-common", "nym-ip-packet-router", "nym-kkt", + "nym-lp", + "nym-lp-transport", "nym-metrics", "nym-mixnet-client", "nym-network-requester", @@ -7208,6 +7210,7 @@ dependencies = [ "nym-noise-keys", "nym-nonexhaustive-delayqueue", "nym-pemstore", + "nym-registration-common", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-forwarding", diff --git a/Makefile b/Makefile index 3b709e9f11..b0971c23af 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,8 @@ sdk-typescript-build: yarn --cwd sdk/typescript/codegen/contract-clients build # NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown) -WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib +# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib +WASM_CRATES = nym-client-wasm nym-node-tester-wasm sdk-wasm-test: #cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index ca508afdac..123c01a272 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -78,13 +78,7 @@ nym-authenticator-requests = { workspace = true } nym-client-core = { workspace = true, features = ["cli"] } nym-id = { workspace = true } nym-service-provider-requests-common = { workspace = true } - -# LP dependencies -nym-lp = { path = "../common/nym-lp" } -nym-lp-transport = { path = "../common/nym-lp-transport" } -nym-kcp = { path = "../common/nym-kcp" } nym-registration-common = { path = "../common/registration" } -bytes = { workspace = true } defguard_wireguard_rs = { workspace = true } diff --git a/gateway/src/config.rs b/gateway/src/config.rs index f12189b1d0..8df528674b 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -15,8 +15,6 @@ pub struct Config { pub upgrade_mode_watcher: UpgradeModeWatcher, - pub lp: crate::node::lp_listener::LpConfig, - pub debug: Debug, } @@ -26,7 +24,6 @@ impl Config { network_requester: impl Into, ip_packet_router: impl Into, upgrade_mode_watcher: impl Into, - lp: impl Into, debug: impl Into, ) -> Self { Config { @@ -34,7 +31,6 @@ impl Config { network_requester: network_requester.into(), ip_packet_router: ip_packet_router.into(), upgrade_mode_watcher: upgrade_mode_watcher.into(), - lp: lp.into(), debug: debug.into(), } } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index c45732f1fe..c419246bcd 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -3,7 +3,6 @@ pub use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError; use crate::node::internal_service_providers::authenticator::error::AuthenticatorError; -use crate::node::lp_listener::error::LpHandlerError; use crate::node::wireguard::GatewayWireguardError; use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_gateway_stats_storage::error::StatsStorageError; @@ -134,12 +133,6 @@ pub enum GatewayError { #[error("Internal error: {0}")] InternalError(String), - #[error("Failed to bind listener to {address}: {source}")] - ListenerBindFailure { - address: String, - source: Box, - }, - #[error("Failed to parse ip address: {source}")] IpAddrParseError { #[from] @@ -154,9 +147,6 @@ pub enum GatewayError { #[error(transparent)] WireguardFailure(#[from] GatewayWireguardError), - - #[error(transparent)] - LpFailure(#[from] LpHandlerError), } impl From for GatewayError { diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 30ddf19475..eacd7cf658 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -18,7 +18,6 @@ use nym_credential_verification::upgrade_mode::{ }; use nym_crypto::asymmetric::ed25519; use nym_ip_packet_router::IpPacketRouter; -use nym_lp::peer::{DHKeyPair, KEMKeys, LpLocalPeer}; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::NRServiceProviderBuilder; @@ -33,14 +32,12 @@ use rand::thread_rng; use std::net::IpAddr; use std::path::PathBuf; use std::sync::Arc; -use tokio::sync::Semaphore; use tracing::*; use zeroize::Zeroizing; pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher; use crate::node::wireguard::{PeerManager, PeerRegistrator}; pub use client_handling::active_clients::ActiveClientsStore; -pub use lp_listener::LpConfig; pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender; pub use nym_gateway_stats_storage::PersistentStatsStorage; pub use nym_gateway_storage::{ @@ -48,12 +45,10 @@ pub use nym_gateway_storage::{ traits::{BandwidthGatewayStorage, InboxGatewayStorage}, GatewayStorage, }; -use nym_lp::LpSession; pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; pub(crate) mod client_handling; pub(crate) mod internal_service_providers; -pub mod lp_listener; mod stale_data_cleaner; pub mod upgrade_mode; pub mod wireguard; @@ -96,12 +91,6 @@ pub struct GatewayTasksBuilder { /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, - /// x25519 keypair used within KTT exchange - x25519_keypair: Arc, - - /// x25519 (for now, to be changed into MlKem) keypair used for the PSQ derivation - kem_psq_keys: KEMKeys, - storage: GatewayStorage, mix_packet_sender: MixForwardingSender, @@ -117,6 +106,8 @@ pub struct GatewayTasksBuilder { shutdown_tracker: ShutdownTracker, // populated and cached as necessary + use_mock_ecash: bool, + ecash_manager: Option>, @@ -130,8 +121,6 @@ impl GatewayTasksBuilder { pub fn new( config: Config, identity: Arc, - x25519: Arc, - kem_psq_keys: KEMKeys, storage: GatewayStorage, mix_packet_sender: MixForwardingSender, metrics_sender: MetricEventsSender, @@ -139,6 +128,7 @@ impl GatewayTasksBuilder { mnemonic: Arc>, user_agent: UserAgent, upgrade_mode_state: UpgradeModeState, + use_mock_ecash: bool, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -149,8 +139,6 @@ impl GatewayTasksBuilder { wireguard_data: None, user_agent, identity_keypair: identity, - x25519_keypair: x25519, - kem_psq_keys, storage, mix_packet_sender, metrics_sender, @@ -158,6 +146,7 @@ impl GatewayTasksBuilder { upgrade_mode_state, mnemonic, shutdown_tracker, + use_mock_ecash, ecash_manager: None, wireguard_peers: None, wireguard_networks: None, @@ -236,7 +225,7 @@ impl GatewayTasksBuilder { GatewayError, > { // Check if we should use mock ecash for testing - if self.config.lp.debug.use_mock_ecash { + if self.use_mock_ecash { warn!("Using MockEcashManager for LP testing (credentials NOT verified)"); let mock_manager = MockEcashManager::new(Box::new(self.storage.clone())); return Ok(Arc::new(mock_manager) @@ -345,36 +334,6 @@ impl GatewayTasksBuilder { )) } - pub async fn build_lp_listener( - &mut self, - peer_registrator: Option, - active_clients_store: ActiveClientsStore, - ) -> Result { - let handler_state = lp_listener::LpHandlerState { - ecash_verifier: self.ecash_manager().await?, - storage: self.storage.clone(), - local_lp_peer: LpLocalPeer::new( - LpSession::default_ciphersuite(), - self.x25519_keypair.clone(), - ) - .with_kem_keys(self.kem_psq_keys.clone()), - metrics: self.metrics.clone(), - active_clients_store, - peer_registrator, - lp_config: self.config.lp, - outbound_mix_sender: self.mix_packet_sender.clone(), - session_states: Arc::new(dashmap::DashMap::new()), - forward_semaphore: Arc::new(Semaphore::new( - self.config.lp.debug.max_concurrent_forwards, - )), - }; - - Ok(lp_listener::LpListener::new( - handler_state, - self.shutdown_tracker.clone(), - )) - } - fn build_network_requester( &mut self, topology_provider: Box, diff --git a/gateway/src/node/wireguard/new_peer_registration/lp.rs b/gateway/src/node/wireguard/new_peer_registration/lp.rs index dcc505ab94..14601e634e 100644 --- a/gateway/src/node/wireguard/new_peer_registration/lp.rs +++ b/gateway/src/node/wireguard/new_peer_registration/lp.rs @@ -1,7 +1,6 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::node::lp_listener::ReceiverIndex; use crate::node::wireguard::new_peer_registration::pending::{ PendingRegistration, PendingRegistrationData, }; @@ -58,9 +57,10 @@ impl PeerRegistrator { pub(super) async fn check_pending_lp_registration( &self, - sender: ReceiverIndex, + receiver_index: u64, ) -> Result, GatewayWireguardError> { - let Some(pending_registration) = self.pending_registrations.check_lp(sender).await else { + let Some(pending_registration) = self.pending_registrations.check_lp(receiver_index).await + else { return Ok(None); }; @@ -104,7 +104,7 @@ impl PeerRegistrator { pub(super) async fn process_fresh_initial_lp_registration( &self, - sender: ReceiverIndex, + receiver_index: u64, remote_public: PeerPublicKey, psk: Key, ) -> Result { @@ -121,7 +121,7 @@ impl PeerRegistrator { .lp .write() .await - .insert(sender, pending); + .insert(receiver_index, pending); Ok(response) } diff --git a/gateway/src/node/wireguard/new_peer_registration/mod.rs b/gateway/src/node/wireguard/new_peer_registration/mod.rs index 43dcce7bb0..5e0e503d33 100644 --- a/gateway/src/node/wireguard/new_peer_registration/mod.rs +++ b/gateway/src/node/wireguard/new_peer_registration/mod.rs @@ -11,7 +11,6 @@ //! 2. Finalisation request message is received, where credential has to be attached is verified. //! Upon successful completion, pending registration is transformed into a properly inserted peer. -use crate::node::lp_listener::ReceiverIndex; use crate::node::wireguard::new_peer_registration::pending::{ PendingRegistration, PendingRegistrations, }; @@ -225,7 +224,7 @@ impl PeerRegistrator { Ok(()) } - pub(crate) async fn on_initial_authenticator_request( + pub async fn on_initial_authenticator_request( &mut self, init_message: Box, protocol: Protocol, @@ -262,7 +261,7 @@ impl PeerRegistrator { .await } - pub(crate) async fn on_final_authenticator_request( + pub async fn on_final_authenticator_request( &mut self, final_message: Box, protocol: Protocol, @@ -307,10 +306,10 @@ impl PeerRegistrator { ) } - pub(crate) async fn on_initial_lp_request( + pub async fn on_initial_lp_request( &self, init_msg: LpDvpnRegistrationInitialRequest, - sender: ReceiverIndex, + receiver_index: u64, ) -> Result { let remote_public = init_msg.wg_public_key; let psk = Key::new(init_msg.psk); @@ -318,7 +317,9 @@ impl PeerRegistrator { // 1. check if there's any pending registration already in progress, // if so, return the same data again without additional processing, // but update stored PSK - if let Some(pending_registration) = self.check_pending_lp_registration(sender).await? { + if let Some(pending_registration) = + self.check_pending_lp_registration(receiver_index).await? + { self.update_peer_psk(remote_public, psk).await?; return Ok(pending_registration); } @@ -332,19 +333,19 @@ impl PeerRegistrator { } // 3. process fresh registration request - self.process_fresh_initial_lp_registration(sender, remote_public, psk) + self.process_fresh_initial_lp_registration(receiver_index, remote_public, psk) .await } - pub(crate) async fn on_final_lp_request( + pub async fn on_final_lp_request( &self, final_msg: LpDvpnRegistrationFinalisation, - sender: ReceiverIndex, + receiver_index: u64, ) -> Result { // 1. check if there's any pending registration associated with this peer let pending_data = self .pending_registrations - .check_lp(sender) + .check_lp(receiver_index) .await .ok_or(GatewayWireguardError::RegistrationNotInProgress)? .clone(); @@ -356,7 +357,7 @@ impl PeerRegistrator { .await?; // 3 remove pending registration - self.pending_registrations.remove_lp(sender).await; + self.pending_registrations.remove_lp(receiver_index).await; // 4. construct and return the response Ok(pending_data.to_registered_lp_response(self.upgrade_mode_enabled())) diff --git a/gateway/src/node/wireguard/new_peer_registration/pending.rs b/gateway/src/node/wireguard/new_peer_registration/pending.rs index cb933514db..6a4936e4f1 100644 --- a/gateway/src/node/wireguard/new_peer_registration/pending.rs +++ b/gateway/src/node/wireguard/new_peer_registration/pending.rs @@ -1,7 +1,6 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::node::lp_listener::ReceiverIndex; use crate::node::wireguard::new_peer_registration::helpers::{ build_final_authenticator_response, build_pending_authenticator_response, }; @@ -116,9 +115,8 @@ pub(crate) struct PendingRegistrations { pub(crate) authenticator: Arc>>, /// Registrations in progress received from the LP Listener via the - /// [`crate::node::lp_listener::handler::LpConnectionHandler`] and handle through - /// [`crate::node::lp_listener::registration::LpHandlerState`] - pub(crate) lp: Arc>>, + /// `LpConnectionHandler` and handle through `LpHandlerState` + pub(crate) lp: Arc>>, } impl PendingRegistrations { @@ -133,14 +131,11 @@ impl PendingRegistrations { self.authenticator.write().await.remove(peer); } - pub(crate) async fn remove_lp(&self, receiver_index: ReceiverIndex) { + pub(crate) async fn remove_lp(&self, receiver_index: u64) { self.lp.write().await.remove(&receiver_index); } - pub(crate) async fn check_lp( - &self, - receiver_index: ReceiverIndex, - ) -> Option { + pub(crate) async fn check_lp(&self, receiver_index: u64) -> Option { self.lp.read().await.get(&receiver_index).cloned() } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index a914128f4d..768708552c 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -23,7 +23,8 @@ nym-credentials-interface = { path = "../common/credentials-interface" } nym-test-utils = { path = "../common/test-utils" } nym-registration-client = { path = "../nym-registration-client" } nym-lp-transport = { path = "../common/nym-lp-transport", features = ["io-mocks"] } -nym-gateway = { path = "../gateway" } +nym-node = { path = "../nym-node" } +nym-lp = { path = "../common/nym-lp" } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } tracing = { workspace = true } futures = { workspace = true } diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index aad5b128e4..444104f538 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -9,18 +9,18 @@ mod tests { use nym_credential_verification::upgrade_mode::testing::mock_dummy_upgrade_mode_details; use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::{ed25519, x25519}; - use nym_gateway::node::lp_listener::error::LpHandlerError; - use nym_gateway::node::lp_listener::handler::LpConnectionHandler; - use nym_gateway::node::lp_listener::{ - KEMKeys, LpDebug, LpHandlerState, LpLocalPeer, MixForwardingReceiver, PeerControlRequest, - WireguardGatewayData, mix_forwarding_channels, - }; - use nym_gateway::node::wireguard::{PeerManager, PeerRegistrator}; - use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig}; use nym_kkt::key_utils::{ generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, }; + use nym_kkt::keys::KEMKeys; use nym_kkt_ciphersuite::Ciphersuite; + use nym_lp::peer::LpLocalPeer; + use nym_node::config::{LpConfig, LpDebug}; + use nym_node::node::GatewayStorage; + use nym_node::node::lp::error::LpHandlerError; + use nym_node::node::lp::handler::LpConnectionHandler; + use nym_node::node::lp::{LpHandlerState, MixForwardingReceiver, mix_forwarding_channels}; + use nym_node::wireguard::{PeerManager, PeerRegistrator}; use nym_registration_client::{LpClientError, LpRegistrationClient}; use nym_test_utils::helpers::{CryptoRng09, seeded_rng}; use nym_test_utils::mocks::async_read_write::MockIOStream; @@ -30,7 +30,7 @@ mod tests { Key, KeyWrapper, MockPeerController, MockPeerControllerState, PeerControlRequestType, RegisteredResponse, mock_peer_controller, }; - use nym_wireguard::{IpPool, WireguardConfig}; + use nym_wireguard::{IpPool, PeerControlRequest, WireguardConfig, WireguardGatewayData}; use std::mem; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; @@ -232,19 +232,10 @@ mod tests { ); let lp_state = LpHandlerState { - // use mock instance of ecash verifier - ecash_verifier, - - // use in-memory database (no need for persistency) - storage, - local_lp_peer: base.peer.clone(), metrics: Default::default(), - // no clients at the beginning - active_clients_store: ActiveClientsStore::new(), - // use default lp config (with enabled flag) lp_config, @@ -570,7 +561,7 @@ mod tests { #[tokio::test] async fn test_basic_lp_exit_registration() -> anyhow::Result<()> { - nym_test_utils::helpers::setup_test_logger(); + // nym_test_utils::helpers::setup_test_logger(); // TODO: update the test once mceliece works let kem = KEM::MlKem768; diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 3fc765e1fa..574d06eee3 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -13,6 +13,8 @@ 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 } @@ -27,6 +29,7 @@ colored = { workspace = true } console-subscriber = { workspace = true, optional = true } csv = { workspace = true } clap = { workspace = true, features = ["cargo", "env"] } +dashmap = { workspace = true } futures = { workspace = true } hex = { workspace = true } humantime-serde = { workspace = true } @@ -113,6 +116,12 @@ nym-gateway = { path = "../gateway" } 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-transport = { path = "../common/nym-lp-transport" } +nym-registration-common = { path = "../common/registration" } +bincode = { workspace = true } + # throughput tester to recreate lioness # we don't care about particular versions - just pull whatever is used by sphinx diff --git a/nym-node/src/cli/commands/run/args.rs b/nym-node/src/cli/commands/run/args.rs index 8c214f5e5e..2fbc4d3623 100644 --- a/nym-node/src/cli/commands/run/args.rs +++ b/nym-node/src/cli/commands/run/args.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::helpers::{ - ConfigArgs, EntryGatewayArgs, ExitGatewayArgs, HostArgs, HttpArgs, MetricsArgs, MixnetArgs, - VerlocArgs, WireguardArgs, + ConfigArgs, EntryGatewayArgs, ExitGatewayArgs, HostArgs, HttpArgs, LpArgs, MetricsArgs, + MixnetArgs, VerlocArgs, WireguardArgs, }; use crate::config::persistence::NymNodePaths; use crate::config::{Config, ConfigBuilder, NodeMode, NodeModes}; @@ -125,6 +125,9 @@ pub(crate) struct Args { #[clap(flatten)] exit_gateway: ExitGatewayArgs, + + #[clap(flatten)] + lp: LpArgs, } impl Args { @@ -174,6 +177,7 @@ impl Args { .with_metrics(self.metrics.build_config_section()) .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)?) .with_service_providers(self.exit_gateway.build_config_section(&data_dir)) + .with_lp(self.lp.build_config_section()) .build() } @@ -193,6 +197,7 @@ impl Args { config.service_providers = self .exit_gateway .override_config_section(config.service_providers); + config.lp = self.lp.override_config_section(config.lp); config } } diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 00193698bf..147a5a0129 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -458,15 +458,6 @@ pub(crate) struct EntryGatewayArgs { )] #[zeroize(skip)] pub(crate) upgrade_mode_attester_public_key: Option, - - /// Use mock ecash manager for LP testing. - /// WARNING: Only use this for local testing! Never enable in production. - /// When enabled, the LP listener will accept any credential without blockchain verification. - #[clap( - long, - env = NYMNODE_LP_USE_MOCK_ECASH_ARG - )] - pub(crate) lp_use_mock_ecash: Option, } impl EntryGatewayArgs { @@ -500,9 +491,6 @@ impl EntryGatewayArgs { if let Some(upgrade_mode_attester_public_key) = self.upgrade_mode_attester_public_key { section.upgrade_mode.attester_public_key = upgrade_mode_attester_public_key } - if let Some(use_mock_ecash) = self.lp_use_mock_ecash { - section.lp.debug.use_mock_ecash = use_mock_ecash - } section } @@ -549,3 +537,62 @@ impl ExitGatewayArgs { section } } + +#[derive(clap::Args, Debug)] +pub(crate) struct LpArgs { + /// Bind address for the TCP LP control traffic. + /// default: `[::]:41264` + #[clap( + long, + env = NYMNODE_LP_CONTROL_BIND_ADDRESS_ARG + )] + pub(crate) lp_control_bind_address: Option, + + /// Custom announced port for listening for the TCP LP control traffic. + /// If unspecified, the value from the `lp_control_bind_address` will be used instead + #[clap( + long, + env = NYMNODE_LP_CONTROL_ANNOUNCE_PORT_ARG + )] + pub(crate) lp_control_announce_port: Option, + + /// Bind address for the UDP LP data traffic. + /// default: `[::]:51264` + #[clap( + long, + env = NYMNODE_LP_DATA_BIND_ADDRESS_ARG + )] + pub(crate) lp_data_bind_address: Option, + + /// Custom announced port for listening for the UDP LP data traffic. + /// If unspecified, the value from the `lp_data_bind_address` will be used instead + #[clap( + long, + env = NYMNODE_LP_DATA_ANNOUNCE_PORT_ARG + )] + pub(crate) lp_data_announce_port: Option, + + /// Use mock ecash manager for LP testing. + /// WARNING: Only use this for local testing! Never enable in production. + /// When enabled, the LP listener will accept any credential without blockchain verification. + #[clap( + long, + env = NYMNODE_LP_USE_MOCK_ECASH_ARG + )] + pub(crate) lp_use_mock_ecash: Option, +} + +impl LpArgs { + // TODO: could we perhaps make a clap error here and call `safe_exit` instead? + pub(crate) fn build_config_section(self) -> config::LpConfig { + self.override_config_section(config::LpConfig::default()) + } + + pub(crate) fn override_config_section(self, mut section: config::LpConfig) -> config::LpConfig { + if let Some(use_mock_ecash) = self.lp_use_mock_ecash { + section.debug.use_mock_ecash = use_mock_ecash + } + + section + } +} diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 99577f82ca..ed6335fed8 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -46,9 +46,6 @@ pub struct GatewayTasksConfig { pub upgrade_mode: UpgradeModeWatcher, - #[serde(default)] - pub lp: nym_gateway::node::LpConfig, - #[serde(default)] pub debug: Debug, } @@ -228,7 +225,6 @@ impl GatewayTasksConfig { announce_ws_port: None, announce_wss_port: None, upgrade_mode: UpgradeModeWatcher::new()?, - lp: Default::default(), debug: Default::default(), }) } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 20aac7cf0c..7c2e96215b 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -27,7 +27,6 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { enabled: config.service_providers.network_requester.debug.enabled, }, config.gateway_tasks.upgrade_mode.clone(), - config.gateway_tasks.lp, nym_gateway::config::Debug { client_bandwidth_max_flushing_rate: config .gateway_tasks @@ -92,8 +91,6 @@ pub struct GatewayTasksConfig { pub auth_opts: Option, #[allow(dead_code)] pub wg_opts: LocalWireguardOpts, - #[allow(dead_code)] - pub lp: nym_gateway::node::LpConfig, } // that function is rather disgusting, but I hope it's not going to live for too long @@ -227,7 +224,6 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { ipr_opts: Some(ipr_opts), auth_opts: Some(auth_opts), wg_opts, - lp: config.gateway_tasks.lp, } } diff --git a/nym-node/src/config/lp.rs b/nym-node/src/config/lp.rs new file mode 100644 index 0000000000..260fea64b1 --- /dev/null +++ b/nym-node/src/config/lp.rs @@ -0,0 +1,153 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_config::serde_helpers::de_maybe_port; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::time::Duration; + +/// Configuration for LP listener +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct LpConfig { + /// Bind address for the TCP LP control traffic. + /// default: `[::]:41264` + pub control_bind_address: SocketAddr, + + /// Bind address for the UDP LP data traffic. + /// default: `[::]:51264` + pub data_bind_address: SocketAddr, + + /// Custom announced port for listening for the TCP LP control traffic. + /// If unspecified, the value from the `control_bind_address` will be used instead + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_control_port: Option, + + /// Custom announced port for listening for the UDP LP data traffic. + /// If unspecified, the value from the `data_bind_address` will be used instead + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_data_port: Option, + + /// Auxiliary configuration + #[serde(default)] + pub debug: LpDebug, +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct LpDebug { + /// Maximum concurrent connections + pub max_connections: usize, + + /// Use mock ecash manager for testing (default: false) + /// + /// When enabled, the LP listener will use a mock ecash verifier that + /// accepts any credential without blockchain verification. This is + /// useful for testing the LP protocol implementation without requiring + /// a full blockchain/contract setup. + /// + /// WARNING: Only use this for local testing! Never enable in production. + pub use_mock_ecash: bool, + + /// Maximum age of in-progress handshakes before cleanup (default: 90s) + /// + /// Handshakes should complete quickly (3-5 packets). This TTL accounts for: + /// - Network latency and retransmits + /// - Slow clients + /// - Clock skew tolerance + /// + /// Stale handshakes are removed by the cleanup task to prevent memory leaks. + #[serde(with = "humantime_serde")] + pub handshake_ttl: Duration, + + /// Maximum age of established sessions before cleanup (default: 24h) + /// + /// Sessions can be long-lived for dVPN tunnels. This TTL should be set + /// high enough to accommodate expected usage patterns: + /// - dVPN sessions: hours to days + /// - Registration: minutes + /// + /// Sessions with no activity for this duration are removed by the cleanup task. + #[serde(with = "humantime_serde")] + pub session_ttl: Duration, + + /// How often to run the state cleanup task (default: 5 minutes) + /// + /// The cleanup task scans for and removes stale handshakes and sessions. + /// Lower values = more frequent cleanup but higher overhead. + /// Higher values = less overhead but slower memory reclamation. + #[serde(with = "humantime_serde")] + pub state_cleanup_interval: Duration, + + /// Maximum concurrent forward connections (default: 1000) + /// + /// Limits simultaneous outbound connections when forwarding LP packets to other gateways + /// during telescope setup. This prevents file descriptor exhaustion under high load. + /// + /// When at capacity, new forward requests return an error, signaling the client + /// to choose a different gateway. + pub max_concurrent_forwards: usize, +} + +impl LpConfig { + pub const DEFAULT_CONTROL_PORT: u16 = 41264; + pub const DEFAULT_DATA_PORT: u16 = 51264; + + pub fn announced_control_port(&self) -> u16 { + self.announce_control_port + .unwrap_or(self.control_bind_address.port()) + } + + pub fn announced_data_port(&self) -> u16 { + self.announce_data_port + .unwrap_or(self.data_bind_address.port()) + } +} + +impl Default for LpConfig { + fn default() -> Self { + LpConfig { + control_bind_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + Self::DEFAULT_CONTROL_PORT, + ), + data_bind_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + Self::DEFAULT_DATA_PORT, + ), + announce_control_port: None, + announce_data_port: None, + debug: Default::default(), + } + } +} + +impl LpDebug { + pub const DEFAULT_MAX_CONNECTIONS: usize = 10000; + + // 90 seconds - handshakes should complete quickly + pub const DEFAULT_HANDSHAKE_TTL: Duration = Duration::from_secs(90); + + // 24 hours - for long-lived dVPN sessions + pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(86400); + + // 5 minutes - balances memory reclamation with task overhead + pub const DEFAULT_STATE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300); + + // Limits concurrent outbound connections to prevent fd exhaustion + pub const DEFAULT_MAX_CONCURRENT_FORWARDS: usize = 1000; +} + +impl Default for LpDebug { + fn default() -> Self { + LpDebug { + max_connections: Self::DEFAULT_MAX_CONNECTIONS, + use_mock_ecash: false, + handshake_ttl: Self::DEFAULT_HANDSHAKE_TTL, + session_ttl: Self::DEFAULT_SESSION_TTL, + state_cleanup_interval: Self::DEFAULT_STATE_CLEANUP_INTERVAL, + max_concurrent_forwards: Self::DEFAULT_MAX_CONCURRENT_FORWARDS, + } + } +} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 064efc8b6a..14b00b7f5e 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -4,6 +4,7 @@ use crate::config::persistence::{NymNodePaths, ReplayProtectionPaths}; use crate::config::template::CONFIG_TEMPLATE; use crate::error::NymNodeError; +use crate::node::replay_protection::{bitmap_size, items_in_bloomfilter}; use celes::Country; use clap::ValueEnum; use human_repr::HumanCount; @@ -35,6 +36,7 @@ use url::Url; pub mod authenticator; pub mod gateway_tasks; pub mod helpers; +pub mod lp; pub mod metrics; mod old_configs; pub mod persistence; @@ -43,9 +45,9 @@ mod template; pub mod upgrade_helpers; pub use crate::config::gateway_tasks::GatewayTasksConfig; +pub use crate::config::lp::{LpConfig, LpDebug}; pub use crate::config::metrics::MetricsConfig; pub use crate::config::service_providers::ServiceProvidersConfig; -use crate::node::replay_protection::{bitmap_size, items_in_bloomfilter}; const DEFAULT_NYMNODES_DIR: &str = "nym-nodes"; @@ -186,6 +188,8 @@ pub struct ConfigBuilder { pub metrics: Option, + pub lp: Option, + pub logging: Option, } @@ -205,6 +209,7 @@ impl ConfigBuilder { gateway_tasks: None, service_providers: None, metrics: None, + lp: None, logging: None, } } @@ -234,6 +239,11 @@ impl ConfigBuilder { self } + pub fn with_lp(mut self, section: impl Into>) -> Self { + self.lp = section.into(); + self + } + pub fn with_wireguard(mut self, section: impl Into>) -> Self { self.wireguard = section.into(); self @@ -284,6 +294,7 @@ impl ConfigBuilder { .storage_paths .unwrap_or_else(|| NymNodePaths::new(&self.data_dir)), metrics: self.metrics.unwrap_or_default(), + lp: self.lp.unwrap_or_default(), gateway_tasks, service_providers: self .service_providers @@ -323,6 +334,9 @@ pub struct Config { pub wireguard: Wireguard, + #[serde(default)] + pub lp: LpConfig, + #[serde(alias = "entry_gateway")] pub gateway_tasks: GatewayTasksConfig, diff --git a/nym-node/src/config/old_configs/old_config_v12.rs b/nym-node/src/config/old_configs/old_config_v12.rs index 9f7352eac8..b44d870f5e 100644 --- a/nym-node/src/config/old_configs/old_config_v12.rs +++ b/nym-node/src/config/old_configs/old_config_v12.rs @@ -34,8 +34,6 @@ use nym_config::read_config_from_toml_file; use nym_config::serde_helpers::de_maybe_port; use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; -use nym_gateway::node::LpConfig; -use nym_gateway::node::lp_listener::LpDebug; use nym_kkt::key_utils::{ generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, }; @@ -48,6 +46,7 @@ use std::time::Duration; use tracing::{debug, info, instrument}; use url::Url; +use crate::config::lp::{LpConfig, LpDebug}; pub use unchanged_v12_types::*; // (while some of those are technically unused, they might be needed in future migrations, @@ -720,6 +719,20 @@ pub async fn try_upgrade_config_v12>( .public_diffie_hellman_key_file, }, }, + lp: LpConfig { + control_bind_address: old_cfg.gateway_tasks.lp.control_bind_address, + data_bind_address: old_cfg.gateway_tasks.lp.data_bind_address, + announce_control_port: old_cfg.gateway_tasks.lp.announce_control_port, + announce_data_port: old_cfg.gateway_tasks.lp.announce_data_port, + debug: LpDebug { + max_connections: old_cfg.gateway_tasks.lp.debug.max_connections, + use_mock_ecash: old_cfg.gateway_tasks.lp.debug.use_mock_ecash, + handshake_ttl: old_cfg.gateway_tasks.lp.debug.handshake_ttl, + session_ttl: old_cfg.gateway_tasks.lp.debug.session_ttl, + state_cleanup_interval: old_cfg.gateway_tasks.lp.debug.state_cleanup_interval, + max_concurrent_forwards: old_cfg.gateway_tasks.lp.debug.max_concurrent_forwards, + }, + }, gateway_tasks: GatewayTasksConfig { storage_paths: GatewayTasksPaths { clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, @@ -748,20 +761,6 @@ pub async fn try_upgrade_config_v12>( .expedited_poll_interval, }, }, - lp: LpConfig { - control_bind_address: old_cfg.gateway_tasks.lp.control_bind_address, - data_bind_address: old_cfg.gateway_tasks.lp.data_bind_address, - announce_control_port: old_cfg.gateway_tasks.lp.announce_control_port, - announce_data_port: old_cfg.gateway_tasks.lp.announce_data_port, - debug: LpDebug { - max_connections: old_cfg.gateway_tasks.lp.debug.max_connections, - use_mock_ecash: old_cfg.gateway_tasks.lp.debug.use_mock_ecash, - handshake_ttl: old_cfg.gateway_tasks.lp.debug.handshake_ttl, - session_ttl: old_cfg.gateway_tasks.lp.debug.session_ttl, - state_cleanup_interval: old_cfg.gateway_tasks.lp.debug.state_cleanup_interval, - max_concurrent_forwards: old_cfg.gateway_tasks.lp.debug.max_concurrent_forwards, - }, - }, debug: gateway_tasks::Debug { message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 318f96999f..01f8a7113d 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -66,9 +66,15 @@ pub mod vars { "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; pub const NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG: &str = "NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY"; - pub const NYMNODE_LP_USE_MOCK_ECASH_ARG: &str = "NYMNODE_LP_USE_MOCK_ECASH"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; pub const NYMNODE_OPEN_PROXY_ARG: &str = "NYMNODE_OPEN_PROXY"; + + // LP: + pub const NYMNODE_LP_CONTROL_BIND_ADDRESS_ARG: &str = "NYMNODE_LP_CONTROL_BIND_ADDRESS"; + pub const NYMNODE_LP_CONTROL_ANNOUNCE_PORT_ARG: &str = "NYMNODE_LP_CONTROL_ANNOUNCE_PORT"; + pub const NYMNODE_LP_DATA_BIND_ADDRESS_ARG: &str = "NYMNODE_LP_DATA_BIND_ADDRESS"; + pub const NYMNODE_LP_DATA_ANNOUNCE_PORT_ARG: &str = "NYMNODE_LP_DATA_ANNOUNCE_PORT"; + pub const NYMNODE_LP_USE_MOCK_ECASH_ARG: &str = "NYMNODE_LP_USE_MOCK_ECASH"; } diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index ca6f0c7c65..f1328c61c9 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::error::NymNodeHttpError; +use crate::node::lp::error::LpHandlerError; use crate::wireguard::error::WireguardError; use nym_http_api_client::HttpClientError; use nym_ip_packet_router::error::ClientCoreError; @@ -9,7 +10,7 @@ use nym_kkt::keys::storage_wrappers::MalformedStoredKeyError; use nym_validator_client::ValidatorClientError; use nym_validator_client::nyxd::error::NyxdError; use std::io; -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use thiserror::Error; @@ -235,6 +236,15 @@ pub enum NymNodeError { #[error(transparent)] MalformedStoredKey(#[from] MalformedStoredKeyError), + + #[error("failed to bind LP to {address}: {source}")] + LpBindFailure { + address: SocketAddr, + source: io::Error, + }, + + #[error(transparent)] + LpFailure(#[from] LpHandlerError), } impl From for NymNodeError { diff --git a/nym-node/src/lib.rs b/nym-node/src/lib.rs new file mode 100644 index 0000000000..9882b20cb7 --- /dev/null +++ b/nym-node/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// allow dead code in the lib imports +#![allow(dead_code)] + +pub mod config; +pub mod error; +pub mod node; +pub mod wireguard; diff --git a/nym-node/src/main.rs b/nym-node/src/main.rs index 8c7a8aa2e2..3b83a1ee96 100644 --- a/nym-node/src/main.rs +++ b/nym-node/src/main.rs @@ -10,13 +10,13 @@ use nym_bin_common::logging::maybe_print_banner; use nym_config::defaults::setup_env; mod cli; -pub(crate) mod config; +pub mod config; mod env; -pub(crate) mod error; +pub mod error; mod logging; -pub(crate) mod node; +pub mod node; pub(crate) mod throughput_tester; -pub(crate) mod wireguard; +pub mod wireguard; fn main() -> anyhow::Result<()> { // std::env::set_var( diff --git a/gateway/src/node/lp_listener/data_handler.rs b/nym-node/src/node/lp/data_handler.rs similarity index 96% rename from gateway/src/node/lp_listener/data_handler.rs rename to nym-node/src/node/lp/data_handler.rs index 7436027d8c..620113f1a9 100644 --- a/gateway/src/node/lp_listener/data_handler.rs +++ b/nym-node/src/node/lp/data_handler.rs @@ -16,8 +16,8 @@ //! use super::LpHandlerState; -use crate::node::lp_listener::error::LpHandlerError; -use crate::GatewayError; +use crate::error::NymNodeError; +use crate::node::lp::error::LpHandlerError; use nym_lp::OuterHeader; use nym_metrics::inc; use std::net::SocketAddr; @@ -48,12 +48,12 @@ impl LpDataHandler { bind_addr: SocketAddr, state: LpHandlerState, shutdown: nym_task::ShutdownToken, - ) -> Result { - let socket = UdpSocket::bind(bind_addr).await.map_err(|e| { - error!("Failed to bind LP data socket to {bind_addr}: {e}"); - GatewayError::ListenerBindFailure { - address: bind_addr.to_string(), - source: Box::new(e), + ) -> Result { + let socket = UdpSocket::bind(bind_addr).await.map_err(|source| { + error!("Failed to bind LP data socket to {bind_addr}: {source}"); + NymNodeError::LpBindFailure { + address: bind_addr, + source, } })?; diff --git a/gateway/src/node/lp_listener/error.rs b/nym-node/src/node/lp/error.rs similarity index 98% rename from gateway/src/node/lp_listener/error.rs rename to nym-node/src/node/lp/error.rs index e98fbcf916..d8e5c38e2d 100644 --- a/gateway/src/node/lp_listener/error.rs +++ b/nym-node/src/node/lp/error.rs @@ -1,7 +1,7 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::node::lp_listener::ReceiverIndex; +use crate::node::lp::ReceiverIndex; use nym_lp::state_machine::{LpAction, LpDataKind}; use nym_lp::{LpError, MalformedLpPacketError}; use nym_lp_transport::LpTransportError; diff --git a/gateway/src/node/lp_listener/handler.rs b/nym-node/src/node/lp/handler.rs similarity index 96% rename from gateway/src/node/lp_listener/handler.rs rename to nym-node/src/node/lp/handler.rs index c5b0d523b9..3046e81c5b 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/nym-node/src/node/lp/handler.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only use super::{LpHandlerState, ReceiverIndex, TimestampedState}; -use crate::node::lp_listener::error::LpHandlerError; +use crate::node::lp::error::LpHandlerError; use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput}; use nym_lp::{ EncryptedLpPacket, ExpectedResponseSize, ForwardPacketData, LpSession, LpStateMachine, }; -use nym_lp_transport::traits::LpTransportChannel; use nym_lp_transport::LpHandshakeChannel; +use nym_lp_transport::traits::LpTransportChannel; use nym_metrics::{add_histogram_obs, inc}; use nym_registration_common::{LpRegistrationRequest, RegistrationStatus}; use std::net::SocketAddr; @@ -305,7 +305,8 @@ where debug!( "LP registration request from {remote} (receiver_idx={receiver_idx}): mode={:?}", - request.mode()); + request.mode() + ); self.handle_registration_request(receiver_idx, request) .await @@ -318,7 +319,10 @@ where } typ @ LpDataKind::Opaque => { // Neither registration nor forwarding - unknown payload type - warn!("Unknown transport payload type from {remote} (receiver_idx={receiver_idx}). dropping {} bytes", bytes.len()); + warn!( + "Unknown transport payload type from {remote} (receiver_idx={receiver_idx}). dropping {} bytes", + bytes.len() + ); inc!("lp_errors_unknown_payload_type"); Err(LpHandlerError::UnexpectedLpPayload { typ }) } @@ -420,6 +424,7 @@ where } /// Returns reference to the established forwarding channel to the exit. + #[allow(dead_code)] pub fn forwarding_channel(&self) -> &Option<(S, SocketAddr)> { &self.exit_stream } @@ -658,10 +663,10 @@ where #[cfg(test)] mod tests { use super::*; - use crate::node::lp_listener::{LpConfig, LpDebug}; - use crate::node::ActiveClientsStore; - use nym_lp::peer::{generate_keypair_mceliece, generate_keypair_mlkem, KEMKeys, LpLocalPeer}; - use nym_lp::{sessions_for_tests, Ciphersuite, SessionManager}; + use crate::config::lp::LpDebug; + use crate::node::lp::LpConfig; + use nym_lp::peer::{KEMKeys, LpLocalPeer, generate_keypair_mceliece, generate_keypair_mlkem}; + use nym_lp::{Ciphersuite, SessionManager, sessions_for_tests}; use nym_test_utils::helpers::{deterministic_rng, deterministic_rng_09}; use std::sync::Arc; // ==================== Test Helpers ==================== @@ -673,15 +678,6 @@ mod tests { let mut rng = deterministic_rng(); let mut rng09 = deterministic_rng_09(); - // Create in-memory storage for testing - let storage = nym_gateway_storage::GatewayStorage::init(":memory:", 100) - .await - .expect("Failed to create test storage"); - - // Create mock ecash manager for testing - let ecash_verifier = - nym_credential_verification::ecash::MockEcashManager::new(Box::new(storage.clone())); - let lp_config = LpConfig { debug: LpDebug { ..Default::default() @@ -706,12 +702,8 @@ mod tests { LpHandlerState { lp_config, - ecash_verifier: Arc::new(ecash_verifier) - as Arc, - storage, local_lp_peer: lp_peer, metrics: nym_node_metrics::NymNodeMetrics::default(), - active_clients_store: ActiveClientsStore::new(), outbound_mix_sender: mix_sender, session_states: Arc::new(dashmap::DashMap::new()), peer_registrator: None, diff --git a/gateway/src/node/lp_listener/mod.rs b/nym-node/src/node/lp/mod.rs similarity index 72% rename from gateway/src/node/lp_listener/mod.rs rename to nym-node/src/node/lp/mod.rs index 9c348f1740..8adf6a5e52 100644 --- a/gateway/src/node/lp_listener/mod.rs +++ b/nym-node/src/node/lp/mod.rs @@ -67,28 +67,23 @@ // To view metrics, the nym-metrics registry automatically collects all metrics. // They can be exported via Prometheus format using the metrics endpoint. -use crate::error::GatewayError; -use crate::node::wireguard::PeerRegistrator; -use crate::node::ActiveClientsStore; +use crate::config::lp::LpConfig; +use crate::error::NymNodeError; use dashmap::DashMap; -use nym_config::serde_helpers::de_maybe_port; -use nym_credential_verification::ecash::traits::EcashManager; -use nym_gateway_storage::GatewayStorage; +use nym_gateway::node::wireguard::PeerRegistrator; +use nym_lp::peer::LpLocalPeer; use nym_lp::state_machine::LpStateMachine; +use nym_mixnet_client::forwarder::MixForwardingSender; use nym_node_metrics::NymNodeMetrics; use nym_task::ShutdownTracker; -use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tokio::net::TcpListener; use tokio::sync::Semaphore; use tracing::*; -pub use nym_lp::peer::{DHKeyPair, DHPublicKey, KEMKeys, LpLocalPeer}; -pub use nym_mixnet_client::forwarder::{ - mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, -}; -pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData}; +pub use nym_mixnet_client::forwarder::{MixForwardingReceiver, mix_forwarding_channels}; mod data_handler; pub mod error; @@ -97,153 +92,6 @@ mod registration; pub type ReceiverIndex = u64; -/// Configuration for LP listener -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub struct LpConfig { - /// Bind address for the TCP LP control traffic. - /// default: `[::]:41264` - pub control_bind_address: SocketAddr, - - /// Bind address for the UDP LP data traffic. - /// default: `[::]:51264` - pub data_bind_address: SocketAddr, - - /// Custom announced port for listening for the TCP LP control traffic. - /// If unspecified, the value from the `control_bind_address` will be used instead - /// (default: None) - #[serde(deserialize_with = "de_maybe_port")] - pub announce_control_port: Option, - - /// Custom announced port for listening for the UDP LP data traffic. - /// If unspecified, the value from the `data_bind_address` will be used instead - /// (default: None) - #[serde(deserialize_with = "de_maybe_port")] - pub announce_data_port: Option, - - /// Auxiliary configuration - #[serde(default)] - pub debug: LpDebug, -} - -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub struct LpDebug { - /// Maximum concurrent connections - pub max_connections: usize, - - /// Use mock ecash manager for testing (default: false) - /// - /// When enabled, the LP listener will use a mock ecash verifier that - /// accepts any credential without blockchain verification. This is - /// useful for testing the LP protocol implementation without requiring - /// a full blockchain/contract setup. - /// - /// WARNING: Only use this for local testing! Never enable in production. - pub use_mock_ecash: bool, - - /// Maximum age of in-progress handshakes before cleanup (default: 90s) - /// - /// Handshakes should complete quickly (3-5 packets). This TTL accounts for: - /// - Network latency and retransmits - /// - Slow clients - /// - Clock skew tolerance - /// - /// Stale handshakes are removed by the cleanup task to prevent memory leaks. - #[serde(with = "humantime_serde")] - pub handshake_ttl: Duration, - - /// Maximum age of established sessions before cleanup (default: 24h) - /// - /// Sessions can be long-lived for dVPN tunnels. This TTL should be set - /// high enough to accommodate expected usage patterns: - /// - dVPN sessions: hours to days - /// - Registration: minutes - /// - /// Sessions with no activity for this duration are removed by the cleanup task. - #[serde(with = "humantime_serde")] - pub session_ttl: Duration, - - /// How often to run the state cleanup task (default: 5 minutes) - /// - /// The cleanup task scans for and removes stale handshakes and sessions. - /// Lower values = more frequent cleanup but higher overhead. - /// Higher values = less overhead but slower memory reclamation. - #[serde(with = "humantime_serde")] - pub state_cleanup_interval: Duration, - - /// Maximum concurrent forward connections (default: 1000) - /// - /// Limits simultaneous outbound connections when forwarding LP packets to other gateways - /// during telescope setup. This prevents file descriptor exhaustion under high load. - /// - /// When at capacity, new forward requests return an error, signaling the client - /// to choose a different gateway. - pub max_concurrent_forwards: usize, -} - -impl LpConfig { - pub const DEFAULT_CONTROL_PORT: u16 = 41264; - pub const DEFAULT_DATA_PORT: u16 = 51264; - - pub fn announced_control_port(&self) -> u16 { - self.announce_control_port - .unwrap_or(self.control_bind_address.port()) - } - - pub fn announced_data_port(&self) -> u16 { - self.announce_data_port - .unwrap_or(self.data_bind_address.port()) - } -} - -impl Default for LpConfig { - fn default() -> Self { - LpConfig { - control_bind_address: SocketAddr::new( - IpAddr::V6(Ipv6Addr::UNSPECIFIED), - Self::DEFAULT_CONTROL_PORT, - ), - data_bind_address: SocketAddr::new( - IpAddr::V6(Ipv6Addr::UNSPECIFIED), - Self::DEFAULT_DATA_PORT, - ), - announce_control_port: None, - announce_data_port: None, - debug: Default::default(), - } - } -} - -impl LpDebug { - pub const DEFAULT_MAX_CONNECTIONS: usize = 10000; - - // 90 seconds - handshakes should complete quickly - pub const DEFAULT_HANDSHAKE_TTL: Duration = Duration::from_secs(90); - - // 24 hours - for long-lived dVPN sessions - pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(86400); - - // 5 minutes - balances memory reclamation with task overhead - pub const DEFAULT_STATE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300); - - // Limits concurrent outbound connections to prevent fd exhaustion - pub const DEFAULT_MAX_CONCURRENT_FORWARDS: usize = 1000; -} - -impl Default for LpDebug { - fn default() -> Self { - LpDebug { - max_connections: Self::DEFAULT_MAX_CONNECTIONS, - use_mock_ecash: false, - handshake_ttl: Self::DEFAULT_HANDSHAKE_TTL, - session_ttl: Self::DEFAULT_SESSION_TTL, - state_cleanup_interval: Self::DEFAULT_STATE_CLEANUP_INTERVAL, - max_concurrent_forwards: Self::DEFAULT_MAX_CONCURRENT_FORWARDS, - } - } -} - /// Wrapper for state entries with timestamp tracking for cleanup /// /// This wrapper adds `created_at` and `last_activity` timestamps to state entries, @@ -289,6 +137,7 @@ impl TimestampedState { } /// Get age since creation + #[allow(dead_code)] pub fn age(&self) -> Duration { self.created_at.elapsed() } @@ -309,21 +158,12 @@ impl TimestampedState { /// Shared state for LP connection handlers #[derive(Clone)] pub struct LpHandlerState { - /// Ecash verifier for bandwidth credentials - pub ecash_verifier: Arc, - - /// Storage backend for persistence - pub storage: GatewayStorage, - /// Encapsulates all required key information of a local Lewes Protocol Peer. pub local_lp_peer: LpLocalPeer, /// Metrics collection pub metrics: NymNodeMetrics, - /// Active clients tracking - pub active_clients_store: ActiveClientsStore, - /// Handle registering new wireguard peers pub peer_registrator: Option, @@ -334,6 +174,7 @@ pub struct LpHandlerState { /// /// Used by the LP data handler (UDP:51264) to forward decrypted Sphinx packets /// from LP clients into the mixnet for routing. + #[allow(dead_code)] pub outbound_mix_sender: MixForwardingSender, /// Established sessions keyed by session_id @@ -397,16 +238,18 @@ impl LpListener { self.handler_state.lp_config } - pub async fn run(&mut self) -> Result<(), GatewayError> { + pub async fn run(&mut self) -> Result<(), NymNodeError> { let control_bind_address = self.lp_config().control_bind_address; let data_bind_address = self.lp_config().data_bind_address; - let listener = TcpListener::bind(control_bind_address).await.map_err(|e| { - error!("Failed to bind LP listener to {control_bind_address}: {e}",); - GatewayError::ListenerBindFailure { - address: control_bind_address.to_string(), - source: Box::new(e), - } - })?; + let listener = TcpListener::bind(control_bind_address) + .await + .map_err(|source| { + error!("Failed to bind LP listener to {control_bind_address}: {source}",); + NymNodeError::LpBindFailure { + address: control_bind_address, + source, + } + })?; let shutdown_token = self.shutdown.clone_shutdown_token(); @@ -489,7 +332,7 @@ impl LpListener { /// The data handler listens on UDP port 51264 and processes LP-wrapped Sphinx packets /// from registered clients. It decrypts the LP layer and forwards the Sphinx packets /// into the mixnet. - async fn spawn_data_handler(&self) -> Result, GatewayError> { + async fn spawn_data_handler(&self) -> Result, NymNodeError> { // Create data handler let data_handler = data_handler::LpDataHandler::new( self.lp_config().data_bind_address, @@ -550,7 +393,8 @@ impl LpListener { } pub(crate) mod cleanup_task { - use crate::node::lp_listener::{LpDebug, ReceiverIndex, TimestampedState}; + use crate::config::lp::LpDebug; + use crate::node::lp::{ReceiverIndex, TimestampedState}; use dashmap::DashMap; use nym_lp::LpStateMachine; use nym_metrics::inc_by; diff --git a/gateway/src/node/lp_listener/registration.rs b/nym-node/src/node/lp/registration.rs similarity index 98% rename from gateway/src/node/lp_listener/registration.rs rename to nym-node/src/node/lp/registration.rs index 7f2e7b875c..294b0d47e7 100644 --- a/gateway/src/node/lp_listener/registration.rs +++ b/nym-node/src/node/lp/registration.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::lp_listener::{LpHandlerState, ReceiverIndex}; +use crate::node::lp::{LpHandlerState, ReceiverIndex}; use nym_metrics::{add_histogram_obs, inc}; use nym_registration_common::dvpn::{ LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest, diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 1acf98a908..8b7c85aeec 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -22,6 +22,7 @@ use crate::node::http::{HttpServerConfig, NymNodeHttpServer, NymNodeRouter}; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::node::key_rotation::controller::KeyRotationController; use crate::node::key_rotation::manager::SphinxKeyManager; +use crate::node::lp::{LpHandlerState, LpListener}; use crate::node::metrics::aggregator::MetricsAggregator; use crate::node::metrics::console_logger::ConsoleLogger; use crate::node::metrics::handler::client_sessions::GatewaySessionStatsHandler; @@ -43,12 +44,14 @@ use crate::node::shared_network::{ use nym_bin_common::bin_info; use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_gateway::node::lp_listener::DHKeyPair; -use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; +use nym_gateway::node::wireguard::PeerRegistrator; +use nym_gateway::node::{GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_kkt::key_utils::{ generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, }; -use nym_kkt::keys::KEMKeys; +use nym_kkt::keys::{DHKeyPair, KEMKeys}; +use nym_lp::LpSession; +use nym_lp::peer::LpLocalPeer; use nym_mixnet_client::client::ActiveConnections; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ @@ -77,15 +80,19 @@ use std::net::SocketAddr; use std::ops::Deref; use std::path::Path; use std::sync::Arc; -use tokio::sync::mpsc; +use tokio::sync::{Semaphore, mpsc}; use tracing::{debug, info, trace}; use zeroize::Zeroizing; +pub use nym_gateway::node::ActiveClientsStore; +pub use nym_gateway::node::GatewayStorage; + pub mod bonding_information; pub mod description; pub mod helpers; pub(crate) mod http; pub(crate) mod key_rotation; +pub mod lp; pub(crate) mod metrics; pub(crate) mod mixnet; mod nym_apis_client; @@ -95,7 +102,7 @@ mod shared_network; pub struct GatewayTasksData { mnemonic: Arc>, - client_storage: nym_gateway::node::GatewayStorage, + client_storage: GatewayStorage, stats_storage: nym_gateway::node::PersistentStatsStorage, } @@ -117,7 +124,7 @@ impl GatewayTasksData { } async fn new(config: &GatewayTasksConfig) -> Result { - let client_storage = nym_gateway::node::GatewayStorage::init( + let client_storage = GatewayStorage::init( &config.storage_paths.clients_storage, config.debug.message_retrieval_limit, ) @@ -378,7 +385,7 @@ impl From for nym_wireguard::WireguardData { } } -pub(crate) struct NymNode { +pub struct NymNode { config: Config, accepted_operator_terms_and_conditions: bool, shutdown_manager: ShutdownManager, @@ -402,6 +409,7 @@ pub(crate) struct NymNode { sphinx_key_manager: Option, x25519_noise_keys: Arc, + psq_kem_keys: KEMKeys, x25519_lp_keys: Arc, } @@ -478,6 +486,33 @@ impl NymNode { config.save() } + pub async fn build_lp_listener( + &mut self, + peer_registrator: Option, + mix_packet_sender: MixForwardingSender, + ) -> Result { + let handler_state = LpHandlerState { + local_lp_peer: LpLocalPeer::new( + LpSession::default_ciphersuite(), + self.x25519_lp_keys.clone(), + ) + .with_kem_keys(self.psq_kem_keys.clone()), + metrics: self.metrics.clone(), + peer_registrator, + lp_config: self.config.lp, + outbound_mix_sender: mix_packet_sender, + session_states: Arc::new(dashmap::DashMap::new()), + forward_semaphore: Arc::new(Semaphore::new( + self.config.lp.debug.max_concurrent_forwards, + )), + }; + + Ok(LpListener::new( + handler_state, + self.shutdown_manager.shutdown_tracker().clone(), + )) + } + pub(crate) async fn new(config: Config) -> Result { let wireguard_data = WireguardData::new(&config.wireguard)?; let current_rotation_id = @@ -670,15 +705,14 @@ impl NymNode { let mut gateway_tasks_builder = GatewayTasksBuilder::new( config.gateway, self.ed25519_identity_keys.clone(), - self.x25519_lp_keys.clone(), - self.psq_kem_keys.clone(), self.entry_gateway.client_storage.clone(), - mix_packet_sender, + mix_packet_sender.clone(), metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), Self::user_agent(), self.upgrade_mode_state.clone(), + self.config.lp.debug.use_mock_ecash, self.shutdown_tracker().clone(), ); @@ -736,11 +770,10 @@ impl NymNode { // Start LP listener if enabled info!( "starting the LP listener on {} (data handler on: {})", - self.config.gateway_tasks.lp.control_bind_address, - self.config.gateway_tasks.lp.data_bind_address, + self.config.lp.control_bind_address, self.config.lp.data_bind_address, ); - let mut lp_listener = gateway_tasks_builder - .build_lp_listener(wg_peer_registrator.clone(), active_clients_store.clone()) + let mut lp_listener = self + .build_lp_listener(wg_peer_registrator.clone(), mix_packet_sender) .await?; self.shutdown_tracker() .try_spawn_named(async move { lp_listener.run().await }, "LpListener"); @@ -913,8 +946,8 @@ impl NymNode { let lewes_protocol = LewesProtocol { enabled: self.modes().entry, - control_port: self.config.gateway_tasks.lp.announced_control_port(), - data_port: self.config.gateway_tasks.lp.announced_data_port(), + control_port: self.config.lp.announced_control_port(), + data_port: self.config.lp.announced_data_port(), x25519: self.x25519_lp_keys.pk, kem_keys: self.compute_kem_key_hashes(), }; @@ -1371,7 +1404,7 @@ impl NymNode { Ok(self.shutdown_manager) } - pub(crate) async fn run(mut self) -> Result<(), NymNodeError> { + pub async fn run(mut self) -> Result<(), NymNodeError> { let mut shutdown_signals = self.shutdown_manager.detach_shutdown_signals(); // listen for shutdown signal in case we received it when attempting to spawn all the tasks diff --git a/nym-node/src/wireguard/mod.rs b/nym-node/src/wireguard/mod.rs index 03d1b32e4b..46db2413ef 100644 --- a/nym-node/src/wireguard/mod.rs +++ b/nym-node/src/wireguard/mod.rs @@ -5,3 +5,6 @@ // but let's start putting everything in here pub mod error; + +pub use nym_gateway::node::wireguard::{PeerManager, PeerRegistrator}; +pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData};