From ee68c162b8ebf3d815629aece384d3f62b0cdd80 Mon Sep 17 00:00:00 2001 From: peterc-s Date: Fri, 1 May 2026 05:18:33 +0100 Subject: [PATCH 1/2] Add WTF-PAD lite defence --- crates/chaff-machines/src/lib.rs | 1 + crates/chaff-machines/src/wtf_pad_lite.rs | 98 +++++++++++++++++++++++ crates/chaff-sim/src/lib.rs | 54 ++++++++++++- crates/chaff/src/distr.rs | 15 +++- 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 crates/chaff-machines/src/wtf_pad_lite.rs diff --git a/crates/chaff-machines/src/lib.rs b/crates/chaff-machines/src/lib.rs index d0bae2c..9557358 100644 --- a/crates/chaff-machines/src/lib.rs +++ b/crates/chaff-machines/src/lib.rs @@ -2,3 +2,4 @@ pub mod constant; pub mod test; +pub mod wtf_pad_lite; diff --git a/crates/chaff-machines/src/wtf_pad_lite.rs b/crates/chaff-machines/src/wtf_pad_lite.rs new file mode 100644 index 0000000..9f4a8ca --- /dev/null +++ b/crates/chaff-machines/src/wtf_pad_lite.rs @@ -0,0 +1,98 @@ +//! The "WTF-PAD lite" defence. Simplified version of WTF-PAD for demonstration purposes. +//! +//! This defence does not do both burst and gap level padding, has different stop conditions, and a +//! different way of sampling distributions. + +use chaff::{ + action::{FrameworkAction, IntegratorAction}, + distr::Distr, + event::Event, + machine, + machine::Machine, +}; + +/// Constructs a "WTF-PAD lite" defence. A simplified version of WTF-PAD for demonstration purposes. +/// +/// This defence does not do both burst and gap level padding, has different stop conditions, and a +/// different way of sampling distributions. +/// # Panics +/// +/// Shouldn't panic unless modified. +#[expect(clippy::expect_used)] +pub fn construct( + delay_distr: impl Into + Copy, + timeout: impl Into + Copy, +) -> Machine { + machine! { + queues: [None, None], + budget: Absolute(500), + state init { + transitions: [ + Event::SendNormal => active, + Event::ReceiveNormal => active, + ] + }, + state active { + actions: [ + FrameworkAction::schedule( + IntegratorAction::SendDecoy, + 0, + delay_distr.into() + ), + FrameworkAction::schedule( + IntegratorAction::ReleaseBlock, + 1, + timeout.into() + ), + ], + transitions: [ + // if we send or receive anything, reset the timer and note + // that we received real traffic + Event::SendNormal => active_real, + Event::ReceiveNormal => active_real, + + // if we haven't sent or received anything, then a decoy + // must have been sent, so reset the delay timer + Event::QueuePopped(0) => active, + + // if the timeout happens, safely exit. + Event::QueuePopped(1) => end, + + // if we exhaust the budget, safely exit + Event::MachineBudgetExhausted => end, + ], + }, + state active_real { + actions: [ + FrameworkAction::CancelAll, + FrameworkAction::schedule( + IntegratorAction::SendDecoy, + 0, + delay_distr.into() + ), + FrameworkAction::schedule( + IntegratorAction::ReleaseBlock, + 1, + timeout.into() + ), + ], + transitions: [ + // if we send or receive anything, reset the timer and + // note that we received real traffic + Event::SendNormal => active_real, + Event::ReceiveNormal => active_real, + + // if we haven't sent or received anything, then a decoy + // must have been sent, so reset the timer in active + Event::QueuePopped(0) => active, + + Event::QueuePopped(1) => end, + Event::MachineBudgetExhausted => end, + ], + }, + state end { + actions: [FrameworkAction::CancelAll], + } + } + .expect("preconstructed machine should be valid") +} diff --git a/crates/chaff-sim/src/lib.rs b/crates/chaff-sim/src/lib.rs index b4d6a8d..bc699f8 100644 --- a/crates/chaff-sim/src/lib.rs +++ b/crates/chaff-sim/src/lib.rs @@ -960,13 +960,18 @@ mod tests { #[cfg(test)] mod machines { + use std::time::Duration; + use super::Simulator; - use chaff::framework::Framework; + use chaff::{ + distr::{Distr, DistrKind}, + framework::Framework, + }; use chaff_capture::trace::{Direction, Trace}; - use chaff_machines::constant; + use chaff_machines::{constant, wtf_pad_lite}; #[test] - pub fn test_constant_machine() { + pub fn test_constant() { let machine = constant::construct(); let framework = Framework::new(machine, rand::rng()); let trace = Trace::new( @@ -989,5 +994,48 @@ mod tests { ); assert!(out.len() < 35, "len: {}", out.len()); } + + #[test] + pub fn test_wtf_pad_lite() { + let delay_distr: Distr = DistrKind::Normal { + mean: 0.015, + std_dev: 0.05, + } + .try_into() + .unwrap(); + + let timeout: Distr = Duration::from_secs_f64(0.4).try_into().unwrap(); + + let machine = wtf_pad_lite::construct(delay_distr, timeout); + let framework = Framework::new(machine, rand::rng()); + let trace = Trace::new( + [Direction::Send; 7], + [0, 123, 100_000, 200_000, 104, 204_213, 1_000_000], + [0; 7], + ); + let mut simulator = Simulator::with(framework, trace, rand::rng()); + let (out, _) = simulator.run(); + + assert!(out.len() < 75, "len: {}", out.len()); + } + + #[test] + pub fn test_wtf_pad_lite_constant() { + let delay_distr: Distr = Duration::from_millis(20).try_into().unwrap(); + let timeout: Distr = Duration::from_secs_f64(0.4).try_into().unwrap(); + + let machine = wtf_pad_lite::construct(delay_distr, timeout); + let framework = Framework::new(machine, rand::rng()); + let trace = Trace::new( + [Direction::Send; 7], + [0, 123, 100_000, 200_000, 104, 204_213, 399_999], + [0; 7], + ); + let mut simulator = Simulator::with(framework, trace, rand::rng()); + let (out, _) = simulator.run(); + + assert_eq!(out.len(), 81); + assert!(!out.timing_deltas().iter().any(|delta| *delta > 20_000)); + } } } diff --git a/crates/chaff/src/distr.rs b/crates/chaff/src/distr.rs index 5542d33..7fa0b05 100644 --- a/crates/chaff/src/distr.rs +++ b/crates/chaff/src/distr.rs @@ -291,8 +291,19 @@ fn maybe_clamp(val: f64, min: Option, max: Option) -> f64 { impl Distribution for Distr { fn sample(&self, rng: &mut R) -> Duration { + fn round_to_micros(dur: Duration) -> Duration { + let nanos = dur.subsec_nanos(); + let remainder = nanos % 1_000; + + if remainder == 0 { + dur + } else { + dur + Duration::from_nanos(1_000 - u64::from(remainder)) + } + } + #[expect(clippy::cast_precision_loss)] - Duration::from_secs_f64( + round_to_micros(Duration::from_secs_f64( maybe_clamp( self.offset + match self.distr { @@ -318,7 +329,7 @@ impl Distribution for Distr { self.max, ) .max(0.0), - ) + )) } } From 0db1c2799aabd58de7ea64e530ff531f1cd74f36 Mon Sep 17 00:00:00 2001 From: peterc-s Date: Fri, 1 May 2026 05:21:59 +0100 Subject: [PATCH 2/2] Add a couple small fixes --- crates/chaff-machines/src/constant.rs | 2 +- crates/chaff-machines/src/wtf_pad_lite.rs | 2 ++ crates/chaff/src/machine.rs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/chaff-machines/src/constant.rs b/crates/chaff-machines/src/constant.rs index 0788b3a..2c0d1a3 100644 --- a/crates/chaff-machines/src/constant.rs +++ b/crates/chaff-machines/src/constant.rs @@ -1,4 +1,4 @@ -//! An attempted implementation of a machine that tries to make packets flow at a constant rate. +//! A machine that sends bursts at a constant rate. #![cfg(not(tarpaulin_include))] diff --git a/crates/chaff-machines/src/wtf_pad_lite.rs b/crates/chaff-machines/src/wtf_pad_lite.rs index 9f4a8ca..c3d7545 100644 --- a/crates/chaff-machines/src/wtf_pad_lite.rs +++ b/crates/chaff-machines/src/wtf_pad_lite.rs @@ -3,6 +3,8 @@ //! This defence does not do both burst and gap level padding, has different stop conditions, and a //! different way of sampling distributions. +#![cfg(not(tarpaulin_include))] + use chaff::{ action::{FrameworkAction, IntegratorAction}, distr::Distr, diff --git a/crates/chaff/src/machine.rs b/crates/chaff/src/machine.rs index 2225940..0399db9 100644 --- a/crates/chaff/src/machine.rs +++ b/crates/chaff/src/machine.rs @@ -155,6 +155,7 @@ impl borsh::BorshDeserialize for Machine { borsh::BorshDeserialize::deserialize_reader(reader)?; Self::validate(&states, queues.len(), budget).map_err(|err| { + // TODO: coverage for this was broken while making fixes before deadline. fix coverage. std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{err:?}")) })?;