ba8d6b796f
Live macOS test against the production server uncovered six bugs (one of which turned out to be a port collision with sing-box, not a real bug); this commit addresses all of them and adds v3.4 port discovery so the same collision is handled transparently next time. ## v3.4 server port-discovery - Defaults moved off 443/444 to 8443/8443/8444 (TransportSection::default, ServerInitOpts, ProvisionClientOpts, CLI flags). 443 is heavily contested in practice (sing-box, Hysteria2, reverse proxies) and the previous default silently lost the bind when a co-tenant was already there. - MultiServer::bind_with_outer_or_scan: scans forward up to DEFAULT_PORT_SCAN_MAX (20) candidates per transport when the requested port is occupied; QUIC keeps walking if it lands on the custom-UDP port. - MultiServer::bound_addrs(): the actual addresses each transport bound to. - Server logs the bound addresses and writes a runtime snapshot (server.toml.runtime.json) when they differ from the requested ones, so `aura sign-bridges` can re-sign the bridges manifest later. - BridgeManifest gains an optional `endpoints: Vec<BridgeEndpoint>` field with per-transport ports. Backward-compatible: old v3.3 clients ignore the field and continue to use the v1 `bridges` line. - `aura sign-bridges --endpoints HOST:tcp=N:quic=N:udp=N` to mint v3.4 manifests; bridges line is auto-synthesised for v3.3 clients. ## Bug fixes from the live test - macOS TUN naming (#41): the tun crate rejects names that don't match ^utun[0-9]+$. On macOS we now substitute `""` (kernel auto-assigns utunN), capture the assigned name via inner.tun_name(), and propagate it through to os_routes::OsRouteGuard::install — so `route add -interface utunN` uses the real interface, not "aura0". - Packet counters (#42): Stats { tx_packets, rx_packets } are now actually bumped by the data path. `aura status` shows live numbers instead of permanent zeros. - render_client_toml schema (#44): provisioner emits proper `[[tunnel.split.vpn]] cidr = "..."` / `[[tunnel.split.direct]]` blocks from new --vpn-cidrs / --direct-cidrs flags. The v3.3 `vpn_cidrs = [...]` flat array was silently ignored by serde, leaving users with `rules: 0` even when their CIDRs looked right. - #43 / #46 (TCP/443 dial early-eof / no payload back): diagnosed as the sing-box port collision, not an Aura bug. The v3.4 port-scan path makes it go away — the server picks a free port and clients learn it from the manifest. ## Test coverage Three new unit tests for the port-scanner (UDP busy, TCP busy, zero budget); two new tests for v3.4 BridgeManifest round-trip with endpoints; one integration test for the new `[[tunnel.split.vpn]]` rendering; tests for the runtime-state file write/read round-trip; agent-added router-counter tests in aura-tunnel/tests/routes.rs. cargo test --workspace, cargo clippy --workspace -- -D warnings, and cargo fmt --check all pass. #45 (silent client exit when underlying QUIC transport breaks) is still outstanding — needs deeper investigation; deferred to a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
239 lines
10 KiB
Rust
239 lines
10 KiB
Rust
//! aura-transport — the Aura VPN's QUIC transport with HTTP/3 traffic mimicry (project §7).
|
|
//!
|
|
//! This crate carries the Aura protocol over real QUIC and exposes an established connection as an
|
|
//! [`aura_proto::PacketConnection`]. It has two layers, and which one is the security boundary is
|
|
//! the key design point:
|
|
//!
|
|
//! * **Outer = QUIC/TLS mimicry.** The connection is dressed up to look like ordinary browser
|
|
//! HTTP/3: ALPN `h3`/`h3-29` and Chrome-like transport parameters (see [`mimicry`]). The outer
|
|
//! TLS is **not** the real authentication — so the QUIC client accepts *any* server certificate
|
|
//! ([`quic::AcceptAnyServerCert`]). A passive observer sees what looks like a CDN connection.
|
|
//! * **Inner = the Aura proto handshake.** Over a single bidirectional QUIC stream we run
|
|
//! [`aura_proto::client_handshake`] / [`aura_proto::server_handshake`], which perform the hybrid
|
|
//! post-quantum key agreement and **mutual X.509** verification against the Aura CA. *This* is the
|
|
//! authentication and the source of the session keys.
|
|
//!
|
|
//! ## Layout (project §7)
|
|
//! * [`quic`] — quinn endpoint/config setup and the dangerous outer-TLS verifier.
|
|
//! * [`mimicry`] — ALPN/SNI constants and [`mimicry::chrome_quic_transport_config`].
|
|
//! * [`padding`] — [`padding::pad_to_https_size`] / [`padding::inject_padding_frames`] traffic shaping.
|
|
//! * [`conn`] — [`AuraConnection`], the [`aura_proto::PacketConnection`] implementation.
|
|
//! * [`udp`] — an alternative backend that carries Aura's *own* protocol over **plain UDP**
|
|
//! (no QUIC, no outer TLS): [`UdpServer`] / [`UdpClient`] / [`UdpConnection`]. The Aura PQ
|
|
//! handshake runs over a small DTLS-flight-style reliability adapter; application packets then ride
|
|
//! as unreliable explicit-nonce AEAD datagrams. This is the security-equivalent of the QUIC path
|
|
//! (the inner Aura handshake is the only authentication either way), minus the HTTP/3 disguise.
|
|
//!
|
|
//! ## Usage (Wave 4 / CLI)
|
|
//! ```no_run
|
|
//! # async fn demo(
|
|
//! # ca_cert_pem: String, server_cert_pem: String, server_key_pem: String,
|
|
//! # client_cert_pem: String, client_key_pem: String, server_name: String,
|
|
//! # ) -> anyhow::Result<()> {
|
|
//! use aura_transport::{AuraServer, AuraClient, PacketConnection};
|
|
//! use aura_proto::{ServerConfig, ClientConfig};
|
|
//!
|
|
//! // Server: bind, then accept authenticated connections in a loop.
|
|
//! let server = AuraServer::bind(
|
|
//! "0.0.0.0:4433".parse()?,
|
|
//! &server_cert_pem, // outer QUIC cert (may equal the Aura server cert)
|
|
//! &server_key_pem,
|
|
//! ServerConfig {
|
|
//! ca_cert_pem: ca_cert_pem.clone(),
|
|
//! server_cert_pem: server_cert_pem.clone(),
|
|
//! server_key_pem: server_key_pem.clone(),
|
|
//! },
|
|
//! )?;
|
|
//! let server_conn = server.accept().await?; // -> AuraConnection
|
|
//!
|
|
//! // Client: connect to the server's address with a camouflage SNI.
|
|
//! let client_conn = AuraClient::connect(
|
|
//! "203.0.113.10:4433".parse()?,
|
|
//! "cdn.example.com", // outer SNI (mimicry)
|
|
//! ClientConfig { ca_cert_pem, client_cert_pem, client_key_pem, server_name },
|
|
//! ).await?;
|
|
//!
|
|
//! // Either side: use it as a packet pipe (also works behind Arc<dyn PacketConnection>).
|
|
//! client_conn.send_packet(b"\x45\x00 ...ip packet... ").await?;
|
|
//! let pkt = server_conn.recv_packet().await?;
|
|
//! # let _ = pkt; Ok(())
|
|
//! # }
|
|
//! ```
|
|
|
|
#![forbid(unsafe_code)]
|
|
#![warn(missing_docs)]
|
|
|
|
pub mod conn;
|
|
pub mod dial;
|
|
pub mod mimicry;
|
|
pub mod padding;
|
|
pub mod quic;
|
|
pub mod tcp;
|
|
pub mod udp;
|
|
|
|
pub use conn::AuraConnection;
|
|
pub use dial::{
|
|
dial, Accepted, DialConfig, Endpoints, MultiServer, TransportMode, DEFAULT_PORT_SCAN_MAX,
|
|
};
|
|
pub use mimicry::{alpn_protocols, chrome_quic_transport_config, ALPN_H3, DEFAULT_SNI};
|
|
pub use padding::{
|
|
inject_padding_frames, next_bucket_for_profile, pad_to_bucket, pad_to_https_size,
|
|
HTTPS_SIZE_BUCKETS, PADDING_PROFILES,
|
|
};
|
|
pub use quic::{client_endpoint, server_endpoint, AcceptAnyServerCert};
|
|
pub use tcp::{TcpClient, TcpConnection, TcpOpts, TcpServer, DEFAULT_TCP_ALPN};
|
|
pub use udp::{knock_for_minute, UdpClient, UdpConnection, UdpOpts, UdpServer, KNOCK_LEN};
|
|
|
|
// Re-export the inner proto trait so downstream crates (the CLI) can name the connection as
|
|
// `Arc<dyn aura_transport::PacketConnection>` without a separate `aura_proto` import.
|
|
pub use aura_proto::PacketConnection;
|
|
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
|
|
use aura_proto::{client_handshake, server_handshake, ClientConfig, ServerConfig};
|
|
use thiserror::Error;
|
|
|
|
/// Errors produced by the Aura transport layer.
|
|
#[derive(Debug, Error)]
|
|
pub enum TransportError {
|
|
/// A PEM blob (certificate or private key) could not be parsed.
|
|
#[error("PEM parse error: {0}")]
|
|
Pem(String),
|
|
|
|
/// Building or converting a rustls/quic TLS configuration failed.
|
|
#[error("TLS configuration error: {0}")]
|
|
Tls(String),
|
|
|
|
/// Binding, connecting, or operating the quinn endpoint failed (includes the QUIC handshake).
|
|
#[error("QUIC transport error: {0}")]
|
|
Quic(String),
|
|
|
|
/// The inner Aura protocol handshake failed.
|
|
#[error("Aura handshake error: {0}")]
|
|
Handshake(#[from] aura_proto::ProtoError),
|
|
|
|
/// A generic I/O error (e.g. binding the UDP socket).
|
|
#[error("I/O error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
// quinn's connect/handshake errors are distinct types; fold them into one transport error.
|
|
impl From<quinn::ConnectError> for TransportError {
|
|
fn from(e: quinn::ConnectError) -> Self {
|
|
TransportError::Quic(format!("connect: {e}"))
|
|
}
|
|
}
|
|
impl From<quinn::ConnectionError> for TransportError {
|
|
fn from(e: quinn::ConnectionError) -> Self {
|
|
TransportError::Quic(format!("connection: {e}"))
|
|
}
|
|
}
|
|
|
|
/// An Aura VPN server: a bound QUIC endpoint that accepts authenticated [`AuraConnection`]s.
|
|
///
|
|
/// Each [`accept`](AuraServer::accept) performs the outer QUIC accept, opens the inner bidirectional
|
|
/// stream, runs [`aura_proto::server_handshake`] (mutual auth against the CA), and returns a ready
|
|
/// [`AuraConnection`].
|
|
pub struct AuraServer {
|
|
endpoint: quinn::Endpoint,
|
|
proto_cfg: Arc<ServerConfig>,
|
|
}
|
|
|
|
impl AuraServer {
|
|
/// Bind a server on `addr`.
|
|
///
|
|
/// * `addr` — UDP address to listen on; use `..:0` for an OS-assigned port and read it back with
|
|
/// [`AuraServer::local_addr`].
|
|
/// * `outer_cert_pem` / `outer_key_pem` — the **outer** QUIC/TLS (mimicry) certificate and key.
|
|
/// These may be the same PEM as the Aura server cert in `proto_cfg` (and typically are).
|
|
/// * `proto_cfg` — the inner Aura handshake config (CA + server leaf cert/key) used to mutually
|
|
/// authenticate each client.
|
|
///
|
|
/// # Errors
|
|
/// Returns [`TransportError`] if the certs/keys are unparsable or the UDP socket cannot bind.
|
|
pub fn bind(
|
|
addr: SocketAddr,
|
|
outer_cert_pem: &str,
|
|
outer_key_pem: &str,
|
|
proto_cfg: ServerConfig,
|
|
) -> Result<Self, TransportError> {
|
|
let endpoint = quic::server_endpoint(addr, outer_cert_pem, outer_key_pem)?;
|
|
Ok(Self {
|
|
endpoint,
|
|
proto_cfg: Arc::new(proto_cfg),
|
|
})
|
|
}
|
|
|
|
/// The local address (including the OS-assigned port) this server is bound to.
|
|
///
|
|
/// # Errors
|
|
/// Returns [`TransportError::Io`] if the underlying socket address cannot be read.
|
|
pub fn local_addr(&self) -> Result<SocketAddr, TransportError> {
|
|
Ok(self.endpoint.local_addr()?)
|
|
}
|
|
|
|
/// Accept the next client: outer QUIC handshake, then the inner Aura mutual-auth handshake.
|
|
///
|
|
/// Returns a ready [`AuraConnection`] whose [`peer_id`](AuraConnection::peer_id) is the verified
|
|
/// client Common Name. Call this in a loop (optionally spawning a task per connection).
|
|
///
|
|
/// # Errors
|
|
/// Returns [`TransportError`] if the endpoint is closed, the QUIC handshake fails, or the inner
|
|
/// Aura handshake fails (e.g. the client's certificate does not verify against the CA).
|
|
pub async fn accept(&self) -> Result<AuraConnection, TransportError> {
|
|
let incoming = self
|
|
.endpoint
|
|
.accept()
|
|
.await
|
|
.ok_or_else(|| TransportError::Quic("endpoint closed".into()))?;
|
|
let connection = incoming.await?;
|
|
// The client opens the bidi stream by writing the first ClientHello byte; accept it.
|
|
let (send, recv) = connection.accept_bi().await?;
|
|
// proto reader = RecvStream, proto writer = SendStream.
|
|
let session = server_handshake(recv, send, &self.proto_cfg).await?;
|
|
Ok(AuraConnection::from_session(session))
|
|
}
|
|
|
|
/// Access the underlying quinn endpoint (e.g. for graceful shutdown via `close`/`wait_idle`).
|
|
#[must_use]
|
|
pub fn endpoint(&self) -> &quinn::Endpoint {
|
|
&self.endpoint
|
|
}
|
|
}
|
|
|
|
/// An Aura VPN client entry point.
|
|
pub struct AuraClient;
|
|
|
|
impl AuraClient {
|
|
/// Connect to an Aura server at `server_addr`, presenting `sni` as the outer (mimicry) hostname.
|
|
///
|
|
/// Performs the outer QUIC connect (accepting any server cert — see crate docs), opens a single
|
|
/// bidirectional stream, and runs [`aura_proto::client_handshake`] for hybrid-PQ key agreement
|
|
/// and mutual X.509 auth using `proto_cfg`.
|
|
///
|
|
/// * `server_addr` — the server's UDP socket address.
|
|
/// * `sni` — the Server Name Indication to present on the outer TLS (camouflage, e.g.
|
|
/// `"cdn.example.com"`); this is independent of `proto_cfg.server_name`, which is the name
|
|
/// verified *inside* the Aura handshake against the server's real certificate.
|
|
/// * `proto_cfg` — CA + client leaf cert/key + expected server name for the inner handshake.
|
|
///
|
|
/// # Errors
|
|
/// Returns [`TransportError`] if the QUIC connect/handshake fails or the inner Aura handshake
|
|
/// fails (e.g. the server cert does not chain to the CA or its SAN does not match
|
|
/// `proto_cfg.server_name`).
|
|
pub async fn connect(
|
|
server_addr: SocketAddr,
|
|
sni: &str,
|
|
proto_cfg: ClientConfig,
|
|
) -> Result<AuraConnection, TransportError> {
|
|
let endpoint = quic::client_endpoint()?;
|
|
let connection = endpoint.connect(server_addr, sni)?.await?;
|
|
// open_bi() reserves the stream; the first write (the ClientHello inside the handshake)
|
|
// actually opens it on the wire.
|
|
let (send, recv) = connection.open_bi().await?;
|
|
let session = client_handshake(recv, send, &proto_cfg).await?;
|
|
Ok(AuraConnection::from_session(session))
|
|
}
|
|
}
|