From fbcc9e478247694e869685c70a596e94eec3a3b7 Mon Sep 17 00:00:00 2001 From: durch Date: Fri, 7 Nov 2025 22:51:22 +0100 Subject: [PATCH] lp-reg gw flow working-ish --- Cargo.lock | 5 + common/nym-lp/src/keypair.rs | 19 ++ common/nym-lp/src/state_machine.rs | 56 ++--- common/wireguard/src/lib.rs | 2 +- docker/localnet/README.md | 37 +++ docker/localnet/localnet.sh | 36 ++- gateway/src/node/lp_listener/handler.rs | 57 +++-- gateway/src/node/lp_listener/mod.rs | 18 +- gateway/src/node/lp_listener/registration.rs | 4 +- gateway/src/node/mod.rs | 30 ++- nym-gateway-probe/Cargo.toml | 5 + nym-gateway-probe/src/bandwidth_helpers.rs | 85 +++++++ nym-gateway-probe/src/lib.rs | 233 ++++++++++++++++-- nym-gateway-probe/src/nodes.rs | 179 +++++++++++++- nym-gateway-probe/src/run.rs | 80 ++++-- nym-node/src/cli/helpers.rs | 23 ++ nym-node/src/env.rs | 2 + nym-registration-client/src/builder/config.rs | 19 +- nym-registration-client/src/lib.rs | 61 +++-- .../src/lp_client/client.rs | 85 ++++++- 20 files changed, 883 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da8f2aac92..4b9e037c37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5920,6 +5920,7 @@ dependencies = [ "clap", "futures", "hex", + "nym-api-requests", "nym-authenticator-client", "nym-authenticator-requests", "nym-bandwidth-controller", @@ -5936,6 +5937,9 @@ dependencies = [ "nym-ip-packet-client", "nym-ip-packet-requests", "nym-lp", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-node-requests", "nym-node-status-client", "nym-registration-client", "nym-registration-common", @@ -5947,6 +5951,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.12", + "time", "tokio", "tokio-util", "tracing", diff --git a/common/nym-lp/src/keypair.rs b/common/nym-lp/src/keypair.rs index 20683158ac..abcc14f24a 100644 --- a/common/nym-lp/src/keypair.rs +++ b/common/nym-lp/src/keypair.rs @@ -39,6 +39,10 @@ impl PrivateKey { Ok(PrivateKey(SphinxPrivateKey::from(bytes))) } + pub fn from_bytes(bytes: &[u8; 32]) -> Self { + PrivateKey(SphinxPrivateKey::from(*bytes)) + } + pub fn public_key(&self) -> PublicKey { let public_key = SphinxPublicKey::from(&self.0); PublicKey(public_key) @@ -102,6 +106,21 @@ impl Keypair { } } + pub fn from_private_key(private_key: PrivateKey) -> Self { + let public_key = private_key.public_key(); + Self { + private_key, + public_key, + } + } + + pub fn from_keys(private_key: PrivateKey, public_key: PublicKey) -> Self { + Self { + private_key, + public_key, + } + } + pub fn private_key(&self) -> &PrivateKey { &self.private_key } diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index f5631973dd..95eafe2ee3 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -225,37 +225,37 @@ impl LpStateMachine { // LpState::Closed { reason } LpState::Handshaking { session } } else { - // 4. Check if handshake is now complete - if session.is_handshake_complete() { - result_action = Some(Ok(LpAction::HandshakeComplete)); - LpState::Transport { session } // Transition to Transport - } else { - // 5. Check if we need to send the next handshake message - match session.prepare_handshake_message() { - Some(Ok(message)) => { - match session.next_packet(message) { - Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket(response_packet))); - // Check AGAIN if handshake became complete *after preparing* - if session.is_handshake_complete() { - LpState::Transport { session } // Transition to Transport - } else { - LpState::Handshaking { session } // Remain Handshaking - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } + // 4. First check if we need to send a handshake message (before checking completion) + match session.prepare_handshake_message() { + Some(Ok(message)) => { + match session.next_packet(message) { + Ok(response_packet) => { + result_action = Some(Ok(LpAction::SendPacket(response_packet))); + // Check if handshake became complete after preparing message + if session.is_handshake_complete() { + LpState::Transport { session } // Transition to Transport + } else { + LpState::Handshaking { session } // Remain Handshaking } } + Err(e) => { + let reason = e.to_string(); + result_action = Some(Err(e)); + LpState::Closed { reason } + } } - Some(Err(e)) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - None => { + } + Some(Err(e)) => { + let reason = e.to_string(); + result_action = Some(Err(e)); + LpState::Closed { reason } + } + None => { + // 5. No message to send - check if handshake is complete + if session.is_handshake_complete() { + result_action = Some(Ok(LpAction::HandshakeComplete)); + LpState::Transport { session } // Transition to Transport + } else { // Handshake stalled unexpectedly let err = LpError::NoiseError(NoiseError::Other( "Handshake stalled unexpectedly".to_string(), diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index bc8bfd71f0..2fefa02576 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -164,7 +164,7 @@ pub struct WireguardData { /// Start wireguard device #[cfg(target_os = "linux")] pub async fn start_wireguard( - ecash_manager: Arc, + ecash_manager: Arc, metrics: nym_node_metrics::NymNodeMetrics, peers: Vec, upgrade_mode_status: nym_credential_verification::upgrade_mode::UpgradeModeStatus, diff --git a/docker/localnet/README.md b/docker/localnet/README.md index 19941bab3c..19a2f37a5e 100644 --- a/docker/localnet/README.md +++ b/docker/localnet/README.md @@ -81,6 +81,8 @@ Ports published to host: - 10001-10004 → Mixnet ports - 20001-20004 → Verloc ports - 30001-30004 → HTTP APIs +- 41264 → LP control port (registration) +- 51264 → LP data port ### Startup Flow @@ -246,6 +248,41 @@ container logs nym-gateway --follow & curl -L --socks5 localhost:1080 https://nymtech.com ``` +### LP (Lewes Protocol) Testing + +The gateway is configured with LP listener enabled and **mock ecash verification** for testing: + +```bash +# LP listener ports (exposed on host): +# - 41264: LP control port (TCP registration) +# - 51264: LP data port + +# Check LP ports are listening +nc -zv localhost 41264 +nc -zv localhost 51264 + +# Test LP registration with nym-gateway-probe +cargo run -p nym-gateway-probe run-local \ + --mnemonic "test mnemonic here" \ + --gateway-ip 'localhost:41264' \ + --only-lp-registration +``` + +**Mock Ecash Mode**: +- Gateway uses `--lp.use-mock-ecash true` flag +- Accepts ANY bandwidth credential without blockchain verification +- Perfect for testing LP protocol implementation +- **WARNING**: Never use mock ecash in production! + +**Testing without blockchain**: +The mock ecash manager allows testing the complete LP registration flow without requiring: +- Running nyxd blockchain +- Deploying smart contracts +- Acquiring real bandwidth credentials +- Setting up coconut signers + +This makes localnet perfect for rapid LP protocol development and testing. + ## File Structure ``` diff --git a/docker/localnet/localnet.sh b/docker/localnet/localnet.sh index 34b090f841..a1bb681c00 100755 --- a/docker/localnet/localnet.sh +++ b/docker/localnet/localnet.sh @@ -230,6 +230,8 @@ start_gateway() { -p 10004:10004 \ -p 20004:20004 \ -p 30004:30004 \ + -p 41264:41264 \ + -p 51264:51264 \ -v "$VOLUME_PATH:/localnet" \ -v "$NYM_VOLUME_PATH:/root/.nym" \ -d \ @@ -250,14 +252,17 @@ start_gateway() { --http-bind-address=0.0.0.0:30004 \ --http-access-token=lala \ --public-ips $CONTAINER_IP \ + --enable-lp true \ + --lp-use-mock-ecash true \ --output=json \ + --wireguard-enabled true \ --bonding-information-output="/localnet/gateway.json"; echo "Waiting for network.json..."; while [ ! -f /localnet/network.json ]; do sleep 2; done; - echo "Starting gateway..."; + echo "Starting gateway with LP listener (mock ecash)..."; exec nym-node run --id gateway-localnet --unsafe-disable-replay-protection --local ' @@ -429,13 +434,36 @@ show_status() { echo "" log_info "Port status:" - for port in 9000 1080 10001 10002 10003 10004; do + echo " Mixnet:" + for port in 10001 10002 10003 10004; do if nc -z 127.0.0.1 $port 2>/dev/null; then - echo -e " ${GREEN}●${NC} Port $port - listening" + echo -e " ${GREEN}●${NC} Port $port - listening" else - echo -e " ${RED}○${NC} Port $port - not listening" + echo -e " ${RED}○${NC} Port $port - not listening" fi done + echo " Gateway:" + for port in 9000 30004; do + if nc -z 127.0.0.1 $port 2>/dev/null; then + echo -e " ${GREEN}●${NC} Port $port - listening" + else + echo -e " ${RED}○${NC} Port $port - not listening" + fi + done + echo " LP (Lewes Protocol):" + for port in 41264 51264; do + if nc -z 127.0.0.1 $port 2>/dev/null; then + echo -e " ${GREEN}●${NC} Port $port - listening" + else + echo -e " ${RED}○${NC} Port $port - not listening" + fi + done + echo " SOCKS5:" + if nc -z 127.0.0.1 1080 2>/dev/null; then + echo -e " ${GREEN}●${NC} Port 1080 - listening" + else + echo -e " ${RED}○${NC} Port 1080 - not listening" + fi } # Build network topology with container IPs diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index ee63fa16bc..3897deac3c 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -7,7 +7,7 @@ use super::registration::process_registration; use super::LpHandlerState; use crate::error::GatewayError; use nym_lp::{ - keypair::{Keypair, PublicKey}, + keypair::{Keypair, PrivateKey as LpPrivateKey, PublicKey}, LpMessage, LpPacket, LpSession, }; use nym_metrics::{add_histogram_obs, inc}; @@ -109,10 +109,21 @@ impl LpConnectionHandler { // 2. Client's public key (will be received during handshake) // 3. PSK (pre-shared key) - for now use a placeholder - // Generate fresh LP keypair (x25519) for this connection - // Using Keypair::default() which generates a new random x25519 keypair - // This is secure and simple - each connection gets its own keypair - let gateway_keypair = Keypair::default(); + // Derive LP keypair from gateway's ed25519 identity using proper conversion + // This creates a valid x25519 keypair for ECDH operations in Noise protocol + let x25519_private = self.state.local_identity.private_key().to_x25519(); + let x25519_public = self.state.local_identity.public_key().to_x25519() + .map_err(|e| GatewayError::LpHandshakeError( + format!("Failed to convert ed25519 public key to x25519: {}", e) + ))?; + + let lp_private = LpPrivateKey::from_bytes(x25519_private.as_bytes()); + let lp_public = PublicKey::from_bytes(x25519_public.as_bytes()) + .map_err(|e| GatewayError::LpHandshakeError( + format!("Failed to create LP public key: {}", e) + ))?; + + let gateway_keypair = Keypair::from_keys(lp_private, lp_public); // Receive client's public key and salt via ClientHello message // The client initiates by sending ClientHello as first packet @@ -289,11 +300,7 @@ impl LpConnectionHandler { .duration_since(UNIX_EPOCH) .expect("System time before UNIX epoch") .as_secs(); - if now >= timestamp { - now - timestamp - } else { - timestamp - now - } + now.abs_diff(timestamp) }, self.state.lp_config.timestamp_tolerance_secs ); @@ -333,22 +340,20 @@ impl LpConnectionHandler { ))); } - // Extract registration request from LP message - match packet.message() { - LpMessage::EncryptedData(data) => { - // Deserialize registration request - bincode::deserialize(&data).map_err(|e| { - GatewayError::LpProtocolError(format!( - "Failed to deserialize registration request: {}", - e - )) - }) - } - other => Err(GatewayError::LpProtocolError(format!( - "Expected EncryptedData message, got {:?}", - other - ))), - } + // Decrypt the packet payload using the established session + let decrypted_bytes = session + .decrypt_data(packet.message()) + .map_err(|e| { + GatewayError::LpProtocolError(format!("Failed to decrypt registration request: {}", e)) + })?; + + // Deserialize the decrypted bytes into LpRegistrationRequest + bincode::deserialize(&decrypted_bytes).map_err(|e| { + GatewayError::LpProtocolError(format!( + "Failed to deserialize registration request: {}", + e + )) + }) } /// Send registration response after processing diff --git a/gateway/src/node/lp_listener/mod.rs b/gateway/src/node/lp_listener/mod.rs index f26490206a..11c989c16d 100644 --- a/gateway/src/node/lp_listener/mod.rs +++ b/gateway/src/node/lp_listener/mod.rs @@ -108,17 +108,29 @@ pub struct LpConfig { /// Recommended: 30-60 seconds #[serde(default = "default_timestamp_tolerance_secs")] pub timestamp_tolerance_secs: u64, + + /// 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. + #[serde(default = "default_use_mock_ecash")] + pub use_mock_ecash: bool, } impl Default for LpConfig { fn default() -> Self { Self { - enabled: false, + enabled: true, bind_address: default_bind_address(), control_port: default_control_port(), data_port: default_data_port(), max_connections: default_max_connections(), timestamp_tolerance_secs: default_timestamp_tolerance_secs(), + use_mock_ecash: default_use_mock_ecash(), } } } @@ -143,6 +155,10 @@ fn default_timestamp_tolerance_secs() -> u64 { 30 // 30 seconds - balances security vs clock skew tolerance } +fn default_use_mock_ecash() -> bool { + false // Always default to real ecash for security +} + /// Shared state for LP connection handlers #[derive(Clone)] pub struct LpHandlerState { diff --git a/gateway/src/node/lp_listener/registration.rs b/gateway/src/node/lp_listener/registration.rs index acf8b51413..edfa3fc2a2 100644 --- a/gateway/src/node/lp_listener/registration.rs +++ b/gateway/src/node/lp_listener/registration.rs @@ -315,8 +315,8 @@ async fn register_wg_peer( .unwrap_or_else(|_| SocketAddr::from_str("0.0.0.0:51820").unwrap()), ); peer.allowed_ips = vec![ - format!("{}/32", client_ipv4).parse().unwrap(), - format!("{}/128", client_ipv6).parse().unwrap(), + format!("{client_ipv4}/32").parse().unwrap(), + format!("{client_ipv6}/128").parse().unwrap(), ]; peer.persistent_keepalive_interval = Some(25); diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 0f6e415159..43cb59da5c 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -11,7 +11,7 @@ use crate::node::internal_service_providers::{ use crate::node::stale_data_cleaner::StaleMessagesCleaner; use futures::channel::oneshot; use nym_credential_verification::ecash::{ - credential_sender::CredentialHandlerConfig, EcashManager, + credential_sender::CredentialHandlerConfig, EcashManager, MockEcashManager, }; use nym_credential_verification::upgrade_mode::{ UpgradeModeCheckConfig, UpgradeModeDetails, UpgradeModeState, @@ -106,7 +106,8 @@ pub struct GatewayTasksBuilder { shutdown_tracker: ShutdownTracker, // populated and cached as necessary - ecash_manager: Option>, + ecash_manager: + Option>, wireguard_peers: Option>, @@ -213,7 +214,21 @@ impl GatewayTasksBuilder { Ok(nyxd_client) } - async fn build_ecash_manager(&self) -> Result, GatewayError> { + async fn build_ecash_manager( + &self, + ) -> Result< + Arc, + GatewayError, + > { + // Check if we should use mock ecash for testing + if self.config.lp.use_mock_ecash { + info!("Using MockEcashManager for LP testing (credentials NOT verified)"); + let mock_manager = MockEcashManager::new(Box::new(self.storage.clone())); + return Ok(Arc::new(mock_manager) + as Arc); + } + + // Production path: use real EcashManager with blockchain verification let handler_config = CredentialHandlerConfig { revocation_bandwidth_penalty: self .config @@ -246,7 +261,8 @@ impl GatewayTasksBuilder { "EcashCredentialHandler", ); - Ok(Arc::new(ecash_manager)) + Ok(Arc::new(ecash_manager) + as Arc) } async fn ecash_manager( @@ -303,11 +319,7 @@ impl GatewayTasksBuilder { active_clients_store: ActiveClientsStore, ) -> Result { // Get WireGuard peer controller if available - let wg_peer_controller = if let Some(wg_data) = &self.wireguard_data { - Some(wg_data.inner.peer_tx().clone()) - } else { - None - }; + let wg_peer_controller = self.wireguard_data.as_ref().map(|wg_data| wg_data.inner.peer_tx().clone()); let handler_state = lp_listener::LpHandlerState { ecash_verifier: self.ecash_manager().await?, diff --git a/nym-gateway-probe/Cargo.toml b/nym-gateway-probe/Cargo.toml index 4ad46d3d55..8a41137fa5 100644 --- a/nym-gateway-probe/Cargo.toml +++ b/nym-gateway-probe/Cargo.toml @@ -41,6 +41,7 @@ x25519-dalek = { workspace = true, features = [ "static_secrets", ] } +nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-authenticator-requests = { path = "../common/authenticator-requests" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } nym-bin-common = { path = "../common/bin-common" } @@ -59,9 +60,13 @@ nym-credentials = { path = "../common/credentials" } nym-http-api-client-macro = { path = "../common/http-api-client-macro" } nym-http-api-client = { path = "../common/http-api-client" } nym-node-status-client = { path = "../nym-node-status-api/nym-node-status-client" } +nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-registration-client = { path = "../nym-registration-client" } nym-lp = { path = "../common/nym-lp" } +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-network-defaults = { path = "../common/network-defaults" } nym-registration-common = { path = "../common/registration" } +time = { workspace = true } # TEMP: REMOVE BEFORE PR nym-topology = { path = "../common/topology" } diff --git a/nym-gateway-probe/src/bandwidth_helpers.rs b/nym-gateway-probe/src/bandwidth_helpers.rs index 4753265166..06788ba825 100644 --- a/nym-gateway-probe/src/bandwidth_helpers.rs +++ b/nym-gateway-probe/src/bandwidth_helpers.rs @@ -4,6 +4,7 @@ use anyhow::{Context, bail}; use nym_bandwidth_controller::error::BandwidthControllerError; use nym_client_core::client::base_client::storage::OnDiskPersistent; +use nym_credentials::CredentialSpendingData; use nym_credentials_interface::TicketType; use nym_node_status_client::models::AttachedTicketMaterials; use nym_sdk::bandwidth::BandwidthImporter; @@ -155,3 +156,87 @@ pub(crate) async fn acquire_bandwidth( bail!("failed to acquire bandwidth after {MAX_RETRIES} attempts") } + +/// Create a dummy credential for mock ecash testing +/// +/// Gateway with --lp-use-mock-ecash accepts any credential without verification, +/// so we only need to provide properly structured data with correct types. +/// +/// This is useful for local testing without requiring blockchain access or funded accounts. +/// +/// This uses a pre-serialized test credential from the wireguard tests - since MockEcashManager +/// doesn't verify anything, any valid CredentialSpendingData structure will work. +pub(crate) fn create_dummy_credential( + _gateway_identity: &[u8; 32], + _ticket_type: TicketType, +) -> CredentialSpendingData { + // This is a valid serialized CredentialSpendingData taken from integration tests + // See: common/wireguard-private-metadata/tests/src/lib.rs:CREDENTIAL_BYTES + const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES) + .expect("Failed to deserialize test credential - this is a bug in the test harness") +} diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index 057d877324..6349f03c90 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -60,7 +60,7 @@ pub mod nodes; mod types; use crate::bandwidth_helpers::{acquire_bandwidth, import_bandwidth}; -use crate::nodes::NymApiDirectory; +use crate::nodes::{DirectoryNode, NymApiDirectory}; use nym_node_status_client::models::AttachedTicketMaterials; pub use types::{IpPingReplies, ProbeOutcome, ProbeResult}; @@ -155,6 +155,8 @@ pub struct Probe { amnezia_args: String, netstack_args: NetstackArgs, credentials_args: CredentialArgs, + /// Pre-queried gateway node (used when --gateway-ip is specified) + direct_gateway_node: Option, } impl Probe { @@ -170,8 +172,28 @@ impl Probe { amnezia_args: "".into(), netstack_args, credentials_args, + direct_gateway_node: None, } } + + /// Create a probe with a pre-queried gateway node (for direct IP mode) + pub fn new_with_gateway( + entrypoint: NodeIdentity, + tested_node: TestedNode, + netstack_args: NetstackArgs, + credentials_args: CredentialArgs, + gateway_node: DirectoryNode, + ) -> Self { + Self { + entrypoint, + tested_node, + amnezia_args: "".into(), + netstack_args, + credentials_args, + direct_gateway_node: Some(gateway_node), + } + } + pub fn with_amnezia(&mut self, args: &str) -> &Self { self.amnezia_args = args.to_string(); self @@ -179,10 +201,11 @@ impl Probe { pub async fn probe( self, - directory: NymApiDirectory, + directory: Option, nyxd_url: Url, ignore_egress_epoch_role: bool, only_wireguard: bool, + only_lp_registration: bool, min_mixnet_performance: Option, ) -> anyhow::Result { let tickets_materials = self.credentials_args.decode_attached_ticket_materials()?; @@ -218,6 +241,8 @@ impl Probe { nyxd_url, tested_entry, only_wireguard, + only_lp_registration, + false, // Not using mock ecash in regular probe mode ) .await } @@ -226,13 +251,23 @@ impl Probe { pub async fn probe_run_locally( self, config_dir: &PathBuf, - mnemonic: &str, - directory: NymApiDirectory, + mnemonic: Option<&str>, + directory: Option, nyxd_url: Url, ignore_egress_epoch_role: bool, only_wireguard: bool, + only_lp_registration: bool, min_mixnet_performance: Option, + use_mock_ecash: bool, ) -> anyhow::Result { + // If only testing LP registration, use the dedicated LP-only path + // This skips mixnet setup entirely and allows testing local gateways + if only_lp_registration { + return self + .probe_lp_only(config_dir, directory, nyxd_url, use_mock_ecash) + .await; + } + let tested_entry = self.tested_node.is_same_as_entry(); let (mixnet_entry_gateway_id, node_info) = self.lookup_gateway(&directory).await?; @@ -278,7 +313,11 @@ impl Probe { info!("Credential store contains {} ticketbooks", ticketbook_count); - if ticketbook_count < 1 { + // Only acquire real bandwidth if not using mock ecash + if ticketbook_count < 1 && !use_mock_ecash { + let mnemonic = mnemonic.ok_or_else(|| { + anyhow::anyhow!("mnemonic is required when not using mock ecash (--use-mock-ecash)") + })?; for ticketbook_type in [ TicketType::V1MixnetEntry, TicketType::V1WireguardEntry, @@ -286,6 +325,8 @@ impl Probe { ] { acquire_bandwidth(mnemonic, &disconnected_mixnet_client, ticketbook_type).await?; } + } else if use_mock_ecash { + info!("Using mock ecash mode - skipping bandwidth acquisition"); } let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await; @@ -298,14 +339,113 @@ impl Probe { nyxd_url, tested_entry, only_wireguard, + only_lp_registration, + use_mock_ecash, ) .await } + /// Probe LP registration only, skipping all mixnet tests + /// This is useful for testing local dev gateways that aren't registered in nym-api + pub async fn probe_lp_only( + self, + config_dir: &PathBuf, + directory: Option, + nyxd_url: Url, + use_mock_ecash: bool, + ) -> anyhow::Result { + let tested_entry = self.tested_node.is_same_as_entry(); + let (mixnet_entry_gateway_id, node_info) = self.lookup_gateway(&directory).await?; + + if config_dir.is_file() { + bail!("provided configuration directory is a file"); + } + + if !config_dir.exists() { + std::fs::create_dir_all(config_dir)?; + } + + let storage_paths = StoragePaths::new_from_dir(config_dir)?; + let storage = storage_paths + .initialise_default_persistent_storage() + .await?; + + let key_store = storage.key_store(); + let mut rng = OsRng; + + // Generate client keys if they don't exist + if key_store.load_keys().await.is_err() { + tracing::log::debug!("Generating new client keys"); + nym_client_core::init::generate_new_client_keys(&mut rng, key_store).await?; + } + + // Check if node has LP address + let (lp_address, ip_address) = match (node_info.lp_address, node_info.ip_address) { + (Some(lp_addr), Some(ip_addr)) => (lp_addr, ip_addr), + _ => { + bail!("Gateway does not have LP address configured"); + } + }; + + info!("Testing LP registration for gateway {}", node_info.identity); + + // Create bandwidth controller for credential preparation + let config = nym_validator_client::nyxd::Config::try_from_nym_network_details( + &NymNetworkDetails::new_from_env(), + )?; + let client = nym_validator_client::nyxd::NyxdClient::connect(config, nyxd_url.as_str())?; + let bw_controller = + nym_bandwidth_controller::BandwidthController::new(storage.credential_store().clone(), client); + + // Run LP registration probe + let lp_outcome = lp_registration_probe( + node_info.identity, + lp_address, + ip_address, + &bw_controller, + use_mock_ecash, + ) + .await + .unwrap_or_default(); + + // Return result with only LP outcome + Ok(ProbeResult { + node: node_info.identity.to_string(), + used_entry: mixnet_entry_gateway_id.to_string(), + outcome: types::ProbeOutcome { + as_entry: types::Entry::NotTested, + as_exit: if tested_entry { + None + } else { + Some(types::Exit::fail_to_connect()) + }, + wg: None, + lp: Some(lp_outcome), + }, + }) + } + pub async fn lookup_gateway( &self, - directory: &NymApiDirectory, + directory: &Option, ) -> anyhow::Result<(NodeIdentity, TestedNodeDetails)> { + // If we have a pre-queried gateway node (direct IP mode), use that + if let Some(direct_node) = &self.direct_gateway_node { + info!("Using pre-queried gateway node from direct IP query"); + let node_info = direct_node.to_testable_node()?; + info!("connecting to entry gateway: {}", direct_node.identity()); + debug!( + "authenticator version: {:?}", + node_info.authenticator_version + ); + return Ok((self.entrypoint, node_info)); + } + + // Otherwise, use the directory (original behavior) + let directory = directory + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Directory is required when not using --gateway-ip"))?; + // Setup the entry gateways let entry_gateway = directory.entry_gateway(&self.entrypoint)?; @@ -341,6 +481,8 @@ impl Probe { nyxd_url: Url, tested_entry: bool, only_wireguard: bool, + only_lp_registration: bool, + use_mock_ecash: bool, ) -> anyhow::Result where T: MixnetClientStorage + Clone + 'static, @@ -375,7 +517,7 @@ impl Probe { info!("Our nym address: {nym_address}"); // Now that we have a connected mixnet client, we can start pinging - let (outcome, mixnet_client) = if only_wireguard { + let (outcome, mixnet_client) = if only_wireguard || only_lp_registration { ( Ok(ProbeOutcome { as_entry: if tested_entry { @@ -399,7 +541,10 @@ impl Probe { .await }; - let wg_outcome = if let (Some(authenticator), Some(ip_address)) = + let wg_outcome = if only_lp_registration { + // Skip WireGuard test when only testing LP registration + WgProbeResults::default() + } else if let (Some(authenticator), Some(ip_address)) = (node_info.authenticator_address, node_info.ip_address) { // Start the mixnet listener that the auth clients use to receive messages. @@ -474,6 +619,7 @@ impl Probe { lp_address, ip_address, &bw_controller, + use_mock_ecash, ) .await .unwrap_or_default(); @@ -684,6 +830,7 @@ async fn lp_registration_probe( nym_validator_client::nyxd::NyxdClient, St, >, + use_mock_ecash: bool, ) -> anyhow::Result where St: nym_sdk::mixnet::CredentialStorage + Clone + Send + Sync + 'static, @@ -699,11 +846,21 @@ where // Generate LP keypair for this connection let client_lp_keypair = std::sync::Arc::new(LpKeypair::default()); - // Derive gateway LP public key from gateway identity (as done in registration-client) - let gateway_lp_key = match LpPublicKey::from_bytes(&gateway_identity.to_bytes()) { + // Derive gateway LP public key from gateway identity using proper ed25519→x25519 conversion + let gateway_x25519_pub = match gateway_identity.to_x25519() { Ok(key) => key, Err(e) => { - let error_msg = format!("Failed to derive gateway LP key: {}", e); + let error_msg = format!("Failed to convert gateway ed25519 key to x25519: {}", e); + error!("{}", error_msg); + lp_outcome.error = Some(error_msg); + return Ok(lp_outcome); + } + }; + + let gateway_lp_key = match LpPublicKey::from_bytes(gateway_x25519_pub.as_bytes()) { + Ok(key) => key, + Err(e) => { + let error_msg = format!("Failed to create LP key from x25519 bytes: {}", e); error!("{}", error_msg); lp_outcome.error = Some(error_msg); return Ok(lp_outcome); @@ -766,20 +923,48 @@ where } }; - match client.send_registration_request( - &wg_keypair, - &gateway_ed25519_pubkey, - bandwidth_controller, - TicketType::V1WireguardEntry, - ).await { - Ok(_) => { - info!("LP registration request sent successfully"); + // Generate credential based on mode + let ticket_type = TicketType::V1WireguardEntry; + if use_mock_ecash { + info!("Using mock ecash credential for LP registration"); + let credential = crate::bandwidth_helpers::create_dummy_credential( + &gateway_ed25519_pubkey.to_bytes(), + ticket_type, + ); + + match client.send_registration_request_with_credential( + &wg_keypair, + &gateway_ed25519_pubkey, + credential, + ticket_type, + ).await { + Ok(_) => { + info!("LP registration request sent successfully with mock ecash"); + } + Err(e) => { + let error_msg = format!("Failed to send LP registration request: {}", e); + error!("{}", error_msg); + lp_outcome.error = Some(error_msg); + return Ok(lp_outcome); + } } - Err(e) => { - let error_msg = format!("Failed to send LP registration request: {}", e); - error!("{}", error_msg); - lp_outcome.error = Some(error_msg); - return Ok(lp_outcome); + } else { + info!("Using real bandwidth controller for LP registration"); + match client.send_registration_request( + &wg_keypair, + &gateway_ed25519_pubkey, + bandwidth_controller, + ticket_type, + ).await { + Ok(_) => { + info!("LP registration request sent successfully with real ecash"); + } + Err(e) => { + let error_msg = format!("Failed to send LP registration request: {}", e); + error!("{}", error_msg); + lp_outcome.error = Some(error_msg); + return Ok(lp_outcome); + } } } diff --git a/nym-gateway-probe/src/nodes.rs b/nym-gateway-probe/src/nodes.rs index 5b868bb7df..cd67c724bb 100644 --- a/nym-gateway-probe/src/nodes.rs +++ b/nym-gateway-probe/src/nodes.rs @@ -3,14 +3,25 @@ use crate::TestedNodeDetails; use anyhow::{Context, anyhow, bail}; +use nym_api_requests::models::{ + AuthenticatorDetails, DeclaredRoles, DescribedNodeType, HostInformation, + IpPacketRouterDetails, NetworkRequesterDetails, NymNodeData, OffsetDateTimeJsonSchemaWrapper, + WebSockets, WireguardDetails, +}; use nym_authenticator_requests::AuthenticatorVersion; +use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_http_api_client::UserAgent; +use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_node_requests::api::client::NymNodeApiClientExt; +use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails; use nym_sdk::mixnet::NodeIdentity; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::NymNodeDescription; use rand::prelude::IteratorRandom; use std::collections::HashMap; -use tracing::{debug, info}; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::{debug, info, warn}; use url::Url; // in the old behaviour we were getting all skimmed nodes to retrieve performance @@ -133,6 +144,172 @@ impl DirectoryNode { } } +/// Query a gateway directly by address using its self-described HTTP API endpoints. +/// This bypasses the need for directory service lookup. +/// +/// # Arguments +/// * `address` - The address of the gateway (IP, IP:PORT, or HOST:PORT format) +/// +/// # Returns +/// A `DirectoryNode` containing all gateway metadata, or an error if the query fails +pub async fn query_gateway_by_ip(address: String) -> anyhow::Result { + info!("Querying gateway directly at address: {}", address); + + // Parse the address to check if it contains a port + let addresses_to_try = if address.contains(':') { + // Address already has port specified, use it directly + vec![ + format!("http://{}", address), + format!("https://{}", address), + ] + } else { + // No port specified, try multiple ports in order of likelihood + vec![ + format!("http://{}:{}", address, DEFAULT_NYM_NODE_HTTP_PORT), // Standard port 8080 + format!("https://{}", address), // HTTPS proxy (443) + format!("http://{}", address), // HTTP proxy (80) + ] + }; + + let user_agent: UserAgent = nym_bin_common::bin_info_local_vergen!().into(); + let mut last_error = None; + + for address in addresses_to_try { + debug!("Trying to connect to gateway at: {}", address); + + // Build client with timeout + let client = match nym_node_requests::api::Client::builder(address.clone()) { + Ok(builder) => match builder + .with_timeout(Duration::from_secs(5)) + .no_hickory_dns() + .with_user_agent(user_agent.clone()) + .build() + { + Ok(c) => c, + Err(e) => { + warn!("Failed to build client for {}: {}", address, e); + last_error = Some(e.into()); + continue; + } + }, + Err(e) => { + warn!("Failed to create client builder for {}: {}", address, e); + last_error = Some(e.into()); + continue; + } + }; + + // Check if the node is up + match client.get_health().await { + Ok(health) if health.status.is_up() => { + info!("Successfully connected to gateway at {}", address); + + // Query all required metadata concurrently + let host_info_result = client.get_host_information().await; + let roles_result = client.get_roles().await; + let build_info_result = client.get_build_information().await; + let aux_details_result = client.get_auxiliary_details().await; + let websockets_result = client.get_mixnet_websockets().await; + + // These are optional, so we use ok() to ignore errors + let ipr_result = client.get_ip_packet_router().await.ok(); + let authenticator_result = client.get_authenticator().await.ok(); + let wireguard_result = client.get_wireguard().await.ok(); + + // Check required fields + let host_info = host_info_result.context("Failed to get host information")?; + let roles = roles_result.context("Failed to get roles")?; + let build_info = build_info_result.context("Failed to get build information")?; + let aux_details: NodeAuxiliaryDetails = aux_details_result.unwrap_or_default(); + let websockets = websockets_result.context("Failed to get websocket info")?; + + // Verify node signature + if !host_info.verify_host_information() { + bail!("Gateway host information signature verification failed"); + } + + // Verify it's actually a gateway + if !roles.gateway_enabled { + bail!("Node at {} is not configured as an entry gateway", address); + } + + // Convert to our internal types + let network_requester: Option = None; // Not needed for LP testing + let ip_packet_router: Option = + ipr_result.map(|ipr| IpPacketRouterDetails { + address: ipr.address, + }); + let authenticator: Option = + authenticator_result.map(|auth| AuthenticatorDetails { + address: auth.address, + }); + let wireguard: Option = wireguard_result.map(|wg| WireguardDetails { + port: wg.port, + tunnel_port: wg.tunnel_port, + metadata_port: wg.metadata_port, + public_key: wg.public_key, + }); + + // Construct NymNodeData + let node_data = NymNodeData { + last_polled: OffsetDateTimeJsonSchemaWrapper(OffsetDateTime::now_utc()), + host_information: HostInformation { + ip_address: host_info.data.ip_address, + hostname: host_info.data.hostname, + keys: host_info.data.keys.into(), + }, + declared_role: DeclaredRoles { + mixnode: roles.mixnode_enabled, + entry: roles.gateway_enabled, + exit_nr: roles.network_requester_enabled, + exit_ipr: roles.ip_packet_router_enabled, + }, + auxiliary_details: aux_details, + build_information: BinaryBuildInformationOwned { + binary_name: build_info.binary_name, + build_timestamp: build_info.build_timestamp, + build_version: build_info.build_version, + commit_sha: build_info.commit_sha, + commit_timestamp: build_info.commit_timestamp, + commit_branch: build_info.commit_branch, + rustc_version: build_info.rustc_version, + rustc_channel: build_info.rustc_channel, + cargo_triple: build_info.cargo_triple, + cargo_profile: build_info.cargo_profile, + }, + network_requester, + ip_packet_router, + authenticator, + wireguard, + mixnet_websockets: WebSockets { + ws_port: websockets.ws_port, + wss_port: websockets.wss_port, + }, + }; + + // Create NymNodeDescription + let described = NymNodeDescription { + node_id: 0, // We don't have a node_id from direct query + contract_node_type: DescribedNodeType::NymNode, // All new nodes are NymNode type + description: node_data, + }; + + return Ok(DirectoryNode { described }); + } + Ok(_) => { + warn!("Gateway at {} is not healthy", address); + last_error = Some(anyhow!("Gateway is not healthy")); + } + Err(e) => { + warn!("Health check failed for {}: {}", address, e); + last_error = Some(e.into()); + } + } + } + + Err(last_error.unwrap_or_else(|| anyhow!("Failed to connect to gateway at {}", address))) +} + pub struct NymApiDirectory { // nodes: HashMap, nodes: HashMap, diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 9487b10905..c76b45c96c 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -4,7 +4,7 @@ use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; use nym_config::defaults::setup_env; -use nym_gateway_probe::nodes::NymApiDirectory; +use nym_gateway_probe::nodes::{query_gateway_by_ip, NymApiDirectory}; use nym_gateway_probe::{CredentialArgs, NetstackArgs, ProbeResult, TestedNode}; use nym_sdk::mixnet::NodeIdentity; use std::path::Path; @@ -37,6 +37,11 @@ struct CliArgs { #[arg(long, short = 'g', alias = "gateway", global = true)] entry_gateway: Option, + /// The address of the gateway to probe directly (bypasses directory lookup) + /// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004) + #[arg(long, global = true)] + gateway_ip: Option, + /// Identity of the node to test #[arg(long, short, value_parser = validate_node_identity, global = true)] node: Option, @@ -50,6 +55,9 @@ struct CliArgs { #[arg(long, global = true)] only_wireguard: bool, + #[arg(long, global = true)] + only_lp_registration: bool, + /// Disable logging during probe #[arg(long, global = true)] ignore_egress_epoch_role: bool, @@ -76,12 +84,16 @@ const DEFAULT_CONFIG_DIR: &str = "/tmp/nym-gateway-probe/config/"; enum Commands { /// Run the probe locally RunLocal { - /// Provide a mnemonic to get credentials + /// Provide a mnemonic to get credentials (optional when using --use-mock-ecash) #[arg(long)] - mnemonic: String, + mnemonic: Option, #[arg(long)] config_dir: Option, + + /// Use mock ecash credentials for testing (requires gateway with --lp-use-mock-ecash) + #[arg(long)] + use_mock_ecash: bool, }, } @@ -116,18 +128,42 @@ pub(crate) async fn run() -> anyhow::Result { .first() .map(|ep| ep.nyxd_url()) .ok_or(anyhow::anyhow!("missing nyxd url"))?; - let api_url = network - .endpoints - .first() - .and_then(|ep| ep.api_url()) - .ok_or(anyhow::anyhow!("missing nyxd url"))?; + // If gateway IP is provided, query it directly without using the directory + let (entry, directory, gateway_node) = if let Some(gateway_ip) = args.gateway_ip { + info!("Using direct IP query mode for gateway: {}", gateway_ip); + let gateway_node = query_gateway_by_ip(gateway_ip).await?; + let identity = gateway_node.identity(); - let directory = NymApiDirectory::new(api_url).await?; + // Still create the directory for potential secondary lookups, + // but only if API URL is available + let directory = if let Some(api_url) = network + .endpoints + .first() + .and_then(|ep| ep.api_url()) + { + Some(NymApiDirectory::new(api_url).await?) + } else { + None + }; - let entry = if let Some(gateway) = &args.entry_gateway { - NodeIdentity::from_base58_string(gateway)? + (identity, directory, Some(gateway_node)) } else { - directory.random_exit_with_ipr()? + // Original behavior: use directory service + let api_url = network + .endpoints + .first() + .and_then(|ep| ep.api_url()) + .ok_or(anyhow::anyhow!("missing api url"))?; + + let directory = NymApiDirectory::new(api_url).await?; + + let entry = if let Some(gateway) = &args.entry_gateway { + NodeIdentity::from_base58_string(gateway)? + } else { + directory.random_exit_with_ipr()? + }; + + (entry, Some(directory), None) }; let test_point = if let Some(node) = args.node { @@ -136,8 +172,18 @@ pub(crate) async fn run() -> anyhow::Result { TestedNode::SameAsEntry }; - let mut trial = - nym_gateway_probe::Probe::new(entry, test_point, args.netstack_args, args.credential_args); + let mut trial = if let Some(gw_node) = gateway_node { + nym_gateway_probe::Probe::new_with_gateway( + entry, + test_point, + args.netstack_args, + args.credential_args, + gw_node, + ) + } else { + nym_gateway_probe::Probe::new(entry, test_point, args.netstack_args, args.credential_args) + }; + if let Some(awg_args) = args.amnezia_args { trial.with_amnezia(&awg_args); } @@ -146,6 +192,7 @@ pub(crate) async fn run() -> anyhow::Result { Some(Commands::RunLocal { mnemonic, config_dir, + use_mock_ecash, }) => { let config_dir = config_dir .clone() @@ -158,12 +205,14 @@ pub(crate) async fn run() -> anyhow::Result { Box::pin(trial.probe_run_locally( &config_dir, - mnemonic, + mnemonic.as_deref(), directory, nyxd_url, args.ignore_egress_epoch_role, args.only_wireguard, + args.only_lp_registration, args.min_gateway_mixnet_performance, + *use_mock_ecash, )) .await } @@ -173,6 +222,7 @@ pub(crate) async fn run() -> anyhow::Result { nyxd_url, args.ignore_egress_epoch_role, args.only_wireguard, + args.only_lp_registration, args.min_gateway_mixnet_performance, )) .await diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 08ccef0857..0239b5b6c1 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -446,6 +446,23 @@ pub(crate) struct EntryGatewayArgs { )] #[zeroize(skip)] pub(crate) upgrade_mode_attester_public_key: Option, + + /// Enable LP (Lewes Protocol) listener for client registration. + /// LP provides an alternative registration protocol with improved security features. + #[clap( + long, + env = NYMNODE_ENABLE_LP_ARG + )] + pub(crate) enable_lp: 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 { @@ -479,6 +496,12 @@ 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(enable_lp) = self.enable_lp { + section.lp.enabled = enable_lp + } + if let Some(use_mock_ecash) = self.lp_use_mock_ecash { + section.lp.use_mock_ecash = use_mock_ecash + } section } diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 1564d087a4..56f78f8971 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -65,6 +65,8 @@ 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_ENABLE_LP_ARG: &str = "NYMNODE_ENABLE_LP"; + 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"; diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs index 9edb48adca..af73e92b06 100644 --- a/nym-registration-client/src/builder/config.rs +++ b/nym-registration-client/src/builder/config.rs @@ -286,6 +286,7 @@ pub enum BuilderConfigError { /// /// This provides a more convenient way to construct a `BuilderConfig` compared to the /// `new()` constructor with many arguments. +#[derive(Default)] pub struct BuilderConfigBuilder { entry_node: Option, exit_node: Option, @@ -300,24 +301,6 @@ pub struct BuilderConfigBuilder { connection_fd_callback: Option>, } -impl Default for BuilderConfigBuilder { - fn default() -> Self { - Self { - entry_node: None, - exit_node: None, - data_path: None, - mixnet_client_config: None, - mode: None, - user_agent: None, - custom_topology_provider: None, - network_env: None, - cancel_token: None, - #[cfg(unix)] - connection_fd_callback: None, - } - } -} - impl BuilderConfigBuilder { pub fn new() -> Self { Self::default() diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs index 328d0f6a6a..ab4c79e3de 100644 --- a/nym-registration-client/src/lib.rs +++ b/nym-registration-client/src/lib.rs @@ -171,29 +171,46 @@ impl RegistrationClient { tracing::debug!("Entry gateway LP address: {}", entry_lp_address); tracing::debug!("Exit gateway LP address: {}", exit_lp_address); - // For now, use gateway identities as LP public keys - // TODO(nym-87): Implement proper key derivation - let entry_gateway_lp_key = - LpPublicKey::from_bytes(&self.config.entry.node.identity.to_bytes()).map_err(|e| { - RegistrationClientError::LpRegistrationNotPossible { - node_id: format!( - "{}: invalid LP key: {}", - self.config.entry.node.identity.to_base58_string(), - e - ), - } - })?; + // Convert gateway ed25519 identities to x25519 LP public keys using proper conversion + let entry_x25519_pub = self.config.entry.node.identity.to_x25519().map_err(|e| { + RegistrationClientError::LpRegistrationNotPossible { + node_id: format!( + "{}: failed to convert ed25519 to x25519: {}", + self.config.entry.node.identity.to_base58_string(), + e + ), + } + })?; - let exit_gateway_lp_key = - LpPublicKey::from_bytes(&self.config.exit.node.identity.to_bytes()).map_err(|e| { - RegistrationClientError::LpRegistrationNotPossible { - node_id: format!( - "{}: invalid LP key: {}", - self.config.exit.node.identity.to_base58_string(), - e - ), - } - })?; + let entry_gateway_lp_key = LpPublicKey::from_bytes(entry_x25519_pub.as_bytes()).map_err(|e| { + RegistrationClientError::LpRegistrationNotPossible { + node_id: format!( + "{}: invalid LP key: {}", + self.config.entry.node.identity.to_base58_string(), + e + ), + } + })?; + + let exit_x25519_pub = self.config.exit.node.identity.to_x25519().map_err(|e| { + RegistrationClientError::LpRegistrationNotPossible { + node_id: format!( + "{}: failed to convert ed25519 to x25519: {}", + self.config.exit.node.identity.to_base58_string(), + e + ), + } + })?; + + let exit_gateway_lp_key = LpPublicKey::from_bytes(exit_x25519_pub.as_bytes()).map_err(|e| { + RegistrationClientError::LpRegistrationNotPossible { + node_id: format!( + "{}: invalid LP key: {}", + self.config.exit.node.identity.to_base58_string(), + e + ), + } + })?; // Generate LP keypairs for this connection let client_lp_keypair = Arc::new(LpKeypair::default()); diff --git a/nym-registration-client/src/lp_client/client.rs b/nym-registration-client/src/lp_client/client.rs index d59f60afb5..f377ad6eb0 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -8,7 +8,7 @@ use super::error::{LpClientError, Result}; use super::transport::LpTransport; use bytes::BytesMut; use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND}; -use nym_credentials_interface::TicketType; +use nym_credentials_interface::{CredentialSpendingData, TicketType}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_lp::LpPacket; use nym_lp::codec::{parse_lp_packet, serialize_lp_packet}; @@ -267,7 +267,7 @@ impl LpRegistrationClient { // Step 4: Create state machine as initiator with derived PSK let mut state_machine = LpStateMachine::new( true, // is_initiator - &*self.local_keypair, + &self.local_keypair, &self.gateway_public_key, &psk, )?; @@ -300,6 +300,13 @@ impl LpRegistrationClient { LpAction::SendPacket(response_packet) => { tracing::trace!("Sending handshake response packet"); Self::send_packet(stream, &response_packet).await?; + + // Check if handshake completed after sending this packet + // (e.g., initiator completes after sending final message) + if state_machine.session()?.is_handshake_complete() { + tracing::info!("LP handshake completed after sending packet"); + break; + } } LpAction::HandshakeComplete => { tracing::info!("LP handshake completed successfully"); @@ -500,6 +507,80 @@ impl LpRegistrationClient { } } + /// Sends LP registration request with a pre-generated credential. + /// This is useful for testing with mock ecash credentials. + /// + /// This implements the LP registration request sending: + /// 1. Uses pre-provided bandwidth credential (skips acquisition) + /// 2. Constructs LpRegistrationRequest with dVPN mode + /// 3. Serializes request to bytes using bincode + /// 4. Encrypts via LP state machine (LpInput::SendData) + /// 5. Sends encrypted packet to gateway + pub async fn send_registration_request_with_credential( + &mut self, + wg_keypair: &x25519::KeyPair, + _gateway_identity: &ed25519::PublicKey, + credential: CredentialSpendingData, + ticket_type: TicketType, + ) -> Result<()> { + // Ensure we have a TCP connection + let stream = self.tcp_stream.as_mut().ok_or_else(|| { + LpClientError::Transport("Cannot send registration: not connected".to_string()) + })?; + + // Ensure handshake is complete (state machine exists and is in Transport state) + let state_machine = self.state_machine.as_mut().ok_or_else(|| { + LpClientError::Transport( + "Cannot send registration: handshake not completed".to_string(), + ) + })?; + + tracing::debug!("Using pre-generated credential for registration"); + + // Build registration request with pre-generated credential + let wg_public_key = PeerPublicKey::new(wg_keypair.public_key().to_bytes().into()); + let request = + LpRegistrationRequest::new_dvpn(wg_public_key, credential, ticket_type, self.client_ip); + + tracing::trace!("Built registration request: {:?}", request); + + // Serialize the request + let request_bytes = bincode::serialize(&request).map_err(|e| { + LpClientError::SendRegistrationRequest(format!("Failed to serialize request: {}", e)) + })?; + + tracing::debug!( + "Sending registration request ({} bytes)", + request_bytes.len() + ); + + // Encrypt and prepare packet via state machine + let action = state_machine + .process_input(LpInput::SendData(request_bytes)) + .ok_or_else(|| { + LpClientError::Transport("State machine returned no action".to_string()) + })? + .map_err(|e| { + LpClientError::SendRegistrationRequest(format!( + "Failed to encrypt registration request: {}", + e + )) + })?; + + // Send the encrypted packet + match action { + LpAction::SendPacket(packet) => { + Self::send_packet(stream, &packet).await?; + tracing::info!("Successfully sent registration request to gateway"); + Ok(()) + } + other => Err(LpClientError::Transport(format!( + "Unexpected action when sending registration data: {:?}", + other + ))), + } + } + /// Receives and processes the registration response from the gateway. /// /// This must be called after sending a registration request. The method: