feat(transport): TCP/443 fallback + unified dialer with UDP->TCP->QUIC handover

- tcp.rs: Aura proto handshake + Session directly over TcpStream (TcpServer/
  TcpClient/TcpConnection: PacketConnection), with an optional light HTTP/1.1
  masquerade preamble. Fallback for UDP-blocking networks. (Full TLS-443 mimicry
  is a documented follow-up.)
- dial.rs: TransportMode {Udp,Tcp,Quic}, Endpoints, DialConfig; client `dial()`
  tries transports in order and hands over on failure/timeout; MultiServer binds
  and accepts on every enabled transport at once (TCP/QUIC multi-client; UDP
  single-peer-per-accept in v1).
- Tests: tcp loopback (plain + masquerade), dial handover (dead TCP -> UDP).
  clippy/fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-25 19:15:31 +03:00
parent 866b9f427a
commit d72fbe8d68
5 changed files with 703 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
//! Unified transport selection: a client [`dial`] that tries transports in order (UDP → TCP →
//! QUIC, the "handover") and a [`MultiServer`] that accepts on every enabled transport at once.
//!
//! All three backends produce an `Arc<dyn aura_proto::PacketConnection>`, so the tunnel router does
//! not care which transport carried a connection. The primary path is Aura's own UDP transport;
//! TCP/443 and QUIC are fallbacks for networks that throttle or block plain UDP.
use std::fmt;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
use tokio::sync::mpsc;
use crate::{AuraClient, AuraServer, TcpClient, TcpOpts, TcpServer, UdpClient, UdpOpts, UdpServer};
/// Which wire transport carries an Aura connection.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransportMode {
/// Aura's own protocol over plain UDP (primary).
Udp,
/// Aura over TCP (fallback for UDP-blocking networks; optional HTTP masquerade).
Tcp,
/// Aura inside QUIC/HTTP3 mimicry (fallback / strong camouflage).
Quic,
}
impl FromStr for TransportMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"udp" => Ok(Self::Udp),
"tcp" => Ok(Self::Tcp),
"quic" => Ok(Self::Quic),
other => Err(format!("unknown transport '{other}' (expected udp|tcp|quic)")),
}
}
}
impl fmt::Display for TransportMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Udp => "udp",
Self::Tcp => "tcp",
Self::Quic => "quic",
})
}
}
/// Per-transport server addresses. Any subset may be set; `None` means that transport is disabled.
///
/// The UDP transport and QUIC both use UDP and therefore must use *different* ports; TCP can share a
/// port number with the UDP transport (different protocol).
#[derive(Clone, Debug, Default)]
pub struct Endpoints {
/// Address of the custom-UDP transport.
pub udp: Option<SocketAddr>,
/// Address of the TCP transport.
pub tcp: Option<SocketAddr>,
/// Address of the QUIC transport.
pub quic: Option<SocketAddr>,
}
/// Client-side dial configuration: where the server is per transport, the fallback order, and
/// per-transport options.
#[derive(Clone, Debug)]
pub struct DialConfig {
/// Server addresses per transport.
pub endpoints: Endpoints,
/// SNI / masquerade hostname (QUIC outer SNI; TCP masquerade Host).
pub sni: String,
/// Transports to try, in order. The first that connects wins.
pub order: Vec<TransportMode>,
/// Options for the UDP transport.
pub udp: UdpOpts,
/// Options for the TCP transport.
pub tcp: TcpOpts,
/// Per-attempt timeout before moving to the next transport in `order`.
pub attempt_timeout: Duration,
}
impl Default for DialConfig {
fn default() -> Self {
Self {
endpoints: Endpoints::default(),
sni: "cdn.example.com".to_string(),
order: vec![TransportMode::Udp, TransportMode::Tcp, TransportMode::Quic],
udp: UdpOpts::default(),
tcp: TcpOpts::default(),
attempt_timeout: Duration::from_secs(8),
}
}
}
/// Connect to the server, trying each transport in `cfg.order` until one succeeds ("handover").
///
/// Returns the established connection and which transport carried it. A transport with no configured
/// address is skipped; a transport that errors or times out moves on to the next.
///
/// # Errors
/// Returns the last error if every configured transport fails (or an error if none were configured).
pub async fn dial(
proto_cfg: ClientConfig,
cfg: DialConfig,
) -> anyhow::Result<(Arc<dyn PacketConnection>, TransportMode)> {
let mut last_err: Option<anyhow::Error> = None;
for mode in &cfg.order {
let addr = match mode {
TransportMode::Udp => cfg.endpoints.udp,
TransportMode::Tcp => cfg.endpoints.tcp,
TransportMode::Quic => cfg.endpoints.quic,
};
let Some(addr) = addr else {
continue; // transport not configured
};
tracing::info!("dial: trying {mode} at {addr}");
let attempt =
tokio::time::timeout(cfg.attempt_timeout, dial_one(*mode, addr, &proto_cfg, &cfg)).await;
match attempt {
Ok(Ok(conn)) => {
tracing::info!("dial: connected via {mode}");
return Ok((conn, *mode));
}
Ok(Err(e)) => {
tracing::warn!("dial: {mode} failed: {e:#}");
last_err = Some(e);
}
Err(_) => {
tracing::warn!("dial: {mode} timed out after {:?}", cfg.attempt_timeout);
last_err = Some(anyhow::anyhow!("{mode} connect timed out"));
}
}
}
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("no transports configured to dial")))
}
async fn dial_one(
mode: TransportMode,
addr: SocketAddr,
proto_cfg: &ClientConfig,
cfg: &DialConfig,
) -> anyhow::Result<Arc<dyn PacketConnection>> {
Ok(match mode {
TransportMode::Udp => UdpClient::connect(addr, proto_cfg.clone(), cfg.udp)
.await?
.into_dyn(),
TransportMode::Tcp => TcpClient::connect(addr, proto_cfg.clone(), cfg.tcp.clone())
.await?
.into_dyn(),
TransportMode::Quic => AuraClient::connect(addr, &cfg.sni, proto_cfg.clone())
.await?
.into_dyn(),
})
}
/// An accepted connection plus which transport carried it and the verified peer id.
pub struct Accepted {
/// The established packet pipe.
pub conn: Arc<dyn PacketConnection>,
/// Which transport accepted it.
pub mode: TransportMode,
/// Verified peer Common Name (client id), if any.
pub peer_id: Option<String>,
}
/// A server that listens on every enabled transport simultaneously and yields accepted connections
/// from all of them through one [`MultiServer::accept`] call.
///
/// TCP and QUIC accept loops handle many clients. The custom-UDP backend is single-peer-per-accept
/// in v1 (a multi-client UDP demux is a documented follow-up), so with several clients prefer TCP or
/// QUIC, or run one UDP server per client.
pub struct MultiServer {
rx: mpsc::Receiver<Accepted>,
tasks: Vec<tokio::task::JoinHandle<()>>,
}
impl MultiServer {
/// Bind and start accept loops for every transport whose address is set in `endpoints`.
/// The QUIC outer-TLS cert reuses the Aura server cert from `proto_cfg`.
///
/// # Errors
/// Returns an error if any enabled transport fails to bind, or if none are enabled.
pub async fn bind(
endpoints: Endpoints,
proto_cfg: ServerConfig,
udp: UdpOpts,
tcp: TcpOpts,
) -> anyhow::Result<Self> {
let (txc, rx) = mpsc::channel::<Accepted>(32);
let mut tasks = Vec::new();
if let Some(addr) = endpoints.udp {
let server = UdpServer::bind(addr, proto_cfg.clone(), udp)?;
tasks.push(tokio::spawn(udp_accept_loop(server, txc.clone())));
}
if let Some(addr) = endpoints.tcp {
let server = TcpServer::bind(addr, proto_cfg.clone(), tcp.clone()).await?;
tasks.push(tokio::spawn(tcp_accept_loop(server, txc.clone())));
}
if let Some(addr) = endpoints.quic {
let server = AuraServer::bind(
addr,
&proto_cfg.server_cert_pem,
&proto_cfg.server_key_pem,
proto_cfg.clone(),
)?;
tasks.push(tokio::spawn(quic_accept_loop(server, txc.clone())));
}
if tasks.is_empty() {
anyhow::bail!("MultiServer: no transports enabled");
}
Ok(Self { rx, tasks })
}
/// Wait for the next accepted connection from any enabled transport. Returns `None` when all
/// accept loops have stopped.
pub async fn accept(&mut self) -> Option<Accepted> {
self.rx.recv().await
}
}
impl Drop for MultiServer {
fn drop(&mut self) {
for t in &self.tasks {
t.abort();
}
}
}
async fn udp_accept_loop(server: UdpServer, tx: mpsc::Sender<Accepted>) {
loop {
match server.accept().await {
Ok(conn) => {
let peer_id = conn.peer_id().map(str::to_owned);
let accepted = Accepted {
conn: conn.into_dyn(),
mode: TransportMode::Udp,
peer_id,
};
if tx.send(accepted).await.is_err() {
break;
}
}
Err(e) => {
tracing::warn!("udp accept failed: {e:#}");
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
}
async fn tcp_accept_loop(server: TcpServer, tx: mpsc::Sender<Accepted>) {
loop {
match server.accept().await {
Ok(conn) => {
let peer_id = conn.peer_id().map(str::to_owned);
let accepted = Accepted {
conn: conn.into_dyn(),
mode: TransportMode::Tcp,
peer_id,
};
if tx.send(accepted).await.is_err() {
break;
}
}
Err(e) => {
tracing::warn!("tcp accept failed: {e:#}");
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
}
async fn quic_accept_loop(server: AuraServer, tx: mpsc::Sender<Accepted>) {
loop {
match server.accept().await {
Ok(conn) => {
let peer_id = conn.peer_id().map(str::to_owned);
let accepted = Accepted {
conn: conn.into_dyn(),
mode: TransportMode::Quic,
peer_id,
};
if tx.send(accepted).await.is_err() {
break;
}
}
Err(e) => {
tracing::warn!("quic accept failed: {e:#}");
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
}
+4
View File
@@ -64,15 +64,19 @@
#![warn(missing_docs)] #![warn(missing_docs)]
pub mod conn; pub mod conn;
pub mod dial;
pub mod mimicry; pub mod mimicry;
pub mod padding; pub mod padding;
pub mod quic; pub mod quic;
pub mod tcp;
pub mod udp; pub mod udp;
pub use conn::AuraConnection; pub use conn::AuraConnection;
pub use dial::{dial, Accepted, DialConfig, Endpoints, MultiServer, TransportMode};
pub use mimicry::{alpn_protocols, chrome_quic_transport_config, ALPN_H3, DEFAULT_SNI}; pub use mimicry::{alpn_protocols, chrome_quic_transport_config, ALPN_H3, DEFAULT_SNI};
pub use padding::{inject_padding_frames, pad_to_https_size, HTTPS_SIZE_BUCKETS}; pub use padding::{inject_padding_frames, pad_to_https_size, HTTPS_SIZE_BUCKETS};
pub use quic::{client_endpoint, server_endpoint, AcceptAnyServerCert}; pub use quic::{client_endpoint, server_endpoint, AcceptAnyServerCert};
pub use tcp::{TcpClient, TcpConnection, TcpOpts, TcpServer};
pub use udp::{UdpClient, UdpConnection, UdpOpts, UdpServer}; pub use udp::{UdpClient, UdpConnection, UdpOpts, UdpServer};
// Re-export the inner proto trait so downstream crates (the CLI) can name the connection as // Re-export the inner proto trait so downstream crates (the CLI) can name the connection as
+256
View File
@@ -0,0 +1,256 @@
//! Aura over plain **TCP** — a fallback transport for networks that block UDP/QUIC (project §7).
//!
//! This runs the SAME Aura proto handshake (hybrid X25519 + ML-KEM-768 + mutual X.509) and
//! [`aura_proto::Session`] directly over a [`TcpStream`], which already implements
//! [`AsyncRead`](tokio::io::AsyncRead) + [`AsyncWrite`](tokio::io::AsyncWrite). No extra crypto and
//! no QUIC are involved — the security boundary is the inner Aura handshake, exactly as for the UDP
//! backend.
//!
//! ## Optional HTTP masquerade
//!
//! With [`TcpOpts::masquerade`] the peers exchange a minimal HTTP/1.1 request/response preamble
//! before the Aura handshake, so the start of the connection resembles a plain HTTP session to a
//! casual observer. This is a **light disguise, not TLS** — full HTTPS/TLS-443 mimicry (reusing the
//! rustls outer layer from the QUIC backend) is a planned enhancement; for now TCP's main job is to
//! get bytes through where UDP is blocked.
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait;
use bytes::Bytes;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex;
use aura_proto::{
client_handshake, server_handshake, ClientConfig, Frame, PacketConnection, ServerConfig,
Session, SessionReceiver, SessionSender,
};
/// Tunables for the TCP transport.
#[derive(Clone, Debug)]
pub struct TcpOpts {
/// When `true`, exchange a minimal HTTP/1.1 preamble before the Aura handshake so the connection
/// opening resembles plain HTTP. A light disguise only (not TLS).
pub masquerade: bool,
/// `Host:` header value used in the client's masquerade preamble.
pub host: String,
}
impl Default for TcpOpts {
fn default() -> Self {
Self {
masquerade: false,
host: "cdn.example.com".to_string(),
}
}
}
/// The concrete session type carried over TCP: a proto session over TcpStream's owned halves.
type TcpSession = Session<OwnedReadHalf, OwnedWriteHalf>;
/// An established Aura connection carried over **plain TCP**, exposed as a packet pipe.
///
/// Implements [`aura_proto::PacketConnection`] (so it works behind `Arc<dyn PacketConnection>`):
/// outbound packets are sealed as [`Frame::Data`] on `stream_id 0`; inbound `Data` payloads are
/// returned; `Ping` is answered with `Pong`, stray `Pong` ignored, `Close` surfaced as an error.
/// Send and receive use **separate** [`tokio::sync::Mutex`]es so the two directions run concurrently.
pub struct TcpConnection {
sender: Mutex<SessionSender<OwnedWriteHalf>>,
receiver: Mutex<SessionReceiver<OwnedReadHalf>>,
peer_id: Option<String>,
}
impl TcpConnection {
fn from_session(session: TcpSession) -> Self {
let peer_id = session.peer_id().map(str::to_owned);
let (sender, receiver) = session.split();
Self {
sender: Mutex::new(sender),
receiver: Mutex::new(receiver),
peer_id,
}
}
/// The verified identity (Common Name) of the peer learned during the handshake.
#[must_use]
pub fn peer_id(&self) -> Option<&str> {
self.peer_id.as_deref()
}
/// Wrap this connection as a trait object for the tunnel/dialer layer.
#[must_use]
pub fn into_dyn(self) -> Arc<dyn PacketConnection> {
Arc::new(self)
}
}
#[async_trait]
impl PacketConnection for TcpConnection {
async fn send_packet(&self, packet: &[u8]) -> anyhow::Result<()> {
self.sender
.lock()
.await
.send_frame(Frame::Data {
stream_id: 0,
payload: Bytes::copy_from_slice(packet),
})
.await?;
Ok(())
}
async fn recv_packet(&self) -> anyhow::Result<Vec<u8>> {
let mut receiver = self.receiver.lock().await;
loop {
match receiver.recv_frame().await? {
Frame::Data { payload, .. } => return Ok(payload.to_vec()),
Frame::Ping { seq } => {
// Separate mutex from the receive lock we hold => no deadlock.
self.sender.lock().await.send_frame(Frame::Pong { seq }).await?;
}
Frame::Pong { .. } => continue,
Frame::Close { code, reason } => {
anyhow::bail!("peer closed connection (code {code}): {reason}");
}
}
}
}
}
// ---------------------------------------------------------------------------------------------
// HTTP masquerade preamble helpers
// ---------------------------------------------------------------------------------------------
/// Write a plausible HTTP/1.1 request line + headers (client side of the masquerade).
async fn write_client_preamble(stream: &mut TcpStream, host: &str) -> io::Result<()> {
let req = format!(
"GET / HTTP/1.1\r\nHost: {host}\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n"
);
stream.write_all(req.as_bytes()).await?;
stream.flush().await
}
/// Write a plausible HTTP/1.1 response head (server side of the masquerade).
async fn write_server_preamble(stream: &mut TcpStream) -> io::Result<()> {
let resp =
"HTTP/1.1 200 OK\r\nServer: nginx\r\nContent-Type: application/octet-stream\r\nConnection: keep-alive\r\n\r\n";
stream.write_all(resp.as_bytes()).await?;
stream.flush().await
}
/// Read (and discard) bytes up to and including the `\r\n\r\n` header terminator.
///
/// Reads one byte at a time so it never consumes past the terminator into the handshake stream. The
/// preamble is tiny and one-time, so byte-at-a-time is fine and keeps the boundary exact.
async fn read_until_headers_end(stream: &mut TcpStream) -> io::Result<()> {
let mut last4 = [0u8; 4];
let mut count = 0usize;
let mut one = [0u8; 1];
loop {
let n = stream.read(&mut one).await?;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"eof during masquerade preamble",
));
}
last4.rotate_left(1);
last4[3] = one[0];
count += 1;
if count >= 4 && &last4 == b"\r\n\r\n" {
return Ok(());
}
if count > 8192 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"masquerade preamble exceeded 8 KiB without terminator",
));
}
}
}
// ---------------------------------------------------------------------------------------------
// Server / client
// ---------------------------------------------------------------------------------------------
/// An Aura TCP server: a bound [`TcpListener`] that accepts authenticated [`TcpConnection`]s.
pub struct TcpServer {
listener: TcpListener,
proto_cfg: Arc<ServerConfig>,
opts: TcpOpts,
}
impl TcpServer {
/// Bind a TCP server on `addr` (use `..:0` for an OS-assigned port, read back with
/// [`TcpServer::local_addr`]).
///
/// # Errors
/// Returns an [`io::Error`] if the listener cannot bind.
pub async fn bind(
addr: SocketAddr,
proto_cfg: ServerConfig,
opts: TcpOpts,
) -> io::Result<Self> {
let listener = TcpListener::bind(addr).await?;
Ok(Self {
listener,
proto_cfg: Arc::new(proto_cfg),
opts,
})
}
/// The local address (incl. the OS-assigned port) this server is bound to.
///
/// # Errors
/// Returns an [`io::Error`] if the address cannot be read.
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.listener.local_addr()
}
/// Accept the next client: optional masquerade preamble, then the Aura mutual-auth handshake.
///
/// # Errors
/// Returns an error if accepting fails, the masquerade preamble is malformed, or the Aura
/// handshake fails (e.g. the client's certificate does not verify against the CA).
pub async fn accept(&self) -> anyhow::Result<TcpConnection> {
let (mut stream, _peer) = self.listener.accept().await?;
stream.set_nodelay(true).ok();
if self.opts.masquerade {
read_until_headers_end(&mut stream).await?;
write_server_preamble(&mut stream).await?;
}
let (reader, writer) = stream.into_split();
let session = server_handshake(reader, writer, &self.proto_cfg).await?;
Ok(TcpConnection::from_session(session))
}
}
/// An Aura TCP client entry point.
pub struct TcpClient;
impl TcpClient {
/// Connect to an Aura TCP server at `server`: optional masquerade preamble, then the Aura
/// mutual-auth handshake over the TCP stream.
///
/// # Errors
/// Returns an error if the TCP connect fails, the masquerade preamble is malformed, or the Aura
/// handshake fails (bad server cert chain, SAN mismatch, ...).
pub async fn connect(
server: SocketAddr,
proto_cfg: ClientConfig,
opts: TcpOpts,
) -> anyhow::Result<TcpConnection> {
let mut stream = TcpStream::connect(server).await?;
stream.set_nodelay(true).ok();
if opts.masquerade {
write_client_preamble(&mut stream, &opts.host).await?;
read_until_headers_end(&mut stream).await?;
}
let (reader, writer) = stream.into_split();
let session = client_handshake(reader, writer, &proto_cfg).await?;
Ok(TcpConnection::from_session(session))
}
}
@@ -0,0 +1,74 @@
//! Verifies the client dialer's handover: when an earlier transport in the order is unreachable,
//! `dial` moves on to the next and connects.
use std::net::SocketAddr;
use std::time::Duration;
use aura_pki::AuraCa;
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
use aura_transport::{dial, DialConfig, Endpoints, TcpOpts, TransportMode, UdpOpts, UdpServer};
fn make_configs() -> (ServerConfig, ClientConfig) {
let ca = AuraCa::generate("Aura Test CA").expect("generate CA");
let server = ca.issue_server_cert("localhost").expect("issue server cert");
let client = ca.issue_client_cert("client-dial").expect("issue client cert");
let ca_pem = ca.ca_cert_pem();
(
ServerConfig {
ca_cert_pem: ca_pem.clone(),
server_cert_pem: server.cert_pem,
server_key_pem: server.key_pem,
},
ClientConfig {
ca_cert_pem: ca_pem,
client_cert_pem: client.cert_pem,
client_key_pem: client.key_pem,
server_name: "localhost".to_string(),
},
)
}
#[tokio::test]
async fn dial_falls_back_from_dead_tcp_to_udp() {
let (scfg, ccfg) = make_configs();
// A real UDP server (the working fallback target).
let udp_server =
UdpServer::bind("127.0.0.1:0".parse().unwrap(), scfg, UdpOpts::default()).expect("bind udp");
let udp_addr = udp_server.local_addr().expect("udp addr");
let srv = tokio::spawn(async move {
let conn = udp_server.accept().await.expect("server accept");
let p = conn.recv_packet().await.expect("server recv");
conn.send_packet(&p).await.expect("server echo");
});
// Port 1 on loopback has nothing listening → TCP connect is refused fast.
let dead_tcp: SocketAddr = "127.0.0.1:1".parse().unwrap();
let cfg = DialConfig {
endpoints: Endpoints {
udp: Some(udp_addr),
tcp: Some(dead_tcp),
quic: None,
},
sni: "cdn.example.com".to_string(),
// Deliberately try the (dead) TCP first to force the handover to UDP.
order: vec![TransportMode::Tcp, TransportMode::Udp],
udp: UdpOpts::default(),
tcp: TcpOpts::default(),
attempt_timeout: Duration::from_secs(3),
};
let (conn, mode) = dial(ccfg, cfg).await.expect("dial should fall back to UDP");
assert_eq!(
mode,
TransportMode::Udp,
"must hand over to UDP after TCP is refused"
);
conn.send_packet(b"hello-fallback").await.expect("send");
let echoed = conn.recv_packet().await.expect("recv");
assert_eq!(echoed, b"hello-fallback");
srv.await.expect("server task");
}
@@ -0,0 +1,72 @@
//! End-to-end loopback test for the TCP fallback transport: real TCP on 127.0.0.1, full Aura
//! mutual-auth handshake, packet echo — with the HTTP masquerade both off and on.
use aura_pki::AuraCa;
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
use aura_transport::{TcpClient, TcpOpts, TcpServer};
/// Mint a fresh CA + server("localhost") + client("client-tcp") and build the proto configs.
fn make_configs() -> (ServerConfig, ClientConfig) {
let ca = AuraCa::generate("Aura Test CA").expect("generate CA");
let server = ca.issue_server_cert("localhost").expect("issue server cert");
let client = ca.issue_client_cert("client-tcp").expect("issue client cert");
let ca_pem = ca.ca_cert_pem();
let scfg = ServerConfig {
ca_cert_pem: ca_pem.clone(),
server_cert_pem: server.cert_pem,
server_key_pem: server.key_pem,
};
let ccfg = ClientConfig {
ca_cert_pem: ca_pem,
client_cert_pem: client.cert_pem,
client_key_pem: client.key_pem,
server_name: "localhost".to_string(),
};
(scfg, ccfg)
}
async fn run_case(opts: TcpOpts) {
let (scfg, ccfg) = make_configs();
let server = TcpServer::bind("127.0.0.1:0".parse().unwrap(), scfg, opts.clone())
.await
.expect("bind server");
let addr = server.local_addr().expect("local addr");
let server_task = tokio::spawn(async move {
let conn = server.accept().await.expect("server handshake");
assert_eq!(conn.peer_id(), Some("client-tcp"), "verified client id");
// Echo three packets back to the client.
for _ in 0..3 {
let pkt = conn.recv_packet().await.expect("server recv");
conn.send_packet(&pkt).await.expect("server echo");
}
});
let client = TcpClient::connect(addr, ccfg, opts)
.await
.expect("client handshake");
// Exchange packets of varying sizes (incl. a large one) and assert the echo matches.
for i in 0..3u16 {
let payload = vec![(i as u8).wrapping_add(1); 100 + (i as usize) * 600]; // 100, 700, 1300 bytes
client.send_packet(&payload).await.expect("client send");
let echoed = client.recv_packet().await.expect("client recv");
assert_eq!(echoed, payload, "round-trip payload mismatch");
}
server_task.await.expect("server task");
}
#[tokio::test]
async fn tcp_loopback_end_to_end_plain() {
run_case(TcpOpts::default()).await;
}
#[tokio::test]
async fn tcp_loopback_end_to_end_masquerade() {
run_case(TcpOpts {
masquerade: true,
host: "cdn.example.com".to_string(),
})
.await;
}