feat: add traffic analysis resistance (Phase 7.7 + F8)

- Core: add pad_uniform/unpad_uniform for configurable boundary padding
  and generate_decoy for indistinguishable decoy messages
- Server: add traffic_resistance module with payload padding, timing
  jitter, and background decoy traffic generator (feature-gated)
- P2P: add mesh traffic_resistance module with padded envelopes and
  mesh decoy injection (feature-gated)
- All gated behind --features traffic-resistance
- 22 new tests across core (8), server (4), and P2P (5)
This commit is contained in:
2026-03-04 20:50:19 +01:00
parent c401caec60
commit f4621b3425
7 changed files with 590 additions and 0 deletions

View File

@@ -16,6 +16,8 @@ pub mod broadcast;
pub mod envelope;
pub mod identity;
pub mod store;
#[cfg(feature = "traffic-resistance")]
pub mod traffic_resistance;
use std::sync::{Arc, Mutex};

View File

@@ -0,0 +1,204 @@
//! Mesh traffic analysis resistance — uniform envelope padding and decoy injection.
//!
//! When the `traffic-resistance` feature is enabled:
//!
//! 1. [`MeshEnvelope`] payloads are padded to a configurable boundary (default 256 bytes)
//! before signing, so all envelopes on the wire have uniform-sized payloads.
//! 2. A background decoy injector periodically creates fake mesh envelopes and stores
//! them in the mesh store, making real vs. decoy traffic indistinguishable.
use std::sync::{Arc, Mutex};
use rand::Rng;
use tokio::sync::Notify;
use crate::envelope::MeshEnvelope;
use crate::identity::MeshIdentity;
use crate::store::MeshStore;
/// Configuration for mesh traffic analysis resistance.
#[derive(Clone, Debug)]
pub struct MeshTrafficConfig {
/// Padding boundary in bytes for envelope payloads.
pub padding_boundary: usize,
/// Mean interval in milliseconds between decoy envelope injections.
/// Set to 0 to disable.
pub decoy_interval_ms: u64,
}
impl Default for MeshTrafficConfig {
fn default() -> Self {
Self {
padding_boundary: quicproquo_core::padding::DEFAULT_PADDING_BOUNDARY,
decoy_interval_ms: 5000,
}
}
}
/// Pad a mesh payload to the nearest boundary before wrapping in a [`MeshEnvelope`].
pub fn pad_mesh_payload(payload: &[u8], boundary: usize) -> Vec<u8> {
quicproquo_core::padding::pad_uniform(payload, boundary)
}
/// Create a [`MeshEnvelope`] with a uniformly padded payload.
pub fn padded_envelope(
identity: &MeshIdentity,
recipient_key: &[u8],
payload: Vec<u8>,
ttl_secs: u32,
max_hops: u8,
boundary: usize,
) -> MeshEnvelope {
let padded = pad_mesh_payload(&payload, boundary);
MeshEnvelope::new(identity, recipient_key, padded, ttl_secs, max_hops)
}
/// Spawn a background task that injects decoy mesh envelopes into the store.
///
/// Decoy envelopes have random recipient keys and empty (padded) payloads.
/// They are indistinguishable from real padded envelopes on the wire.
pub fn spawn_mesh_decoy_generator(
identity: MeshIdentity,
store: Arc<Mutex<MeshStore>>,
config: MeshTrafficConfig,
shutdown: Arc<Notify>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
if config.decoy_interval_ms == 0 {
shutdown.notified().await;
return;
}
let base_interval = std::time::Duration::from_millis(config.decoy_interval_ms);
loop {
let jitter_factor: f64 = rand::thread_rng().gen_range(0.5..1.5);
let interval = base_interval.mul_f64(jitter_factor);
tokio::select! {
() = tokio::time::sleep(interval) => {}
() = shutdown.notified() => {
tracing::debug!("mesh decoy generator shutting down");
return;
}
}
// Generate a decoy: padded empty payload with a random recipient.
let decoy_payload = quicproquo_core::padding::generate_decoy(config.padding_boundary);
let mut fake_recipient = [0u8; 32];
rand::thread_rng().fill(&mut fake_recipient);
let envelope = MeshEnvelope::new(
&identity,
&fake_recipient,
decoy_payload,
60, // Short TTL.
0,
);
match store.lock() {
Ok(mut s) => {
let _ = s.store(envelope);
tracing::trace!("mesh decoy envelope injected");
}
Err(e) => {
tracing::warn!(error = %e, "mesh store lock poisoned in decoy generator");
}
}
}
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn pad_mesh_payload_boundary_aligned() {
let payload = b"hello mesh";
let padded = pad_mesh_payload(payload, 256);
assert_eq!(padded.len() % 256, 0);
let unpadded = quicproquo_core::padding::unpad_uniform(&padded).unwrap();
assert_eq!(unpadded, payload);
}
#[test]
fn padded_envelope_has_uniform_payload() {
let id = MeshIdentity::generate();
let recipient = [0xAA; 32];
let payload = b"short".to_vec();
let env = padded_envelope(&id, &recipient, payload, 3600, 5, 256);
assert_eq!(env.payload.len() % 256, 0);
assert!(env.verify());
// The inner payload should unpad correctly.
let unpadded = quicproquo_core::padding::unpad_uniform(&env.payload).unwrap();
assert_eq!(unpadded, b"short");
}
#[test]
fn padded_envelope_large_payload() {
let id = MeshIdentity::generate();
let payload = vec![0xBB; 500];
let env = padded_envelope(&id, &[0xCC; 32], payload.clone(), 60, 3, 256);
assert_eq!(env.payload.len() % 256, 0);
assert_eq!(env.payload.len(), 512); // 500 + 4 = 504, rounds to 512
let unpadded = quicproquo_core::padding::unpad_uniform(&env.payload).unwrap();
assert_eq!(unpadded, payload);
}
#[tokio::test]
async fn mesh_decoy_generator_injects_envelopes() {
let id = MeshIdentity::generate();
let store = Arc::new(Mutex::new(MeshStore::new(100)));
let shutdown = Arc::new(Notify::new());
let config = MeshTrafficConfig {
padding_boundary: 256,
decoy_interval_ms: 50,
};
let handle = spawn_mesh_decoy_generator(
id,
Arc::clone(&store),
config,
Arc::clone(&shutdown),
);
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
shutdown.notify_one();
handle.await.unwrap();
let s = store.lock().unwrap();
let (total, _) = s.stats();
assert!(total > 0, "decoy generator should have stored at least one envelope");
}
#[tokio::test]
async fn mesh_decoy_generator_disabled_when_zero() {
let id = MeshIdentity::generate();
let store = Arc::new(Mutex::new(MeshStore::new(100)));
let shutdown = Arc::new(Notify::new());
let config = MeshTrafficConfig {
padding_boundary: 256,
decoy_interval_ms: 0,
};
let handle = spawn_mesh_decoy_generator(
id,
store,
config,
Arc::clone(&shutdown),
);
shutdown.notify_one();
handle.await.unwrap();
}
}