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>
This commit is contained in:
@@ -28,15 +28,47 @@ use tokio::sync::RwLock;
|
||||
|
||||
use crate::admin::{self, AdminState, Stats};
|
||||
use crate::config::ClientConfigFile;
|
||||
use crate::masks::MaskRotator;
|
||||
|
||||
/// Entry point for `aura client --config <PATH>` (and optional `--admin-socket`).
|
||||
pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
||||
let cfg = ClientConfigFile::load(config_path)?;
|
||||
let local_ip = cfg.local_ip()?;
|
||||
let proto_cfg = cfg.to_proto()?;
|
||||
let dial_cfg = cfg.dial_config()?;
|
||||
let mut dial_cfg = cfg.dial_config()?;
|
||||
let (table, domains) = cfg.build_route_table()?;
|
||||
|
||||
// Build the daily mask rotator (HKDF over the CA fingerprint + MSK date). When enabled, the
|
||||
// current mask overrides the static `[client] sni` / `[transport] obfuscate` values on the
|
||||
// `DialConfig` so the connect we are about to do already uses today's mask. The rotator's
|
||||
// background task keeps `rot.handle()` updated for any future re-dials.
|
||||
let masks_enabled = cfg.transport.masks.enabled;
|
||||
let mask_rotator = if masks_enabled {
|
||||
let rot = Arc::new(MaskRotator::new(&proto_cfg.ca_cert_pem)?);
|
||||
let initial = rot.current().await;
|
||||
dial_cfg.sni = initial.sni.clone();
|
||||
dial_cfg.tcp.host = initial.http_host.clone();
|
||||
dial_cfg.tcp.user_agent = initial.user_agent.clone();
|
||||
dial_cfg.tcp.server_header = initial.server_header.clone();
|
||||
dial_cfg.udp.padding_profile = initial.padding_profile_id;
|
||||
tracing::info!(
|
||||
sni = %initial.sni,
|
||||
padding_profile = initial.padding_profile_id,
|
||||
"mask rotation enabled; initial mask applied to dial"
|
||||
);
|
||||
// Keep the rotation task running in the background; v1's client only dials once, so the
|
||||
// background rotation is informational here — but it is the same wiring the server uses
|
||||
// and keeps the abstraction symmetric for future per-event re-dials.
|
||||
let _bg = rot.spawn();
|
||||
Some(rot)
|
||||
} else {
|
||||
tracing::info!("mask rotation disabled in config; using static TOML values");
|
||||
None
|
||||
};
|
||||
// Suppress the unused-binding warning when the rotator is `Some` but the local does not get
|
||||
// read again (the spawn keeps the rotator alive via its own Arc clone).
|
||||
let _ = &mask_rotator;
|
||||
|
||||
tracing::info!(
|
||||
name = %cfg.client.name,
|
||||
server_addr = %cfg.client.server_addr,
|
||||
|
||||
@@ -216,6 +216,8 @@ pub struct TransportSection {
|
||||
pub obfuscate: bool,
|
||||
/// TCP transport: prepend a minimal HTTP/1.1 preamble so the open resembles plain HTTP.
|
||||
pub masquerade: bool,
|
||||
/// `[transport.masks]`: daily protocol-mask rotation knobs.
|
||||
pub masks: MasksSection,
|
||||
}
|
||||
|
||||
impl Default for TransportSection {
|
||||
@@ -227,10 +229,31 @@ impl Default for TransportSection {
|
||||
quic_port: 444,
|
||||
obfuscate: true,
|
||||
masquerade: true,
|
||||
masks: MasksSection::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[transport.masks]` section: automatic daily rotation of the obfuscation surface (SNI, HTTP
|
||||
/// preamble headers, padding profile) at 05:00 MSK.
|
||||
///
|
||||
/// Both peers derive the current mask from `(CA fingerprint, MSK date)`; no wire coordination is
|
||||
/// needed. When disabled, the static values from `[client] sni` / `[transport] obfuscate` /
|
||||
/// `[mimicry] sni` are used as before (pre-rotation behaviour).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct MasksSection {
|
||||
/// `true` (default): rotate the obfuscation surface daily at 05:00 MSK = 02:00 UTC. `false`:
|
||||
/// use the static TOML values verbatim.
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for MasksSection {
|
||||
fn default() -> Self {
|
||||
Self { enabled: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportSection {
|
||||
/// Parse `order` into [`TransportMode`]s, rejecting unknown names and duplicates.
|
||||
pub fn modes(&self) -> anyhow::Result<Vec<TransportMode>> {
|
||||
@@ -475,6 +498,7 @@ impl ClientConfigFile {
|
||||
tcp: TcpOpts {
|
||||
masquerade: self.transport.masquerade,
|
||||
host: self.client.sni.clone(),
|
||||
..TcpOpts::default()
|
||||
},
|
||||
attempt_timeout: Duration::from_secs(8),
|
||||
})
|
||||
|
||||
@@ -16,5 +16,6 @@ pub mod admin;
|
||||
pub mod bench;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod masks;
|
||||
pub mod pki;
|
||||
pub mod server;
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
//! Daily protocol-mask rotator: keeps a shared [`MaskSet`] up to date at **05:00 MSK** (= 02:00
|
||||
//! UTC) without any network coordination.
|
||||
//!
|
||||
//! Both server and client run their own [`MaskRotator`] right after loading the PKI. The rotator:
|
||||
//!
|
||||
//! 1. derives the current day's [`MaskSet`] from `(CA fingerprint, MSK date)` at startup;
|
||||
//! 2. sleeps until the next 02:00 UTC (today's if still in the future, else tomorrow's);
|
||||
//! 3. derives the new day's mask and swaps it into the shared `RwLock`;
|
||||
//! 4. logs the rotation and loops.
|
||||
//!
|
||||
//! Each new connection (`UdpServer::accept`, `UdpClient::connect`, `TcpClient::connect`, ...)
|
||||
//! reads the **current** mask once when constructing its [`UdpOpts`] / [`TcpOpts`] / QUIC SNI, so
|
||||
//! already-established connections keep their original mask and only fresh connections see the
|
||||
//! rotation. There is no need to coordinate with the peer: each side independently derived the same
|
||||
//! set from the CA fingerprint it already trusts.
|
||||
//!
|
||||
//! ## Date arithmetic (no external date crate)
|
||||
//!
|
||||
//! We compute the calendar date from a Unix timestamp using Howard Hinnant's `civil_from_days`
|
||||
//! algorithm (`days_from_epoch -> (year, month, day)`), so we do not pull in `chrono` / `time` for
|
||||
//! this crate. MSK is UTC+3 with no DST since 2014, so the MSK date is simply
|
||||
//! `floor((unix + 3*3600) / 86400)` days since the epoch.
|
||||
//!
|
||||
//! ## What rotates and what does not
|
||||
//!
|
||||
//! The mask only controls the **obfuscation surface**: SNI / HTTP header strings, padding
|
||||
//! profile. The Aura cryptographic handshake (hybrid X25519 + ML-KEM-768 + mutual X.509) is
|
||||
//! unchanged; rotation does not weaken or alter authentication, only the on-wire fingerprint a
|
||||
//! passive observer can latch onto.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use aura_crypto::{ca_fingerprint, derive_mask_for_msk_date, MaskSet};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Shared current mask handle: server and client both clone this `Arc<RwLock<MaskSet>>` into the
|
||||
/// code that builds per-connection transport options.
|
||||
pub type MaskHandle = Arc<RwLock<MaskSet>>;
|
||||
|
||||
/// Daily protocol-mask rotator. Spawn one in `aura server` / `aura client`'s `run` after loading
|
||||
/// the PKI; share its [`handle`](MaskRotator::handle) with the transport-options builders.
|
||||
pub struct MaskRotator {
|
||||
active: MaskHandle,
|
||||
ca_fp: [u8; 32],
|
||||
}
|
||||
|
||||
impl MaskRotator {
|
||||
/// Build a rotator from the CA PEM the rest of the stack already trusts.
|
||||
///
|
||||
/// The initial mask is the one current at the calling instant (today's MSK day). Use
|
||||
/// [`Self::spawn`] to start the daily rotation task that updates the shared handle.
|
||||
///
|
||||
/// # Errors
|
||||
/// Propagates [`aura_crypto::CryptoError`] from [`aura_crypto::ca_fingerprint`] (typically a
|
||||
/// malformed CA PEM).
|
||||
pub fn new(ca_cert_pem: &str) -> anyhow::Result<Self> {
|
||||
let ca_fp = ca_fingerprint(ca_cert_pem)?;
|
||||
let now = unix_now_utc();
|
||||
let (y, m, d) = msk_today(now);
|
||||
let initial = derive_mask_for_msk_date(&ca_fp, y, m, d);
|
||||
Ok(Self {
|
||||
active: Arc::new(RwLock::new(initial)),
|
||||
ca_fp,
|
||||
})
|
||||
}
|
||||
|
||||
/// A snapshot of the current mask. This locks the inner `RwLock` briefly and clones; suitable
|
||||
/// for the once-per-`connect`/`accept` use case (not for hot per-packet paths).
|
||||
pub async fn current(&self) -> MaskSet {
|
||||
self.active.read().await.clone()
|
||||
}
|
||||
|
||||
/// Blocking-friendly current-mask snapshot. Uses `blocking_read` so it is safe to call from
|
||||
/// synchronous CLI bootstrap code that has not yet entered a tokio context.
|
||||
#[must_use]
|
||||
pub fn current_blocking(&self) -> MaskSet {
|
||||
self.active.blocking_read().clone()
|
||||
}
|
||||
|
||||
/// Cloneable handle into the shared current-mask `RwLock`. Pass this to the transport layer so
|
||||
/// each new connection reads the latest mask under one cheap async lock.
|
||||
#[must_use]
|
||||
pub fn handle(&self) -> MaskHandle {
|
||||
Arc::clone(&self.active)
|
||||
}
|
||||
|
||||
/// Spawn the background rotation task: sleeps until the next 02:00 UTC, then loops forever
|
||||
/// updating the shared handle once per MSK day.
|
||||
///
|
||||
/// Returns the [`JoinHandle`] so the caller can keep ownership (the task aborts when the
|
||||
/// rotator's handle is dropped only because the inner `Arc` keeps the `RwLock` alive while the
|
||||
/// task holds it; in normal operation the task lives for the program's lifetime).
|
||||
#[must_use]
|
||||
pub fn spawn(self: &Arc<Self>) -> JoinHandle<()> {
|
||||
let this = Arc::clone(self);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let now = unix_now_utc();
|
||||
let next = next_rotation_unix(now);
|
||||
let wait = (next - now).max(0) as u64;
|
||||
tracing::debug!(
|
||||
in_secs = wait,
|
||||
"mask rotator: sleeping until next 02:00 UTC"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(wait)).await;
|
||||
|
||||
// Recompute "today's MSK date" right after waking so we pick the post-rotation day
|
||||
// (the alarm fires at 02:00 UTC = 05:00 MSK, which is the new MSK day).
|
||||
let after = unix_now_utc();
|
||||
let (y, m, d) = msk_today(after);
|
||||
let new_mask = derive_mask_for_msk_date(&this.ca_fp, y, m, d);
|
||||
{
|
||||
let mut guard = this.active.write().await;
|
||||
if *guard != new_mask {
|
||||
tracing::info!(
|
||||
year = y, month = m, day = d,
|
||||
sni = %new_mask.sni,
|
||||
padding_profile = new_mask.padding_profile_id,
|
||||
"rotated protocol mask (05:00 MSK)"
|
||||
);
|
||||
*guard = new_mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Date / time helpers (no external date crate; pure stdlib).
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// Current Unix time in UTC seconds. Saturates to 0 if the clock is somehow before the epoch
|
||||
/// (impossible on a normal system).
|
||||
pub fn unix_now_utc() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `civil_from_days` (Howard Hinnant): convert days-since-1970-01-01 into `(year, month, day)`.
|
||||
///
|
||||
/// Handles dates from year ~-5.8M to ~+5.8M correctly; for our purposes any 32-bit days-since-epoch
|
||||
/// is fine (covers ±5.8M years around 1970).
|
||||
///
|
||||
/// Returns `(year, month[1..=12], day[1..=31])`.
|
||||
#[must_use]
|
||||
pub fn ymd_from_days(days: i32) -> (i32, u32, u32) {
|
||||
// Shift the epoch to 0000-03-01 (Hinnant's reference) so the leap-cycle math is uniform.
|
||||
let z = days + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097);
|
||||
// doe ("day of era") is in [0, 146096]; the wrapping_sub-style subtraction ends up positive
|
||||
// because `era * 146_097 <= z` by construction, so the cast to u32 is safe.
|
||||
let doe = (z - era * 146_097) as u32;
|
||||
// yoe ("year of era") in [0, 399].
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||
let y = yoe as i32 + era * 400;
|
||||
// doy ("day of year") in [0, 365], where the year begins on March 1 for the algorithm.
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
// mp ("month pivot") in [0, 11], where 0 = March.
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y_real = if m <= 2 { y + 1 } else { y };
|
||||
(y_real, m, d)
|
||||
}
|
||||
|
||||
/// Today's MSK calendar date for a given Unix-UTC timestamp. MSK = UTC+3, no DST since 2014.
|
||||
#[must_use]
|
||||
pub fn msk_today(unix: i64) -> (i32, u32, u32) {
|
||||
let msk_secs = unix + 3 * 3600;
|
||||
let days = msk_secs.div_euclid(86_400) as i32;
|
||||
ymd_from_days(days)
|
||||
}
|
||||
|
||||
/// The next 02:00 UTC (= 05:00 MSK) instant strictly *after* `now`, as a Unix timestamp.
|
||||
///
|
||||
/// If `now` lands exactly at 02:00:00 UTC we still return *tomorrow's* 02:00 (the comparison is
|
||||
/// strict on `>`). That keeps the sleep monotonic — we never schedule a zero-wait rotation that
|
||||
/// would re-derive the same mask in a tight loop.
|
||||
#[must_use]
|
||||
pub fn next_rotation_unix(now: i64) -> i64 {
|
||||
let today_utc_midnight = now.div_euclid(86_400) * 86_400;
|
||||
let today_0200_utc = today_utc_midnight + 2 * 3600;
|
||||
if today_0200_utc > now {
|
||||
today_0200_utc
|
||||
} else {
|
||||
today_0200_utc + 86_400
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Days-since-epoch for a handful of fixed, known calendar dates. The Hinnant algorithm has to
|
||||
/// match a manual cross-check at the boundaries (Y2K, leap year, year transitions, before
|
||||
/// epoch).
|
||||
#[test]
|
||||
fn ymd_from_days_known_points() {
|
||||
// 1970-01-01 == days 0.
|
||||
assert_eq!(ymd_from_days(0), (1970, 1, 1));
|
||||
// 1969-12-31 == days -1.
|
||||
assert_eq!(ymd_from_days(-1), (1969, 12, 31));
|
||||
// 2000-02-29 (a leap day): days from 1970-01-01.
|
||||
// 30 years between 1970 and 2000, with leap years 1972, 76, 80, 84, 88, 92, 96 = 7 leaps.
|
||||
// Jan + Feb of 2000 == 31 + 29 = 60 days, so day 29 of Feb is day 59 (0-indexed) of 2000.
|
||||
// 1970..2000 has 30 years * 365 + 7 leap days = 10957 days. 2000-02-29 == 10957 + 59 = 11016.
|
||||
assert_eq!(ymd_from_days(11016), (2000, 2, 29));
|
||||
// 2024-12-31 -> 2025-01-01 transition.
|
||||
// 1970..2024 = 54 years; leap years strictly *before* 2024 are 1972,76,80,84,88,92,96,2000,
|
||||
// 04,08,12,16,20 = 13 (2100 is not a leap and is out of range anyway). So 2024-01-01 lives
|
||||
// at day 54*365 + 13 = 19723. 2024 itself is leap (366 days), so 2024-12-31 is 19723 + 365
|
||||
// = 20088 and 2025-01-01 is 20089.
|
||||
assert_eq!(ymd_from_days(20088), (2024, 12, 31));
|
||||
assert_eq!(ymd_from_days(20089), (2025, 1, 1));
|
||||
}
|
||||
|
||||
/// Rotation should land exactly at the next 02:00 UTC, with the "==" boundary biased
|
||||
/// strictly forward to avoid a zero-wait rotate loop.
|
||||
#[test]
|
||||
fn next_rotation_at_boundaries() {
|
||||
// Exact 02:00:00 UTC on a known day (2025-01-01 00:00:00 UTC = 1735689600).
|
||||
let jan1_2025_00 = 1735689600i64;
|
||||
let jan1_2025_02 = jan1_2025_00 + 2 * 3600;
|
||||
assert_eq!(
|
||||
next_rotation_unix(jan1_2025_02),
|
||||
jan1_2025_02 + 86_400,
|
||||
"exactly at 02:00 UTC -> tomorrow's 02:00"
|
||||
);
|
||||
// One second before 02:00 -> today's 02:00.
|
||||
assert_eq!(
|
||||
next_rotation_unix(jan1_2025_02 - 1),
|
||||
jan1_2025_02,
|
||||
"01:59:59 UTC -> today's 02:00"
|
||||
);
|
||||
// One second after 02:00 -> tomorrow's 02:00.
|
||||
assert_eq!(
|
||||
next_rotation_unix(jan1_2025_02 + 1),
|
||||
jan1_2025_02 + 86_400,
|
||||
"02:00:01 UTC -> tomorrow's 02:00"
|
||||
);
|
||||
}
|
||||
|
||||
/// MSK = UTC+3: a UTC moment around midnight reports the *next* MSK day already, so the mask
|
||||
/// rolls over at 21:00 UTC every day (= 00:00 MSK).
|
||||
#[test]
|
||||
fn msk_today_handles_offset() {
|
||||
// 2025-01-01 01:00 UTC == 2025-01-01 04:00 MSK => still the 1st in MSK.
|
||||
let utc_0100 = 1735689600i64 + 3600;
|
||||
assert_eq!(msk_today(utc_0100), (2025, 1, 1));
|
||||
// 2025-01-01 22:00 UTC == 2025-01-02 01:00 MSK => already the 2nd in MSK.
|
||||
let utc_2200 = 1735689600i64 + 22 * 3600;
|
||||
assert_eq!(msk_today(utc_2200), (2025, 1, 2));
|
||||
// 2025-01-01 00:00 UTC == 2025-01-01 03:00 MSK => still the 1st in MSK.
|
||||
assert_eq!(msk_today(1735689600), (2025, 1, 1));
|
||||
// 2025-01-01 21:00 UTC == 2025-01-02 00:00 MSK => first instant of the 2nd in MSK.
|
||||
let utc_2100 = 1735689600i64 + 21 * 3600;
|
||||
assert_eq!(msk_today(utc_2100), (2025, 1, 2));
|
||||
}
|
||||
|
||||
/// End-to-end on the rotator API: derived mask is deterministic and the handle reflects the
|
||||
/// current snapshot.
|
||||
#[tokio::test]
|
||||
async fn mask_rotator_new_yields_today_mask() {
|
||||
// Build a tiny CA PEM so the fingerprint pipeline runs realistically.
|
||||
let ca = aura_pki::AuraCa::generate("aura-mask-test-ca").expect("generate CA");
|
||||
let pem = ca.ca_cert_pem();
|
||||
|
||||
let rotator = MaskRotator::new(&pem).expect("rotator");
|
||||
let m1 = rotator.current().await;
|
||||
// Re-derive directly and assert equality (same `(ca_fp, MSK today)`).
|
||||
let now = unix_now_utc();
|
||||
let (y, mo, d) = msk_today(now);
|
||||
let fp = ca_fingerprint(&pem).expect("fp");
|
||||
let m2 = derive_mask_for_msk_date(&fp, y, mo, d);
|
||||
assert_eq!(m1, m2);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use aura_transport::MultiServer;
|
||||
@@ -30,6 +31,7 @@ use tokio::sync::RwLock;
|
||||
|
||||
use crate::admin::{self, AdminState, Stats};
|
||||
use crate::config::ServerConfigFile;
|
||||
use crate::masks::MaskRotator;
|
||||
|
||||
/// Entry point for `aura server --config <PATH>` (and optional `--admin-socket`).
|
||||
pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
||||
@@ -40,8 +42,31 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
||||
|
||||
// Per-transport endpoints (UDP/TCP/QUIC) derived from the listen IP + `[transport]` ports.
|
||||
let endpoints = cfg.transport_endpoints()?;
|
||||
let udp_opts = cfg.udp_opts();
|
||||
let tcp_opts = cfg.tcp_opts();
|
||||
let mut udp_opts = cfg.udp_opts();
|
||||
let mut tcp_opts = cfg.tcp_opts();
|
||||
|
||||
// Build the daily mask rotator (HKDF over the CA fingerprint + MSK date). When enabled in the
|
||||
// config, the *initial* mask overrides the static SNI / padding-profile / header values from
|
||||
// the TOML so the first accepts already use today's mask; the rotator's background task then
|
||||
// updates the bound MultiServer's opts each day at 02:00 UTC (= 05:00 MSK).
|
||||
let masks_enabled = cfg.transport.masks.enabled;
|
||||
let mask_rotator = if masks_enabled {
|
||||
let rot = Arc::new(MaskRotator::new(&proto_cfg.ca_cert_pem)?);
|
||||
let initial = rot.current().await;
|
||||
udp_opts.padding_profile = initial.padding_profile_id;
|
||||
tcp_opts.host = initial.http_host.clone();
|
||||
tcp_opts.user_agent = initial.user_agent.clone();
|
||||
tcp_opts.server_header = initial.server_header.clone();
|
||||
tracing::info!(
|
||||
sni = %initial.sni,
|
||||
padding_profile = initial.padding_profile_id,
|
||||
"mask rotation enabled; initial mask applied"
|
||||
);
|
||||
Some(rot)
|
||||
} else {
|
||||
tracing::info!("mask rotation disabled in config; using static TOML values");
|
||||
None
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
name = %cfg.server.name,
|
||||
@@ -62,11 +87,50 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
||||
|
||||
// Bind every enabled transport at once. The QUIC outer (mimicry) cert reuses the Aura server
|
||||
// leaf inside `proto_cfg`, matching the transport's guidance.
|
||||
let mut server = MultiServer::bind(endpoints, proto_cfg.clone(), udp_opts, tcp_opts)
|
||||
let server = MultiServer::bind(endpoints, proto_cfg.clone(), udp_opts, tcp_opts.clone())
|
||||
.await
|
||||
.context("binding Aura multi-transport server")?;
|
||||
tracing::info!("Aura server bound on all enabled transports");
|
||||
|
||||
// Spawn the mask rotation loop AFTER bind so the rotator can push new opts into the live
|
||||
// server each day. Existing connections keep their accept-time snapshot.
|
||||
let server = Arc::new(tokio::sync::Mutex::new(server));
|
||||
if let Some(rot) = &mask_rotator {
|
||||
let _bg = rot.spawn();
|
||||
let server_for_apply = Arc::clone(&server);
|
||||
let rot_for_apply = Arc::clone(rot);
|
||||
let base_udp = udp_opts;
|
||||
let base_tcp = tcp_opts.clone();
|
||||
tokio::spawn(async move {
|
||||
// Poll the rotator's handle for a change once a minute, and push it into the live
|
||||
// MultiServer when it changes. The actual rotation timer lives inside the rotator's
|
||||
// spawn; this loop is just the "apply to bound sockets" bridge.
|
||||
let handle = rot_for_apply.handle();
|
||||
let mut last = rot_for_apply.current().await;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
let current = handle.read().await.clone();
|
||||
if current != last {
|
||||
let mut new_udp = base_udp;
|
||||
new_udp.padding_profile = current.padding_profile_id;
|
||||
let mut new_tcp = base_tcp.clone();
|
||||
new_tcp.host = current.http_host.clone();
|
||||
new_tcp.user_agent = current.user_agent.clone();
|
||||
new_tcp.server_header = current.server_header.clone();
|
||||
let srv = server_for_apply.lock().await;
|
||||
srv.set_udp_opts(new_udp).await;
|
||||
srv.set_tcp_opts(new_tcp).await;
|
||||
tracing::info!(
|
||||
sni = %current.sni,
|
||||
padding_profile = current.padding_profile_id,
|
||||
"applied rotated mask to bound MultiServer"
|
||||
);
|
||||
last = current;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Shared routing table (server-side classification is trivial in v1: everything via VPN) +
|
||||
// stats, exposed over the admin socket.
|
||||
let routes = Arc::new(RwLock::new(RouteTable::new(RouteAction::Vpn)));
|
||||
@@ -87,7 +151,12 @@ pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
|
||||
// Accept loop. Each accepted connection (from any transport) gets a server-side TUN and a router
|
||||
// task. `MultiServer::accept` yields `None` only when every transport's accept loop has stopped.
|
||||
let mtu = cfg.tunnel.mtu;
|
||||
while let Some(accepted) = server.accept().await {
|
||||
loop {
|
||||
let next = {
|
||||
let mut srv = server.lock().await;
|
||||
srv.accept().await
|
||||
};
|
||||
let Some(accepted) = next else { break };
|
||||
let peer = accepted.peer_id.clone();
|
||||
let mode = accepted.mode;
|
||||
let conn = accepted.conn;
|
||||
|
||||
Reference in New Issue
Block a user