821f7711e7
The TCP fallback now does a full outer TLS handshake (tokio-rustls 0.26 over
rustls 0.23, ring provider) before the Aura proto handshake, exactly like the
QUIC backend: on the wire it is indistinguishable from genuine HTTPS until the
inner Aura mutual-auth handshake starts. Removes v1's "light HTTP masquerade"
limitation; the real security boundary remains the inner PQ handshake.
- aura-transport::tcp: dropped the HTTP/1.1 preamble helpers and TcpOpts
fields (masquerade, host, user_agent, server_header). New flow:
TlsAcceptor::accept (server) / TlsConnector::connect (client) →
tokio::io::split(TlsStream) → server_handshake / client_handshake → Session.
Client reuses crate::quic::AcceptAnyServerCert (outer SNI not authenticated;
inner handshake is the security boundary). Outer server cert auto-sourced
from proto_cfg.server_cert_pem (no API change for the CLI's bind).
- ALPN default: ["h2", "http/1.1"] (DEFAULT_TCP_ALPN, exported).
- TcpOpts: now just { alpn: Option<Vec<Vec<u8>>> }.
- TcpClient::connect gains an outer-SNI &str param; DialConfig.sni passes it
through (separate from the inner proto_cfg.server_name).
- tokio-rustls 0.26 added as a transport-local dependency (not workspace).
CLI updates: removed dead host/user_agent/server_header wiring; mask rotation
no longer touches TCP outer parameters (TLS doesn't have a Host header on
the wire). [transport] masquerade kept as a no-op for back-compat with old
configs (documented).
3 new tcp_loopback tests (default ALPN end-to-end, custom ALPN, outer SNI
mismatch still connects = proves accept-any is in effect). Workspace: 142
tests passed (+1), clippy -D warnings clean, fmt clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
100 lines
4.0 KiB
Rust
100 lines
4.0 KiB
Rust
//! End-to-end loopback test for the TLS-443 / TCP fallback transport: real outer rustls TLS over
|
|
//! plain TCP on 127.0.0.1, full inner Aura mutual-auth handshake, packet echo.
|
|
//!
|
|
//! Also covers:
|
|
//! * A custom (non-default) ALPN advertisement.
|
|
//! * The "accept-any" outer-cert guarantee: the client connects with an outer SNI that does NOT
|
|
//! match the server's outer-TLS certificate, the outer TLS handshake completes anyway (because
|
|
//! the client uses [`AcceptAnyServerCert`]), and the inner Aura mutual auth still succeeds.
|
|
|
|
use aura_pki::AuraCa;
|
|
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
|
|
use aura_transport::{TcpClient, TcpOpts, TcpServer};
|
|
|
|
const SERVER_NAME: &str = "localhost";
|
|
const CLIENT_ID: &str = "client-tcp";
|
|
|
|
/// 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(SERVER_NAME)
|
|
.expect("issue server cert");
|
|
let client = ca.issue_client_cert(CLIENT_ID).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: SERVER_NAME.to_string(),
|
|
};
|
|
(scfg, ccfg)
|
|
}
|
|
|
|
/// Drive a single loopback handshake + 3-packet echo. `client_sni` is the OUTER TLS SNI the client
|
|
/// presents; it is independent of the server cert (the client uses an accept-any verifier).
|
|
async fn run_case(opts: TcpOpts, client_sni: &str) {
|
|
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_ID), "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, client_sni, ccfg, opts)
|
|
.await
|
|
.expect("client handshake");
|
|
assert_eq!(
|
|
client.peer_id(),
|
|
Some(SERVER_NAME),
|
|
"inner handshake verified the server CN"
|
|
);
|
|
|
|
// 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");
|
|
}
|
|
|
|
/// Baseline: default ALPN advert (`h2`, `http/1.1`), outer SNI matches the server cert SAN.
|
|
#[tokio::test]
|
|
async fn tcp_loopback_end_to_end() {
|
|
run_case(TcpOpts::default(), SERVER_NAME).await;
|
|
}
|
|
|
|
/// A custom ALPN list still negotiates and runs the handshake.
|
|
#[tokio::test]
|
|
async fn tcp_loopback_with_custom_alpn() {
|
|
let opts = TcpOpts {
|
|
alpn: Some(vec![b"http/1.1".to_vec()]),
|
|
};
|
|
run_case(opts, SERVER_NAME).await;
|
|
}
|
|
|
|
/// The client uses [`AcceptAnyServerCert`] on the outer TLS layer, so an outer SNI that has nothing
|
|
/// to do with the server's real certificate must still complete the TLS handshake; the inner Aura
|
|
/// mutual auth then proves identity. This is the security model: outer = camouflage, inner = trust.
|
|
#[tokio::test]
|
|
async fn tcp_loopback_outer_sni_mismatch_still_connects() {
|
|
run_case(TcpOpts::default(), "definitely-not-the-server.example").await;
|
|
}
|