Skip to main content

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}