lp-reg gw flow working-ish

This commit is contained in:
durch
2025-11-07 22:51:22 +01:00
parent 55e891ae51
commit fbcc9e4782
20 changed files with 883 additions and 153 deletions
Generated
+5
View File
@@ -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",
+19
View File
@@ -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
}
+28 -28
View File
@@ -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(),
+1 -1
View File
@@ -164,7 +164,7 @@ pub struct WireguardData {
/// Start wireguard device
#[cfg(target_os = "linux")]
pub async fn start_wireguard(
ecash_manager: Arc<EcashManager>,
ecash_manager: Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
metrics: nym_node_metrics::NymNodeMetrics,
peers: Vec<Peer>,
upgrade_mode_status: nym_credential_verification::upgrade_mode::UpgradeModeStatus,
+37
View File
@@ -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
```
+32 -4
View File
@@ -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
+31 -26
View File
@@ -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
+17 -1
View File
@@ -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 {
+2 -2
View File
@@ -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);
+21 -9
View File
@@ -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<Arc<EcashManager>>,
ecash_manager:
Option<Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>>,
wireguard_peers: Option<Vec<defguard_wireguard_rs::host::Peer>>,
@@ -213,7 +214,21 @@ impl GatewayTasksBuilder {
Ok(nyxd_client)
}
async fn build_ecash_manager(&self) -> Result<Arc<EcashManager>, GatewayError> {
async fn build_ecash_manager(
&self,
) -> Result<
Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
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<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>);
}
// 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<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>)
}
async fn ecash_manager(
@@ -303,11 +319,7 @@ impl GatewayTasksBuilder {
active_clients_store: ActiveClientsStore,
) -> Result<lp_listener::LpListener, GatewayError> {
// 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?,
+5
View File
@@ -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" }
@@ -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")
}
+209 -24
View File
@@ -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<DirectoryNode>,
}
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<NymApiDirectory>,
nyxd_url: Url,
ignore_egress_epoch_role: bool,
only_wireguard: bool,
only_lp_registration: bool,
min_mixnet_performance: Option<u8>,
) -> anyhow::Result<ProbeResult> {
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<NymApiDirectory>,
nyxd_url: Url,
ignore_egress_epoch_role: bool,
only_wireguard: bool,
only_lp_registration: bool,
min_mixnet_performance: Option<u8>,
use_mock_ecash: bool,
) -> anyhow::Result<ProbeResult> {
// 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<NymApiDirectory>,
nyxd_url: Url,
use_mock_ecash: bool,
) -> anyhow::Result<ProbeResult> {
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<NymApiDirectory>,
) -> 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<ProbeResult>
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<St>(
nym_validator_client::nyxd::NyxdClient<nym_validator_client::HttpRpcClient>,
St,
>,
use_mock_ecash: bool,
) -> anyhow::Result<types::LpProbeResults>
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);
}
}
}
+178 -1
View File
@@ -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<DirectoryNode> {
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<NetworkRequesterDetails> = None; // Not needed for LP testing
let ip_packet_router: Option<IpPacketRouterDetails> =
ipr_result.map(|ipr| IpPacketRouterDetails {
address: ipr.address,
});
let authenticator: Option<AuthenticatorDetails> =
authenticator_result.map(|auth| AuthenticatorDetails {
address: auth.address,
});
let wireguard: Option<WireguardDetails> = 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<NodeIdentity, DescribedNodeWithPerformance>,
nodes: HashMap<NodeIdentity, DirectoryNode>,
+65 -15
View File
@@ -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<String>,
/// 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<String>,
/// Identity of the node to test
#[arg(long, short, value_parser = validate_node_identity, global = true)]
node: Option<NodeIdentity>,
@@ -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<String>,
#[arg(long)]
config_dir: Option<PathBuf>,
/// 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<ProbeResult> {
.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<ProbeResult> {
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<ProbeResult> {
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<ProbeResult> {
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<ProbeResult> {
nyxd_url,
args.ignore_egress_epoch_role,
args.only_wireguard,
args.only_lp_registration,
args.min_gateway_mixnet_performance,
))
.await
+23
View File
@@ -446,6 +446,23 @@ pub(crate) struct EntryGatewayArgs {
)]
#[zeroize(skip)]
pub(crate) upgrade_mode_attester_public_key: Option<ed25519::PublicKey>,
/// 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<bool>,
/// 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<bool>,
}
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
}
+2
View File
@@ -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";
+1 -18
View File
@@ -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<NymNodeWithKeys>,
exit_node: Option<NymNodeWithKeys>,
@@ -300,24 +301,6 @@ pub struct BuilderConfigBuilder {
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
}
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()
+39 -22
View File
@@ -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());
@@ -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: