diff --git a/smolmix/examples/cloudflare_ping.rs b/smolmix/examples/cloudflare_ping.rs new file mode 100644 index 0000000000..a8d08c6e0f --- /dev/null +++ b/smolmix/examples/cloudflare_ping.rs @@ -0,0 +1,283 @@ +use rustls::{pki_types::ServerName, ClientConfig, ClientConnection}; +use smolmix::{create_device, SmolmixError}; +use std::{ + io::{self, Read, Write}, + sync::Arc, +}; +use tracing::info; + +use nym_sdk::stream_wrapper::IpMixStream; +use smoltcp::{ + iface::{Config, Interface, SocketSet}, + socket::tcp, + time::Instant, + wire::{HardwareAddress, IpAddress, IpCidr, Ipv4Address}, +}; +use std::sync::Once; +use std::time::Duration; + +static INIT: Once = Once::new(); + +pub struct TlsOverTcp { + pub conn: ClientConnection, +} + +impl TlsOverTcp { + pub fn new(domain: &str) -> Result { + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = ServerName::try_from(domain) + .map_err(|_| SmolmixError::InvalidDnsName)? + .to_owned(); + + let conn = ClientConnection::new(Arc::new(config), server_name) + .map_err(|_| SmolmixError::TlsHandshakeFailed)?; + + Ok(Self { conn }) + } + + /// Move data from TLS connection to TCP socket + pub fn write_tls(&mut self, socket: &mut tcp::Socket) -> Result<(), SmolmixError> { + let mut buf = [0u8; 4096]; + while self.conn.wants_write() { + match self.conn.write_tls(&mut buf.as_mut_slice()) { + Ok(n) if n > 0 => { + socket + .send_slice(&buf[..n]) + .map_err(|_| SmolmixError::TlsHandshakeFailed)?; + } + _ => break, + } + } + Ok(()) + } + + /// Move data from TCP socket to TLS connection + pub fn read_tls(&mut self, socket: &mut tcp::Socket) -> Result<(), SmolmixError> { + if socket.can_recv() { + let _ = socket.recv(|chunk| { + if !chunk.is_empty() { + inspect_tls_packet(chunk); + let _ = self.conn.read_tls(&mut io::Cursor::new(&mut *chunk)); + let _ = self.conn.process_new_packets(); + } + (chunk.len(), ()) + }); + } + Ok(()) + } + + pub fn send(&mut self, data: &[u8], socket: &mut tcp::Socket) -> Result<(), SmolmixError> { + self.conn + .writer() + .write_all(data) + .map_err(|_| SmolmixError::TlsHandshakeFailed)?; + self.write_tls(socket) + } + + pub fn recv(&mut self, socket: &mut tcp::Socket) -> Result, SmolmixError> { + self.read_tls(socket)?; + let mut result = Vec::new(); + let mut buf = vec![0u8; 4096]; + match self.conn.reader().read(&mut buf) { + Ok(n) if n > 0 => result.extend_from_slice(&buf[..n]), + _ => {} + } + Ok(result) + } +} + +fn inspect_tls_packet(data: &[u8]) { + if data.len() < 5 { + return; + } + let content_type = data[0]; + if content_type < 0x14 || content_type > 0x17 { + return; + } + let version = u16::from_be_bytes([data[1], data[2]]); + let length = u16::from_be_bytes([data[3], data[4]]); + info!( + "TLS packet: ContentType={:#04x}, Version={:#06x}, Length={}", + content_type, version, length + ); + if content_type == 0x16 && data.len() > 5 { + let handshake_type = data[5]; + let handshake_types = match handshake_type { + 0x01 => "ClientHello", + 0x02 => "ServerHello", + 0x0b => "Certificate", + 0x0c => "ServerKeyExchange", + 0x0d => "CertificateRequest", + 0x0e => "ServerHelloDone", + 0x0f => "CertificateVerify", + 0x10 => "ClientKeyExchange", + 0x14 => "Finished", + _ => "Unknown", + }; + info!( + "Handshake type: {:#04x} ({}), Length: {}", + handshake_type, handshake_types, length + ); + } +} + +fn init_logging() { + if tracing::dispatcher::has_been_set() { + return; + } + INIT.call_once(|| { + nym_bin_common::logging::setup_tracing_logger(); + }); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + init_logging(); + + let ipr_stream = IpMixStream::new().await?; + let (mut device, bridge, allocated_ips) = create_device(ipr_stream).await?; + info!("Allocated IP: {}", allocated_ips.ipv4); + + tokio::spawn(async move { + bridge.run().await.unwrap(); + }); + + let config = Config::new(HardwareAddress::Ip); + let mut iface = Interface::new(config, &mut device, Instant::now()); + iface.update_ip_addrs(|ip_addrs| { + ip_addrs + .push(IpCidr::new(IpAddress::from(allocated_ips.ipv4), 32)) + .unwrap(); + }); + iface + .routes_mut() + .add_default_ipv4_route(Ipv4Address::UNSPECIFIED) + .unwrap(); + + let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; 16384]); + let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; 4096]); + let tcp_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer); + let mut sockets = SocketSet::new(vec![]); + let tcp_handle = sockets.add(tcp_socket); + + let target_ip = Ipv4Address::new(1, 1, 1, 1); + let target_port = 443; + + let mut timestamp = Instant::from_millis(0); + let start = tokio::time::Instant::now(); + let mut connected = false; + let mut tls = None; + let mut handshake_completed = false; + let mut request_sent = false; + + loop { + if start.elapsed() > Duration::from_secs(60) { + info!("Test timeout after 60 seconds"); + break; + } + + iface.poll(timestamp, &mut device, &mut sockets); + timestamp += smoltcp::time::Duration::from_millis(1); + let socket = sockets.get_mut::(tcp_handle); + + // TCP connection setup + if !connected && !socket.is_open() { + match socket.connect(iface.context(), (target_ip, target_port), 49152) { + Ok(_) => { + info!("TCP connect started"); + connected = true; + } + Err(e) => { + info!("TCP connect failed: {}", e); + break; + } + } + } + + // TLS setup after TCP established + if socket.state() == tcp::State::Established && tls.is_none() { + info!("TCP established - creating TLS connection"); + match TlsOverTcp::new("cloudflare.com") { + Ok(t) => tls = Some(t), + Err(e) => { + info!("TLS create failed: {}", e); + break; + } + } + } + + // TLS handshake and request + if let Some(ref mut tls_conn) = tls { + let _ = tls_conn.read_tls(socket); + let _ = tls_conn.write_tls(socket); + + // Complete handshake + if !tls_conn.conn.is_handshaking() && !handshake_completed { + handshake_completed = true; + info!("TLS handshake completed - ready for HTTPS"); + + // Send simple HTTP request + let request = b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: cloudflare.com\r\nUser-Agent: smolmix-test/1.0\r\nAccept: */*\r\nConnection: close\r\n\r\n"; + match tls_conn.send(request, socket) { + Ok(_) => { + info!("HTTPS request sent"); + request_sent = true; + } + Err(e) => { + info!("HTTPS send failed: {}", e); + break; + } + } + } + + // Read response after request sent + if request_sent { + let mut response_data = Vec::new(); + let mut buf = vec![0u8; 4096]; + + match tls_conn.conn.reader().read(&mut buf) { + Ok(0) => { + info!("Response complete - connection closed"); + break; + } + Ok(n) if n > 0 => { + response_data.extend_from_slice(&buf[..n]); + info!("Received {} bytes", n); + + if let Ok(response_str) = std::str::from_utf8(&response_data) { + if response_str.contains("\r\n\r\n") { + info!("HTTPS response received!"); + + if let Some(status_end) = response_str.find("\r\n") { + info!("HTTP Status: {}", &response_str[..status_end]); + } + + info!("Full response: {}", response_str); + return Ok(()); + } + } + } + Ok(1_usize..) => { + todo!() + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Keep polling + } + Err(e) => { + info!("Read error: {}", e); + break; + } + } + } + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + + Err("No HTTP response received".into()) +} diff --git a/smolmix/src/reqwest_client.rs b/smolmix/examples/https_client.rs similarity index 73% rename from smolmix/src/reqwest_client.rs rename to smolmix/examples/https_client.rs index daa9c815a3..5e8dcbb5c5 100644 --- a/smolmix/src/reqwest_client.rs +++ b/smolmix/examples/https_client.rs @@ -1,12 +1,14 @@ -use crate::{create_device, IpMixStream, NymIprDevice, SmolmixError}; +use nym_sdk::stream_wrapper::IpMixStream; use reqwest::StatusCode; use rustls::{pki_types::ServerName, ClientConfig, ClientConnection}; +use smolmix::{create_device, NymIprDevice, SmolmixError}; use smoltcp::{ iface::{Config, Interface, SocketSet}, socket::tcp, time::Instant, wire::{HardwareAddress, IpAddress, IpCidr, Ipv4Address}, }; +use std::sync::Once; use std::{ io::{self, Read, Write}, sync::Arc, @@ -14,6 +16,8 @@ use std::{ }; use tracing::info; +static INIT: Once = Once::new(); + pub struct TlsOverTcp { pub conn: ClientConnection, } @@ -74,11 +78,11 @@ impl TlsOverTcp { } } -/// Reqwest-ish client right now, just GET +/// Reqwest-ish client right now, just a handrolled GET request for the example pub struct SmolmixReqwestClient { device: Arc>, - bridge: tokio::task::JoinHandle<()>, - allocated_ip: Ipv4Address, + _bridge: tokio::task::JoinHandle<()>, + _allocated_ip: Ipv4Address, } impl SmolmixReqwestClient { @@ -112,8 +116,8 @@ impl SmolmixReqwestClient { Ok(Self { device, - bridge: bridge_handle, - allocated_ip: allocated_ips.ipv4, + _bridge: bridge_handle, + _allocated_ip: allocated_ips.ipv4, }) } @@ -268,95 +272,67 @@ impl SmolmixResponse { } } -#[cfg(test)] -mod tests { - use super::*; - use serde_json; - use std::sync::Once; - - static INIT: Once = Once::new(); - - fn init_logging() { - if tracing::dispatcher::has_been_set() { - return; - } - INIT.call_once(|| { - nym_bin_common::logging::setup_tracing_logger(); - }); - } - #[tokio::test] - async fn mixnet_vs_plain_reqwest() -> Result<(), Box> { - init_logging(); - - let test_url = "https://cloudflare.com/cdn-cgi/trace"; - - info!("Fetching with plain reqwest..."); - let start = tokio::time::Instant::now(); - let plain_response = reqwest::get(test_url).await?; - let plain_status = plain_response.status(); - let plain_text = plain_response.text().await?; - let plain_duration = start.elapsed(); - - info!( - "Plain reqwest - Status: {}, Time: {:?}", - plain_status, plain_duration - ); - - info!("Setting up mixnet client..."); - let client = SmolmixReqwestClient::new().await?; - let start = tokio::time::Instant::now(); - let mixnet_response = client.get(test_url).await?; - let mixnet_status = mixnet_response.status(); - let mixnet_text = mixnet_response.text().await?; - let mixnet_duration = start.elapsed(); - - info!( - "Mixnet reqwest - Status: {}, Time: {:?}", - mixnet_status, mixnet_duration - ); - - info!("Status codes match: {}", plain_status == mixnet_status); - info!( - "Response lengths match: {}", - plain_text.len() == mixnet_text.len() - ); - - let key_fields = ["fl=", "ip=", "ts=", "visit_scheme="]; - for field in key_fields { - let plain_has = plain_text.contains(field); - let mixnet_has = mixnet_text.contains(field); - info!( - "Field '{}' - Plain: {}, Mixnet: {}", - field, plain_has, mixnet_has - ); - assert_eq!(plain_has, mixnet_has, "Field '{}' mismatch", field); - } - - info!("Plain reqwest time: {:?}", plain_duration); - info!("Mixnet reqwest time: {:?}", mixnet_duration); - let slowdown = mixnet_duration.as_millis() as f64 / plain_duration.as_millis() as f64; - info!("Mixnet slowdown: {:.1}x", slowdown); - info!("Both responses match"); - Ok(()) - } - - #[tokio::test] - async fn compare_exit_ip() -> Result<(), Box> { - init_logging(); - let plain: serde_json::Value = reqwest::get("https://api.ipify.org?format=json") - .await? - .json() - .await?; - let plain_ip = plain["ip"].as_str().expect("no ip field"); - let client = SmolmixReqwestClient::new().await?; - // MAX: this returns 403, looks like they detect we're some sort of proxy - // let resp = client.get("https://api.ipify.org?format=json").await?; - // info!("{:?}", client.allocated_ip); - assert_ne!( - plain_ip, - client.allocated_ip.to_string(), - "IPs should not be the same" - ); - Ok(()) +fn init_logging() { + if tracing::dispatcher::has_been_set() { + return; } + INIT.call_once(|| { + nym_bin_common::logging::setup_tracing_logger(); + }); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + init_logging(); + + let test_url = "https://cloudflare.com/cdn-cgi/trace"; + + info!("Fetching with plain reqwest..."); + let start = tokio::time::Instant::now(); + let plain_response = reqwest::get(test_url).await?; + let plain_status = plain_response.status(); + let plain_text = plain_response.text().await?; + let plain_duration = start.elapsed(); + + info!( + "Plain reqwest - Status: {}, Time: {:?}", + plain_status, plain_duration + ); + + info!("Setting up mixnet client..."); + let client = SmolmixReqwestClient::new().await?; + let start = tokio::time::Instant::now(); + let mixnet_response = client.get(test_url).await?; + let mixnet_status = mixnet_response.status(); + let mixnet_text = mixnet_response.text().await?; + let mixnet_duration = start.elapsed(); + + info!( + "Mixnet reqwest - Status: {}, Time: {:?}", + mixnet_status, mixnet_duration + ); + + info!("Status codes match: {}", plain_status == mixnet_status); + info!( + "Response lengths match: {}", + plain_text.len() == mixnet_text.len() + ); + + let key_fields = ["fl=", "ip=", "ts=", "visit_scheme="]; + for field in key_fields { + let plain_has = plain_text.contains(field); + let mixnet_has = mixnet_text.contains(field); + info!( + "Field '{}' - Plain: {}, Mixnet: {}", + field, plain_has, mixnet_has + ); + assert_eq!(plain_has, mixnet_has, "Field '{}' mismatch", field); + } + + info!("Plain reqwest time: {:?}", plain_duration); + info!("Mixnet reqwest time: {:?}", mixnet_duration); + let slowdown = mixnet_duration.as_millis() as f64 / plain_duration.as_millis() as f64; + info!("Mixnet slowdown: {:.1}x", slowdown); + info!("Both responses match"); + Ok(()) } diff --git a/smolmix/examples/tls.rs b/smolmix/examples/tls.rs new file mode 100644 index 0000000000..9dcc70d3fd --- /dev/null +++ b/smolmix/examples/tls.rs @@ -0,0 +1,264 @@ +use rustls::{pki_types::ServerName, ClientConfig, ClientConnection}; +use smolmix::{create_device, SmolmixError}; +use std::{ + io::{self, Read, Write}, + sync::Arc, +}; +use tracing::info; + +use nym_sdk::stream_wrapper::IpMixStream; +use smoltcp::{ + iface::{Config, Interface, SocketSet}, + socket::tcp, + time::Instant, + wire::{HardwareAddress, IpAddress, IpCidr, Ipv4Address}, +}; +use std::sync::Once; +use std::time::Duration; + +static INIT: Once = Once::new(); + +pub struct TlsOverTcp { + pub conn: ClientConnection, +} + +impl TlsOverTcp { + pub fn new(domain: &str) -> Result { + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = ServerName::try_from(domain) + .map_err(|_| SmolmixError::InvalidDnsName)? + .to_owned(); + + let conn = ClientConnection::new(Arc::new(config), server_name) + .map_err(|_| SmolmixError::TlsHandshakeFailed)?; + + Ok(Self { conn }) + } + + /// Move data from TLS connection to TCP socket + pub fn write_tls(&mut self, socket: &mut tcp::Socket) -> Result<(), SmolmixError> { + let mut buf = [0u8; 4096]; + while self.conn.wants_write() { + match self.conn.write_tls(&mut buf.as_mut_slice()) { + Ok(n) if n > 0 => { + socket + .send_slice(&buf[..n]) + .map_err(|_| SmolmixError::TlsHandshakeFailed)?; + } + _ => break, + } + } + Ok(()) + } + + /// Move data from TCP socket to TLS connection + pub fn read_tls(&mut self, socket: &mut tcp::Socket) -> Result<(), SmolmixError> { + if socket.can_recv() { + let _ = socket.recv(|chunk| { + if !chunk.is_empty() { + inspect_tls_packet(chunk); + let _ = self.conn.read_tls(&mut io::Cursor::new(&mut *chunk)); + let _ = self.conn.process_new_packets(); + } + (chunk.len(), ()) + }); + } + Ok(()) + } + + pub fn send(&mut self, data: &[u8], socket: &mut tcp::Socket) -> Result<(), SmolmixError> { + self.conn + .writer() + .write_all(data) + .map_err(|_| SmolmixError::TlsHandshakeFailed)?; + self.write_tls(socket) + } + + pub fn recv(&mut self, socket: &mut tcp::Socket) -> Result, SmolmixError> { + self.read_tls(socket)?; + let mut result = Vec::new(); + let mut buf = vec![0u8; 4096]; + match self.conn.reader().read(&mut buf) { + Ok(n) if n > 0 => result.extend_from_slice(&buf[..n]), + _ => {} + } + Ok(result) + } +} + +fn inspect_tls_packet(data: &[u8]) { + if data.len() < 5 { + return; + } + let content_type = data[0]; + if content_type < 0x14 || content_type > 0x17 { + return; + } + let version = u16::from_be_bytes([data[1], data[2]]); + let length = u16::from_be_bytes([data[3], data[4]]); + info!( + "TLS packet: ContentType={:#04x}, Version={:#06x}, Length={}", + content_type, version, length + ); + if content_type == 0x16 && data.len() > 5 { + let handshake_type = data[5]; + let handshake_types = match handshake_type { + 0x01 => "ClientHello", + 0x02 => "ServerHello", + 0x0b => "Certificate", + 0x0c => "ServerKeyExchange", + 0x0d => "CertificateRequest", + 0x0e => "ServerHelloDone", + 0x0f => "CertificateVerify", + 0x10 => "ClientKeyExchange", + 0x14 => "Finished", + _ => "Unknown", + }; + info!( + "Handshake type: {:#04x} ({}), Length: {}", + handshake_type, handshake_types, length + ); + } +} + +fn init_logging() { + if tracing::dispatcher::has_been_set() { + return; + } + INIT.call_once(|| { + nym_bin_common::logging::setup_tracing_logger(); + }); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + init_logging(); + + let ipr_stream = IpMixStream::new().await?; + let (mut device, bridge, allocated_ips) = create_device(ipr_stream).await?; + info!("Allocated IP: {}", allocated_ips.ipv4); + + tokio::spawn(async move { + bridge.run().await.unwrap(); + }); + + let config = Config::new(HardwareAddress::Ip); + let mut iface = Interface::new(config, &mut device, Instant::now()); + iface.update_ip_addrs(|ip_addrs| { + ip_addrs + .push(IpCidr::new(IpAddress::from(allocated_ips.ipv4), 32)) + .unwrap(); + }); + iface + .routes_mut() + .add_default_ipv4_route(Ipv4Address::UNSPECIFIED) + .unwrap(); + + let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; 16384]); + let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; 4096]); + let tcp_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer); + let mut sockets = SocketSet::new(vec![]); + let tcp_handle = sockets.add(tcp_socket); + + let target_ip = Ipv4Address::new(1, 1, 1, 1); // Pinging Cloudflare + let target_port = 443; + info!("Connecting to {}:{} through mixnet", target_ip, target_port); + + let mut timestamp = Instant::from_millis(0); + let start = tokio::time::Instant::now(); + let mut connected = false; + let mut tls = None; + let mut handshake_completed = false; + + loop { + if start.elapsed() > Duration::from_secs(120) { + info!("Test timeout after 120 seconds"); + break; + } + + iface.poll(timestamp, &mut device, &mut sockets); + timestamp += smoltcp::time::Duration::from_millis(1); + let socket = sockets.get_mut::(tcp_handle); + + if !connected && !socket.is_open() { + match socket.connect(iface.context(), (target_ip, target_port), 49152) { + Ok(_) => { + info!("TCP connect started"); + connected = true; + } + Err(e) => { + info!("TCP connect failed: {}", e); + break; + } + } + } + + if start.elapsed().as_secs() % 5 == 0 && start.elapsed().as_millis() % 1000 < 100 { + info!( + "State: TCP={:?}, established={}, can_send={}, can_recv={}", + socket.state(), + socket.state() == tcp::State::Established, + socket.may_send(), + socket.can_recv() + ); + } + + if socket.state() == tcp::State::Established && tls.is_none() { + info!("TCP established - creating TLS connection"); + match TlsOverTcp::new("cloudflare.com") { + Ok(t) => tls = Some(t), + Err(e) => { + info!("TLS create failed: {}", e); + break; + } + } + } + + if let Some(ref mut tls_conn) = tls { + let _ = tls_conn.read_tls(socket); + let _ = tls_conn.write_tls(socket); + + if start.elapsed().as_secs() % 10 == 0 && start.elapsed().as_millis() % 1000 < 100 { + info!( + "TLS state: handshaking={}, wants_read={}, wants_write={}", + tls_conn.conn.is_handshaking(), + tls_conn.conn.wants_read(), + tls_conn.conn.wants_write() + ); + } + + if !tls_conn.conn.is_handshaking() && !handshake_completed { + info!("TLS handshake complete"); + info!( + "TLS verification: handshake_complete=true, wants_read={}, wants_write={}", + tls_conn.conn.wants_read(), + tls_conn.conn.wants_write() + ); + + match tls_conn.recv(socket) { + Ok(data) if data.is_empty() => { + info!("No unexpected application data waiting to be read"); + } + Ok(data) => { + info!("Unexpected application data received: {} bytes", data.len()); + } + Err(e) => { + info!("TLS recv check failed: {}", e); + } + } + info!("TLS handshake successful with cloudflare"); + break; + } + } + tokio::time::sleep(Duration::from_millis(1)).await; + } + + info!("Test completed"); + Ok(()) +} diff --git a/smolmix/src/tls.rs b/smolmix/src/tls.rs deleted file mode 100644 index e037315309..0000000000 --- a/smolmix/src/tls.rs +++ /dev/null @@ -1,419 +0,0 @@ -use crate::SmolmixError; -use rustls::{pki_types::ServerName, ClientConfig, ClientConnection}; -use smoltcp::socket::tcp; -use std::{ - io::{self, Read, Write}, - sync::Arc, -}; -use tracing::info; - -pub struct TlsOverTcp { - pub conn: ClientConnection, -} - -impl TlsOverTcp { - pub fn new(domain: &str) -> Result { - let mut root_store = rustls::RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - - let config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let server_name = ServerName::try_from(domain) - .map_err(|_| SmolmixError::InvalidDnsName)? - .to_owned(); - - let conn = ClientConnection::new(Arc::new(config), server_name) - .map_err(|_| SmolmixError::TlsHandshakeFailed)?; - - Ok(Self { conn }) - } - - /// Move data from TLS connection to TCP socket - pub fn write_tls(&mut self, socket: &mut tcp::Socket) -> Result<(), SmolmixError> { - let mut buf = [0u8; 4096]; - while self.conn.wants_write() { - match self.conn.write_tls(&mut buf.as_mut_slice()) { - Ok(n) if n > 0 => { - socket - .send_slice(&buf[..n]) - .map_err(|_| SmolmixError::TlsHandshakeFailed)?; - } - _ => break, - } - } - Ok(()) - } - - /// Move data from TCP socket to TLS connection - pub fn read_tls(&mut self, socket: &mut tcp::Socket) -> Result<(), SmolmixError> { - if socket.can_recv() { - let _ = socket.recv(|chunk| { - if !chunk.is_empty() { - inspect_tls_packet(chunk); - let _ = self.conn.read_tls(&mut io::Cursor::new(&mut *chunk)); - let _ = self.conn.process_new_packets(); - } - (chunk.len(), ()) - }); - } - Ok(()) - } - - /// Send data - TODO FRAMING - pub fn send(&mut self, data: &[u8], socket: &mut tcp::Socket) -> Result<(), SmolmixError> { - self.conn - .writer() - .write_all(data) - .map_err(|_| SmolmixError::TlsHandshakeFailed)?; - self.write_tls(socket) - } - - /// Receive data - TODO FRAMING - pub fn recv(&mut self, socket: &mut tcp::Socket) -> Result, SmolmixError> { - self.read_tls(socket)?; - let mut result = Vec::new(); - let mut buf = vec![0u8; 4096]; - match self.conn.reader().read(&mut buf) { - Ok(n) if n > 0 => result.extend_from_slice(&buf[..n]), - _ => {} - } - Ok(result) - } -} - -fn inspect_tls_packet(data: &[u8]) { - if data.len() < 5 { - return; - } - let content_type = data[0]; - if content_type < 0x14 || content_type > 0x17 { - return; - } - let version = u16::from_be_bytes([data[1], data[2]]); - let length = u16::from_be_bytes([data[3], data[4]]); - info!( - "TLS packet: ContentType={:#04x}, Version={:#06x}, Length={}", - content_type, version, length - ); - if content_type == 0x16 && data.len() > 5 { - let handshake_type = data[5]; - let handshake_types = match handshake_type { - 0x01 => "ClientHello", - 0x02 => "ServerHello", - 0x0b => "Certificate", - 0x0c => "ServerKeyExchange", - 0x0d => "CertificateRequest", - 0x0e => "ServerHelloDone", - 0x0f => "CertificateVerify", - 0x10 => "ClientKeyExchange", - 0x14 => "Finished", - _ => "Unknown", - }; - info!( - "Handshake type: {:#04x} ({}), Length: {}", - handshake_type, handshake_types, length - ); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::create_device; - use nym_sdk::stream_wrapper::IpMixStream; - use smoltcp::{ - iface::{Config, Interface, SocketSet}, - socket::tcp, - time::Instant, - wire::{HardwareAddress, IpAddress, IpCidr, Ipv4Address}, - }; - use std::sync::Once; - use std::time::Duration; - use tracing::info; - - static INIT: Once = Once::new(); - - fn init_logging() { - if tracing::dispatcher::has_been_set() { - return; - } - INIT.call_once(|| { - nym_bin_common::logging::setup_tracing_logger(); - }); - } - - #[tokio::test] - async fn simple_tls_check() -> Result<(), Box> { - init_logging(); - - let ipr_stream = IpMixStream::new().await?; - let (mut device, bridge, allocated_ips) = create_device(ipr_stream).await?; - info!("Allocated IP: {}", allocated_ips.ipv4); - - tokio::spawn(async move { - bridge.run().await.unwrap(); - }); - - let config = Config::new(HardwareAddress::Ip); - let mut iface = Interface::new(config, &mut device, Instant::now()); - iface.update_ip_addrs(|ip_addrs| { - ip_addrs - .push(IpCidr::new(IpAddress::from(allocated_ips.ipv4), 32)) - .unwrap(); - }); - iface - .routes_mut() - .add_default_ipv4_route(Ipv4Address::UNSPECIFIED) - .unwrap(); - - let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; 16384]); - let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; 4096]); - let tcp_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer); - let mut sockets = SocketSet::new(vec![]); - let tcp_handle = sockets.add(tcp_socket); - - let target_ip = Ipv4Address::new(1, 1, 1, 1); // Pinging Cloudflare - let target_port = 443; - info!("Connecting to {}:{} through mixnet", target_ip, target_port); - - let mut timestamp = Instant::from_millis(0); - let start = tokio::time::Instant::now(); - let mut connected = false; - let mut tls = None; - let mut handshake_completed = false; - - loop { - if start.elapsed() > Duration::from_secs(120) { - info!("Test timeout after 120 seconds"); - break; - } - - iface.poll(timestamp, &mut device, &mut sockets); - timestamp += smoltcp::time::Duration::from_millis(1); - let socket = sockets.get_mut::(tcp_handle); - - if !connected && !socket.is_open() { - match socket.connect(iface.context(), (target_ip, target_port), 49152) { - Ok(_) => { - info!("TCP connect started"); - connected = true; - } - Err(e) => { - info!("TCP connect failed: {}", e); - break; - } - } - } - - if start.elapsed().as_secs() % 5 == 0 && start.elapsed().as_millis() % 1000 < 100 { - info!( - "State: TCP={:?}, established={}, can_send={}, can_recv={}", - socket.state(), - socket.state() == tcp::State::Established, - socket.may_send(), - socket.can_recv() - ); - } - - if socket.state() == tcp::State::Established && tls.is_none() { - info!("TCP established - creating TLS connection"); - match TlsOverTcp::new("cloudflare.com") { - Ok(t) => tls = Some(t), - Err(e) => { - info!("TLS create failed: {}", e); - break; - } - } - } - - if let Some(ref mut tls_conn) = tls { - let _ = tls_conn.read_tls(socket); - let _ = tls_conn.write_tls(socket); - - if start.elapsed().as_secs() % 10 == 0 && start.elapsed().as_millis() % 1000 < 100 { - info!( - "TLS state: handshaking={}, wants_read={}, wants_write={}", - tls_conn.conn.is_handshaking(), - tls_conn.conn.wants_read(), - tls_conn.conn.wants_write() - ); - } - - if !tls_conn.conn.is_handshaking() && !handshake_completed { - info!("TLS handshake complete"); - info!( - "TLS verification: handshake_complete=true, wants_read={}, wants_write={}", - tls_conn.conn.wants_read(), - tls_conn.conn.wants_write() - ); - - match tls_conn.recv(socket) { - Ok(data) if data.is_empty() => { - info!("No unexpected application data waiting to be read"); - } - Ok(data) => { - info!("Unexpected application data received: {} bytes", data.len()); - } - Err(e) => { - info!("TLS recv check failed: {}", e); - } - } - info!("TLS handshake successful with cloudflare"); - break; - } - } - tokio::time::sleep(Duration::from_millis(1)).await; - } - - info!("Test completed"); - Ok(()) - } - - #[tokio::test] - async fn https_req_res() -> Result<(), Box> { - init_logging(); - - let ipr_stream = IpMixStream::new().await?; - let (mut device, bridge, allocated_ips) = create_device(ipr_stream).await?; - info!("Allocated IP: {}", allocated_ips.ipv4); - - tokio::spawn(async move { - bridge.run().await.unwrap(); - }); - - let config = Config::new(HardwareAddress::Ip); - let mut iface = Interface::new(config, &mut device, Instant::now()); - iface.update_ip_addrs(|ip_addrs| { - ip_addrs - .push(IpCidr::new(IpAddress::from(allocated_ips.ipv4), 32)) - .unwrap(); - }); - iface - .routes_mut() - .add_default_ipv4_route(Ipv4Address::UNSPECIFIED) - .unwrap(); - - let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; 16384]); - let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; 4096]); - let tcp_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer); - let mut sockets = SocketSet::new(vec![]); - let tcp_handle = sockets.add(tcp_socket); - - let target_ip = Ipv4Address::new(1, 1, 1, 1); - let target_port = 443; - - let mut timestamp = Instant::from_millis(0); - let start = tokio::time::Instant::now(); - let mut connected = false; - let mut tls = None; - let mut handshake_completed = false; - let mut request_sent = false; - - loop { - if start.elapsed() > Duration::from_secs(60) { - info!("Test timeout after 60 seconds"); - break; - } - - iface.poll(timestamp, &mut device, &mut sockets); - timestamp += smoltcp::time::Duration::from_millis(1); - let socket = sockets.get_mut::(tcp_handle); - - // TCP connection setup - if !connected && !socket.is_open() { - match socket.connect(iface.context(), (target_ip, target_port), 49152) { - Ok(_) => { - info!("TCP connect started"); - connected = true; - } - Err(e) => { - info!("TCP connect failed: {}", e); - break; - } - } - } - - // TLS setup after TCP established - if socket.state() == tcp::State::Established && tls.is_none() { - info!("TCP established - creating TLS connection"); - match TlsOverTcp::new("cloudflare.com") { - Ok(t) => tls = Some(t), - Err(e) => { - info!("TLS create failed: {}", e); - break; - } - } - } - - // TLS handshake and request - if let Some(ref mut tls_conn) = tls { - let _ = tls_conn.read_tls(socket); - let _ = tls_conn.write_tls(socket); - - // Complete handshake - if !tls_conn.conn.is_handshaking() && !handshake_completed { - handshake_completed = true; - info!("TLS handshake completed - ready for HTTPS"); - - // Send simple HTTP request - let request = b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: cloudflare.com\r\nUser-Agent: smolmix-test/1.0\r\nAccept: */*\r\nConnection: close\r\n\r\n"; - match tls_conn.send(request, socket) { - Ok(_) => { - info!("HTTPS request sent"); - request_sent = true; - } - Err(e) => { - info!("HTTPS send failed: {}", e); - break; - } - } - } - - // Read response after request sent - if request_sent { - let mut response_data = Vec::new(); - let mut buf = vec![0u8; 4096]; - - match tls_conn.conn.reader().read(&mut buf) { - Ok(0) => { - info!("Response complete - connection closed"); - break; - } - Ok(n) if n > 0 => { - response_data.extend_from_slice(&buf[..n]); - info!("Received {} bytes", n); - - if let Ok(response_str) = std::str::from_utf8(&response_data) { - if response_str.contains("\r\n\r\n") { - info!("HTTPS response received!"); - - if let Some(status_end) = response_str.find("\r\n") { - info!("HTTP Status: {}", &response_str[..status_end]); - } - - info!("Full response: {}", response_str); - return Ok(()); - } - } - } - Ok(1_usize..) => { - todo!() - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - // Keep polling - } - Err(e) => { - info!("Read error: {}", e); - break; - } - } - } - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - - Err("No HTTP response received".into()) - } -}