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
+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))
}
}