sdk: remove superseded stream_wrapper module

This commit is contained in:
mfahampshire
2026-03-17 15:29:34 +00:00
parent 846dbba363
commit 3c92ce60ca
5 changed files with 0 additions and 2103 deletions
-2
View File
@@ -12,8 +12,6 @@ pub mod client_pool;
pub mod ip_packet_client;
pub mod ipr_wrapper;
pub mod mixnet;
// stream_wrapper is superseded by ipr_wrapper (LP frame envelope over mixnet)
// pub mod stream_wrapper;
pub mod tcp_proxy;
pub use error::{Error, Result};
-52
View File
@@ -1,52 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-2.0-only
//! High-level streaming interface for the mixnet.
//!
//! # Basic Usage
//! ## Simple Send/Receive
//!
//! ```no_run
//! use nym_sdk::stream_wrapper::{MixSocket, MixStream, NetworkEnvironment};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let env = NetworkEnvironment::Mainnet;
//!
//! // Create listener (no peer)
//! let listener_socket = MixSocket::new(env.env_file_path()).await?;
//! let listener_address = *listener_socket.local_addr();
//! let mut listener_stream = listener_socket.into_stream();
//!
//! // Create sender connected to listener
//! let mut sender_stream = MixStream::connect(listener_address, env.env_file_path()).await?;
//!
//! // Sender initiates
//! sender_stream.send(b"Hello, Mixnet!").await?;
//!
//! // Listener receives and extracts SURB tag
//! let msg = listener_stream.recv().await?;
//! assert_eq!(msg.message, b"Hello, Mixnet!");
//!
//! // Store SURB and reply anonymously
//! if let Some(surbs) = msg.sender_tag {
//! listener_stream.store_surb_tag(surbs);
//! listener_stream.send(b"Hello back!").await?;
//! }
//!
//! // Sender receives anonymous reply
//! let reply = sender_stream.recv().await?;
//! assert_eq!(reply.message, b"Hello back!");
//!
//! Ok(())
//! }
//! ```
//!
mod mixnet_stream_wrapper;
mod mixnet_stream_wrapper_ipr;
mod network_env;
pub use mixnet_stream_wrapper::{MixSocket, MixStream, MixStreamReader, MixStreamWriter};
pub use mixnet_stream_wrapper_ipr::{IpMixStream, IpMixStreamReader, IpMixStreamWriter};
pub use network_env::NetworkEnvironment;
File diff suppressed because it is too large Load Diff
@@ -1,937 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use super::mixnet_stream_wrapper::{MixStream, MixStreamReader, MixStreamWriter};
use super::network_env::NetworkEnvironment;
use crate::ip_packet_client::{
helpers::check_ipr_message_version, IprListener, MixnetMessageOutcome,
};
use crate::{mixnet::Recipient, Error};
use std::collections::HashMap;
use bytes::Bytes;
use futures::StreamExt;
use nym_crypto::asymmetric::ed25519;
use nym_ip_packet_requests::{
v8::{
request::IpPacketRequest,
response::{ConnectResponseReply, ControlResponse, IpPacketResponse, IpPacketResponseData},
},
IpPair,
};
use nym_network_defaults::ApiUrl;
use nym_validator_client::nym_api::NymApiClientExt;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::sync::oneshot;
use tracing::{debug, error, info};
const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Clone)]
pub struct IprWithPerformance {
pub(crate) address: Recipient,
pub(crate) identity: ed25519::PublicKey,
pub(crate) performance: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
Disconnected,
Connecting,
Connected,
}
/// Create a Nym API client with the provided URLs.
///
/// # Arguments
/// * `nym_api_urls` - Vector of API URLs to use for the client
///
/// # Returns
/// Configured `nym_http_api_client::Client` or an error if URLs are invalid or empty
#[allow(clippy::result_large_err)]
fn create_nym_api_client(nym_api_urls: Vec<ApiUrl>) -> Result<nym_http_api_client::Client, Error> {
let user_agent = format!("nym-sdk/{}", env!("CARGO_PKG_VERSION"));
let urls = nym_api_urls
.into_iter()
.map(|url| url.url.parse())
.collect::<Result<Vec<nym_http_api_client::Url>, _>>()
.map_err(|err| {
error!("malformed nym-api url: {err}");
Error::NoNymAPIUrl
})?;
if urls.is_empty() {
return Err(Error::NoNymAPIUrl);
}
let client = nym_http_api_client::ClientBuilder::new_with_urls(urls)?
.with_user_agent(user_agent)
.build()?;
Ok(client)
}
/// Retrieve all exit nodes with their performance scores.
///
/// Queries the network for all described nodes and filters for those with IP packet router
/// capabilities, combining node information with performance metadata.
///
/// # Arguments
/// * `client` - Nym API client to use for queries
///
/// # Returns
/// Vector of `IprWithPerformance` containing exit node addresses, identities, and performance scores
async fn retrieve_exit_nodes_with_performance(
client: nym_http_api_client::Client,
) -> Result<Vec<IprWithPerformance>, Error> {
// retrieve all nym-nodes on the network
let all_nodes = client
.get_all_described_nodes()
.await?
.into_iter()
.map(|described| (described.ed25519_identity_key(), described))
.collect::<HashMap<_, _>>();
// annoyingly there's no convenient way of doing this in a single query
// retrieve performance scores of all exit gateways
let exit_gateways = client
.get_all_basic_nodes_with_metadata()
.await?
.nodes;
let mut described = Vec::new();
for exit in exit_gateways {
if let Some(ipr_info) = all_nodes
.get(&exit.ed25519_identity_pubkey)
.and_then(|n| n.description.ip_packet_router.clone())
{
if let Ok(parsed_address) = ipr_info.address.parse() {
described.push(IprWithPerformance {
address: parsed_address,
identity: exit.ed25519_identity_pubkey,
performance: exit.performance.round_to_integer(),
})
}
}
}
Ok(described)
}
/// Select a random IPR (IP Packet Router) from available exit gateways.
///
/// Currently selects the gateway with the highest performance score.
///
/// # Arguments
/// * `client` - Nym API client to use for gateway discovery
///
/// # Returns
/// `Recipient` address of the selected IPR
async fn get_random_ipr(client: nym_http_api_client::Client) -> Result<Recipient, Error> {
let nodes = retrieve_exit_nodes_with_performance(client).await?;
info!("Found {} Exit Gateways", nodes.len());
// JS: I'm leaving the old behaviour here of choosing node with the highest performance,
// but I think you should reconsider making a pseudorandom selection weighted by some scaled performance
// otherwise all clients might end up choosing exactly the same node (I will leave this as PR comment when I get here : D)
let selected_gateway = nodes
.into_iter()
.max_by_key(|gw| gw.performance)
.ok_or_else(|| Error::NoGatewayAvailable)?;
let ipr_address = selected_gateway.address;
info!(
"Using IPR: {} (Gateway: {}, Performance: {:?})",
ipr_address, selected_gateway.identity, selected_gateway.performance
);
Ok(ipr_address)
}
/// A bidirectional stream for sending and receiving IP packets through the mixnet.
///
/// Manages connection to an IP Packet Router (IPR), handles tunnel establishment,
/// and maintains allocated IP addresses. Implements `AsyncRead` and `AsyncWrite` for
/// standard async I/O operations.
///
/// # Example
/// ```no_run
/// use nym_sdk::stream_wrapper::{IpMixStream, NetworkEnvironment};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut stream = IpMixStream::new(NetworkEnvironment::Mainnet).await?;
/// let ip_pair = stream.connect_tunnel().await?;
/// let packet_data = vec![0u8; 100];
/// stream.send_ip_packet(&packet_data).await?;
/// Ok(())
/// }
/// ```
pub struct IpMixStream {
stream: MixStream,
ipr_address: Recipient,
listener: IprListener,
allocated_ips: Option<IpPair>,
connection_state: ConnectionState,
}
impl IpMixStream {
/// Create a new IP packet router stream connected to the mixnet.
///
/// Initializes connection to mainnet by default and selects an IPR gateway.
/// Does not establish tunnel connection - call `connect_tunnel()` separately.
///
/// # Returns
/// New `IpMixStream` instance ready to connect
pub async fn new(env: NetworkEnvironment) -> Result<Self, Error> {
let network_defaults = env.network_defaults();
let api_client = create_nym_api_client(network_defaults.nym_api_urls.unwrap_or_default())?;
let ipr_address = get_random_ipr(api_client).await?;
let stream = MixStream::new(None, Some(ipr_address), Some(env.env_file_path())).await?;
Ok(Self {
stream,
ipr_address,
listener: IprListener::new(),
allocated_ips: None,
connection_state: ConnectionState::Disconnected,
})
}
/// Get the Nym network address of this stream.
///
/// # Returns
/// Reference to the stream's `Recipient` address
pub fn nym_address(&self) -> &Recipient {
self.stream.client.nym_address()
}
/// Send an IP packet request to the connected IPR.
///
/// # Arguments
/// * `request` - The `IpPacketRequest` to send
///
/// # Returns
/// `Ok(())` on success, error otherwise
async fn send_ipr_request(&mut self, request: IpPacketRequest) -> Result<(), Error> {
let request_bytes = request.to_bytes()?;
self.stream.send(&request_bytes).await
}
/// Establish tunnel connection with the IPR and allocate IP addresses.
///
/// Sends a connect request and waits for IP allocation response.
/// Updates connection state and stores allocated IPs on success.
///
/// # Returns
/// `IpPair` containing allocated IPv4 and IPv6 addresses
pub async fn connect_tunnel(&mut self) -> Result<IpPair, Error> {
if self.connection_state != ConnectionState::Disconnected {
return Err(Error::IprStreamClientAlreadyConnectedOrConnecting);
}
self.connection_state = ConnectionState::Connecting;
info!("Connecting to IP packet router: {}", self.ipr_address);
match self.connect_inner().await {
Ok(ip_pair) => {
self.allocated_ips = Some(ip_pair);
self.connection_state = ConnectionState::Connected;
info!(
"Connected to IPv4: {}, IPv6: {}",
ip_pair.ipv4, ip_pair.ipv6
);
Ok(ip_pair)
}
Err(e) => {
self.connection_state = ConnectionState::Disconnected;
error!("Failed to connect: {:?}", e);
Err(e)
}
}
}
/// Internal connection logic for establishing the tunnel.
///
/// # Returns
/// `IpPair` containing allocated IP addresses
async fn connect_inner(&mut self) -> Result<IpPair, Error> {
let (request, request_id) = IpPacketRequest::new_connect_request(None);
debug!("Sending connect request with ID: {}", request_id);
self.send_ipr_request(request).await?;
self.listen_for_connect_response(request_id).await
}
/// Listen for and process the connect response from the IPR.
///
/// Waits up to `IPR_CONNECT_TIMEOUT` for a response matching the request ID.
///
/// # Arguments
/// * `request_id` - ID of the connect request to match against responses
///
/// # Returns
/// `IpPair` containing allocated IP addresses
async fn listen_for_connect_response(&mut self, request_id: u64) -> Result<IpPair, Error> {
let timeout = tokio::time::sleep(IPR_CONNECT_TIMEOUT);
tokio::pin!(timeout);
let mut framed = self.stream.framed_read();
loop {
tokio::select! {
_ = &mut timeout => {
return Err(Error::IPRConnectResponseTimeout);
}
frame = framed.next() => {
match frame {
None => {
return Err(Error::IPRClientStreamClosed);
}
Some(Err(e)) => {
return Err(Error::MessageRecovery(e));
}
Some(Ok(reconstructed)) => {
if let Err(e) = check_ipr_message_version(&reconstructed) {
return Err(Error::IPRMessageVersionCheckFailed(e.to_string()));
}
if let Ok(response) = IpPacketResponse::from_reconstructed_message(&reconstructed) {
if response.id() == Some(request_id) {
return self.handle_connect_response(response).await;
}
else {
return Err(Error::IPRNoId)
}
}
}
}
}
}
}
}
/// Handle the connect response from the IPR.
///
/// Extracts IP allocation from successful response or returns error on failure.
///
/// # Arguments
/// * `response` - The `IpPacketResponse` to process
///
/// # Returns
/// `IpPair` on successful connection, error otherwise
async fn handle_connect_response(&self, response: IpPacketResponse) -> Result<IpPair, Error> {
let control_response = match response.data {
IpPacketResponseData::Control(c) => c,
other => return Err(Error::UnexpectedResponseType(other)),
};
match *control_response {
ControlResponse::Connect(connect_resp) => match connect_resp.reply {
ConnectResponseReply::Success(success) => Ok(success.ips),
ConnectResponseReply::Failure(reason) => Err(Error::ConnectDenied(reason)),
},
_ => Err(Error::UnexpectedResponseType(
IpPacketResponseData::Control(control_response.clone()),
)),
}
}
/// Send an IP packet through the tunnel.
///
/// Requires an active tunnel connection.
///
/// # Arguments
/// * `packet` - Raw IP packet bytes to send
///
/// # Returns
/// `Ok(())` on success, error if not connected or send fails
pub async fn send_ip_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
if self.connection_state != ConnectionState::Connected {
return Err(Error::IprStreamClientNotConnected);
}
let request = IpPacketRequest::new_data_request(packet.to_vec().into());
self.send_ipr_request(request).await
}
/// Handle incoming messages from the mixnet.
///
/// Processes reconstructed messages and extracts IP packets, disconnect signals,
/// or self-ping messages. Times out after 10 seconds if no message received.
///
/// # Returns
/// Vector of received IP packet data as `Bytes`, empty vector if no packets or on timeout
pub async fn handle_incoming(&mut self) -> Result<Vec<Bytes>, Error> {
let mut framed = self.stream.framed_read();
match tokio::time::timeout(Duration::from_secs(10), framed.next()).await {
Ok(Some(reconstructed)) => {
match self
.listener
.handle_reconstructed_message(reconstructed?)
.await
{
Ok(Some(MixnetMessageOutcome::IpPackets(packets))) => {
info!("Extracted {} IP packets", packets.len());
Ok(packets)
}
Ok(Some(MixnetMessageOutcome::Disconnect)) => {
info!("Received disconnect");
self.connection_state = ConnectionState::Disconnected;
self.allocated_ips = None;
Ok(Vec::new())
}
Ok(Some(MixnetMessageOutcome::MixnetSelfPing)) => {
debug!("Received mixnet self ping");
Ok(Vec::new())
}
Ok(None) => Ok(Vec::new()),
Err(e) => {
error!("Failed to handle message: {}", e);
Ok(Vec::new())
}
}
}
_ => Ok(Vec::new()),
}
}
/// Get the allocated IP addresses for this tunnel.
///
/// # Returns
/// `Some(&IpPair)` if IPs are allocated, `None` otherwise
pub fn allocated_ips(&self) -> Option<&IpPair> {
self.allocated_ips.as_ref()
}
/// Check if the tunnel is currently connected.
///
/// # Returns
/// `true` if connected, `false` otherwise
pub fn is_connected(&self) -> bool {
self.connection_state == ConnectionState::Connected
}
/// Disconnect inner stream client from the Mixnet - note that disconnected clients cannot currently be reconnected.
pub async fn disconnect_stream(self) {
debug!("Disconnecting");
self.stream.client.disconnect().await;
debug!("Disconnected");
}
/// Split the stream into separate reader and writer halves.
///
/// Enables concurrent read and write operations similar to `TcpStream::split()`.
/// State updates (connection status, allocated IPs) are synchronized between halves
/// via oneshot channels.
///
/// # Returns
/// Tuple of `(IpMixStreamReader, IpMixStreamWriter)`
pub fn split(self) -> (IpMixStreamReader, IpMixStreamWriter) {
debug!("Splitting IpMixStream");
let local_addr = *self.stream.client.nym_address();
let (stream_reader, stream_writer) = self.stream.split();
debug!("Split IpMixStream into Reader and Writer");
let (state_tx, state_rx) = oneshot::channel();
let (ips_tx, ips_rx) = oneshot::channel();
(
IpMixStreamReader {
stream_reader,
listener: self.listener,
allocated_ips: self.allocated_ips,
connection_state: self.connection_state.clone(),
state_tx: Some(state_tx),
ips_tx: Some(ips_tx),
},
IpMixStreamWriter {
stream_writer,
local_addr,
allocated_ips: self.allocated_ips,
connection_state: self.connection_state,
state_rx: Some(state_rx),
ips_rx: Some(ips_rx),
},
)
}
}
impl AsyncRead for IpMixStream {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.stream).poll_read(cx, buf)
}
}
impl AsyncWrite for IpMixStream {
fn poll_write(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
std::pin::Pin::new(&mut self.stream).poll_write(cx, buf)
}
fn poll_flush(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.stream).poll_flush(cx)
}
fn poll_shutdown(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.stream).poll_shutdown(cx)
}
}
/// Read half of a split `IpMixStream`.
///
/// Handles incoming messages from the mixnet and processes IP packets, disconnect
/// signals, and control messages. Synchronizes connection state changes with the
/// writer half via oneshot channels.
///
/// Created by calling `IpMixStream::split()`. Implements `AsyncRead` for standard
/// async read operations.
pub struct IpMixStreamReader {
stream_reader: MixStreamReader,
listener: IprListener,
allocated_ips: Option<IpPair>,
connection_state: ConnectionState,
state_tx: Option<oneshot::Sender<ConnectionState>>,
ips_tx: Option<oneshot::Sender<Option<IpPair>>>,
}
impl IpMixStreamReader {
/// Get the Nym network address of this reader.
///
/// # Returns
/// The reader's `Recipient` address
pub fn nym_address(self) -> Recipient {
*self.stream_reader.local_addr()
}
/// Handle incoming messages from the mixnet (reader half).
///
/// Processes reconstructed messages and extracts IP packets, disconnect signals,
/// or self-ping messages. Updates connection state and notifies writer on disconnect.
/// Times out after 10 seconds if no message received.
///
/// # Returns
/// Vector of received IP packet data as `Bytes`, empty vector if no packets or on timeout
pub async fn handle_incoming(&mut self) -> Result<Vec<Bytes>, Error> {
let mut framed = self.stream_reader.framed();
match tokio::time::timeout(Duration::from_secs(10), framed.next()).await {
Ok(Some(reconstructed)) => {
match self
.listener
.handle_reconstructed_message(reconstructed?)
.await
{
Ok(Some(MixnetMessageOutcome::IpPackets(packets))) => {
info!("Extracted {} IP packets", packets.len());
Ok(packets)
}
Ok(Some(MixnetMessageOutcome::Disconnect)) => {
info!("Received disconnect");
self.connection_state = ConnectionState::Disconnected;
self.allocated_ips = None;
// Send state update to writer
if let Some(tx) = self.state_tx.take() {
let _ = tx.send(ConnectionState::Disconnected);
}
if let Some(tx) = self.ips_tx.take() {
let _ = tx.send(None);
}
Ok(Vec::new())
}
Ok(Some(MixnetMessageOutcome::MixnetSelfPing)) => {
debug!("Received mixnet self ping");
Ok(Vec::new())
}
Ok(None) => Ok(Vec::new()),
Err(e) => {
error!("Failed to handle message: {}", e);
Ok(Vec::new())
}
}
}
_ => Ok(Vec::new()),
}
}
/// Get the allocated IP addresses (reader half).
///
/// # Returns
/// `Some(&IpPair)` if IPs are allocated, `None` otherwise
pub fn allocated_ips(&self) -> Option<&IpPair> {
self.allocated_ips.as_ref()
}
/// Check if the tunnel is currently connected (reader half).
///
/// # Returns
/// `true` if connected, `false` otherwise
pub fn is_connected(&self) -> bool {
self.connection_state == ConnectionState::Connected
}
}
impl AsyncRead for IpMixStreamReader {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream_reader).poll_read(cx, buf)
}
}
/// Write half of a split `IpMixStream`.
///
/// Handles outgoing IP packets to the mixnet. Receives connection state updates
/// from the reader half via oneshot channels to maintain synchronized state.
///
/// Created by calling `IpMixStream::split()`. Implements `AsyncWrite` for standard
/// async write operations.
pub struct IpMixStreamWriter {
stream_writer: MixStreamWriter,
local_addr: Recipient,
allocated_ips: Option<IpPair>,
connection_state: ConnectionState,
state_rx: Option<oneshot::Receiver<ConnectionState>>,
ips_rx: Option<oneshot::Receiver<Option<IpPair>>>,
}
impl IpMixStreamWriter {
/// Get the Nym network address of this writer.
///
/// # Returns
/// Reference to the writer's `Recipient` address
pub fn nym_address(&self) -> &Recipient {
&self.local_addr
}
/// Send an IP packet request to the IPR (writer half).
///
/// # Arguments
/// * `request` - The `IpPacketRequest` to send
///
/// # Returns
/// `Ok(())` on success, error otherwise
async fn send_ipr_request(&mut self, request: IpPacketRequest) -> Result<(), Error> {
let request_bytes = request.to_bytes()?;
self.stream_writer.write_bytes(&request_bytes).await
}
/// Send an IP packet through the tunnel (writer half).
///
/// Checks for state updates from reader before sending.
/// Requires an active tunnel connection.
///
/// # Arguments
/// * `packet` - Raw IP packet bytes to send
///
/// # Returns
/// `Ok(())` on success, error if not connected or send fails
pub async fn send_ip_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
// Check for state updates from reader
if let Some(mut rx) = self.state_rx.take() {
if let Ok(new_state) = rx.try_recv() {
self.connection_state = new_state;
} else {
self.state_rx = Some(rx);
}
}
if let Some(mut rx) = self.ips_rx.take() {
if let Ok(new_ips) = rx.try_recv() {
self.allocated_ips = new_ips;
} else {
self.ips_rx = Some(rx);
}
}
if self.connection_state != ConnectionState::Connected {
return Err(Error::IprStreamClientNotConnected);
}
let request = IpPacketRequest::new_data_request(packet.to_vec().into());
self.send_ipr_request(request).await
}
/// Get the allocated IP addresses (writer half).
///
/// # Returns
/// `Some(&IpPair)` if IPs are allocated, `None` otherwise
pub fn allocated_ips(&self) -> Option<&IpPair> {
self.allocated_ips.as_ref()
}
/// Check if the tunnel is connected (writer half).
///
/// # Returns
/// `true` if connected, `false` otherwise
pub fn is_connected(&self) -> bool {
self.connection_state == ConnectionState::Connected
}
}
impl AsyncWrite for IpMixStreamWriter {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.stream_writer).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream_writer).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream_writer).poll_shutdown(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ip_packet_client::helpers::{
icmp_identifier, is_icmp_echo_reply, is_icmp_v6_echo_reply, send_ping_v4, send_ping_v6,
};
use std::net::{Ipv4Addr, Ipv6Addr};
#[tokio::test]
async fn connect_to_ipr() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = IpMixStream::new(NetworkEnvironment::Mainnet).await?;
let ip_pair = stream.connect_tunnel().await?;
let ipv4: Ipv4Addr = ip_pair.ipv4;
assert!(!ipv4.is_unspecified(), "IPv4 address should not be 0.0.0.0");
let ipv6: Ipv6Addr = ip_pair.ipv6;
assert!(!ipv6.is_unspecified(), "IPv6 address should not be ::");
assert!(stream.is_connected(), "Stream should be connected");
assert!(
stream.allocated_ips().is_some(),
"Should have allocated IPs"
);
stream.disconnect_stream().await;
Ok(())
}
#[tokio::test]
#[ignore]
async fn dns_ping_checks() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = IpMixStream::new(NetworkEnvironment::Mainnet).await?;
let ip_pair = stream.connect_tunnel().await?;
info!(
"Connected with IPs - IPv4: {}, IPv6: {}",
ip_pair.ipv4, ip_pair.ipv6
);
let external_v4_targets = vec![
("Google DNS", Ipv4Addr::new(8, 8, 8, 8)),
("Cloudflare DNS", Ipv4Addr::new(1, 1, 1, 1)),
("Quad9 DNS", Ipv4Addr::new(9, 9, 9, 9)),
];
let external_v6_targets = vec![
("Google DNS", "2001:4860:4860::8888".parse::<Ipv6Addr>()?),
(
"Cloudflare DNS",
"2606:4700:4700::1111".parse::<Ipv6Addr>()?,
),
("Quad9 DNS", "2620:fe::fe".parse::<Ipv6Addr>()?),
];
let identifier = icmp_identifier();
let mut successful_v4_pings = 0;
let mut total_v4_pings = 0;
let mut successful_v6_pings = 0;
let mut total_v6_pings = 0;
for (name, target) in &external_v4_targets {
info!("Testing IPv4 connectivity to {} ({})", name, target);
for seq in 0..3 {
send_ping_v4(&mut stream, &ip_pair, seq, identifier, *target).await?;
total_v4_pings += 1;
}
}
for (name, target) in &external_v6_targets {
info!("Testing IPv6 connectivity to {} ({})", name, target);
for seq in 0..3 {
send_ping_v6(&mut stream, &ip_pair, seq, *target).await?;
total_v6_pings += 1;
}
}
let collect_timeout = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(collect_timeout);
loop {
tokio::select! {
_ = &mut collect_timeout => {
info!("Finished collecting replies");
break;
}
result = stream.handle_incoming() => {
if let Ok(packets) = result {
for packet in packets {
if let Some((reply_id, source, dest)) = is_icmp_echo_reply(&packet) {
if reply_id == identifier && dest == ip_pair.ipv4 {
successful_v4_pings += 1;
debug!("IPv4 reply from {}", source);
}
}
if let Some((reply_id, source, dest)) = is_icmp_v6_echo_reply(&packet) {
if reply_id == identifier && dest == ip_pair.ipv6 {
successful_v6_pings += 1;
debug!("IPv6 reply from {}", source);
}
}
}
}
}
}
}
let v4_success_rate = (successful_v4_pings as f64 / total_v4_pings as f64) * 100.0;
let v6_success_rate = (successful_v6_pings as f64 / total_v6_pings as f64) * 100.0;
info!(
"IPv4 external connectivity: {}/{} pings successful ({:.1}%)",
successful_v4_pings, total_v4_pings, v4_success_rate
);
info!(
"IPv6 external connectivity: {}/{} pings successful ({:.1}%)",
successful_v6_pings, total_v6_pings, v6_success_rate
);
assert!(successful_v4_pings > 0, "No IPv4 pings successful");
assert!(
v4_success_rate >= 75.0,
"IPv4 success rate < 75% (got {:.1}%)",
v4_success_rate
);
assert!(successful_v6_pings > 0, "No IPv6 pings successful");
assert!(
v6_success_rate >= 75.0,
"IPv6 success rate < 75% (got {:.1}%)",
v6_success_rate
);
stream.disconnect_stream().await;
Ok(())
}
#[tokio::test]
#[ignore]
async fn split_dns_ping_checks() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = IpMixStream::new(NetworkEnvironment::Mainnet).await?;
let ip_pair = stream.connect_tunnel().await?;
info!(
"Connected with IPs - IPv4: {}, IPv6: {}",
ip_pair.ipv4, ip_pair.ipv6
);
let (mut reader, mut writer) = stream.split();
let external_v4_targets = vec![("Google DNS", Ipv4Addr::new(8, 8, 8, 8))];
let identifier = icmp_identifier();
let mut successful_v4_pings = 0;
let mut total_v4_pings = 0;
for (name, target) in &external_v4_targets {
info!("Testing IPv4 connectivity to {} ({})", name, target);
for seq in 0..2 {
use crate::ip_packet_client::helpers::{
create_icmpv4_echo_request, wrap_icmp_in_ipv4,
};
use nym_ip_packet_requests::codec::MultiIpPacketCodec;
use pnet_packet::Packet;
let icmp_echo_request = create_icmpv4_echo_request(seq, identifier)?;
let ipv4_packet = wrap_icmp_in_ipv4(icmp_echo_request, ip_pair.ipv4, *target)?;
let bundled_packet =
MultiIpPacketCodec::bundle_one_packet(ipv4_packet.packet().to_vec().into());
writer.send_ip_packet(&bundled_packet).await?;
total_v4_pings += 1;
}
}
let collect_timeout = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(collect_timeout);
loop {
tokio::select! {
_ = &mut collect_timeout => {
info!("Finished collecting responses");
break;
}
result = reader.handle_incoming() => {
if let Ok(packets) = result {
for packet in packets {
if let Some((reply_id, source, dest)) = is_icmp_echo_reply(&packet) {
if reply_id == identifier && dest == ip_pair.ipv4 {
successful_v4_pings += 1;
debug!("IPv4 reply from {}", source);
}
}
}
}
}
}
}
let v4_success_rate = if total_v4_pings > 0 {
(successful_v4_pings as f64 / total_v4_pings as f64) * 100.0
} else {
0.0
};
info!(
"Split test - IPv4 external connectivity: {}/{} pings successful ({:.1}%)",
successful_v4_pings, total_v4_pings, v4_success_rate
);
assert!(successful_v4_pings > 0, "No pings successful");
assert!(
v4_success_rate >= 75.0,
"IPv4 success rate < 75% (got {:.1}%)",
v4_success_rate
);
Ok(())
}
}
@@ -1,55 +0,0 @@
use std::fs;
use std::path::PathBuf;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkEnvironment {
#[default]
Mainnet,
// Sandbox,
}
fn find_workspace_root() -> PathBuf {
let mut current = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(contents) = fs::read_to_string(&cargo_toml) {
// Check if this Cargo.toml defines a workspace
if contents.contains("[workspace]") {
return current;
}
}
}
if !current.pop() {
panic!("Could not find workspace root");
}
}
}
impl NetworkEnvironment {
pub fn env_file_path(&self) -> PathBuf {
let root = find_workspace_root();
match self {
Self::Mainnet => root.join("envs/mainnet.env"),
// Self::Sandbox => root.join("envs/sandbox.env"),
}
}
pub fn network_defaults(&self) -> crate::NymNetworkDetails {
match self {
Self::Mainnet => crate::NymNetworkDetails::new_mainnet(),
// Self::Sandbox => crate::NymNetworkDetails::new_sandbox(), // TODO
}
}
pub fn parse_network(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"mainnet" | "main" => Ok(Self::Mainnet),
// "sandbox" | "sand" => Ok(Self::Sandbox),
_ => Err(format!("Unknown env: {}", s)),
}
}
}