maybenot/lib.rs
1//! Maybenot is a framework for traffic analysis defenses that hide patterns in
2//! encrypted communication.
3//!
4//! Consider encrypted communication protocols such as QUIC, TLS, Tor, and
5//! WireGuard. While the connections are encrypted, *patterns* in the encrypted
6//! communication may still leak information about the underlying plaintext
7//! despite being encrypted. Maybenot is a framework for creating and executing
8//! defenses that hide such patterns. Defenses are implemented as probabilistic
9//! state machines.
10//!
11//! If you want to use Maybenot, see the below example and [`Framework`] for
12//! details. As a user, that is typically all that you need and the other
13//! modules can be ignored. Note that you create an existing [`Machine`] (for
14//! use with the [`Framework`]) using the [`core::str::FromStr`] trait.
15//!
16//! If you want to build machines for the [`Framework`], take a look at all the
17//! modules. For top-down, start with the [`Machine`] type. For bottom-up, start
18//! with [`dist`], [`event`], [`action`], and [`counter`] before [`state`] and
19//! finally [`Machine`].
20//!
21//! ## Example usage
22//! ```
23//! use maybenot::{Framework, Machine, TriggerAction, TriggerEvent};
24//! use std::{str::FromStr, time::Instant};
25//! // This is a large example usage of the Maybenot framework. Some parts
26//! // are a bit odd due to avoiding everything async but should convey the
27//! // general idea.
28//!
29//! // Parse machine, this is a "no-op" machine that does nothing.
30//! // Typically, you should expect to get one or more serialized machines,
31//! // not build them from scratch. The framework takes a vector with zero
32//! // or more machines as input when created. To add or remove a machine,
33//! // just recreate the framework. If you expect to create many instances
34//! // of the framework for the same machines, then share the same vector
35//! // across framework instances. All runtime information is allocated
36//! // internally in the framework without modifying the machines.
37//! let s = "02eNpjYEAHjOgCAAA0AAI=";
38//! // machines will error if invalid
39//! let m = vec![Machine::from_str(s).unwrap()];
40//!
41//! // You create the framework, a lightweight operation, with the following
42//! // parameters:
43//! // - A vector of zero or more machines.
44//! // - Max fractions prevent machines from causing too much overhead: note
45//! // that machines can be defined to be allowed a fixed amount of
46//! // padding/blocking, bypassing these limits until having used up their
47//! // allowed budgets. This means that it is possible to create machines
48//! // that trigger actions to block outgoing traffic indefinitely and/or
49//! // send a lot of outgoing traffic.
50//! // - The current time. For normal use, just provide the current time as
51//! // below. This is exposed mainly for testing purposes (can also be used
52//! // to make the creation of some odd types of machines easier).
53//! // - A random number generator. Typically, this should be a secure
54// random number generator, like the one provided by the `rand` crate.
55//! //
56//! // The framework validates all machines (like ::From_str() above) and
57//! // verifies that the fractions are fractions, so it can return an error.
58//! let mut f = Framework::new(&m, 0.0, 0.0, Instant::now(), rand::rng()).unwrap();
59//!
60//! // Below is the main loop for operating the framework. This should run
61//! // for as long as the underlying connection the framework is attached to
62//! // can communicate (user or protocol-specific data, depending on what is
63//! // being defended).
64//! loop {
65//! // Wait for one or more new events (e.g., on a channel) that should
66//! // be triggered in the framework. Below we just set one example
67//! // event. How you wait and collect events is likely going to be a
68//! // bottleneck. If you have to consider dropping events, it is better
69//! // to drop older events than newer. Ideally, it should be possible
70//! // to process all events one-by-one.
71//! let events = [TriggerEvent::NormalSent];
72//!
73//! // Trigger the event(s) in the framework. This takes linear time
74//! // with the number of events but is very fast (time should be
75//! // dominated by a few calls to sample randomness per event per
76//! // machine).
77//! for action in f.trigger_events(&events, Instant::now()) {
78//! // After triggering all the events, the framework will provide
79//! // zero or more actions to take, up to a maximum of one action
80//! // per machine (regardless of the number of events). It is your
81//! // responsibility to perform those actions according to the
82//! // specification. To do so, you will need two timers per
83//! // machine: an action timer (for action timeouts) and an
84//! // internal timer (part of the machine's internal logic). The
85//! // machine identifier (machine in each TriggerAction) uniquely
86//! // and deterministically maps to a single machine running in the
87//! // framework, so it is suitable as a key for a data structure
88//! // storing your timers per framework instance, e.g.,
89//! // HashMap<MachineId, (SomeTimerDataStructure,
90//! // SomeTimerDataStructure)>).
91//! match action {
92//! TriggerAction::Cancel {
93//! machine: _,
94//! timer: _,
95//! } => {
96//! // Cancel the specified timer (action, internal, or
97//! // both) for the machine in question.
98//! }
99//! TriggerAction::SendPadding {
100//! timeout: _,
101//! bypass: _,
102//! replace: _,
103//! machine: _,
104//! } => {
105//! // Set the action timer with the specified timeout. On
106//! // expiry, do the following:
107//! //
108//! // 1. Send a padding packet.
109//! // 2. Trigger TriggerEvent::PaddingSent { machine:
110//! // machine }.
111//! //
112//! // If bypass is true, then the padding MUST be sent even
113//! // if there is active blocking of outgoing traffic AND
114//! // the active blocking had the bypass flag set. If the
115//! // active blocking had bypass set to false, then the
116//! // padding MUST NOT be sent. This is to support
117//! // completely fail-closed defenses.
118//! //
119//! // If replace is true, then the padding MAY be replaced
120//! // by another packet. The other packet could be an
121//! // encrypted packet already queued but not already sent
122//! // in the tunnel, containing either padding or normal
123//! // data (ideally, the user of the framework cannot tell,
124//! // because encrypted). The other data could also be
125//! // normal data about to be turned into a normal packet
126//! // and sent. Regardless of if the padding is replaced or
127//! // not, the event should still be triggered (steps 2).
128//! // If enqueued normal data sent instead of padding, then
129//! // the NormalSent event should be triggered as well.
130//! //
131//! // Above, note the use case of having bypass and replace
132//! // set to true. This is to support constant-rate
133//! // defenses.
134//! //
135//! // Also, note that if there already is an action timer
136//! // for an earlier action for the machine in question,
137//! // overwrite it with the new timer. This will happen
138//! // very frequently so make effort to make it efficient
139//! // (typically, efficient machines will always have
140//! // something scheduled but try to minimize actual
141//! // padding sent).
142//! }
143//! TriggerAction::BlockOutgoing {
144//! timeout: _,
145//! duration: _,
146//! bypass: _,
147//! replace: _,
148//! machine: _,
149//! } => {
150//! // Set an action timer with the specified timeout,
151//! // overwriting any existing action timer for the machine
152//! // (be it to block or to send padding). On expiry, do
153//! // the following (all or nothing):
154//! //
155//! // 1. If no blocking is currently taking place (globally
156//! // across all machines, so for this instance of the
157//! // framework), start blocking all outgoing traffic
158//! // for the specified duration. If blocking is already
159//! // taking place (due to any machine), there are two
160//! // cases. If replace is true, replace the existing
161//! // blocking duration with the specified duration in
162//! // this action. If replace is false, pick the longest
163//! // duration of the specified duration and the
164//! // *remaining* duration to block already in place.
165//! // 2. Trigger TriggerEvent::BlockingBegin { machine:
166//! // machine } regardless of logic outcome in 1. (From
167//! // the point of view of the machine, blocking is now
168//! // taking place).
169//! //
170//! // Note that blocking is global across all machines,
171//! // since the intent is to block all outgoing traffic.
172//! // Further, you MUST ensure that when blocking ends, you
173//! // trigger TriggerEvent::BlockingEnd.
174//! //
175//! // If bypass is true and blocking was activated,
176//! // extended, or replaced in step 1, then a bypass flag
177//! // MUST be set and be available to check as part of
178//! // dealing with TriggerAction::SendPadding actions (see
179//! // above).
180//! }
181//! TriggerAction::UpdateTimer {
182//! duration: _,
183//! replace: _,
184//! machine: _,
185//! } => {
186//! // If the replace flag is true, overwrite the machine's
187//! // internal timer with the specified duration. If
188//! // replace is false, use the longest of the remaining
189//! // and specified durations.
190//! //
191//! // Regardless of the outcome of the preceding logic,
192//! // trigger TriggerEvent::TimerBegin { machine: machine
193//! // }.
194//! //
195//! // Trigger TriggerEvent::TimerEnd { machine: machine }
196//! // when the timer expires.
197//! }
198//! }
199//! }
200//!
201//! // All done, continue the loop. We break below for the example test
202//! // to not get stuck.
203//! break;
204//! }
205//! ```
206//! ## Key concepts
207//!
208//! ### Packets
209//!
210//! We assume that all traffic is sent in "packets" of uniform size, which may
211//! either be padding or non-padding ("normal").
212//!
213//! ### Tunnels
214//!
215//! We assume that incoming and outgoing traffic is queued in a "tunnel" on its
216//! way to or from the network.
217//!
218//! In the incoming direction, when we receive a packet, it is first queued on
219//! the tunnel, and then eventually processed to find out whether it is padding
220//! or not.
221//!
222//! In the outgoing direction, when we generate a packet, it is encrypted ASAP,
223//! queued on the tunnel, and eventually transmitted on the network.
224//!
225//! ### Framework state, and per-machine state.
226//!
227//! For each [`Machine`] in a [`Framework`], you will need to maintain a certain
228//! amount of state. Specifically, you will need to track:
229//!
230//! - A single "internal" timer, which the machine will manage via
231//! [`TriggerAction::UpdateTimer`] and [`TriggerAction::Cancel`]. If it
232//! expires, you will need to trigger [`TriggerEvent::TimerEnd`].
233//! - A single "action" timer, which the machine will manage via
234//! [`TriggerAction::SendPadding`], [`TriggerAction::BlockOutgoing`], and
235//! [`TriggerAction::Cancel`].
236//! - An action to be taken if and when the "action" timer expires. This
237//! action may be "begin blocking for a certain Duration" or "Send a padding
238//! packet". (There are additional flags associated with these actions.)
239//!
240//! Additionally, for the [`Framework`] itself, you will need to track:
241//! - Whether traffic blocking has been enabled, and when it will expire.
242//! - Whether the enabled traffic blocking is "bypassable" (q.v.).
243//!
244//! ### Blocking
245//!
246//! In addition to sending padding, a Maybenot [`Machine`] can tell the
247//! application to temporarily _block_ traffic.
248//!
249//! While traffic is blocked on a connection, no packets should ordinarily be
250//! sent to the network until traffic becomes unblocked. Instead, normal traffic
251//! should be queued.
252//!
253//! Traffic blocking may be "bypassable" or "non-bypassable". This difference
254//! affects whether padding packets marked with the "bypass" flag can still be
255//! sent while the blocking is in effect.
256//!
257//! By cases:
258//!
259//! | Blocking | Padding | Action |
260//! | -------------- | --------------- | -------------- |
261//! | non-bypassable | none | queue padding |
262//! | | bypass | queue padding |
263//! | | replace | queue padding if queue is empty |
264//! | | bypass, replace | queue padding if queue is empty
265//! | bypassable | none | queue padding |
266//! | | bypass | send padding immediately |
267//! | | replace | queue padding if queue is empty |
268//! | | bypass, replace | send packet from queue immediately, or padding if queue is empty |
269
270pub mod action;
271pub mod constants;
272pub mod counter;
273pub mod dist;
274mod error;
275pub mod event;
276mod framework;
277mod machine;
278mod rate_limited_framework;
279pub mod state;
280pub mod time;
281
282pub use crate::action::{Timer, TriggerAction};
283pub use crate::error::Error;
284pub use crate::event::TriggerEvent;
285pub use crate::rate_limited_framework::RateLimitedFramework;
286pub use framework::{Framework, MachineId};
287pub use machine::Machine;
288
289#[cfg(test)]
290mod tests {
291
292 #[test]
293 fn constants_set() {
294 assert_eq!(crate::constants::VERSION, 2);
295 }
296
297 #[test]
298 fn example_usage() {
299 use crate::{Framework, Machine, TriggerAction, TriggerEvent};
300 use std::{str::FromStr, time::Instant};
301 // This is a large example usage of the Maybenot framework. Some parts
302 // are a bit odd due to avoiding everything async but should convey the
303 // general idea.
304
305 // Parse machine, this is a "no-op" machine that does nothing.
306 // Typically, you should expect to get one or more serialized machines,
307 // not build them from scratch. The framework takes a vector with zero
308 // or more machines as input when created. To add or remove a machine,
309 // just recreate the framework. If you expect to create many instances
310 // of the framework for the same machines, then share the same vector
311 // across framework instances. All runtime information is allocated
312 // internally in the framework without modifying the machines.
313 let s = "02eNpjYEAHjOgCAAA0AAI=";
314 // machines will error if invalid
315 let m = vec![Machine::from_str(s).unwrap()];
316
317 // You create the framework, a lightweight operation, with the following
318 // parameters:
319 // - A vector of zero or more machines.
320 // - Max fractions prevent machines from causing too much overhead: note
321 // that machines can be defined to be allowed a fixed amount of
322 // padding/blocking, bypassing these limits until having used up their
323 // allowed budgets. This means that it is possible to create machines
324 // that trigger actions to block outgoing traffic indefinitely and/or
325 // send a lot of outgoing traffic.
326 // - The current time. For normal use, just provide the current time as
327 // below. This is exposed mainly for testing purposes (can also be used
328 // to make the creation of some odd types of machines easier).
329 // - A random number generator. Typically, this should be a secure
330 // random number generator, like the one provided by the `rand` crate.
331 //
332 // The framework validates all machines (like ::From_str() above) and
333 // verifies that the fractions are fractions, so it can return an error.
334 let mut f = Framework::new(&m, 0.0, 0.0, Instant::now(), rand::rng()).unwrap();
335
336 // Below is the main loop for operating the framework. This should run
337 // for as long as the underlying connection the framework is attached to
338 // can communicate (user or protocol-specific data, depending on what is
339 // being defended).
340 loop {
341 // Wait for one or more new events (e.g., on a channel) that should
342 // be triggered in the framework. Below we just set one example
343 // event. How you wait and collect events is likely going to be a
344 // bottleneck. If you have to consider dropping events, it is better
345 // to drop older events than newer. Ideally, it should be possible
346 // to process all events one-by-one.
347 let events = [TriggerEvent::NormalSent];
348
349 // Trigger the event(s) in the framework. This takes linear time
350 // with the number of events but is very fast (time should be
351 // dominated by a few calls to sample randomness per event per
352 // machine).
353 for action in f.trigger_events(&events, Instant::now()) {
354 // After triggering all the events, the framework will provide
355 // zero or more actions to take, up to a maximum of one action
356 // per machine (regardless of the number of events). It is your
357 // responsibility to perform those actions according to the
358 // specification. To do so, you will need two timers per
359 // machine: an action timer (for action timeouts) and an
360 // internal timer (part of the machine's internal logic). The
361 // machine identifier (machine in each TriggerAction) uniquely
362 // and deterministically maps to a single machine running in the
363 // framework, so it is suitable as a key for a data structure
364 // storing your timers per framework instance, e.g.,
365 // HashMap<MachineId, (SomeTimerDataStructure,
366 // SomeTimerDataStructure)>).
367 match action {
368 TriggerAction::Cancel {
369 machine: _,
370 timer: _,
371 } => {
372 // Cancel the specified timer (action, internal, or
373 // both) for the machine in question.
374 }
375 TriggerAction::SendPadding {
376 timeout: _,
377 bypass: _,
378 replace: _,
379 machine: _,
380 } => {
381 // Set the action timer with the specified timeout. On
382 // expiry, do the following:
383 //
384 // 1. Send a padding packet.
385 // 2. Trigger TriggerEvent::PaddingSent { machine:
386 // machine }.
387 //
388 // If bypass is true, then the padding MUST be sent even
389 // if there is active blocking of outgoing traffic AND
390 // the active blocking had the bypass flag set. If the
391 // active blocking had bypass set to false, then the
392 // padding MUST NOT be sent. This is to support
393 // completely fail-closed defenses.
394 //
395 // If replace is true, then the padding MAY be replaced
396 // by another packet. The other packet could be an
397 // encrypted packet already queued but not already sent
398 // in the tunnel, containing either padding or normal
399 // data (ideally, the user of the framework cannot tell,
400 // because encrypted). The other data could also be
401 // normal data about to be turned into a normal packet
402 // and sent. Regardless of if the padding is replaced or
403 // not, the event should still be triggered (steps 2).
404 // If enqueued normal data sent instead of padding, then
405 // the NormalSent event should be triggered as well.
406 //
407 // Above, note the use case of having bypass and replace
408 // set to true. This is to support constant-rate
409 // defenses.
410 //
411 // Also, note that if there already is an action timer
412 // for an earlier action for the machine in question,
413 // overwrite it with the new timer. This will happen
414 // very frequently so make effort to make it efficient
415 // (typically, efficient machines will always have
416 // something scheduled but try to minimize actual
417 // padding sent).
418 }
419 TriggerAction::BlockOutgoing {
420 timeout: _,
421 duration: _,
422 bypass: _,
423 replace: _,
424 machine: _,
425 } => {
426 // Set an action timer with the specified timeout,
427 // overwriting any existing action timer for the machine
428 // (be it to block or to send padding). On expiry, do
429 // the following (all or nothing):
430 //
431 // 1. If no blocking is currently taking place (globally
432 // across all machines, so for this instance of the
433 // framework), start blocking all outgoing traffic
434 // for the specified duration. If blocking is already
435 // taking place (due to any machine), there are two
436 // cases. If replace is true, replace the existing
437 // blocking duration with the specified duration in
438 // this action. If replace is false, pick the longest
439 // duration of the specified duration and the
440 // *remaining* duration to block already in place.
441 // 2. Trigger TriggerEvent::BlockingBegin { machine:
442 // machine } regardless of logic outcome in 1. (From
443 // the point of view of the machine, blocking is now
444 // taking place).
445 //
446 // Note that blocking is global across all machines,
447 // since the intent is to block all outgoing traffic.
448 // Further, you MUST ensure that when blocking ends, you
449 // trigger TriggerEvent::BlockingEnd.
450 //
451 // If bypass is true and blocking was activated,
452 // extended, or replaced in step 1, then a bypass flag
453 // MUST be set and be available to check as part of
454 // dealing with TriggerAction::SendPadding actions (see
455 // above).
456 }
457 TriggerAction::UpdateTimer {
458 duration: _,
459 replace: _,
460 machine: _,
461 } => {
462 // If the replace flag is true, overwrite the machine's
463 // internal timer with the specified duration. If
464 // replace is false, use the longest of the remaining
465 // and specified durations.
466 //
467 // Regardless of the outcome of the preceding logic,
468 // trigger TriggerEvent::TimerBegin { machine: machine
469 // }.
470 //
471 // Trigger TriggerEvent::TimerEnd { machine: machine }
472 // when the timer expires.
473 }
474 }
475 }
476
477 // In real usage the loop would continue here. But since this is just an example test
478 // that should terminate, we add a break here to make the test finish.
479 if true {
480 break;
481 }
482 }
483 }
484}