Telescoping, first pass
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user