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:
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
description = "P2P transport layer for quicproquo using iroh."
|
||||
license = "MIT"
|
||||
|
||||
[features]
|
||||
traffic-resistance = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
204
crates/quicproquo-p2p/src/traffic_resistance.rs
Normal file
204
crates/quicproquo-p2p/src/traffic_resistance.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user