Files
AuraVPN/crates/aura-cli/tests/mask_loopback.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

98 lines
3.9 KiB
Rust

//! End-to-end smoke test for the daily mask rotator: build a CA, derive today's [`MaskSet`], plug
//! its `padding_profile_id` into the server / client `UdpOpts`, run a UDP loopback handshake, and
//! exchange a packet. This proves:
//!
//! * The crypto-layer derivation produces values that the transport layer accepts.
//! * The padding profile id derived from `MaskSet` is a valid argument for `pad_to_bucket` /
//! `next_bucket_for_profile`.
//! * Wire compatibility is preserved when both ends use the same mask.
//!
//! It does NOT exercise the time-based rotation (that runs at 05:00 MSK and would require freezing
//! the clock); the algorithm itself is unit-tested in `aura_cli::masks::tests`.
use std::sync::Arc;
use aura_cli::masks::MaskRotator;
use aura_crypto::derive_mask_for_msk_date;
use aura_pki::AuraCa;
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
use aura_transport::{UdpClient, UdpOpts, UdpServer};
const SERVER_NAME: &str = "localhost";
const CLIENT_ID: &str = "cli-mask-client";
fn make_configs(ca: &AuraCa) -> (ServerConfig, ClientConfig) {
let server_cert = ca
.issue_server_cert(SERVER_NAME)
.expect("issue server cert");
let client_cert = ca.issue_client_cert(CLIENT_ID).expect("issue client cert");
let ca_pem = ca.ca_cert_pem();
let server_cfg = ServerConfig {
ca_cert_pem: ca_pem.clone(),
server_cert_pem: server_cert.cert_pem.clone(),
server_key_pem: server_cert.key_pem.clone(),
};
let client_cfg = ClientConfig {
ca_cert_pem: ca_pem,
client_cert_pem: client_cert.cert_pem,
client_key_pem: client_cert.key_pem,
server_name: SERVER_NAME.to_string(),
};
(server_cfg, client_cfg)
}
#[tokio::test]
async fn mask_drives_udp_loopback_with_obfuscation() {
// Real CA → real CA PEM → real fingerprint → today's MaskSet, both sides.
let ca = AuraCa::generate("aura-mask-loopback-ca").expect("CA");
let pem = ca.ca_cert_pem();
// Rotator drives the *current* mask (matches a direct derive for today).
let rotator = MaskRotator::new(&pem).expect("rotator");
let current = rotator.current().await;
// Cross-check against a direct crypto-layer derive for today's MSK day.
let now = aura_cli::masks::unix_now_utc();
let (y, m, d) = aura_cli::masks::msk_today(now);
let fp = aura_crypto::ca_fingerprint(&pem).expect("fp");
let direct = derive_mask_for_msk_date(&fp, y, m, d);
assert_eq!(current, direct, "rotator should match direct derivation");
// Build UdpOpts with obfuscation on and the *mask's* padding profile id. Both sides agree
// because they derived from the same CA and date.
let opts = UdpOpts {
obfuscate: true,
padding_profile: current.padding_profile_id,
..UdpOpts::default()
};
let (server_cfg, client_cfg) = make_configs(&ca);
let server =
UdpServer::bind("127.0.0.1:0".parse().unwrap(), server_cfg, opts).expect("bind udp server");
let server_addr = server.local_addr().expect("server addr");
let accept_task = tokio::spawn(async move { server.accept().await });
let connect_task =
tokio::spawn(async move { UdpClient::connect(server_addr, client_cfg, opts).await });
let server_conn = accept_task.await.expect("accept join").expect("accept");
let client_conn = connect_task.await.expect("connect join").expect("connect");
assert_eq!(server_conn.peer_id(), Some(CLIENT_ID));
assert_eq!(client_conn.peer_id(), Some(SERVER_NAME));
let server_conn: Arc<dyn PacketConnection> = Arc::new(server_conn);
let client_conn: Arc<dyn PacketConnection> = Arc::new(client_conn);
for pkt in [
b"hello-mask".to_vec(),
vec![0xA5u8; 1300],
(0..200u8).collect::<Vec<u8>>(),
] {
client_conn.send_packet(&pkt).await.expect("client send");
let got = server_conn.recv_packet().await.expect("server recv");
assert_eq!(got, pkt, "padded round trip preserves the payload");
}
}