Skip to main content

simoom_quic/
sim.rs

1//! A deterministic in-memory network for tests and benchmarks: two endpoints joined by
2//! links that can drop, duplicate, reorder and delay datagrams, with an optional
3//! bottleneck (bandwidth and queue). Time is virtual and advanced by the simulation.
4
5use crate::config::Config;
6use crate::connection::Transmit;
7use crate::endpoint::{ConnHandle, DatagramEvent, Endpoint};
8use std::cmp::Reverse;
9use std::collections::BinaryHeap;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14/// Link behaviour, per direction. Probabilities are in parts per million.
15#[derive(Clone, Debug)]
16pub struct Link {
17    pub delay: Duration,
18    pub jitter: Duration,
19    pub loss_ppm: u32,
20    pub duplicate_ppm: u32,
21    /// Reordered datagrams get an extra delay of up to `reorder_delay`.
22    pub reorder_ppm: u32,
23    pub reorder_delay: Duration,
24    /// Bytes per second; `None` is unlimited.
25    pub bandwidth: Option<u64>,
26    /// Bottleneck queue in bytes (tail drop).
27    pub queue: usize,
28    /// Datagrams larger than this are dropped (path MTU).
29    pub mtu: usize,
30}
31
32impl Default for Link {
33    fn default() -> Self {
34        Link {
35            delay: Duration::from_millis(10),
36            jitter: Duration::ZERO,
37            loss_ppm: 0,
38            duplicate_ppm: 0,
39            reorder_ppm: 0,
40            reorder_delay: Duration::ZERO,
41            bandwidth: None,
42            queue: usize::MAX,
43            mtu: 1500,
44        }
45    }
46}
47
48#[derive(Clone, Copy, Debug, Default)]
49pub struct LinkStats {
50    pub sent: u64,
51    pub delivered: u64,
52    pub dropped: u64,
53    pub duplicated: u64,
54    pub bytes: u64,
55}
56
57struct Dgram {
58    from: SocketAddr,
59    to: SocketAddr,
60    data: Vec<u8>,
61}
62
63/// A queued datagram, ordered by delivery time then sequence.
64struct Queued(Instant, u64, Dgram);
65
66impl PartialEq for Queued {
67    fn eq(&self, o: &Self) -> bool {
68        (self.0, self.1) == (o.0, o.1)
69    }
70}
71impl Eq for Queued {}
72impl PartialOrd for Queued {
73    fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
74        Some(self.cmp(o))
75    }
76}
77impl Ord for Queued {
78    fn cmp(&self, o: &Self) -> std::cmp::Ordering {
79        (self.0, self.1).cmp(&(o.0, o.1))
80    }
81}
82
83/// xorshift64*: deterministic and good enough for simulation.
84#[derive(Clone)]
85pub struct Rng(u64);
86
87impl Rng {
88    pub fn new(seed: u64) -> Self {
89        Rng(seed.wrapping_mul(0x9e3779b97f4a7c15) | 1)
90    }
91    pub fn next(&mut self) -> u64 {
92        self.0 ^= self.0 >> 12;
93        self.0 ^= self.0 << 25;
94        self.0 ^= self.0 >> 27;
95        self.0.wrapping_mul(0x2545f4914f6cdd1d)
96    }
97    pub fn below(&mut self, n: u64) -> u64 {
98        if n == 0 {
99            0
100        } else {
101            self.next() % n
102        }
103    }
104    pub fn chance(&mut self, ppm: u32) -> bool {
105        ppm > 0 && self.below(1_000_000) < ppm as u64
106    }
107}
108
109/// Two endpoints and the links between them.
110pub struct Pair {
111    pub now: Instant,
112    pub client: Endpoint,
113    pub server: Endpoint,
114    pub client_addr: SocketAddr,
115    pub server_addr: SocketAddr,
116    /// Old client addresses that still reach the client (after a rebinding).
117    pub client_aliases: Vec<SocketAddr>,
118    pub to_server: Link,
119    pub to_client: Link,
120    pub to_server_stats: LinkStats,
121    pub to_client_stats: LinkStats,
122    pub rng: Rng,
123    queue: BinaryHeap<Reverse<Queued>>,
124    seq: u64,
125    busy_until: [Instant; 2],
126    queued_bytes: [usize; 2],
127    buf: Vec<u8>,
128    resp: Vec<u8>,
129    /// Server connections accepted so far.
130    pub accepted: Vec<ConnHandle>,
131    /// GSO batch size used when polling endpoints.
132    pub batch: usize,
133    /// Optional hook to inspect or rewrite datagrams in flight; return false to drop.
134    pub tamper: Option<Box<dyn FnMut(bool, &mut Vec<u8>) -> bool>>,
135}
136
137impl Pair {
138    pub fn new(client_cfg: Arc<Config>, server_cfg: Arc<Config>, link: Link, seed: u64) -> Self {
139        let now = Instant::now();
140        Pair {
141            now,
142            client: Endpoint::new(client_cfg, false),
143            server: Endpoint::new(server_cfg, true),
144            client_addr: "10.0.0.1:5000".parse().unwrap(),
145            server_addr: "10.0.0.2:443".parse().unwrap(),
146            client_aliases: Vec::new(),
147            to_server: link.clone(),
148            to_client: link,
149            to_server_stats: LinkStats::default(),
150            to_client_stats: LinkStats::default(),
151            rng: Rng::new(seed),
152            queue: BinaryHeap::new(),
153            seq: 0,
154            busy_until: [now; 2],
155            queued_bytes: [0; 2],
156            buf: Vec::with_capacity(64 * 1024),
157            resp: Vec::new(),
158            accepted: Vec::new(),
159            batch: 8,
160            tamper: None,
161        }
162    }
163
164    /// Starts a client connection to the server.
165    pub fn connect(&mut self) -> ConnHandle {
166        let addr = self.server_addr;
167        self.client.connect(self.now, addr, "localhost", &[]).unwrap()
168    }
169
170    fn enqueue(&mut self, to_server: bool, from: SocketAddr, to: SocketAddr, mut data: Vec<u8>) {
171        let d = if to_server { 0 } else { 1 };
172        let link = if to_server { self.to_server.clone() } else { self.to_client.clone() };
173        let stats = if to_server { &mut self.to_server_stats } else { &mut self.to_client_stats };
174        stats.sent += 1;
175        if let Some(t) = self.tamper.as_mut() {
176            if !t(to_server, &mut data) {
177                stats.dropped += 1;
178                return;
179            }
180        }
181        if data.len() > link.mtu || self.rng.chance(link.loss_ppm) {
182            stats.dropped += 1;
183            return;
184        }
185        // Bottleneck: serialisation delay and a tail-drop queue.
186        let mut depart = self.now;
187        if let Some(bw) = link.bandwidth {
188            if self.queued_bytes[d] + data.len() > link.queue && self.busy_until[d] > self.now {
189                stats.dropped += 1;
190                return;
191            }
192            let start = self.busy_until[d].max(self.now);
193            let tx = Duration::from_nanos(data.len() as u64 * 1_000_000_000 / bw);
194            self.busy_until[d] = start + tx;
195            depart = start + tx;
196            self.queued_bytes[d] += data.len();
197        }
198        let mut at = depart + link.delay;
199        if !link.jitter.is_zero() {
200            at += Duration::from_nanos(self.rng.below(link.jitter.as_nanos() as u64));
201        }
202        if self.rng.chance(link.reorder_ppm) {
203            at += Duration::from_nanos(self.rng.below(link.reorder_delay.as_nanos() as u64 + 1));
204        }
205        let dup = self.rng.chance(link.duplicate_ppm);
206        if dup {
207            stats.duplicated += 1;
208        }
209        stats.bytes += data.len() as u64;
210        let n = data.len();
211        if dup {
212            let copy = data.clone();
213            self.push(at + Duration::from_micros(50), Dgram { from, to, data: copy });
214        }
215        let _ = n;
216        self.push(at, Dgram { from, to, data });
217    }
218
219    fn push(&mut self, at: Instant, d: Dgram) {
220        self.seq += 1;
221        self.queue.push(Reverse(Queued(at, self.seq, d)));
222    }
223
224    fn flush(&mut self) {
225        loop {
226            let mut any = false;
227            while let Some((_, t)) = self.client.poll_transmit(self.now, &mut self.buf, self.batch) {
228                any = true;
229                let from = self.client_addr;
230                self.split_send(true, from, t);
231            }
232            while let Some((_, t)) = self.server.poll_transmit(self.now, &mut self.buf, self.batch) {
233                any = true;
234                let from = self.server_addr;
235                self.split_send(false, from, t);
236            }
237            if !any {
238                break;
239            }
240        }
241    }
242
243    fn split_send(&mut self, to_server: bool, from: SocketAddr, t: Transmit) {
244        let seg = t.segment_size.unwrap_or(t.size);
245        let data = std::mem::take(&mut self.buf);
246        for chunk in data[..t.size].chunks(seg) {
247            self.enqueue(to_server, from, t.destination, chunk.to_vec());
248        }
249        self.buf = data;
250    }
251
252    /// Runs one step: send what is ready, then deliver the next datagram or fire the next
253    /// timer. Returns false when nothing is left to do.
254    pub fn step(&mut self) -> bool {
255        self.step_until(None)
256    }
257
258    /// Like [`Pair::step`], but never advances the clock past `limit`.
259    pub fn step_until(&mut self, limit: Option<Instant>) -> bool {
260        self.flush();
261        let next_dgram = self.queue.peek().map(|r| r.0 .0);
262        let next_timer = [self.client.poll_timeout(), self.server.poll_timeout()].into_iter().flatten().min();
263        let next = match (next_dgram, next_timer) {
264            (None, None) => return false,
265            (Some(a), None) => a,
266            (None, Some(b)) => b,
267            (Some(a), Some(b)) => a.min(b),
268        };
269        if let Some(l) = limit {
270            if next > l {
271                self.now = self.now.max(l);
272                return false;
273            }
274        }
275        if next > self.now {
276            self.now = next;
277        }
278        if next_dgram.is_some_and(|a| a <= self.now) {
279            let Reverse(Queued(_, _, mut d)) = self.queue.pop().unwrap();
280            let to_server = d.to == self.server_addr;
281            let di = if to_server { 0 } else { 1 };
282            self.queued_bytes[di] = self.queued_bytes[di].saturating_sub(d.data.len());
283            if to_server {
284                self.to_server_stats.delivered += 1;
285                let ev = self.server.recv(self.now, d.from, &mut d.data, &mut self.resp);
286                match ev {
287                    DatagramEvent::NewConnection(h) => self.accepted.push(h),
288                    DatagramEvent::Response(t) => {
289                        let r = self.resp[..t.size].to_vec();
290                        let from = self.server_addr;
291                        self.enqueue(false, from, t.destination, r);
292                    }
293                    _ => {}
294                }
295            } else if d.to == self.client_addr || self.client_aliases.contains(&d.to) {
296                self.to_client_stats.delivered += 1;
297                let ev = self.client.recv(self.now, d.from, &mut d.data, &mut self.resp);
298                if let DatagramEvent::Response(t) = ev {
299                    let r = self.resp[..t.size].to_vec();
300                    let from = self.client_addr;
301                    self.enqueue(true, from, t.destination, r);
302                }
303            } else {
304                self.to_client_stats.dropped += 1;
305            }
306        } else {
307            let mut touched = Vec::new();
308            self.client.handle_timeout(self.now, &mut touched);
309            self.server.handle_timeout(self.now, &mut touched);
310        }
311        true
312    }
313
314    /// Steps until `cond` holds or `limit` of virtual time passes; returns whether it held.
315    pub fn run_until(&mut self, limit: Duration, mut cond: impl FnMut(&mut Pair) -> bool) -> bool {
316        let deadline = self.now + limit;
317        loop {
318            if cond(self) {
319                return true;
320            }
321            if self.now >= deadline || !self.step_until(Some(deadline)) {
322                return cond(self);
323            }
324        }
325    }
326
327    /// Runs until both sides are idle (no datagrams or timers due within `limit`).
328    pub fn run_for(&mut self, limit: Duration) {
329        let deadline = self.now + limit;
330        while self.now < deadline {
331            if !self.step_until(Some(deadline)) {
332                break;
333            }
334        }
335    }
336
337    /// The first accepted server connection.
338    pub fn server_conn(&self) -> Option<ConnHandle> {
339        self.accepted.first().copied()
340    }
341
342    /// Simulates a NAT rebinding: the client's datagrams now come from `addr`.
343    pub fn rebind_client(&mut self, addr: SocketAddr) {
344        let old = std::mem::replace(&mut self.client_addr, addr);
345        self.client_aliases.push(old);
346    }
347
348    /// Datagrams in flight.
349    pub fn in_flight(&self) -> usize {
350        self.queue.len()
351    }
352}