Telescoping, first pass

This commit is contained in:
durch
2025-11-25 10:33:28 +01:00
parent 4317ad3031
commit fca9f6ab13
13 changed files with 1649 additions and 162 deletions
+3
View File
@@ -1,2 +1,5 @@
nym-validator-rewarder/.sqlx/** diff=nodiff
nym-node-status-api/nym-node-status-api/.sqlx/** diff=nodiff
# Use bd merge for beads JSONL files
.beads/beads.jsonl merge=beads
+5 -2
View File
@@ -896,7 +896,8 @@ impl Client {
}
fn matches_current_host(&self, url: &Url) -> bool {
if cfg!(feature = "tunneling") {
#[cfg(feature = "tunneling")]
{
if let Some(ref front) = self.front
&& front.is_enabled()
{
@@ -904,7 +905,9 @@ impl Client {
} else {
url.host_str() == self.current_url().host_str()
}
} else {
}
#[cfg(not(feature = "tunneling"))]
{
url.host_str() == self.current_url().host_str()
}
}
+48 -2
View File
@@ -3,8 +3,8 @@
use crate::LpError;
use crate::message::{
ClientHelloData, EncryptedDataPayload, HandshakeData, KKTRequestData, KKTResponseData,
LpMessage, MessageType,
ClientHelloData, EncryptedDataPayload, ForwardPacketData, HandshakeData, KKTRequestData,
KKTResponseData, LpMessage, MessageType,
};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use bytes::BytesMut;
@@ -74,6 +74,12 @@ pub fn parse_lp_packet(src: &[u8]) -> Result<LpPacket, LpError> {
// KKT response contains serialized KKTFrame bytes
LpMessage::KKTResponse(KKTResponseData(payload_slice.to_vec()))
}
MessageType::ForwardPacket => {
// ForwardPacket has structured data
let data: ForwardPacketData = bincode::deserialize(payload_slice)
.map_err(|e| LpError::DeserializationError(e.to_string()))?;
LpMessage::ForwardPacket(data)
}
};
// Extract trailer
@@ -572,4 +578,44 @@ mod tests {
}
}
}
#[test]
fn test_forward_packet_encode_decode_roundtrip() {
let mut dst = BytesMut::new();
let forward_data = crate::message::ForwardPacketData {
target_gateway_identity: [77u8; 32],
target_lp_address: "1.2.3.4:41264".to_string(),
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 999,
counter: 555,
},
message: LpMessage::ForwardPacket(forward_data),
trailer: [0xff; TRAILER_LEN],
};
// Serialize
serialize_lp_packet(&packet, &mut dst).unwrap();
// Parse back
let decoded = parse_lp_packet(&dst).unwrap();
// Verify LP protocol handling works correctly
assert_eq!(decoded.header.session_id, 999);
assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket));
if let LpMessage::ForwardPacket(data) = decoded.message {
assert_eq!(data.target_gateway_identity, [77u8; 32]);
assert_eq!(data.target_lp_address, "1.2.3.4:41264");
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
} else {
panic!("Expected ForwardPacket message");
}
}
}
+29 -1
View File
@@ -72,6 +72,7 @@ pub enum MessageType {
ClientHello = 0x0003,
KKTRequest = 0x0004,
KKTResponse = 0x0005,
ForwardPacket = 0x0006,
}
impl MessageType {
@@ -98,6 +99,20 @@ pub struct KKTRequestData(pub Vec<u8>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KKTResponseData(pub Vec<u8>);
/// Packet forwarding request with embedded inner LP packet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForwardPacketData {
/// Target gateway's Ed25519 identity (32 bytes)
pub target_gateway_identity: [u8; 32],
/// Target gateway's LP address (IP:port string)
pub target_lp_address: String,
/// Complete inner LP packet bytes (serialized LpPacket)
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
pub inner_packet_bytes: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum LpMessage {
Busy,
@@ -106,6 +121,7 @@ pub enum LpMessage {
ClientHello(ClientHelloData),
KKTRequest(KKTRequestData),
KKTResponse(KKTResponseData),
ForwardPacket(ForwardPacketData),
}
impl Display for LpMessage {
@@ -117,6 +133,7 @@ impl Display for LpMessage {
LpMessage::ClientHello(_) => write!(f, "ClientHello"),
LpMessage::KKTRequest(_) => write!(f, "KKTRequest"),
LpMessage::KKTResponse(_) => write!(f, "KKTResponse"),
LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"),
}
}
}
@@ -127,9 +144,10 @@ impl LpMessage {
LpMessage::Busy => &[],
LpMessage::Handshake(payload) => payload.0.as_slice(),
LpMessage::EncryptedData(payload) => payload.0.as_slice(),
LpMessage::ClientHello(_) => unimplemented!(), // Structured data, serialized in encode_content
LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content
LpMessage::KKTRequest(payload) => payload.0.as_slice(),
LpMessage::KKTResponse(payload) => payload.0.as_slice(),
LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content
}
}
@@ -141,6 +159,7 @@ impl LpMessage {
LpMessage::ClientHello(_) => false, // Always has data
LpMessage::KKTRequest(payload) => payload.0.is_empty(),
LpMessage::KKTResponse(payload) => payload.0.is_empty(),
LpMessage::ForwardPacket(_) => false, // Always has data
}
}
@@ -152,6 +171,9 @@ impl LpMessage {
LpMessage::ClientHello(_) => 97, // 32 bytes x25519 key + 32 bytes ed25519 key + 32 bytes salt + 1 byte bincode overhead
LpMessage::KKTRequest(payload) => payload.0.len(),
LpMessage::KKTResponse(payload) => payload.0.len(),
LpMessage::ForwardPacket(data) => {
32 + data.target_lp_address.len() + data.inner_packet_bytes.len() + 10
}
}
}
@@ -163,6 +185,7 @@ impl LpMessage {
LpMessage::ClientHello(_) => MessageType::ClientHello,
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
LpMessage::KKTResponse(_) => MessageType::KKTResponse,
LpMessage::ForwardPacket(_) => MessageType::ForwardPacket,
}
}
@@ -187,6 +210,11 @@ impl LpMessage {
LpMessage::KKTResponse(payload) => {
dst.put_slice(&payload.0);
}
LpMessage::ForwardPacket(data) => {
let serialized =
bincode::serialize(data).expect("Failed to serialize ForwardPacketData");
dst.put_slice(&serialized);
}
}
}
}
+16 -13
View File
@@ -178,30 +178,31 @@ def create_mixnode_entry(base_dir, mix_id, port_delta, suffix, host_ip):
return entry
def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip):
def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip, gateway_name="gateway"):
"""Create a node_details entry for a gateway"""
debug(f"\n=== Creating gateway entry ===")
gateway_file = Path(base_dir) / "gateway.json"
debug(f"\n=== Creating {gateway_name} entry ===")
gateway_file = Path(base_dir) / f"{gateway_name}.json"
debug(f"Reading bonding JSON from: {gateway_file}")
with gateway_file.open("r") as json_blob:
gateway_data = json.load(json_blob)
node_details = read_node_details("gateway", suffix)
node_details = read_node_details(gateway_name, suffix)
# Get identity key from bonding JSON (already byte array)
identity = gateway_data.get("identity_key")
if not identity:
raise RuntimeError("Missing identity_key in gateway.json")
raise RuntimeError(f"Missing identity_key in {gateway_name}.json")
debug(f" ✓ Got identity_key from bonding JSON: {len(identity)} bytes")
# Get sphinx key from node-details (decoded from Base58)
sphinx_key = node_details.get("sphinx_key")
if not sphinx_key:
raise RuntimeError("Missing sphinx_key from node-details for gateway")
raise RuntimeError(f"Missing sphinx_key from node-details for {gateway_name}")
host = host_ip
mix_port = 10000 + port_delta
clients_port = 9000
# Calculate clients_port: gateway uses 9000, gateway2 uses 9001, etc.
clients_port = 9000 + (port_delta - 4)
debug(f" Using host: {host} (mix:{mix_port}, clients:{clients_port})")
entry = {
@@ -229,7 +230,7 @@ def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip):
def main(args):
if not args:
raise SystemExit("Usage: build_topology.py <output_dir> [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip]")
raise SystemExit("Usage: build_topology.py <output_dir> [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip] [gateway2_ip]")
base_dir = args[0]
suffix = args[1] if len(args) > 1 and args[1] else DEFAULT_SUFFIX
@@ -239,18 +240,20 @@ def main(args):
mix2_ip = args[3] if len(args) > 3 else "127.0.0.1"
mix3_ip = args[4] if len(args) > 4 else "127.0.0.1"
gateway_ip = args[5] if len(args) > 5 else "127.0.0.1"
gateway2_ip = args[6] if len(args) > 6 else "127.0.0.1"
debug(f"\n=== Starting topology generation ===")
debug(f"Output directory: {base_dir}")
debug(f"Node suffix: {suffix}")
debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}")
debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}, gateway2={gateway2_ip}")
# Create node_details entries with integer keys
node_details = {
1: create_mixnode_entry(base_dir, 1, 1, suffix, mix1_ip),
2: create_mixnode_entry(base_dir, 2, 2, suffix, mix2_ip),
3: create_mixnode_entry(base_dir, 3, 3, suffix, mix3_ip),
4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip)
4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip, "gateway"),
5: create_gateway_entry(base_dir, 5, 5, suffix, gateway2_ip, "gateway2")
}
# Create the NymTopology structure
@@ -262,8 +265,8 @@ def main(args):
},
"rewarded_set": {
"epoch_id": 0,
"entry_gateways": [4],
"exit_gateways": [4],
"entry_gateways": [4, 5],
"exit_gateways": [4, 5],
"layer1": [1],
"layer2": [2],
"layer3": [3],
@@ -279,7 +282,7 @@ def main(args):
print(f"✓ Generated topology with {len(node_details)} nodes")
print(f" - 3 mixnodes (layers 1, 2, 3)")
print(f" - 1 gateway (entry + exit)")
print(f" - 2 gateways (entry + exit)")
debug(f"\n=== Topology generation complete ===\n")
+80 -7
View File
@@ -20,6 +20,7 @@ MIXNODE1_CONTAINER="nym-mixnode1"
MIXNODE2_CONTAINER="nym-mixnode2"
MIXNODE3_CONTAINER="nym-mixnode3"
GATEWAY_CONTAINER="nym-gateway"
GATEWAY2_CONTAINER="nym-gateway2"
REQUESTER_CONTAINER="nym-network-requester"
SOCKS5_CONTAINER="nym-socks5-client"
@@ -28,6 +29,7 @@ ALL_CONTAINERS=(
"$MIXNODE2_CONTAINER"
"$MIXNODE3_CONTAINER"
"$GATEWAY_CONTAINER"
"$GATEWAY2_CONTAINER"
"$REQUESTER_CONTAINER"
"$SOCKS5_CONTAINER"
)
@@ -57,7 +59,7 @@ log_error() {
cleanup_host_state() {
log_info "Cleaning local nym-node state for suffix ${SUFFIX}"
for node in mix1 mix2 mix3 gateway; do
for node in mix1 mix2 mix3 gateway gateway2; do
rm -rf "$HOME/.nym/nym-nodes/${node}-${SUFFIX}"
done
}
@@ -283,6 +285,73 @@ start_gateway() {
done
log_success "Gateway is ready on port 9000"
}
# Start gateway2
start_gateway2() {
log_info "Starting $GATEWAY2_CONTAINER..."
container run \
--name "$GATEWAY2_CONTAINER" \
-m 2G \
--network "$NETWORK_NAME" \
-p 9001:9001 \
-p 10005:10005 \
-p 20005:20005 \
-p 30005:30005 \
-p 41265:41265 \
-p 51265:51265 \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing gateway2...";
nym-node run --id gateway2-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mode entry-gateway \
--mode exit-gateway \
--mixnet-bind-address=0.0.0.0:10005 \
--entry-bind-address=0.0.0.0:9001 \
--verloc-bind-address=0.0.0.0:20005 \
--http-bind-address=0.0.0.0:30005 \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--enable-lp true \
--lp-use-mock-ecash true \
--output=json \
--wireguard-enabled true \
--wireguard-userspace true \
--bonding-information-output="/localnet/gateway2.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting gateway2 with LP listener (mock ecash)...";
exec nym-node run --id gateway2-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true
'
log_success "$GATEWAY2_CONTAINER started"
# Wait for gateway2 to be ready
log_info "Waiting for gateway2 to listen on port 9001..."
local retries=0
local max_retries=30
while ! nc -z 127.0.0.1 9001 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_error "Gateway2 failed to start on port 9001"
return 1
fi
done
log_success "Gateway2 is ready on port 9001"
}
# Start network requester
start_network_requester() {
log_info "Starting $REQUESTER_CONTAINER..."
@@ -473,7 +542,7 @@ build_topology() {
# Wait for all bonding JSON files to be created
log_info "Waiting for all nodes to complete initialization..."
for file in mix1.json mix2.json mix3.json gateway.json; do
for file in mix1.json mix2.json mix3.json gateway.json gateway2.json; do
while [ ! -f "$VOLUME_PATH/$file" ]; do
echo " Waiting for $file..."
sleep 1
@@ -487,12 +556,14 @@ build_topology() {
MIX2_IP=$(container exec "$MIXNODE2_CONTAINER" hostname -i)
MIX3_IP=$(container exec "$MIXNODE3_CONTAINER" hostname -i)
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
GATEWAY2_IP=$(container exec "$GATEWAY2_CONTAINER" hostname -i)
log_info "Container IPs:"
echo " mix1: $MIX1_IP"
echo " mix2: $MIX2_IP"
echo " mix3: $MIX3_IP"
echo " gateway: $GATEWAY_IP"
echo " mix1: $MIX1_IP"
echo " mix2: $MIX2_IP"
echo " mix3: $MIX3_IP"
echo " gateway: $GATEWAY_IP"
echo " gateway2: $GATEWAY2_IP"
# Run build_topology.py in a container with access to the volumes
container run \
@@ -508,7 +579,8 @@ build_topology() {
"$MIX1_IP" \
"$MIX2_IP" \
"$MIX3_IP" \
"$GATEWAY_IP"
"$GATEWAY_IP" \
"$GATEWAY2_IP"
# Verify network.json was created
if [ -f "$VOLUME_PATH/network.json" ]; then
@@ -532,6 +604,7 @@ start_all() {
start_mixnode 2 "$MIXNODE2_CONTAINER"
start_mixnode 3 "$MIXNODE3_CONTAINER"
start_gateway
start_gateway2
build_topology
start_network_requester
start_socks5_client
+239 -1
View File
@@ -6,7 +6,7 @@ use super::messages::{LpRegistrationRequest, LpRegistrationResponse};
use super::registration::process_registration;
use super::LpHandlerState;
use crate::error::GatewayError;
use nym_lp::{keypair::PublicKey, LpMessage, LpPacket, LpSession};
use nym_lp::{keypair::PublicKey, message::ForwardPacketData, LpMessage, LpPacket, LpSession};
use nym_metrics::{add_histogram_obs, inc};
use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -186,6 +186,21 @@ impl LpConnectionHandler {
"LP registration successful for {} (session {})",
self.remote_addr, response.session_id
);
// After successful registration, keep connection open for forwarding
info!(
"Entering forwarding mode for {} (session {})",
self.remote_addr,
session.id()
);
if let Err(e) = self.handle_forwarding_loop(&session).await {
warn!(
"Forwarding loop error for {} (session {}): {}",
self.remote_addr,
session.id(),
e
);
}
} else {
warn!(
"LP registration failed for {}: {:?}",
@@ -363,6 +378,229 @@ impl LpConnectionHandler {
self.send_lp_packet(&packet).await
}
/// Forward an LP packet to another gateway
///
/// This method connects to the target gateway, forwards the inner packet bytes,
/// and returns the response. Used for hiding client IP from exit gateway.
///
/// # Arguments
/// * `forward_data` - ForwardPacketData containing target gateway info and inner packet
///
/// # Returns
/// * `Ok(Vec<u8>)` - Raw response bytes from target gateway
/// * `Err(GatewayError)` - If forwarding fails
async fn handle_forward_packet(
&mut self,
forward_data: ForwardPacketData,
) -> Result<Vec<u8>, GatewayError> {
use tokio::time::timeout;
use std::time::Duration;
inc!("lp_forward_total");
let start = std::time::Instant::now();
// Parse target gateway address
let target_addr: SocketAddr = forward_data.target_lp_address.parse().map_err(|e| {
inc!("lp_forward_failed");
GatewayError::LpProtocolError(format!("Invalid target address: {}", e))
})?;
// Connect to target gateway with timeout
let mut target_stream = match timeout(Duration::from_secs(5), TcpStream::connect(target_addr)).await {
Ok(Ok(stream)) => stream,
Ok(Err(e)) => {
inc!("lp_forward_failed");
return Err(GatewayError::LpConnectionError(format!(
"Failed to connect to target gateway: {}",
e
)));
}
Err(_) => {
inc!("lp_forward_failed");
return Err(GatewayError::LpConnectionError(
"Target gateway connection timeout".to_string(),
));
}
};
debug!(
"Forwarding packet to {} (target: {})",
target_addr, forward_data.target_lp_address
);
// Forward inner packet bytes (4-byte length prefix + packet data)
let len = forward_data.inner_packet_bytes.len() as u32;
target_stream
.write_all(&len.to_be_bytes())
.await
.map_err(|e| {
inc!("lp_forward_failed");
GatewayError::LpConnectionError(format!("Failed to send length to target: {}", e))
})?;
target_stream
.write_all(&forward_data.inner_packet_bytes)
.await
.map_err(|e| {
inc!("lp_forward_failed");
GatewayError::LpConnectionError(format!("Failed to send packet to target: {}", e))
})?;
target_stream.flush().await.map_err(|e| {
inc!("lp_forward_failed");
GatewayError::LpConnectionError(format!("Failed to flush target stream: {}", e))
})?;
// Read response from target gateway (4-byte length prefix + packet data)
let mut len_buf = [0u8; 4];
target_stream.read_exact(&mut len_buf).await.map_err(|e| {
inc!("lp_forward_failed");
GatewayError::LpConnectionError(format!("Failed to read response length from target: {}", e))
})?;
let response_len = u32::from_be_bytes(len_buf) as usize;
// Sanity check
const MAX_PACKET_SIZE: usize = 65536;
if response_len > MAX_PACKET_SIZE {
inc!("lp_forward_failed");
return Err(GatewayError::LpProtocolError(format!(
"Response size {} exceeds maximum {}",
response_len, MAX_PACKET_SIZE
)));
}
let mut response_buf = vec![0u8; response_len];
target_stream
.read_exact(&mut response_buf)
.await
.map_err(|e| {
inc!("lp_forward_failed");
GatewayError::LpConnectionError(format!("Failed to read response from target: {}", e))
})?;
// Record metrics
let duration = start.elapsed().as_secs_f64();
add_histogram_obs!(
"lp_forward_duration_seconds",
duration,
LP_DURATION_BUCKETS
);
debug!(
"Forwarding successful to {} ({} bytes response, {:.3}s)",
target_addr,
response_len,
duration
);
Ok(response_buf)
}
/// Handle incoming forwarding requests in a loop
///
/// After successful registration, the connection stays open to handle
/// ForwardPacket messages. This allows the entry gateway to relay packets
/// to exit gateways, hiding the client's IP address.
///
/// # Arguments
/// * `session` - The established LP session with the client
///
/// # Returns
/// * `Ok(())` - When connection closes gracefully
/// * `Err(GatewayError)` - On protocol errors
async fn handle_forwarding_loop(&mut self, session: &LpSession) -> Result<(), GatewayError> {
debug!(
"Entering forwarding loop for {} (session {})",
self.remote_addr,
session.id()
);
loop {
// Receive packet from client
let packet = match self.receive_lp_packet().await {
Ok(p) => p,
Err(e) => {
// Connection closed or error - exit loop gracefully
debug!(
"Forwarding loop ended for {} (session {}): {}",
self.remote_addr,
session.id(),
e
);
return Ok(());
}
};
// Verify session ID
if packet.header().session_id != session.id() {
warn!(
"Session ID mismatch in forwarding loop: expected {}, got {}",
session.id(),
packet.header().session_id
);
return Err(GatewayError::LpProtocolError(format!(
"Session ID mismatch: expected {}, got {}",
session.id(),
packet.header().session_id
)));
}
// Decrypt packet
let decrypted_bytes = session.decrypt_data(packet.message()).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to decrypt forwarding packet: {}", e))
})?;
// Deserialize to ForwardPacketData
let forward_request: ForwardPacketData =
bincode::deserialize(&decrypted_bytes).map_err(|e| {
GatewayError::LpProtocolError(format!(
"Failed to deserialize forward request: {}",
e
))
})?;
debug!(
"Forwarding request from {} to {}",
self.remote_addr, forward_request.target_lp_address
);
// Forward the packet
let response_bytes = match self.handle_forward_packet(forward_request).await {
Ok(bytes) => bytes,
Err(e) => {
warn!(
"Forwarding failed for {}: {}. Continuing loop.",
self.remote_addr, e
);
// Send error response back to client
// For now, continue the loop - client will retry if needed
continue;
}
};
// Encrypt response
let encrypted_msg = session.encrypt_data(&response_bytes).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to encrypt response: {}", e))
})?;
let response_packet = session.next_packet(encrypted_msg).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to create response packet: {}", e))
})?;
// Send response back to client
if let Err(e) = self.send_lp_packet(&response_packet).await {
warn!(
"Failed to send forwarding response to {}: {}",
self.remote_addr, e
);
return Err(e);
}
trace!("Forwarding response sent to {}", self.remote_addr);
}
}
/// Receive an LP packet from the stream with proper length-prefixed framing
async fn receive_lp_packet(&mut self) -> Result<LpPacket, GatewayError> {
use nym_lp::codec::parse_lp_packet;
+429 -33
View File
@@ -139,7 +139,7 @@ impl TestedNode {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct TestedNodeDetails {
identity: NodeIdentity,
exit_router_address: Option<Recipient>,
@@ -157,6 +157,8 @@ pub struct Probe {
credentials_args: CredentialArgs,
/// Pre-queried gateway node (used when --gateway-ip is specified)
direct_gateway_node: Option<DirectoryNode>,
/// Pre-queried exit gateway node (used when --exit-gateway-ip is specified for LP forwarding)
exit_gateway_node: Option<DirectoryNode>,
}
impl Probe {
@@ -173,6 +175,7 @@ impl Probe {
netstack_args,
credentials_args,
direct_gateway_node: None,
exit_gateway_node: None,
}
}
@@ -191,6 +194,27 @@ impl Probe {
netstack_args,
credentials_args,
direct_gateway_node: Some(gateway_node),
exit_gateway_node: None,
}
}
/// Create a probe with both entry and exit gateways pre-queried (for LP forwarding tests)
pub fn new_with_gateways(
entrypoint: NodeIdentity,
tested_node: TestedNode,
netstack_args: NetstackArgs,
credentials_args: CredentialArgs,
entry_gateway_node: DirectoryNode,
exit_gateway_node: DirectoryNode,
) -> Self {
Self {
entrypoint,
tested_node,
amnezia_args: "".into(),
netstack_args,
credentials_args,
direct_gateway_node: Some(entry_gateway_node),
exit_gateway_node: Some(exit_gateway_node),
}
}
@@ -206,6 +230,7 @@ impl Probe {
ignore_egress_epoch_role: bool,
only_wireguard: bool,
only_lp_registration: bool,
test_lp_wg: bool,
min_mixnet_performance: Option<u8>,
) -> anyhow::Result<ProbeResult> {
let tickets_materials = self.credentials_args.decode_attached_ticket_materials()?;
@@ -234,14 +259,16 @@ impl Probe {
let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await;
self.do_probe_test(
mixnet_client,
Some(mixnet_client),
storage,
mixnet_entry_gateway_id,
node_info,
directory.as_ref(),
nyxd_url,
tested_entry,
only_wireguard,
only_lp_registration,
test_lp_wg,
false, // Not using mock ecash in regular probe mode
)
.await
@@ -257,9 +284,46 @@ impl Probe {
ignore_egress_epoch_role: bool,
only_wireguard: bool,
only_lp_registration: bool,
test_lp_wg: bool,
min_mixnet_performance: Option<u8>,
use_mock_ecash: bool,
) -> anyhow::Result<ProbeResult> {
// If both gateways are pre-queried via --gateway-ip and --exit-gateway-ip,
// skip mixnet setup entirely - we have all the data we need
if self.direct_gateway_node.is_some() && self.exit_gateway_node.is_some() {
let entry_node = self.direct_gateway_node.as_ref().unwrap();
let exit_node = self.exit_gateway_node.as_ref().unwrap();
// Initialize storage (needed for credentials)
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?;
// Get node details from pre-queried nodes
let mixnet_entry_gateway_id = entry_node.identity();
let node_info = exit_node.to_testable_node()?;
return self
.do_probe_test(
None,
storage,
mixnet_entry_gateway_id,
node_info,
directory.as_ref(),
nyxd_url,
false, // tested_entry
only_wireguard,
only_lp_registration,
test_lp_wg,
use_mock_ecash,
)
.await;
}
// 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 {
@@ -332,14 +396,16 @@ impl Probe {
let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await;
self.do_probe_test(
mixnet_client,
Some(mixnet_client),
storage,
mixnet_entry_gateway_id,
node_info,
directory.as_ref(),
nyxd_url,
tested_entry,
only_wireguard,
only_lp_registration,
test_lp_wg,
use_mock_ecash,
)
.await
@@ -476,14 +542,16 @@ impl Probe {
#[allow(clippy::too_many_arguments)]
pub async fn do_probe_test<T>(
&self,
mixnet_client: nym_sdk::Result<MixnetClient>,
mixnet_client: Option<nym_sdk::Result<MixnetClient>>,
storage: T,
mixnet_entry_gateway_id: NodeIdentity,
node_info: TestedNodeDetails,
directory: Option<&NymApiDirectory>,
nyxd_url: Url,
tested_entry: bool,
only_wireguard: bool,
only_lp_registration: bool,
test_lp_wg: bool,
use_mock_ecash: bool,
) -> anyhow::Result<ProbeResult>
where
@@ -492,8 +560,8 @@ impl Probe {
{
let mut rng = rand::thread_rng();
let mixnet_client = match mixnet_client {
Ok(mixnet_client) => mixnet_client,
Err(err) => {
Some(Ok(mixnet_client)) => Some(mixnet_client),
Some(Err(err)) => {
error!("Failed to connect to mixnet: {err}");
return Ok(ProbeResult {
node: node_info.identity.to_string(),
@@ -510,45 +578,131 @@ impl Probe {
},
});
}
None => None,
};
let nym_address = *mixnet_client.nym_address();
let entry_gateway = nym_address.gateway().to_base58_string();
let (outcome, mixnet_client) = if let Some(mixnet_client) = mixnet_client {
let nym_address = *mixnet_client.nym_address();
let entry_gateway = nym_address.gateway().to_base58_string();
info!("Successfully connected to entry gateway: {entry_gateway}");
info!("Our nym address: {nym_address}");
info!("Successfully connected to entry gateway: {entry_gateway}");
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 || only_lp_registration {
(
Ok(ProbeOutcome {
as_entry: if tested_entry {
Entry::success()
} else {
Entry::NotTested
},
as_exit: None,
wg: None,
lp: None,
}),
mixnet_client,
)
// Now that we have a connected mixnet client, we can start pinging
let (outcome, mixnet_client) = if only_wireguard || only_lp_registration {
(
Ok(ProbeOutcome {
as_entry: if tested_entry {
Entry::success()
} else {
Entry::NotTested
},
as_exit: None,
wg: None,
lp: None,
}),
mixnet_client,
)
} else {
do_ping(
mixnet_client,
nym_address,
node_info.exit_router_address,
tested_entry,
)
.await
};
(outcome, Some(mixnet_client))
} else if test_lp_wg {
// No mixnet client needed for LP-WG test with pre-queried nodes
// Create default outcome and continue to LP-WG test below
(Ok(ProbeOutcome {
as_entry: Entry::NotTested,
as_exit: None,
wg: None,
lp: None,
}), None)
} else {
do_ping(
mixnet_client,
nym_address,
node_info.exit_router_address,
tested_entry,
)
.await
// For non-LP-WG modes, missing mixnet client is a failure
(Ok(ProbeOutcome {
as_entry: if tested_entry {
Entry::fail_to_connect()
} else {
Entry::EntryFailure
},
as_exit: None,
wg: None,
lp: None,
}), None)
};
let wg_outcome = if only_lp_registration {
// Skip WireGuard test when only testing LP registration
WgProbeResults::default()
} else if test_lp_wg {
// Test WireGuard via LP registration (nested session forwarding)
info!("Testing WireGuard via LP registration (no mixnet)");
// Create bandwidth controller for LP registration
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,
);
// Determine entry and exit gateways
let (entry_gateway, exit_gateway) = if let Some(exit_node) = &self.exit_gateway_node {
// Both entry and exit gateways were pre-queried (direct IP mode)
info!("Using pre-queried entry and exit gateways for LP forwarding test");
let entry_node = self
.direct_gateway_node
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Entry gateway not available"))?;
let entry_gateway = entry_node.to_testable_node()?;
let exit_gateway = exit_node.to_testable_node()?;
(entry_gateway, exit_gateway)
} else {
// Original behavior: query from directory
// The tested node is the exit
let exit_gateway = node_info.clone();
let directory = directory
.ok_or_else(|| anyhow::anyhow!("Directory is required for LP-WG test mode"))?;
let entry_gateway_node = directory.entry_gateway(&mixnet_entry_gateway_id)?;
let entry_gateway = entry_gateway_node.to_testable_node()?;
(entry_gateway, exit_gateway)
};
wg_probe_lp(
&entry_gateway,
&exit_gateway,
&bw_controller,
storage.credential_store().clone(),
use_mock_ecash,
self.amnezia_args.clone(),
self.netstack_args.clone(),
)
.await
.unwrap_or_default()
} else if let (Some(authenticator), Some(ip_address)) =
(node_info.authenticator_address, node_info.ip_address)
{
let mixnet_client = if let Some(mixnet_client) = mixnet_client {
mixnet_client
} else {
bail!(
"Mixnet client is required for authenticator WireGuard probe, run in LP mode instead"
);
};
let nym_address = *mixnet_client.nym_address();
// Start the mixnet listener that the auth clients use to receive messages.
let mixnet_listener_task =
AuthClientMixnetListener::new(mixnet_client, CancellationToken::new()).start();
@@ -595,7 +749,6 @@ impl Probe {
outcome
} else {
mixnet_client.disconnect().await;
WgProbeResults::default()
};
@@ -983,6 +1136,249 @@ where
Ok(lp_outcome)
}
/// LP-based WireGuard probe: Tests LP nested session registration + WireGuard tunnel connectivity
///
/// This function tests the full VPN flow using LP registration instead of mixnet+authenticator:
/// 1. Connects to entry gateway (outer LP session)
/// 2. Registers with exit gateway via entry forwarding (nested LP session)
/// 3. Receives WireGuard configuration from both gateways
/// 4. Tests WireGuard tunnel connectivity (IPv4/IPv6)
///
/// This validates that IP hiding works (exit sees entry IP, not client IP) and that the
/// full VPN tunnel operates correctly after LP registration.
async fn wg_probe_lp<St>(
entry_gateway: &TestedNodeDetails,
exit_gateway: &TestedNodeDetails,
bandwidth_controller: &nym_bandwidth_controller::BandwidthController<
nym_validator_client::nyxd::NyxdClient<nym_validator_client::HttpRpcClient>,
St,
>,
_storage: St,
use_mock_ecash: bool,
awg_args: String,
netstack_args: NetstackArgs,
) -> anyhow::Result<WgProbeResults>
where
St: nym_sdk::mixnet::CredentialStorage + Clone + Send + Sync + 'static,
<St as nym_sdk::mixnet::CredentialStorage>::StorageError: Send + Sync,
{
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_registration_client::{LpRegistrationClient, NestedLpSession};
info!("Starting LP-based WireGuard probe (entry→exit via forwarding)");
let mut wg_outcome = WgProbeResults::default();
// Validate that both gateways have required information
let entry_lp_address = entry_gateway
.lp_address
.ok_or_else(|| anyhow::anyhow!("Entry gateway missing LP address"))?;
let exit_lp_address = exit_gateway
.lp_address
.ok_or_else(|| anyhow::anyhow!("Exit gateway missing LP address"))?;
let entry_ip = entry_gateway
.ip_address
.ok_or_else(|| anyhow::anyhow!("Entry gateway missing IP address"))?;
let exit_ip = exit_gateway
.ip_address
.ok_or_else(|| anyhow::anyhow!("Exit gateway missing IP address"))?;
// Generate Ed25519 keypairs for LP protocol
let mut rng = rand::thread_rng();
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
// Generate WireGuard keypairs for VPN registration
let entry_wg_keypair = x25519::KeyPair::new(&mut rng);
let exit_wg_keypair = x25519::KeyPair::new(&mut rng);
// STEP 1: Establish outer LP session with entry gateway
info!("Connecting to entry gateway via LP...");
let mut entry_client = LpRegistrationClient::new_with_default_psk(
entry_lp_keypair,
entry_gateway.identity,
entry_lp_address,
entry_ip,
);
// Connect to entry gateway
if let Err(e) = entry_client.connect().await {
error!("Failed to connect to entry gateway: {}", e);
return Ok(wg_outcome);
}
// Perform handshake with entry gateway
if let Err(e) = entry_client.perform_handshake().await {
error!("Failed to handshake with entry gateway: {}", e);
return Ok(wg_outcome);
}
info!("Outer LP session with entry gateway established");
// STEP 2: Use nested session to register with exit gateway via forwarding
info!("Registering with exit gateway via entry forwarding...");
let mut nested_session = NestedLpSession::new(
exit_gateway.identity.to_bytes(),
exit_lp_address.to_string(),
exit_lp_keypair,
ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?,
);
// Convert exit gateway identity to ed25519 public key for registration
let exit_gateway_pubkey = ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?;
// Perform handshake and registration with exit gateway via forwarding
if use_mock_ecash {
info!("Note: Using mock ecash mode - gateways must be started with --lp-use-mock-ecash");
}
let exit_gateway_data = match nested_session
.handshake_and_register(
&mut entry_client,
&exit_wg_keypair,
&exit_gateway_pubkey,
bandwidth_controller,
TicketType::V1WireguardExit,
exit_ip,
)
.await
{
Ok(data) => data,
Err(e) => {
error!("Failed to register with exit gateway: {}", e);
return Ok(wg_outcome);
}
};
info!("Exit gateway registration successful via forwarding");
// STEP 3: Register with entry gateway
info!("Registering with entry gateway...");
let entry_gateway_pubkey =
ed25519::PublicKey::from_bytes(&entry_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid entry gateway identity: {}", e))?;
if let Err(e) = entry_client
.send_registration_request(
&entry_wg_keypair,
&entry_gateway_pubkey,
bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await
{
error!("Failed to send entry registration request: {}", e);
return Ok(wg_outcome);
}
let _entry_gateway_data = match entry_client.receive_registration_response().await {
Ok(data) => data,
Err(e) => {
error!("Failed to receive entry registration response: {}", e);
return Ok(wg_outcome);
}
};
info!("Entry gateway registration successful");
info!("LP registration successful for both gateways!");
wg_outcome.can_register = true;
// STEP 4: Test WireGuard tunnels using exit gateway configuration
// Convert keys to hex for netstack
let private_key_hex = hex::encode(exit_wg_keypair.private_key().to_bytes());
let public_key_hex = hex::encode(exit_gateway_data.public_key.to_bytes());
// Build WireGuard endpoint address
let wg_endpoint = format!("{}:{}", exit_ip, exit_gateway_data.endpoint.port());
info!("Exit WireGuard configuration:");
info!(" Private IPv4: {}", exit_gateway_data.private_ipv4);
info!(" Private IPv6: {}", exit_gateway_data.private_ipv6);
info!(" Endpoint: {}", wg_endpoint);
// Run tunnel tests (copied from wg_probe)
let netstack_request = crate::netstack::NetstackRequest::new(
&exit_gateway_data.private_ipv4.to_string(),
&exit_gateway_data.private_ipv6.to_string(),
&private_key_hex,
&public_key_hex,
&wg_endpoint,
&format!("http://{WG_TUN_DEVICE_IP_ADDRESS_V4}:{WG_METADATA_PORT}"),
netstack_args.netstack_download_timeout_sec,
&awg_args,
netstack_args,
);
// Perform IPv4 ping test
info!("Testing IPv4 tunnel connectivity...");
let ipv4_request = crate::netstack::NetstackRequestGo::from_rust_v4(&netstack_request);
match crate::netstack::ping(&ipv4_request) {
Ok(NetstackResult::Response(netstack_response_v4)) => {
info!(
"Wireguard probe response for IPv4: {:#?}",
netstack_response_v4
);
wg_outcome.can_query_metadata_v4 = netstack_response_v4.can_query_metadata;
wg_outcome.can_handshake_v4 = netstack_response_v4.can_handshake;
wg_outcome.can_resolve_dns_v4 = netstack_response_v4.can_resolve_dns;
wg_outcome.ping_hosts_performance_v4 =
netstack_response_v4.received_hosts as f32 / netstack_response_v4.sent_hosts as f32;
wg_outcome.ping_ips_performance_v4 =
netstack_response_v4.received_ips as f32 / netstack_response_v4.sent_ips as f32;
wg_outcome.download_duration_sec_v4 = netstack_response_v4.download_duration_sec;
wg_outcome.download_duration_milliseconds_v4 =
netstack_response_v4.download_duration_milliseconds;
wg_outcome.downloaded_file_size_bytes_v4 =
netstack_response_v4.downloaded_file_size_bytes;
wg_outcome.downloaded_file_v4 = netstack_response_v4.downloaded_file;
wg_outcome.download_error_v4 = netstack_response_v4.download_error;
}
Ok(NetstackResult::Error { error }) => {
error!("Netstack runtime error (IPv4): {error}")
}
Err(error) => {
error!("Internal error (IPv4): {error}")
}
}
// Perform IPv6 ping test
info!("Testing IPv6 tunnel connectivity...");
let ipv6_request = crate::netstack::NetstackRequestGo::from_rust_v6(&netstack_request);
match crate::netstack::ping(&ipv6_request) {
Ok(NetstackResult::Response(netstack_response_v6)) => {
info!(
"Wireguard probe response for IPv6: {:#?}",
netstack_response_v6
);
wg_outcome.can_handshake_v6 = netstack_response_v6.can_handshake;
wg_outcome.can_resolve_dns_v6 = netstack_response_v6.can_resolve_dns;
wg_outcome.ping_hosts_performance_v6 =
netstack_response_v6.received_hosts as f32 / netstack_response_v6.sent_hosts as f32;
wg_outcome.ping_ips_performance_v6 =
netstack_response_v6.received_ips as f32 / netstack_response_v6.sent_ips as f32;
wg_outcome.download_duration_sec_v6 = netstack_response_v6.download_duration_sec;
wg_outcome.download_duration_milliseconds_v6 =
netstack_response_v6.download_duration_milliseconds;
wg_outcome.downloaded_file_size_bytes_v6 =
netstack_response_v6.downloaded_file_size_bytes;
wg_outcome.downloaded_file_v6 = netstack_response_v6.downloaded_file;
wg_outcome.download_error_v6 = netstack_response_v6.download_error;
}
Ok(NetstackResult::Error { error }) => {
error!("Netstack runtime error (IPv6): {error}")
}
Err(error) => {
error!("Internal error (IPv6): {error}")
}
}
info!("LP-based WireGuard probe completed");
Ok(wg_outcome)
}
fn mixnet_debug_config(
min_gateway_performance: Option<u8>,
ignore_egress_epoch_role: bool,
+37 -4
View File
@@ -42,6 +42,12 @@ struct CliArgs {
#[arg(long, global = true)]
gateway_ip: Option<String>,
/// The address of the exit gateway for LP forwarding tests (used with --test-lp-wg)
/// When specified, --gateway-ip becomes the entry gateway and this becomes the exit gateway
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
#[arg(long, global = true)]
exit_gateway_ip: Option<String>,
/// Identity of the node to test
#[arg(long, short, value_parser = validate_node_identity, global = true)]
node: Option<NodeIdentity>,
@@ -58,6 +64,10 @@ struct CliArgs {
#[arg(long, global = true)]
only_lp_registration: bool,
/// Test WireGuard via LP registration (no mixnet) - uses nested session forwarding
#[arg(long, global = true)]
test_lp_wg: bool,
/// Disable logging during probe
#[arg(long, global = true)]
ignore_egress_epoch_role: bool,
@@ -129,11 +139,19 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
.map(|ep| ep.nyxd_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 {
let (entry, directory, gateway_node, exit_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();
// Query exit gateway if provided (for LP forwarding tests)
let exit_node = if let Some(exit_gateway_ip) = args.exit_gateway_ip {
info!("Using direct IP query mode for exit gateway: {}", exit_gateway_ip);
Some(query_gateway_by_ip(exit_gateway_ip).await?)
} else {
None
};
// 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())
@@ -143,7 +161,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
None
};
(identity, directory, Some(gateway_node))
(identity, directory, Some(gateway_node), exit_node)
} else {
// Original behavior: use directory service
let api_url = network
@@ -160,7 +178,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
directory.random_exit_with_ipr()?
};
(entry, Some(directory), None)
(entry, Some(directory), None, None)
};
let test_point = if let Some(node) = args.node {
@@ -169,7 +187,19 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
TestedNode::SameAsEntry
};
let mut trial = if let Some(gw_node) = gateway_node {
let mut trial = if let (Some(entry_node), Some(exit_node)) = (&gateway_node, &exit_gateway_node) {
// Both entry and exit gateways provided (for LP telescoping tests)
info!("Using both entry and exit gateways for LP forwarding test");
nym_gateway_probe::Probe::new_with_gateways(
entry,
test_point,
args.netstack_args,
args.credential_args,
entry_node.clone(),
exit_node.clone(),
)
} else if let Some(gw_node) = gateway_node {
// Only entry gateway provided
nym_gateway_probe::Probe::new_with_gateway(
entry,
test_point,
@@ -178,6 +208,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
gw_node,
)
} else {
// No direct gateways, use directory lookup
nym_gateway_probe::Probe::new(entry, test_point, args.netstack_args, args.credential_args)
};
@@ -208,6 +239,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
args.ignore_egress_epoch_role,
args.only_wireguard,
args.only_lp_registration,
args.test_lp_wg,
args.min_gateway_mixnet_performance,
*use_mock_ecash,
))
@@ -220,6 +252,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
args.ignore_egress_epoch_role,
args.only_wireguard,
args.only_lp_registration,
args.test_lp_wg,
args.min_gateway_mixnet_performance,
))
.await
+83 -98
View File
@@ -12,7 +12,6 @@ use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
use std::sync::Arc;
use crate::config::RegistrationClientConfig;
use crate::lp_client::{LpClientError, LpTransport};
mod builder;
mod config;
@@ -27,7 +26,7 @@ pub use builder::config::{
};
pub use config::RegistrationMode;
pub use error::RegistrationClientError;
pub use lp_client::{LpConfig, LpRegistrationClient};
pub use lp_client::{LpConfig, LpRegistrationClient, NestedLpSession};
pub use types::{
LpRegistrationResult, MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult,
};
@@ -153,6 +152,8 @@ impl RegistrationClient {
}
async fn register_lp(self) -> Result<RegistrationResult, RegistrationClientError> {
use crate::lp_client::{LpRegistrationClient, NestedLpSession};
// Extract and validate LP addresses
let entry_lp_address = self.config.entry.node.lp_address.ok_or(
RegistrationClientError::LpRegistrationNotPossible {
@@ -176,118 +177,102 @@ impl RegistrationClient {
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
// Register entry gateway via LP
let entry_fut = {
let bandwidth_controller = &self.bandwidth_controller;
let entry_keys = self.config.entry.keys.clone();
let entry_identity = self.config.entry.node.identity;
let entry_ip = self.config.entry.node.ip_address;
let entry_lp_keys = entry_lp_keypair.clone();
// STEP 1: Establish outer session with entry gateway
// This creates the LP connection that will be used to forward packets to exit
tracing::info!("Establishing outer session with entry gateway");
let mut entry_client = LpRegistrationClient::new_with_default_psk(
entry_lp_keypair.clone(),
self.config.entry.node.identity,
entry_lp_address,
self.config.entry.node.ip_address,
);
async move {
let mut client = LpRegistrationClient::new_with_default_psk(
entry_lp_keys,
entry_identity,
entry_lp_address,
entry_ip,
);
// Connect
client.connect().await?;
// Perform handshake
client.perform_handshake().await?;
// Send registration request
client
.send_registration_request(
&entry_keys,
&entry_identity,
&**bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await?;
// Receive registration response
let gateway_data = client.receive_registration_response().await?;
// Convert to transport for ongoing communication
let transport = client.into_transport()?;
Ok::<(LpTransport, _), LpClientError>((transport, gateway_data))
}
};
// Register exit gateway via LP
let exit_fut = {
let bandwidth_controller = &self.bandwidth_controller;
let exit_keys = self.config.exit.keys.clone();
let exit_identity = self.config.exit.node.identity;
let exit_ip = self.config.exit.node.ip_address;
let exit_lp_keys = exit_lp_keypair;
async move {
let mut client = LpRegistrationClient::new_with_default_psk(
exit_lp_keys,
exit_identity,
exit_lp_address,
exit_ip,
);
// Connect
client.connect().await?;
// Perform handshake
client.perform_handshake().await?;
// Send registration request
client
.send_registration_request(
&exit_keys,
&exit_identity,
&**bandwidth_controller,
TicketType::V1WireguardExit,
)
.await?;
// Receive registration response
let gateway_data = client.receive_registration_response().await?;
// Convert to transport for ongoing communication
let transport = client.into_transport()?;
Ok::<(LpTransport, _), LpClientError>((transport, gateway_data))
}
};
// Execute registrations in parallel
let (entry_result, exit_result) =
Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await;
// Handle entry gateway result
// Note: entry_transport is dropped here, closing the LP connection
let (_entry_transport, entry_gateway_data) =
entry_result.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
// Connect to entry gateway
entry_client
.connect()
.await
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
gateway_id: self.config.entry.node.identity.to_base58_string(),
lp_address: entry_lp_address,
source: Box::new(source),
})?;
// Handle exit gateway result
// Note: exit_transport is dropped here, closing the LP connection
let (_exit_transport, exit_gateway_data) =
exit_result.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
// Perform handshake with entry gateway (outer session now established)
entry_client
.perform_handshake()
.await
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
gateway_id: self.config.entry.node.identity.to_base58_string(),
lp_address: entry_lp_address,
source: Box::new(source),
})?;
tracing::info!("Outer session with entry gateway established");
// STEP 2: Use nested session to register with exit gateway via forwarding
// This hides the client's IP address from the exit gateway
tracing::info!("Registering with exit gateway via entry forwarding");
let mut nested_session = NestedLpSession::new(
self.config.exit.node.identity.to_bytes(),
exit_lp_address.to_string(),
exit_lp_keypair,
self.config.exit.node.identity,
);
// Perform handshake and registration with exit gateway (all via entry forwarding)
let exit_gateway_data = nested_session
.handshake_and_register(
&mut entry_client,
&self.config.exit.keys,
&self.config.exit.node.identity,
&*self.bandwidth_controller,
TicketType::V1WireguardExit,
self.config.exit.node.ip_address,
)
.await
.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
gateway_id: self.config.exit.node.identity.to_base58_string(),
lp_address: exit_lp_address,
source: Box::new(source),
})?;
tracing::info!("Exit gateway registration completed via forwarding");
// STEP 3: Send registration request to entry gateway
tracing::info!("Sending registration request to entry gateway");
entry_client
.send_registration_request(
&self.config.entry.keys,
&self.config.entry.node.identity,
&*self.bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
gateway_id: self.config.entry.node.identity.to_base58_string(),
lp_address: entry_lp_address,
source: Box::new(source),
})?;
// Receive registration response from entry
let entry_gateway_data = entry_client
.receive_registration_response()
.await
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
gateway_id: self.config.entry.node.identity.to_base58_string(),
lp_address: entry_lp_address,
source: Box::new(source),
})?;
tracing::info!("Entry gateway registration successful");
tracing::info!(
"LP registration successful for both gateways (LP connections will be closed)"
);
// LP is registration-only. All data flows through WireGuard after this point.
// The LP transports have been dropped, automatically closing TCP connections.
// The entry LP connection will be dropped, automatically closing TCP connection.
// Exit registration was completed via forwarding through entry, so no direct connection exists.
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
entry_gateway_data,
exit_gateway_data,
@@ -12,6 +12,7 @@ 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};
use nym_lp::message::ForwardPacketData;
use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine};
use nym_registration_common::{GatewayData, LpRegistrationRequest, LpRegistrationResponse};
use nym_wireguard_types::PeerPublicKey;
@@ -734,6 +735,140 @@ impl LpRegistrationClient {
Ok(gateway_data)
}
/// Sends a ForwardPacket message to the entry gateway for forwarding to the exit gateway.
///
/// This method constructs a ForwardPacket containing the target gateway's identity,
/// address, and the inner LP packet bytes, encrypts it through the outer session
/// (client-entry), and receives the response from the exit gateway via the entry gateway.
///
/// # Arguments
/// * `target_identity` - Target gateway's Ed25519 identity (32 bytes)
/// * `target_address` - Target gateway's LP address (e.g., "1.1.1.1:41264")
/// * `inner_packet_bytes` - Complete inner LP packet bytes to forward to exit gateway
///
/// # Returns
/// * `Ok(Vec<u8>)` - Decrypted response bytes from the exit gateway
///
/// # Errors
/// Returns an error if:
/// - No connection is established
/// - Handshake has not been completed
/// - Serialization fails
/// - Encryption or network transmission fails
/// - Response decryption fails
///
/// # Example Flow
/// ```ignore
/// // Construct inner packet for exit gateway (ClientHello, handshake, etc.)
/// let inner_packet = LpPacket::new(...);
/// let inner_bytes = serialize_lp_packet(&inner_packet, &mut BytesMut::new())?;
///
/// // Forward through entry gateway
/// let response_bytes = client.send_forward_packet(
/// exit_identity,
/// "2.2.2.2:41264".to_string(),
/// inner_bytes.to_vec(),
/// ).await?;
/// ```
pub async fn send_forward_packet(
&mut self,
target_identity: [u8; 32],
target_address: String,
inner_packet_bytes: Vec<u8>,
) -> Result<Vec<u8>> {
// Ensure we have a TCP connection
let stream = self.tcp_stream.as_mut().ok_or_else(|| {
LpClientError::Transport("Cannot send forward packet: 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 forward packet: handshake not completed".to_string(),
)
})?;
tracing::debug!(
"Sending ForwardPacket to {} ({} inner bytes)",
target_address,
inner_packet_bytes.len()
);
// 1. Construct ForwardPacketData
let forward_data = ForwardPacketData {
target_gateway_identity: target_identity,
target_lp_address: target_address.clone(),
inner_packet_bytes,
};
// 2. Serialize the ForwardPacketData
let forward_data_bytes = bincode::serialize(&forward_data).map_err(|e| {
LpClientError::Transport(format!("Failed to serialize ForwardPacketData: {}", e))
})?;
tracing::trace!(
"Serialized ForwardPacketData ({} bytes)",
forward_data_bytes.len()
);
// 3. Encrypt and prepare packet via state machine
let action = state_machine
.process_input(LpInput::SendData(forward_data_bytes))
.ok_or_else(|| {
LpClientError::Transport("State machine returned no action".to_string())
})?
.map_err(|e| {
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {}", e))
})?;
// 4. Send the encrypted packet
match action {
LpAction::SendPacket(packet) => {
Self::send_packet(stream, &packet).await?;
tracing::trace!("Sent encrypted ForwardPacket to entry gateway");
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when sending ForwardPacket: {:?}",
other
)));
}
}
// 5. Receive the response from entry gateway
let response_packet = Self::receive_packet(stream).await?;
tracing::trace!("Received response packet from entry gateway");
// 6. Decrypt via state machine
let action = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
.ok_or_else(|| {
LpClientError::Transport("State machine returned no action".to_string())
})?
.map_err(|e| {
LpClientError::Transport(format!("Failed to decrypt forward response: {}", e))
})?;
// 7. Extract decrypted response data
let response_data = match action {
LpAction::DeliverData(data) => data,
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when receiving forward response: {:?}",
other
)));
}
};
tracing::debug!(
"Successfully received forward response from {} ({} bytes)",
target_address,
response_data.len()
);
Ok(response_data.to_vec())
}
/// Converts this client into an LpTransport for ongoing post-handshake communication.
///
/// This consumes the client and transfers ownership of the TCP stream and state machine
+2 -1
View File
@@ -33,9 +33,10 @@
mod client;
mod config;
mod error;
mod nested_session;
mod transport;
pub use client::LpRegistrationClient;
pub use config::LpConfig;
pub use error::LpClientError;
pub use transport::LpTransport;
pub use nested_session::NestedLpSession;
@@ -0,0 +1,543 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Nested LP session for client-exit handshake through entry gateway forwarding.
//!
//! This module implements the inner LP session management where a client establishes
//! a secure connection with an exit gateway by forwarding LP packets through an
//! entry gateway. This hides the client's IP address from the exit gateway.
//!
//! # Architecture
//!
//! ```text
//! Client ←→ Entry Gateway (outer session, encrypted)
//! ↓ forwards
//! Exit Gateway (inner session, client establishes handshake)
//! ```
//!
//! The entry gateway sees the client's IP but doesn't know the final destination.
//! The exit gateway processes the LP handshake but only sees the entry gateway's IP.
use super::client::LpRegistrationClient;
use super::error::{LpClientError, Result};
use bytes::BytesMut;
use nym_bandwidth_controller::BandwidthTicketProvider;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet};
use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine};
use nym_lp::{LpMessage, LpPacket};
use nym_registration_common::{GatewayData, LpRegistrationRequest, LpRegistrationResponse};
use nym_wireguard_types::PeerPublicKey;
use std::net::IpAddr;
use std::sync::Arc;
/// Manages a nested LP session where the client establishes a handshake with
/// an exit gateway by forwarding packets through an entry gateway.
///
/// # Example
///
/// ```ignore
/// // Outer session already established with entry gateway
/// let mut outer_client = LpRegistrationClient::new(...);
/// outer_client.connect().await?;
/// outer_client.perform_handshake().await?;
///
/// // Now establish inner session with exit gateway
/// let nested = NestedLpSession::new(
/// exit_identity,
/// "2.2.2.2:41264".to_string(),
/// client_keypair,
/// exit_public_key,
/// );
///
/// let exit_session = nested.perform_handshake(&mut outer_client).await?;
/// ```
pub struct NestedLpSession {
/// Exit gateway's Ed25519 identity (32 bytes)
exit_identity: [u8; 32],
/// Exit gateway's LP address (e.g., "2.2.2.2:41264")
exit_address: String,
/// Client's Ed25519 keypair (for PSQ authentication and X25519 derivation)
client_keypair: Arc<ed25519::KeyPair>,
/// Exit gateway's Ed25519 public key
exit_public_key: ed25519::PublicKey,
/// LP state machine for exit gateway session (populated after handshake)
state_machine: Option<LpStateMachine>,
}
impl NestedLpSession {
/// Creates a new nested LP session handler.
///
/// # Arguments
/// * `exit_identity` - Exit gateway's Ed25519 identity (32 bytes)
/// * `exit_address` - Exit gateway's LP address (e.g., "2.2.2.2:41264")
/// * `client_keypair` - Client's Ed25519 keypair
/// * `exit_public_key` - Exit gateway's Ed25519 public key
pub fn new(
exit_identity: [u8; 32],
exit_address: String,
client_keypair: Arc<ed25519::KeyPair>,
exit_public_key: ed25519::PublicKey,
) -> Self {
Self {
exit_identity,
exit_address,
client_keypair,
exit_public_key,
state_machine: None,
}
}
/// Performs the LP handshake with the exit gateway by forwarding packets
/// through the entry gateway.
///
/// This method:
/// 1. Generates ClientHello for exit gateway
/// 2. Creates LP state machine for exit handshake
/// 3. Runs handshake loop, forwarding all packets through entry gateway
/// 4. Stores established session in internal state machine
///
/// # Arguments
/// * `outer_client` - Connected LP client with established outer session to entry gateway
///
/// # Errors
/// Returns an error if:
/// - Packet serialization/parsing fails
/// - Forwarding through entry gateway fails
/// - Exit gateway handshake fails
/// - Cryptographic operations fail
async fn perform_handshake(
&mut self,
outer_client: &mut LpRegistrationClient,
) -> Result<()> {
tracing::debug!(
"Starting nested LP handshake with exit gateway {}",
self.exit_address
);
// Step 1: Derive X25519 keys from Ed25519 for Noise protocol
let client_x25519_public = self
.client_keypair
.public_key()
.to_x25519()
.map_err(|e| {
LpClientError::Crypto(format!("Failed to derive X25519 public key: {}", e))
})?;
// Step 2: Generate ClientHello for exit gateway
let client_hello_data = nym_lp::ClientHelloData::new_with_fresh_salt(
client_x25519_public.to_bytes(),
self.client_keypair.public_key().to_bytes(),
);
let salt = client_hello_data.salt;
tracing::trace!(
"Generated ClientHello for exit gateway (timestamp: {})",
client_hello_data.extract_timestamp()
);
// Step 3: Send ClientHello to exit gateway via forwarding
let client_hello_header = nym_lp::packet::LpHeader::new(
0, // session_id not yet established
0, // counter starts at 0
);
let client_hello_packet = nym_lp::LpPacket::new(
client_hello_header,
LpMessage::ClientHello(client_hello_data),
);
// Serialize and forward ClientHello
let client_hello_bytes = Self::serialize_packet(&client_hello_packet)?;
let _response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
client_hello_bytes,
)
.await?;
tracing::debug!("Sent ClientHello to exit gateway via entry");
// Step 4: Create state machine for exit gateway handshake
let mut state_machine = LpStateMachine::new(
true, // is_initiator
(
self.client_keypair.private_key(),
self.client_keypair.public_key(),
),
&self.exit_public_key,
&salt,
)?;
// Step 5: Start handshake - send initial handshake packet
if let Some(action) = state_machine.process_input(LpInput::StartHandshake) {
match action? {
LpAction::SendPacket(packet) => {
tracing::trace!("Sending initial handshake packet to exit");
let packet_bytes = Self::serialize_packet(&packet)?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?;
// Parse response and feed to state machine
let response_packet = Self::parse_packet(&response_bytes)?;
tracing::trace!("Received handshake response from exit");
// Process response through state machine
if let Some(action) =
state_machine.process_input(LpInput::ReceivePacket(response_packet))
{
match action? {
LpAction::SendPacket(response_packet) => {
// Send response packet
tracing::trace!("Sending handshake response to exit");
let packet_bytes = Self::serialize_packet(&response_packet)?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?;
// Check if handshake completed after sending
if state_machine.session()?.is_handshake_complete() {
tracing::info!(
"Nested LP handshake completed with exit gateway"
);
self.state_machine = Some(state_machine);
return Ok(());
}
// Process the response from exit gateway
let response_packet = Self::parse_packet(&response_bytes)?;
if let Some(action) = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
{
match action? {
LpAction::HandshakeComplete => {
tracing::info!(
"Nested LP handshake completed with exit gateway"
);
self.state_machine = Some(state_machine);
return Ok(());
}
LpAction::SendPacket(_) => {
// More rounds needed - fall through to loop
tracing::trace!("More handshake rounds needed");
}
other => {
tracing::trace!("Action after send: {:?}", other);
}
}
}
}
LpAction::HandshakeComplete => {
tracing::info!("Nested LP handshake completed with exit gateway");
self.state_machine = Some(state_machine);
return Ok(());
}
LpAction::KKTComplete => {
tracing::info!("KKT exchange completed with exit, starting Noise");
// After KKT completes, initiator must send first Noise handshake message
let noise_msg = state_machine
.session()?
.prepare_handshake_message()
.ok_or_else(|| {
LpClientError::Transport(
"No handshake message available after KKT".to_string(),
)
})??;
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
tracing::trace!("Sending first Noise handshake message to exit");
let packet_bytes = Self::serialize_packet(&noise_packet)?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?;
// Process the Noise response from exit gateway
let response_packet = Self::parse_packet(&response_bytes)?;
if let Some(action) = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
{
match action? {
LpAction::HandshakeComplete => {
tracing::info!(
"Nested LP handshake completed with exit gateway"
);
self.state_machine = Some(state_machine);
return Ok(());
}
LpAction::SendPacket(final_packet) => {
tracing::trace!("Sending final handshake packet to exit");
let packet_bytes = Self::serialize_packet(&final_packet)?;
let _ = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?;
// Check if complete after sending final packet
if state_machine.session()?.is_handshake_complete() {
tracing::info!(
"Nested LP handshake completed with exit gateway"
);
self.state_machine = Some(state_machine);
return Ok(());
}
}
other => {
tracing::trace!(
"Action after Noise response: {:?}",
other
);
}
}
}
}
other => {
tracing::trace!("Received action during handshake: {:?}", other);
}
}
}
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action at handshake start: {:?}",
other
)));
}
}
}
// If we reach here, the handshake didn't complete properly
Err(LpClientError::Transport(
"Nested handshake completed without reaching HandshakeComplete state".to_string(),
))
}
/// Performs handshake and registration with the exit gateway via forwarding.
///
/// This is the main entry point for nested LP registration. It:
/// 1. Performs handshake with exit gateway (via `perform_handshake`)
/// 2. Builds and sends registration request through the forwarded connection
/// 3. Receives and processes registration response
/// 4. Returns gateway data on successful registration
///
/// # Arguments
/// * `outer_client` - Connected LP client with established outer session to entry gateway
/// * `wg_keypair` - Client's WireGuard x25519 keypair
/// * `gateway_identity` - Exit gateway's Ed25519 identity (for credential verification)
/// * `bandwidth_controller` - Provider for bandwidth credentials
/// * `ticket_type` - Type of bandwidth ticket to use
/// * `client_ip` - Client IP address for registration metadata
///
/// # Returns
/// * `Ok(GatewayData)` - Exit gateway configuration data on successful registration
///
/// # Errors
/// Returns an error if:
/// - Handshake fails
/// - Credential acquisition fails
/// - Request serialization/encryption fails
/// - Forwarding through entry gateway fails
/// - Response decryption/deserialization fails
/// - Gateway rejects the registration
pub async fn handshake_and_register(
&mut self,
outer_client: &mut LpRegistrationClient,
wg_keypair: &x25519::KeyPair,
gateway_identity: &ed25519::PublicKey,
bandwidth_controller: &dyn BandwidthTicketProvider,
ticket_type: TicketType,
client_ip: IpAddr,
) -> Result<GatewayData> {
// Step 1: Perform handshake with exit gateway via forwarding
self.perform_handshake(outer_client).await?;
// Step 2: Get the state machine (must exist after successful handshake)
let state_machine = self.state_machine.as_mut().ok_or_else(|| {
LpClientError::Transport("State machine missing after handshake".to_string())
})?;
tracing::debug!("Building registration request for exit gateway");
// Step 3: Acquire bandwidth credential
let credential = bandwidth_controller
.get_ecash_ticket(ticket_type, *gateway_identity, nym_bandwidth_controller::DEFAULT_TICKETS_TO_SPEND)
.await
.map_err(|e| {
LpClientError::Transport(format!(
"Failed to acquire bandwidth credential: {}",
e
))
})?
.data;
// Step 4: Build registration request
let wg_public_key = PeerPublicKey::new(wg_keypair.public_key().to_bytes().into());
let request = LpRegistrationRequest::new_dvpn(wg_public_key, credential, ticket_type, client_ip);
tracing::trace!("Built registration request: {:?}", request);
// Step 5: Serialize the request
let request_bytes = bincode::serialize(&request).map_err(|e| {
LpClientError::Transport(format!("Failed to serialize registration request: {}", e))
})?;
tracing::debug!(
"Sending registration request to exit gateway via forwarding ({} bytes)",
request_bytes.len()
);
// Step 6: 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::Transport(format!(
"Failed to encrypt registration request: {}",
e
))
})?;
// Step 7: Send the encrypted packet via forwarding
let response_bytes = match action {
LpAction::SendPacket(packet) => {
let packet_bytes = Self::serialize_packet(&packet)?;
outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when sending registration data: {:?}",
other
)));
}
};
tracing::trace!("Received registration response from exit gateway");
// Step 8: Parse response bytes to LP packet
let response_packet = Self::parse_packet(&response_bytes)?;
// Step 9: Decrypt via state machine
let action = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
.ok_or_else(|| {
LpClientError::Transport("State machine returned no action".to_string())
})?
.map_err(|e| {
LpClientError::Transport(format!(
"Failed to decrypt registration response: {}",
e
))
})?;
// Step 10: Extract decrypted data
let response_data = match action {
LpAction::DeliverData(data) => data,
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when receiving registration response: {:?}",
other
)));
}
};
// Step 11: Deserialize the response
let response: LpRegistrationResponse =
bincode::deserialize(&response_data).map_err(|e| {
LpClientError::Transport(format!(
"Failed to deserialize registration response: {}",
e
))
})?;
tracing::debug!(
"Received registration response from exit: success={}, session_id={}",
response.success,
response.session_id
);
// Step 12: Validate and extract GatewayData
if !response.success {
let error_msg = response
.error
.unwrap_or_else(|| "Unknown error".to_string());
tracing::warn!("Exit gateway rejected registration: {}", error_msg);
return Err(LpClientError::RegistrationRejected { reason: error_msg });
}
// Extract gateway_data
let gateway_data = response.gateway_data.ok_or_else(|| {
LpClientError::Transport(
"Gateway response missing gateway_data despite success=true".to_string(),
)
})?;
tracing::info!(
"Exit gateway registration successful! Session ID: {}, Allocated bandwidth: {} bytes",
response.session_id,
response.allocated_bandwidth
);
Ok(gateway_data)
}
/// Serializes an LP packet to bytes.
///
/// # Arguments
/// * `packet` - The LP packet to serialize
///
/// # Returns
/// * `Ok(Vec<u8>)` - Serialized packet bytes
///
/// # Errors
/// Returns an error if serialization fails
fn serialize_packet(packet: &LpPacket) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
serialize_lp_packet(packet, &mut buf).map_err(|e| {
LpClientError::Transport(format!("Failed to serialize LP packet: {}", e))
})?;
Ok(buf.to_vec())
}
/// Parses an LP packet from bytes.
///
/// # Arguments
/// * `bytes` - The bytes to parse
///
/// # Returns
/// * `Ok(LpPacket)` - Parsed LP packet
///
/// # Errors
/// Returns an error if parsing fails
fn parse_packet(bytes: &[u8]) -> Result<LpPacket> {
parse_lp_packet(bytes).map_err(|e| {
LpClientError::Transport(format!("Failed to parse LP packet: {}", e))
})
}
}