feat(transport): real TLS-443 on the TCP backend (replaces HTTP/1.1 masquerade)
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>
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
//! 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.
|
||||
//! 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("localhost")
|
||||
.issue_server_cert(SERVER_NAME)
|
||||
.expect("issue server cert");
|
||||
let client = ca
|
||||
.issue_client_cert("client-tcp")
|
||||
.expect("issue client 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(),
|
||||
@@ -24,12 +31,14 @@ fn make_configs() -> (ServerConfig, ClientConfig) {
|
||||
ca_cert_pem: ca_pem,
|
||||
client_cert_pem: client.cert_pem,
|
||||
client_key_pem: client.key_pem,
|
||||
server_name: "localhost".to_string(),
|
||||
server_name: SERVER_NAME.to_string(),
|
||||
};
|
||||
(scfg, ccfg)
|
||||
}
|
||||
|
||||
async fn run_case(opts: TcpOpts) {
|
||||
/// 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
|
||||
@@ -38,7 +47,7 @@ async fn run_case(opts: TcpOpts) {
|
||||
|
||||
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");
|
||||
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");
|
||||
@@ -46,9 +55,14 @@ async fn run_case(opts: TcpOpts) {
|
||||
}
|
||||
});
|
||||
|
||||
let client = TcpClient::connect(addr, ccfg, opts)
|
||||
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 {
|
||||
@@ -61,17 +75,25 @@ async fn run_case(opts: TcpOpts) {
|
||||
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_plain() {
|
||||
run_case(TcpOpts::default()).await;
|
||||
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_end_to_end_masquerade() {
|
||||
run_case(TcpOpts {
|
||||
masquerade: true,
|
||||
host: "cdn.example.com".to_string(),
|
||||
..TcpOpts::default()
|
||||
})
|
||||
.await;
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user