Skip to main content

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}