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}