Files
AuraVPN/crates/aura-transport/src/lib.rs
T
xah30 c95e1a482c feat(crypto,cli,transport): daily protocol-mask rotation at 05:00 MSK
Both server and client deterministically rotate the on-wire obfuscation mask
(SNI, HTTP Host/User-Agent/Server headers, UDP padding profile) at 05:00 Moscow
time (02:00 UTC) every day, derived from the CA fingerprint + UTC date — no
network coordination needed.

- aura-crypto::masks: MaskSet + 4 palettes (16 SNI, 10 UA, 5 Server, 4 padding
  profiles); derive_mask_for_msk_date via HKDF-SHA256(salt="aura-mask-v1-salt",
  ikm=ca_fp||"YYYY-MM-DD", info="aura-mask-v1"); ca_fingerprint with built-in
  base64 PEM decode (no new deps).
- aura-cli::masks: MaskRotator (Arc<RwLock<MaskSet>>) + Hinnant's civil_from_days
  for manual UTC date math; scheduler picks next 02:00 UTC strictly (avoids
  busy-loop at boundary); spawned at startup in server::run/client::run.
- aura-transport: PADDING_PROFILES + next_bucket_for_profile (profile 0 byte-for-
  byte equals legacy pad_to_https_size); TcpOpts gains user_agent/server_header;
  UdpOpts gains padding_profile; MultiServer holds Arc<UdpServer>/Arc<TcpServer>
  with set_udp_opts/set_tcp_opts so rotation propagates without restart.
- Backward-compatible: defaults preserve previous behavior; existing 97 tests
  unchanged. 17 new tests (derive determinism + date variation, civil-from-days
  known points incl. 1970-01-01/2000-02-29/2024->2025, next-rotation boundary,
  msk_today offset, profile equivalence, base64 round-trip, full mask-driven
  UDP loopback). Total: 114 passed, clippy/fmt clean. No new workspace deps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:11:45 +03:00

237 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};
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};
pub use udp::{UdpClient, UdpConnection, UdpOpts, UdpServer};
// 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))
}
}