str0m_netem/config.rs
1use std::time::Duration;
2
3pub use str0m_proto::{Bitrate, DataSize};
4
5/// Configuration for the network emulator.
6///
7/// Use the builder pattern to configure the emulator:
8///
9/// ```
10/// use std::time::Duration;
11/// use str0m_netem::{NetemConfig, LossModel, GilbertElliot};
12///
13/// let config = NetemConfig::new()
14/// .latency(Duration::from_millis(50))
15/// .jitter(Duration::from_millis(10))
16/// .loss(GilbertElliot::wifi())
17/// .seed(42);
18/// ```
19#[derive(Debug, Clone, Copy)]
20pub struct NetemConfig {
21 pub(crate) latency: Duration,
22 pub(crate) jitter: Duration,
23 pub(crate) delay_correlation: Probability,
24 pub(crate) loss: LossModel,
25 pub(crate) duplicate: Probability,
26 pub(crate) reorder_gap: Option<u32>,
27 pub(crate) link: Option<Link>,
28 pub(crate) seed: u64,
29}
30
31/// A bottleneck link with rate limiting and finite buffer.
32///
33/// When packets arrive faster than the link rate, they queue up.
34/// When the queue exceeds the buffer size, packets are dropped (tail drop).
35#[derive(Debug, Clone, Copy)]
36pub struct Link {
37 /// Link capacity in bits per second.
38 pub(crate) rate: Bitrate,
39 /// Buffer size in bytes. When exceeded, packets are dropped.
40 pub(crate) buffer: DataSize,
41}
42
43impl Default for NetemConfig {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl NetemConfig {
50 /// Create a new network emulator configuration with default values.
51 ///
52 /// Default: no delay, no loss, no rate limit, seed 0.
53 pub const fn new() -> Self {
54 Self {
55 latency: Duration::ZERO,
56 jitter: Duration::ZERO,
57 delay_correlation: Probability::ZERO,
58 loss: LossModel::None,
59 duplicate: Probability::ZERO,
60 reorder_gap: None,
61 link: None,
62 seed: 0,
63 }
64 }
65
66 // --- Preset constructors for common network environments ---
67
68 /// Good WiFi: low latency, minimal loss, high bandwidth.
69 ///
70 /// Simulates a typical home/office WiFi connection with good signal.
71 /// - Latency: 5ms (local network)
72 /// - Jitter: 2ms
73 /// - Loss: ~1% bursty (GilbertElliot::wifi)
74 /// - Bandwidth: 100 Mbps with 200 KB buffer
75 pub fn wifi() -> Self {
76 Self::new()
77 .latency(Duration::from_millis(5))
78 .jitter(Duration::from_millis(2))
79 .loss(GilbertElliot::wifi())
80 .link(Bitrate::mbps(100), DataSize::kbytes(200))
81 }
82
83 /// Lossy WiFi: poor signal, more interference and loss.
84 ///
85 /// Simulates WiFi with weak signal or interference.
86 /// - Latency: 15ms
87 /// - Jitter: 10ms
88 /// - Loss: ~5% bursty (GilbertElliot::wifi_lossy)
89 /// - Bandwidth: 50 Mbps with 100 KB buffer
90 pub fn wifi_lossy() -> Self {
91 Self::new()
92 .latency(Duration::from_millis(15))
93 .jitter(Duration::from_millis(10))
94 .loss(GilbertElliot::wifi_lossy())
95 .link(Bitrate::mbps(50), DataSize::kbytes(100))
96 }
97
98 /// Congested WiFi: shared connection with competing traffic.
99 ///
100 /// Simulates a home WiFi where others are streaming video,
101 /// causing buffer bloat and bandwidth contention.
102 /// - Latency: 10ms
103 /// - Jitter: 20ms (high due to competing traffic)
104 /// - Loss: ~10% (buffer overflow from congestion)
105 /// - Bandwidth: 5 Mbps with 30 KB buffer (your share of the link)
106 pub fn wifi_congested() -> Self {
107 Self::new()
108 .latency(Duration::from_millis(10))
109 .jitter(Duration::from_millis(20))
110 .loss(GilbertElliot::congested())
111 .link(Bitrate::mbps(5), DataSize::kbytes(30))
112 }
113
114 /// Cellular/4G: moderate latency with occasional handoff loss.
115 ///
116 /// Simulates a mobile LTE/4G connection.
117 /// - Latency: 50ms
118 /// - Jitter: 15ms
119 /// - Loss: ~2% bursty (handoffs, signal fading)
120 /// - Bandwidth: 30 Mbps with 100 KB buffer
121 pub fn cellular() -> Self {
122 Self::new()
123 .latency(Duration::from_millis(50))
124 .jitter(Duration::from_millis(15))
125 .loss(GilbertElliot::cellular())
126 .link(Bitrate::mbps(30), DataSize::kbytes(100))
127 }
128
129 /// Satellite (GEO): very high latency, weather-related loss bursts.
130 ///
131 /// Simulates a geostationary satellite link (e.g., Viasat, HughesNet).
132 /// - Latency: 600ms (round-trip to GEO orbit)
133 /// - Jitter: 30ms
134 /// - Loss: ~3% bursty (weather, atmospheric)
135 /// - Bandwidth: 15 Mbps with 500 KB buffer (large buffer for high BDP)
136 pub fn satellite() -> Self {
137 Self::new()
138 .latency(Duration::from_millis(600))
139 .jitter(Duration::from_millis(30))
140 .loss(GilbertElliot::satellite())
141 .link(Bitrate::mbps(15), DataSize::kbytes(500))
142 }
143
144 /// Congested network: high latency/jitter, frequent loss, throttled.
145 ///
146 /// Simulates a severely congested network path.
147 /// - Latency: 80ms
148 /// - Jitter: 40ms
149 /// - Loss: ~10% (queue overflow)
150 /// - Bandwidth: 10 Mbps with 50 KB buffer (small buffer causes loss)
151 pub fn congested() -> Self {
152 Self::new()
153 .latency(Duration::from_millis(80))
154 .jitter(Duration::from_millis(40))
155 .loss(GilbertElliot::congested())
156 .link(Bitrate::mbps(10), DataSize::kbytes(50))
157 }
158
159 /// Set fixed delay added to every packet.
160 ///
161 /// This simulates the propagation delay of a network link. For example:
162 /// - LAN: 0-1ms
163 /// - Same city: 5-20ms
164 /// - Cross-country: 30-70ms
165 /// - Intercontinental: 100-200ms
166 /// - Satellite (GEO): 500-700ms
167 ///
168 /// The actual send time is: `arrival_time + latency + random_jitter`
169 pub fn latency(mut self, latency: Duration) -> Self {
170 self.latency = latency;
171 self
172 }
173
174 /// Set random variation added to the latency (uniform distribution).
175 ///
176 /// Each packet gets a random delay in the range `[-jitter, +jitter]` added
177 /// to its base latency. This simulates the variable queuing delays in routers.
178 ///
179 /// For example, with `latency(50ms)` and `jitter(10ms)`, packets will have
180 /// delays uniformly distributed between 40ms and 60ms.
181 ///
182 /// Real networks typically have jitter of 1-30ms depending on congestion.
183 pub fn jitter(mut self, jitter: Duration) -> Self {
184 self.jitter = jitter;
185 self
186 }
187
188 /// Set correlation between consecutive delay values.
189 ///
190 /// Controls how similar each packet's delay is to the previous packet's delay.
191 /// This models the fact that network conditions change gradually, not randomly.
192 ///
193 /// - `0.0`: Each packet's jitter is completely independent (unrealistic)
194 /// - `0.25`: Low correlation, delays vary somewhat smoothly
195 /// - `0.5`: Medium correlation, delays change gradually
196 /// - `0.75`: High correlation, delays are very similar to previous
197 /// - `1.0`: Perfect correlation, all packets get the same jitter (no variation)
198 ///
199 /// Formula: `next_jitter = random * (1 - correlation) + last_jitter * correlation`
200 ///
201 /// A value around 0.25-0.5 is realistic for most networks.
202 pub fn delay_correlation(mut self, correlation: Probability) -> Self {
203 self.delay_correlation = correlation;
204 self
205 }
206
207 /// Set the loss model.
208 ///
209 /// See [`LossModel`] for available options.
210 pub fn loss(mut self, loss: impl Into<LossModel>) -> Self {
211 self.loss = loss.into();
212 self
213 }
214
215 /// Set probability that each packet is duplicated.
216 ///
217 /// When a packet is duplicated, both the original and the copy are sent
218 /// with the same delay. This simulates network equipment bugs or
219 /// misconfigured routing that causes packets to be sent twice.
220 ///
221 /// - `0.0`: No duplication (normal)
222 /// - `0.01`: 1% of packets are duplicated (rare but happens)
223 /// - `0.1`: 10% duplication (severe misconfiguration)
224 pub fn duplicate(mut self, probability: Probability) -> Self {
225 self.duplicate = probability;
226 self
227 }
228
229 /// Set reordering by sending every Nth packet immediately.
230 ///
231 /// Every Nth packet bypasses the delay queue and is sent immediately,
232 /// causing it to arrive before packets that were sent earlier.
233 /// This simulates multi-path routing where packets take different routes.
234 ///
235 /// - `3`: Every 3rd packet is sent immediately
236 /// - `10`: Every 10th packet is sent immediately
237 ///
238 /// Combined with latency, this creates realistic reordering patterns.
239 pub fn reorder_gap(mut self, gap: u32) -> Self {
240 self.reorder_gap = Some(gap);
241 self
242 }
243
244 /// Set a bottleneck link with rate limiting and finite buffer.
245 ///
246 /// Simulates a network link with limited capacity. When packets arrive faster
247 /// than the link can transmit, they queue up. When the queue (buffer) is full,
248 /// packets are dropped (tail drop), simulating congestion-induced loss.
249 ///
250 /// # Example
251 ///
252 /// ```
253 /// use std::time::Duration;
254 /// use str0m_netem::{NetemConfig, Bitrate, DataSize};
255 ///
256 /// // 10 Mbps link with 200KB buffer (~160ms at full rate)
257 /// let config = NetemConfig::new()
258 /// .latency(Duration::from_millis(50))
259 /// .link(Bitrate::mbps(10), DataSize::bytes(200_000));
260 /// ```
261 ///
262 /// # Behavior
263 ///
264 /// - Below capacity: packets flow with minimal queuing delay
265 /// - At capacity: queuing delay increases as buffer fills
266 /// - Over capacity: buffer fills, then excess packets are dropped
267 pub fn link(mut self, rate: Bitrate, buffer: DataSize) -> Self {
268 self.link = Some(Link { rate, buffer });
269 self
270 }
271
272 /// Set seed for the random number generator.
273 ///
274 /// Using the same seed produces identical packet loss/delay patterns,
275 /// which is essential for reproducible tests. Different seeds produce
276 /// different (but deterministic) random sequences.
277 ///
278 /// - Use a fixed seed (e.g., `42`) for reproducible tests
279 /// - Use `std::time::SystemTime::now().duration_since(UNIX_EPOCH).as_nanos() as u64`
280 /// for random behavior in production
281 pub fn seed(mut self, seed: u64) -> Self {
282 self.seed = seed;
283 self
284 }
285}
286
287/// Probability in range 0.0..=1.0
288#[derive(Debug, Clone, Copy, PartialEq, Default)]
289pub struct Probability(pub f32);
290
291impl Probability {
292 pub const ZERO: Probability = Probability(0.0);
293 pub const ONE: Probability = Probability(1.0);
294
295 pub fn new(value: f32) -> Self {
296 debug_assert!(
297 (0.0..=1.0).contains(&value),
298 "Probability must be in 0.0..=1.0"
299 );
300 Probability(value.clamp(0.0, 1.0))
301 }
302
303 pub fn value(self) -> f32 {
304 self.0
305 }
306}
307
308/// Loss model for packet dropping.
309///
310/// Real networks rarely have uniform random loss. Instead, losses tend to come
311/// in bursts due to congestion, interference, or route changes. This enum
312/// provides different models to simulate various loss patterns.
313#[derive(Debug, Clone, Copy, Default)]
314pub enum LossModel {
315 /// No packet loss. All packets are delivered.
316 #[default]
317 None,
318
319 /// Probability-based random loss with optional correlation.
320 ///
321 /// See [`RandomLoss`] for configuration options.
322 Random(RandomLoss),
323
324 /// Gilbert-Elliot model for realistic bursty packet loss.
325 ///
326 /// This 2-state Markov model alternates between GOOD (low loss) and BAD
327 /// (high loss) states, producing realistic burst patterns where losses
328 /// cluster together rather than being spread evenly.
329 ///
330 /// Use the builder methods or presets on [`GilbertElliot`]:
331 /// - `GilbertElliot::wifi()` - occasional short bursts
332 /// - `GilbertElliot::cellular()` - moderate bursts from handoffs
333 /// - `GilbertElliot::satellite()` - rare but longer bursts
334 /// - `GilbertElliot::congested()` - frequent drops
335 GilbertElliot(GilbertElliot),
336}
337
338/// Random loss model configuration.
339///
340/// Each packet has the given probability of being dropped. By default, each
341/// decision is independent (Bernoulli process), producing unrealistic "spread out"
342/// loss patterns.
343///
344/// Use [`correlation`](RandomLoss::correlation) to make losses more bursty, or
345/// prefer [`GilbertElliot`] for more realistic bursty loss with finer control.
346///
347/// # Example
348///
349/// ```
350/// use str0m_netem::{RandomLoss, Probability};
351///
352/// // 5% loss, independent
353/// let simple = RandomLoss::new(Probability::new(0.05));
354///
355/// // 5% loss, bursty (losses cluster together)
356/// let bursty = RandomLoss::new(Probability::new(0.05))
357/// .correlation(Probability::new(0.5));
358/// ```
359#[derive(Debug, Clone, Copy)]
360pub struct RandomLoss {
361 /// Probability of dropping each packet.
362 pub(crate) probability: f32,
363
364 /// Correlation between consecutive loss decisions.
365 pub(crate) correlation: f32,
366}
367
368impl RandomLoss {
369 /// Create a new random loss model with the given drop probability.
370 ///
371 /// Loss decisions are independent by default (no correlation).
372 pub fn new(probability: Probability) -> Self {
373 Self {
374 probability: probability.0,
375 correlation: 0.0,
376 }
377 }
378
379 /// Set correlation between consecutive loss decisions.
380 ///
381 /// Controls how "bursty" random loss is:
382 ///
383 /// - `0.0`: Each loss decision is independent (Bernoulli process)
384 /// - `0.5`: If a packet was lost, next packet is more likely to be lost too
385 /// - `0.9`: Very bursty - losses come in clusters
386 ///
387 /// For realistic bursty loss, consider using [`GilbertElliot`] instead,
388 /// which provides more control over burst characteristics.
389 pub fn correlation(mut self, correlation: Probability) -> Self {
390 self.correlation = correlation.0;
391 self
392 }
393}
394
395/// Gilbert-Elliot loss model with two states: GOOD and BAD.
396///
397/// This is a 2-state Markov chain that models bursty packet loss:
398///
399/// ```text
400/// p (enter burst)
401/// ┌──────────────────────┐
402/// │ ▼
403/// ┌───────┐ ┌───────┐
404/// │ GOOD │ │ BAD │
405/// │(k=0%) │ │(h=100%)│
406/// └───────┘ └───────┘
407/// ▲ │
408/// └──────────────────────┘
409/// r (exit burst)
410/// ```
411///
412/// **How it works:**
413/// 1. Start in GOOD state (low/no loss)
414/// 2. Each packet, roll dice to potentially transition to BAD state (probability `p`)
415/// 3. In BAD state, packets are lost with probability `h` (default 100%)
416/// 4. Each packet in BAD, roll dice to return to GOOD (probability `r`)
417///
418/// **Key insight:** The average number of packets before transitioning is `1/probability`.
419/// So `p = 0.01` means ~100 packets in GOOD before entering BAD,
420/// and `r = 0.5` means ~2 packets in BAD before returning to GOOD.
421///
422/// # Example
423///
424/// ```
425/// use str0m_netem::GilbertElliot;
426///
427/// // Custom: lose ~3 packets every ~50 packets
428/// let ge = GilbertElliot::new()
429/// .good_duration(50.0) // stay in GOOD for ~50 packets
430/// .bad_duration(3.0); // stay in BAD for ~3 packets (burst length)
431///
432/// // Or use a preset
433/// let wifi = GilbertElliot::wifi();
434/// ```
435#[derive(Debug, Clone, Copy)]
436pub struct GilbertElliot {
437 /// Probability of transitioning from GOOD to BAD state (per packet).
438 /// Average packets in GOOD = 1/p
439 pub(crate) p: f32,
440
441 /// Probability of transitioning from BAD to GOOD state (per packet).
442 /// Average packets in BAD (burst length) = 1/r
443 pub(crate) r: f32,
444
445 /// Probability of loss when in BAD state (default 1.0 = 100%).
446 pub(crate) h: f32,
447
448 /// Probability of loss when in GOOD state (default 0.0 = 0%).
449 pub(crate) k: f32,
450}
451
452impl Default for GilbertElliot {
453 fn default() -> Self {
454 Self::new()
455 }
456}
457
458impl GilbertElliot {
459 /// Create a new Gilbert-Elliot model with default parameters.
460 ///
461 /// Defaults produce **no loss**: stays in GOOD forever (p=0).
462 /// Use the builder methods to configure loss behavior.
463 pub fn new() -> Self {
464 Self {
465 p: 0.0, // never transition to BAD
466 r: 1.0, // immediately return to GOOD
467 h: 1.0, // 100% loss in BAD
468 k: 0.0, // 0% loss in GOOD
469 }
470 }
471
472 /// Set average number of packets in GOOD state before transitioning to BAD.
473 ///
474 /// This controls how often bursts occur. Higher values = rarer bursts.
475 ///
476 /// - `50.0`: Burst starts every ~50 packets on average
477 /// - `100.0`: Burst starts every ~100 packets on average
478 /// - `200.0`: Burst starts every ~200 packets on average
479 ///
480 /// Internally sets `p = 1 / avg_packets`.
481 pub fn good_duration(mut self, avg_packets: f32) -> Self {
482 self.p = if avg_packets > 0.0 {
483 1.0 / avg_packets
484 } else {
485 0.0
486 };
487 self
488 }
489
490 /// Set average number of packets in BAD state (burst length).
491 ///
492 /// This controls how long each burst lasts. Higher values = longer bursts.
493 ///
494 /// - `1.0`: Single packet losses (not really bursty)
495 /// - `2.0`: ~2 packets lost per burst
496 /// - `5.0`: ~5 packets lost per burst
497 /// - `10.0`: ~10 packets lost per burst (severe)
498 ///
499 /// Internally sets `r = 1 / avg_packets`.
500 pub fn bad_duration(mut self, avg_packets: f32) -> Self {
501 self.r = if avg_packets > 0.0 {
502 1.0 / avg_packets
503 } else {
504 1.0
505 };
506 self
507 }
508
509 /// Set loss probability when in BAD state.
510 ///
511 /// Default is `1.0` (100% loss in BAD state).
512 ///
513 /// Setting this below 1.0 means some packets survive even during a burst,
514 /// which can model partial outages or congestion that drops some but not
515 /// all packets.
516 pub fn loss_in_bad(mut self, prob: Probability) -> Self {
517 self.h = prob.0;
518 self
519 }
520
521 /// Set loss probability when in GOOD state.
522 ///
523 /// Default is `0.0` (no loss in GOOD state).
524 ///
525 /// Setting this above 0.0 adds a baseline random loss even outside of
526 /// bursts, simulating a network that always has some background loss.
527 pub fn loss_in_good(mut self, prob: Probability) -> Self {
528 self.k = prob.0;
529 self
530 }
531
532 // --- Preset constructors for common environments ---
533
534 /// Good WiFi: rare short bursts (~1% loss).
535 ///
536 /// Models occasional interference causing brief packet loss.
537 pub fn wifi() -> Self {
538 Self::new()
539 .good_duration(200.0) // ~200 packets between bursts
540 .bad_duration(2.0) // ~2 packets lost per burst
541 .loss_in_bad(Probability::ONE)
542 }
543
544 /// Lossy WiFi: frequent short bursts (~5% loss).
545 ///
546 /// Models poor WiFi signal with frequent interference.
547 pub fn wifi_lossy() -> Self {
548 Self::new()
549 .good_duration(40.0) // ~40 packets between bursts
550 .bad_duration(2.0) // ~2 packets lost per burst
551 .loss_in_bad(Probability::ONE)
552 }
553
554 /// Mobile/cellular: moderate bursts from handoffs (~2% loss).
555 ///
556 /// Models cellular network with occasional handoffs and signal issues.
557 pub fn cellular() -> Self {
558 Self::new()
559 .good_duration(100.0) // ~100 packets between bursts
560 .bad_duration(2.0) // ~2 packets lost per burst
561 .loss_in_bad(Probability::ONE)
562 }
563
564 /// Satellite: rare but longer bursts (~3% loss).
565 ///
566 /// Models satellite link with weather-related outages.
567 pub fn satellite() -> Self {
568 Self::new()
569 .good_duration(100.0) // ~100 packets between bursts
570 .bad_duration(3.0) // ~3 packets lost per burst
571 .loss_in_bad(Probability::ONE)
572 }
573
574 /// Congested network: frequent drops (~10% loss).
575 ///
576 /// Models a heavily loaded network with queue overflow.
577 pub fn congested() -> Self {
578 Self::new()
579 .good_duration(20.0) // ~20 packets between bursts
580 .bad_duration(2.0) // ~2 packets lost per burst
581 .loss_in_bad(Probability::ONE)
582 }
583}
584
585impl From<RandomLoss> for LossModel {
586 fn from(value: RandomLoss) -> Self {
587 LossModel::Random(value)
588 }
589}
590
591impl From<GilbertElliot> for LossModel {
592 fn from(value: GilbertElliot) -> Self {
593 LossModel::GilbertElliot(value)
594 }
595}