nxtquic-sim 0.1.3

Deterministic network simulator for NxtQuic testing
Documentation
//! Network simulation engine with configurable latency, loss, jitter, reordering, and bandwidth.

use crate::channel::{QueuedDatagram, SimChannel};
use rand::{rng, Rng};
use std::net::SocketAddr;
use std::time::{Duration, Instant};

/// Configuration for the simulated network conditions.
#[derive(Debug, Clone)]
pub struct NetworkConfig {
    /// Base latency for packets.
    pub latency: Duration,
    /// Maximum additional random delay added to packets.
    pub jitter: Duration,
    /// Probability of a packet being dropped (0.0 to 1.0).
    pub loss_rate: f64,
    /// Probability of entering or continuing a burst loss state (0.0 to 1.0).
    pub burst_loss_prob: f64,
    /// Probability of a packet being delayed extra to cause reordering (0.0 to 1.0).
    pub reorder_rate: f64,
    /// Probability of a packet being duplicated (0.0 to 1.0).
    pub duplicate_prob: f64,
    /// Maximum bandwidth in bytes per second (for transmission rate delay calculation).
    pub bandwidth: Option<u64>,
    /// Probability of a NAT rebinding occurring, changing the source port.
    pub nat_rebind_prob: f64,
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            latency: Duration::from_millis(10),
            jitter: Duration::from_millis(0),
            loss_rate: 0.0,
            burst_loss_prob: 0.0,
            reorder_rate: 0.0,
            duplicate_prob: 0.0,
            bandwidth: None,
            nat_rebind_prob: 0.0,
        }
    }
}

/// A simulated network that models latency, jitter, loss, duplication, and reordering.
pub struct SimulatedNetwork {
    config: NetworkConfig,
    channel: SimChannel,
    in_burst_loss: bool,
    current_src_port_offset: u16,
}

impl SimulatedNetwork {
    /// Creates a new `SimulatedNetwork` with the given configuration.
    pub fn new(config: NetworkConfig) -> Self {
        Self {
            config,
            channel: SimChannel::new(),
            in_burst_loss: false,
            current_src_port_offset: 0,
        }
    }

    /// Returns a reference to the active network configuration.
    pub fn config(&self) -> &NetworkConfig {
        &self.config
    }

    /// Mutably updates the network configuration during simulation.
    pub fn config_mut(&mut self) -> &mut NetworkConfig {
        &mut self.config
    }

    /// Sends a payload through the simulated network.
    pub fn send(&mut self, payload: Vec<u8>, mut src: SocketAddr, dst: SocketAddr, now: Instant) {
        let mut rng = rng();

        // 1. Packet Loss simulation
        if self.in_burst_loss {
            if rng.random::<f64>() > self.config.burst_loss_prob {
                self.in_burst_loss = false;
            } else {
                return; // Dropped due to burst loss
            }
        } else if rng.random::<f64>() < self.config.loss_rate {
            if rng.random::<f64>() < self.config.burst_loss_prob {
                self.in_burst_loss = true;
            }
            return; // Dropped due to random loss
        }

        // 2. Latency and Jitter simulation
        let mut delay = self.config.latency;
        if self.config.jitter > Duration::ZERO {
            let jitter_ms = rng.random_range(0..=self.config.jitter.as_millis() as u64);
            delay += Duration::from_millis(jitter_ms);
        }

        // 3. Bandwidth throttle simulation
        if let Some(bw) = self.config.bandwidth {
            if bw > 0 {
                let tx_nanos = (payload.len() as u64 * 1_000_000_000) / bw;
                delay += Duration::from_nanos(tx_nanos);
            }
        }

        // 4. Reordering simulation (by adding an extra delay)
        if rng.random::<f64>() < self.config.reorder_rate {
            delay += Duration::from_millis(rng.random_range(10..50));
        }

        // 5. NAT rebinding simulation
        if rng.random::<f64>() < self.config.nat_rebind_prob {
            self.current_src_port_offset = self.current_src_port_offset.wrapping_add(1);
        }

        src.set_port(src.port().wrapping_add(self.current_src_port_offset));

        let delivery_time = now + delay;

        // 6. Packet Duplication simulation
        if rng.random::<f64>() < self.config.duplicate_prob {
            self.channel.push(QueuedDatagram {
                delivery_time: delivery_time + Duration::from_millis(1),
                payload: payload.clone(),
                src,
                dst,
            });
        }

        self.channel.push(QueuedDatagram {
            delivery_time,
            payload,
            src,
            dst,
        });
    }

    /// Receives a datagram from the network if it's ready.
    pub fn receive(&mut self, now: Instant) -> Option<QueuedDatagram> {
        self.channel.pop_ready(now)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_latency_delay() {
        let mut net = SimulatedNetwork::new(NetworkConfig {
            latency: Duration::from_millis(50),
            ..Default::default()
        });

        let src: SocketAddr = "127.0.0.1:1000".parse().unwrap();
        let dst: SocketAddr = "127.0.0.1:2000".parse().unwrap();
        let now = Instant::now();

        net.send(vec![1, 2, 3], src, dst, now);

        assert!(net.receive(now).is_none());
        assert!(net.receive(now + Duration::from_millis(20)).is_none());
        assert!(net.receive(now + Duration::from_millis(50)).is_some());
    }

    #[test]
    fn test_packet_loss_dropping() {
        let mut net = SimulatedNetwork::new(NetworkConfig {
            loss_rate: 1.0,
            ..Default::default()
        });

        let src: SocketAddr = "127.0.0.1:1000".parse().unwrap();
        let dst: SocketAddr = "127.0.0.1:2000".parse().unwrap();
        let now = Instant::now();

        net.send(vec![1, 2, 3], src, dst, now);

        assert!(net.receive(now + Duration::from_secs(10)).is_none());
    }

    #[test]
    fn test_reordering() {
        let mut net = SimulatedNetwork::new(NetworkConfig {
            latency: Duration::from_millis(10),
            reorder_rate: 1.0,
            ..Default::default()
        });

        let src: SocketAddr = "127.0.0.1:1000".parse().unwrap();
        let dst: SocketAddr = "127.0.0.1:2000".parse().unwrap();
        let now = Instant::now();

        net.send(vec![1], src, dst, now);

        assert!(net.receive(now + Duration::from_millis(10)).is_none());
        assert!(net.receive(now + Duration::from_millis(100)).is_some());
    }
}