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:
@@ -265,7 +265,9 @@ pub struct TransportSection {
|
||||
pub quic_port: u16,
|
||||
/// UDP transport: pad datagrams up to HTTPS size buckets to blur on-wire sizes.
|
||||
pub obfuscate: bool,
|
||||
/// TCP transport: prepend a minimal HTTP/1.1 preamble so the open resembles plain HTTP.
|
||||
/// **Deprecated, ignored.** The TCP transport used to optionally prepend a minimal HTTP/1.1
|
||||
/// preamble (a light disguise); in v2 it always uses a real outer TLS-443 handshake (a much
|
||||
/// stronger camouflage), so this knob has no effect. Kept for backwards-compat config parsing.
|
||||
pub masquerade: bool,
|
||||
/// `[transport.masks]`: daily protocol-mask rotation knobs.
|
||||
pub masks: MasksSection,
|
||||
@@ -542,17 +544,14 @@ impl ServerConfigFile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the [`TcpOpts`] for the server's TCP transport from `[transport] masquerade`; the
|
||||
/// masquerade `Host` reuses the mimicry SNI when one is configured.
|
||||
/// Build the [`TcpOpts`] for the server's TCP transport.
|
||||
///
|
||||
/// In v2 the TCP backend always uses a real outer TLS-443 layer, so there are no per-config
|
||||
/// knobs here (ALPN keeps its `[h2, http/1.1]` default). The legacy `[transport] masquerade` /
|
||||
/// `[mimicry] sni` values are still parsed for backwards compatibility but are no longer plumbed
|
||||
/// into [`TcpOpts`].
|
||||
pub fn tcp_opts(&self) -> TcpOpts {
|
||||
let mut opts = TcpOpts {
|
||||
masquerade: self.transport.masquerade,
|
||||
..TcpOpts::default()
|
||||
};
|
||||
if let Some(sni) = &self.mimicry.sni {
|
||||
opts.host = sni.clone();
|
||||
}
|
||||
opts
|
||||
TcpOpts::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,9 +587,10 @@ impl ClientConfigFile {
|
||||
/// Build the [`DialConfig`] the client passes to [`aura_transport::dial`].
|
||||
///
|
||||
/// The server **IP** is taken from `[client] server_addr` (its port is ignored: each transport
|
||||
/// uses its own port from `[transport]`). `order` becomes the fallback order, and the per-
|
||||
/// transport options (UDP `obfuscate`, TCP `masquerade`/`host` and the QUIC SNI) come from
|
||||
/// `[transport]` + `[client] sni`.
|
||||
/// uses its own port from `[transport]`). `order` becomes the fallback order. Per-transport
|
||||
/// options: UDP gets `obfuscate` from `[transport]`; TCP/QUIC both use `[client] sni` as their
|
||||
/// outer-TLS camouflage SNI (TLS is now real on the TCP side too, see
|
||||
/// [`aura_transport::TcpClient::connect`]).
|
||||
pub fn dial_config(&self) -> anyhow::Result<DialConfig> {
|
||||
let ip = self.server_socket_addr()?.ip();
|
||||
let order = self.transport.modes()?;
|
||||
@@ -611,11 +611,7 @@ impl ClientConfigFile {
|
||||
obfuscate: self.transport.obfuscate,
|
||||
..UdpOpts::default()
|
||||
},
|
||||
tcp: TcpOpts {
|
||||
masquerade: self.transport.masquerade,
|
||||
host: self.client.sni.clone(),
|
||||
..TcpOpts::default()
|
||||
},
|
||||
tcp: TcpOpts::default(),
|
||||
attempt_timeout: Duration::from_secs(8),
|
||||
})
|
||||
}
|
||||
@@ -787,9 +783,10 @@ masquerade = true
|
||||
assert_eq!(eps.tcp.unwrap().to_string(), "0.0.0.0:4433");
|
||||
assert_eq!(eps.quic.unwrap().to_string(), "0.0.0.0:4434");
|
||||
assert!(cfg.udp_opts().obfuscate);
|
||||
// TCP options are now ALPN-only (real outer TLS handles the camouflage); the legacy
|
||||
// [transport] masquerade / [mimicry] sni values are parsed but no longer plumbed into TcpOpts.
|
||||
let tcp = cfg.tcp_opts();
|
||||
assert!(tcp.masquerade);
|
||||
assert_eq!(tcp.host, "cdn.example.com"); // reuses mimicry SNI
|
||||
assert!(tcp.alpn.is_none(), "default ALPN is used");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -848,8 +845,9 @@ pool_cidr = "10.7.0.0/24"
|
||||
assert!(dial.endpoints.quic.is_none());
|
||||
assert_eq!(dial.sni, "cdn.example.com");
|
||||
assert!(!dial.udp.obfuscate);
|
||||
assert!(dial.tcp.masquerade);
|
||||
assert_eq!(dial.tcp.host, "cdn.example.com");
|
||||
// TCP is wrapped in real outer TLS now; the legacy HTTP `Host` / masquerade fields are gone.
|
||||
// The outer TLS SNI is `dial.sni`, asserted above.
|
||||
assert!(dial.tcp.alpn.is_none(), "default ALPN is used");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user