Skip to main content

maybenot/
action.rs

1//! Actions for [`State`](crate::state) transitions.
2
3use rand_core::RngCore;
4use serde::{Deserialize, Serialize};
5
6use crate::constants::{
7    MAX_SAMPLED_BLOCK_DURATION, MAX_SAMPLED_TIMEOUT, MAX_SAMPLED_TIMER_DURATION, STATE_LIMIT_MAX,
8};
9use crate::{Error, MachineId, dist};
10use std::fmt;
11use std::hash::Hash;
12
13use self::dist::Dist;
14
15/// The different types of timers used by a [`Machine`](crate::Machine).
16#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
17pub enum Timer {
18    /// The scheduled timer for actions with a timeout.
19    Action,
20    /// The machine's internal timer, updated by the machine using the
21    /// UpdateTimer action.
22    Internal,
23    /// Apply to all timers.
24    All,
25}
26
27/// An Action happens upon transition to a [`State`](crate::state). All actions
28/// (except Cancel) can be limited. The limit is the maximum number of times the
29/// action can be taken upon repeated transitions to the same state.
30#[derive(PartialEq, Debug, Clone, Copy, Serialize, Deserialize)]
31pub enum Action {
32    /// Cancel a timer.
33    Cancel { timer: Timer },
34    /// Schedule padding to be sent after a timeout.
35    ///
36    /// Replaces any previously pending scheduled action timer (set via
37    /// SendPadding or BlockOutgoing) for this machine.
38    ///
39    /// The bypass flag determines if the padding packet MUST bypass any
40    /// existing blocking that was triggered with the bypass flag set.
41    ///
42    /// The replace flag determines if the padding packet MAY be replaced by a
43    /// packet already queued to be sent at the time the padding packet would be
44    /// sent. This applies for data queued to be turned into normal
45    /// (non-padding) packets AND _any_ packet (padding or normal) in the egress
46    /// queue yet to be sent (i.e., before the TunnelSent event is triggered).
47    /// Such a packet could be in the queue due to ongoing blocking or just not
48    /// being sent yet (e.g., due to CC). We assume that packets will be
49    /// encrypted ASAP for the egress queue and we do not want to keep state
50    /// around to distinguish padding and non-padding, hence, any packet.
51    /// Similarly, this implies that a single blocked packet in the egress queue
52    /// can replace multiple padding packets with the replace flag set.
53    SendPadding {
54        bypass: bool,
55        replace: bool,
56        timeout: Dist,
57        limit: Option<Dist>,
58    },
59    /// Schedule blocking of outgoing traffic after a timeout.
60    ///
61    /// Replaces any previously pending scheduled action timer (set via
62    /// SendPadding or BlockOutgoing) for this machine.
63    ///
64    /// The bypass flag determines if padding actions are allowed to bypass this
65    /// blocking action. This allows for machines that can fail closed (never
66    /// bypass blocking) while simultaneously providing support for
67    /// constant-rate defenses, when set along with the replace flag.
68    ///
69    /// The replace flag determines if the action duration MUST replace any
70    /// existing blocking. Note that the blocking with the replace flag is
71    /// always allowed if blocking is currently active, regardless of any limits
72    /// set. This is to make it possible to create a machine that is guaranteed
73    /// to prevent indefinite blocking (but comes at the cost of making it
74    /// possible for a machine that indefinitely refresh blocking by using the
75    /// replace flag).
76    BlockOutgoing {
77        bypass: bool,
78        replace: bool,
79        timeout: Dist,
80        duration: Dist,
81        limit: Option<Dist>,
82    },
83    /// Update the timer duration for a machine.
84    ///
85    /// The replace flag determines if the action duration MUST replace the
86    /// current timer duration, if the timer has already been set.
87    UpdateTimer {
88        replace: bool,
89        duration: Dist,
90        limit: Option<Dist>,
91    },
92}
93
94impl fmt::Display for Action {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(f, "{self:#?}")
97    }
98}
99
100impl Action {
101    /// Sample a timeout for a padding or blocking action.
102    pub(crate) fn sample_timeout<R: RngCore>(&self, rng: &mut R) -> u64 {
103        match self {
104            Action::SendPadding { timeout, .. } | Action::BlockOutgoing { timeout, .. } => {
105                timeout.sample(rng).min(MAX_SAMPLED_TIMEOUT).round() as u64
106            }
107            _ => 0,
108        }
109    }
110
111    /// Sample a duration for a blocking or timer update action.
112    pub(crate) fn sample_duration<R: RngCore>(&self, rng: &mut R) -> u64 {
113        match self {
114            Action::BlockOutgoing { duration, .. } => {
115                duration.sample(rng).min(MAX_SAMPLED_BLOCK_DURATION).round() as u64
116            }
117            Action::UpdateTimer { duration, .. } => {
118                duration.sample(rng).min(MAX_SAMPLED_TIMER_DURATION).round() as u64
119            }
120            _ => 0,
121        }
122    }
123
124    /// Sample a limit.
125    pub(crate) fn sample_limit<R: RngCore>(&self, rng: &mut R) -> u64 {
126        match self {
127            Action::SendPadding { limit, .. }
128            | Action::BlockOutgoing { limit, .. }
129            | Action::UpdateTimer { limit, .. } => {
130                if limit.is_none() {
131                    return STATE_LIMIT_MAX;
132                }
133                limit.unwrap().sample(rng).round() as u64
134            }
135            _ => STATE_LIMIT_MAX,
136        }
137    }
138
139    /// Check if the action has a limit distribution.
140    pub(crate) fn has_limit(&self) -> bool {
141        match self {
142            Action::SendPadding { limit, .. }
143            | Action::BlockOutgoing { limit, .. }
144            | Action::UpdateTimer { limit, .. } => limit.is_some(),
145            _ => false,
146        }
147    }
148
149    /// Validate all distributions contained in this action, if any.
150    pub fn validate(&self) -> Result<(), Error> {
151        match self {
152            Action::SendPadding { timeout, limit, .. } => {
153                timeout.validate()?;
154                if let Some(limit) = limit {
155                    limit.validate()?;
156                }
157            }
158            Action::BlockOutgoing {
159                timeout,
160                duration,
161                limit,
162                ..
163            } => {
164                timeout.validate()?;
165                duration.validate()?;
166                if let Some(limit) = limit {
167                    limit.validate()?;
168                }
169            }
170            Action::UpdateTimer {
171                duration, limit, ..
172            } => {
173                duration.validate()?;
174                if let Some(limit) = limit {
175                    limit.validate()?;
176                }
177            }
178            _ => {}
179        }
180
181        Ok(())
182    }
183}
184
185/// The action to be taken by the framework user.
186#[derive(PartialEq, Eq, Debug, Clone)]
187pub enum TriggerAction<T: crate::time::Instant = std::time::Instant> {
188    /// Cancel one or more timers for a machine.
189    ///
190    /// Depending on the value of `timer`, either the internal timer should be
191    /// cancelled, the external timer should be cancelled, or both.
192    ///
193    /// Cancelling a timer does not cause a
194    /// [`TriggerEvent::TimerEnd`](crate::TriggerEvent::TimerEnd) event.
195    Cancel { machine: MachineId, timer: Timer },
196    /// Schedule padding to be injected after the given timeout for a machine.
197    ///
198    /// The bypass flag indicates if the padding packet MUST be sent despite
199    /// active blocking of outgoing traffic. Note that this is only allowed if
200    /// the active blocking was set with the bypass flag set to true.
201    ///
202    /// The replace flag determines if the padding packet MAY be replaced by a
203    /// packet already queued to be sent at the time the padding packet would be
204    /// sent. This applies for data queued to be turned into normal
205    /// (non-padding) packets AND _any_ packet (padding or normal) in the egress
206    /// queue yet to be sent (i.e., before the TunnelSent event is triggered).
207    /// Such a packet could be in the queue due to ongoing blocking or just not
208    /// being sent yet (e.g., due to CC). We assume that packets will be
209    /// encrypted ASAP for the egress queue and we do not want to keep state
210    /// around to distinguish padding and non-padding, hence, any packet.
211    /// Similarly, this implies that a single blocked packet in the egress queue
212    /// can replace multiple padding packets with the replace flag set.
213    ///
214    /// If the bypass and replace flags are both set to true AND the active
215    /// blocking may be bypassed, then non-padding packets MAY replace the
216    /// padding packet AND bypass the active blocking.
217    ///
218    /// When the padding is queued, a corresponding
219    /// [`TriggerEvent::PaddingSent`](crate::TriggerEvent::PaddingSent) event
220    /// SHOULD always be triggered, with a matching MachineId, even if the
221    /// padding packet is replaced by another packet.
222    /// (If the padding packet is replaced by queueing a _new_ normal
223    /// packet, then a `NormalSent` should _also_ be triggered, along
224    /// with `PaddingSent`.  If the padding packet is "replaced" by
225    /// noting the presence of an already queued packet, then no
226    /// additional event bedes `PaddingSent` needs to be triggered.)
227    ///
228    /// Note that, since only one action timer per machine can be pending at a
229    /// time, this `SendPadding` action should replace any currently pending
230    /// `SendPadding` or `BlockOutgoing` action timer for this machine that has
231    /// not yet expired.
232    SendPadding {
233        timeout: T::Duration,
234        bypass: bool,
235        replace: bool,
236        machine: MachineId,
237    },
238    /// Schedule blocking of outgoing traffic after the given timeout for a
239    /// machine. The duration of the blocking is specified. Note that the
240    /// blocking is framework scoped, i.e., if there are multiple machines
241    /// running, then the blocking will affect all of them.
242    ///
243    /// Whenever the given action timeout expires, a corresponding
244    /// [`TriggerEvent::BlockingBegin`](crate::TriggerEvent::BlockingBegin)
245    /// event should be triggered with the same MachineId, regardless of whether
246    /// the current blocking was adjusted.
247    ///
248    /// The bypass flag indicates if the blocking of outgoing traffic can be
249    /// bypassed by padding packets with the bypass flag set to true.
250    ///
251    /// The replace flag indicates if the duration MUST replace any other
252    /// currently ongoing blocking of outgoing traffic. If the flag is false,
253    /// the longest of the two durations MUST be used.
254    ///
255    /// Whenever the blocking timer of outgoing traffic is replaced or adjusted,
256    /// the "bypassable" status of the blocking is also replaced.
257    ///
258    /// Note that, since only one action timer per machine can be pending at a
259    /// time, this `BlockOutgoing` action should replace any currently pending
260    /// `BlockOutgoing` or `SendPadding` action timer for this machine that has
261    /// not yet expired.
262    BlockOutgoing {
263        timeout: T::Duration,
264        duration: T::Duration,
265        bypass: bool,
266        replace: bool,
267        machine: MachineId,
268    },
269    /// Update the duration of the internal timer for a machine.
270    ///
271    /// The replace flag specifies if the duration should replace the current
272    /// timer duration. If the flag is false, the longest of the two durations
273    /// MUST be used.
274    ///
275    /// Whenever an internal timer is created, and whenever the timer's duration
276    /// is changed, a corresponding
277    /// [`TriggerEvent::TimerBegin`](crate::TriggerEvent::TimerBegin) event
278    /// should be triggered, with a matching [`MachineId`].
279    ///
280    /// Whenever an internal expires, a corresponding
281    /// [`TriggerEvent::TimerEnd`](crate::TriggerEvent::TimerEnd) event should
282    /// be triggered. with a matching [`MachineId`].
283    UpdateTimer {
284        duration: T::Duration,
285        replace: bool,
286        machine: MachineId,
287    },
288}
289
290impl fmt::Display for TriggerAction {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        write!(f, "{self:#?}")
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use crate::{action::*, dist::DistType};
299
300    #[test]
301    fn validate_cancel_action() {
302        // always valid
303
304        // action timer
305        let a = Action::Cancel {
306            timer: Timer::Action,
307        };
308
309        let r = a.validate();
310        assert!(r.is_ok());
311
312        // machine's internal timer
313        let a = Action::Cancel {
314            timer: Timer::Internal,
315        };
316
317        let r = a.validate();
318        assert!(r.is_ok());
319
320        // all timers
321        let a = Action::Cancel { timer: Timer::All };
322
323        let r = a.validate();
324        assert!(r.is_ok());
325    }
326
327    #[test]
328    fn validate_padding_action() {
329        // valid SendPadding action
330        let mut a = Action::SendPadding {
331            bypass: false,
332            replace: false,
333            timeout: Dist {
334                dist: DistType::Uniform {
335                    low: 10.0,
336                    high: 10.0,
337                },
338                start: 0.0,
339                max: 0.0,
340            },
341            limit: Some(Dist {
342                dist: DistType::Normal {
343                    mean: 50.0,
344                    stdev: 10.0,
345                },
346                start: 0.0,
347                max: 0.0,
348            }),
349        };
350
351        let r = a.validate();
352        assert!(r.is_ok());
353
354        // invalid timeout dist, not allowed
355        if let Action::SendPadding { timeout, .. } = &mut a {
356            *timeout = Dist {
357                dist: DistType::Uniform {
358                    low: 15.0, // NOTE low > high
359                    high: 5.0,
360                },
361                start: 0.0,
362                max: 0.0,
363            };
364        }
365
366        let r = a.validate();
367        assert!(r.is_err());
368
369        // repair timeout dist
370        if let Action::SendPadding { timeout, .. } = &mut a {
371            *timeout = Dist {
372                dist: DistType::Uniform {
373                    low: 10.0,
374                    high: 10.0,
375                },
376                start: 0.0,
377                max: 0.0,
378            };
379        }
380
381        // invalid limit dist, not allowed
382        if let Action::SendPadding { limit, .. } = &mut a {
383            *limit = Some(Dist {
384                dist: DistType::Uniform {
385                    low: 15.0, // NOTE low > high
386                    high: 5.0,
387                },
388                start: 0.0,
389                max: 0.0,
390            });
391        }
392
393        let r = a.validate();
394        assert!(r.is_err());
395    }
396
397    #[test]
398    fn validate_blocking_action() {
399        // valid BlockOutgoing action
400        let mut a = Action::BlockOutgoing {
401            bypass: false,
402            replace: false,
403            timeout: Dist {
404                dist: DistType::Uniform {
405                    low: 10.0,
406                    high: 10.0,
407                },
408                start: 0.0,
409                max: 0.0,
410            },
411            duration: Dist {
412                dist: DistType::Uniform {
413                    low: 10.0,
414                    high: 10.0,
415                },
416                start: 0.0,
417                max: 0.0,
418            },
419            limit: Some(Dist {
420                dist: DistType::Normal {
421                    mean: 50.0,
422                    stdev: 10.0,
423                },
424                start: 0.0,
425                max: 0.0,
426            }),
427        };
428
429        let r = a.validate();
430        assert!(r.is_ok());
431
432        // invalid timeout dist, not allowed
433        if let Action::BlockOutgoing { timeout, .. } = &mut a {
434            *timeout = Dist {
435                dist: DistType::Uniform {
436                    low: 15.0, // NOTE low > high
437                    high: 5.0,
438                },
439
440                start: 0.0,
441                max: 0.0,
442            };
443        }
444
445        let r = a.validate();
446        assert!(r.is_err());
447
448        // repair timeout dist
449        if let Action::BlockOutgoing { timeout, .. } = &mut a {
450            *timeout = Dist {
451                dist: DistType::Uniform {
452                    low: 10.0,
453                    high: 10.0,
454                },
455                start: 0.0,
456                max: 0.0,
457            };
458        }
459
460        // invalid duration dist, not allowed
461        if let Action::BlockOutgoing { duration, .. } = &mut a {
462            *duration = Dist {
463                dist: DistType::Uniform {
464                    low: 15.0, // NOTE low > high
465                    high: 5.0,
466                },
467                start: 0.0,
468                max: 0.0,
469            };
470        }
471
472        let r = a.validate();
473        assert!(r.is_err());
474
475        // repair duration dist
476        if let Action::BlockOutgoing { duration, .. } = &mut a {
477            *duration = Dist {
478                dist: DistType::Uniform {
479                    low: 10.0,
480                    high: 10.0,
481                },
482                start: 0.0,
483                max: 0.0,
484            };
485        }
486
487        // invalid limit dist, not allowed
488        if let Action::BlockOutgoing { limit, .. } = &mut a {
489            *limit = Some(Dist {
490                dist: DistType::Uniform {
491                    low: 15.0, // NOTE low > high
492                    high: 5.0,
493                },
494                start: 0.0,
495                max: 0.0,
496            });
497        }
498
499        let r = a.validate();
500        assert!(r.is_err());
501    }
502
503    #[test]
504    fn validate_update_timer_action() {
505        // valid UpdateTimer action
506        let mut a = Action::UpdateTimer {
507            replace: true,
508            duration: Dist {
509                dist: DistType::Uniform {
510                    low: 10.0,
511                    high: 10.0,
512                },
513                start: 0.0,
514                max: 0.0,
515            },
516            limit: Some(Dist {
517                dist: DistType::Normal {
518                    mean: 50.0,
519                    stdev: 10.0,
520                },
521                start: 0.0,
522                max: 0.0,
523            }),
524        };
525
526        let r = a.validate();
527        assert!(r.is_ok());
528
529        // invalid action dist, not allowed
530        if let Action::UpdateTimer { duration, .. } = &mut a {
531            *duration = Dist {
532                dist: DistType::Uniform {
533                    low: 15.0, // NOTE low > high
534                    high: 5.0,
535                },
536                start: 0.0,
537                max: 0.0,
538            };
539        }
540
541        let r = a.validate();
542        assert!(r.is_err());
543
544        // repair action dist
545        if let Action::UpdateTimer { duration, .. } = &mut a {
546            *duration = Dist {
547                dist: DistType::Uniform {
548                    low: 10.0,
549                    high: 10.0,
550                },
551                start: 0.0,
552                max: 0.0,
553            };
554        }
555
556        // invalid limit dist, not allowed
557        if let Action::UpdateTimer { limit, .. } = &mut a {
558            *limit = Some(Dist {
559                dist: DistType::Uniform {
560                    low: 15.0, // NOTE low > high
561                    high: 5.0,
562                },
563                start: 0.0,
564                max: 0.0,
565            });
566        }
567
568        let r = a.validate();
569        assert!(r.is_err());
570    }
571}