ndn_sim/
sim_link.rs

1//! `SimLink` — a configurable simulated link between two faces.
2//!
3//! Creates a bidirectional link with delay, loss, bandwidth, and jitter.
4
5use std::time::Duration;
6
7use ndn_transport::FaceId;
8
9use crate::sim_face::SimFace;
10
11/// Link properties for a simulated connection.
12#[derive(Clone, Debug)]
13pub struct LinkConfig {
14    /// Base one-way propagation delay.
15    pub delay: Duration,
16    /// Random jitter added to each packet's delay (uniform in `[0, jitter]`).
17    pub jitter: Duration,
18    /// Packet loss rate (0.0 = no loss, 1.0 = all packets dropped).
19    pub loss_rate: f64,
20    /// Link bandwidth in bits per second. `0` means unlimited.
21    pub bandwidth_bps: u64,
22}
23
24impl Default for LinkConfig {
25    fn default() -> Self {
26        Self {
27            delay: Duration::ZERO,
28            jitter: Duration::ZERO,
29            loss_rate: 0.0,
30            bandwidth_bps: 0,
31        }
32    }
33}
34
35impl LinkConfig {
36    /// Lossless, zero-delay link (in-process direct connection).
37    pub fn direct() -> Self {
38        Self::default()
39    }
40
41    /// Typical LAN link: 1ms delay, no loss, 1 Gbps.
42    pub fn lan() -> Self {
43        Self {
44            delay: Duration::from_millis(1),
45            jitter: Duration::from_micros(100),
46            loss_rate: 0.0,
47            bandwidth_bps: 1_000_000_000,
48        }
49    }
50
51    /// Typical WiFi link: 5ms delay, 1% loss, 54 Mbps.
52    pub fn wifi() -> Self {
53        Self {
54            delay: Duration::from_millis(5),
55            jitter: Duration::from_millis(2),
56            loss_rate: 0.01,
57            bandwidth_bps: 54_000_000,
58        }
59    }
60
61    /// WAN link: 50ms delay, 0.1% loss, 100 Mbps.
62    pub fn wan() -> Self {
63        Self {
64            delay: Duration::from_millis(50),
65            jitter: Duration::from_millis(5),
66            loss_rate: 0.001,
67            bandwidth_bps: 100_000_000,
68        }
69    }
70
71    /// Lossy wireless link: 10ms delay, 5% loss, 11 Mbps.
72    pub fn lossy_wireless() -> Self {
73        Self {
74            delay: Duration::from_millis(10),
75            jitter: Duration::from_millis(5),
76            loss_rate: 0.05,
77            bandwidth_bps: 11_000_000,
78        }
79    }
80}
81
82/// A simulated bidirectional link between two faces.
83pub struct SimLink;
84
85impl SimLink {
86    /// Create a pair of connected `SimFace`s with the given link properties.
87    ///
88    /// Packets sent on `face_a` arrive at `face_b` (and vice versa) after
89    /// the configured delay, subject to loss and bandwidth constraints.
90    ///
91    /// The same `LinkConfig` is applied in both directions. For asymmetric
92    /// links, use [`pair_asymmetric`](Self::pair_asymmetric).
93    ///
94    /// ```rust,no_run
95    /// # use ndn_sim::{SimLink, LinkConfig};
96    /// # use ndn_transport::FaceId;
97    /// let (face_a, face_b) = SimLink::pair(
98    ///     FaceId(10), FaceId(11),
99    ///     LinkConfig::wifi(),
100    ///     128,  // channel buffer size
101    /// );
102    /// ```
103    pub fn pair(
104        id_a: FaceId,
105        id_b: FaceId,
106        config: LinkConfig,
107        buffer: usize,
108    ) -> (SimFace, SimFace) {
109        Self::pair_asymmetric(id_a, id_b, config.clone(), config, buffer)
110    }
111
112    /// Create a pair with different link properties per direction.
113    ///
114    /// `config_a_to_b` is applied when `face_a` sends to `face_b`;
115    /// `config_b_to_a` is applied when `face_b` sends to `face_a`.
116    pub fn pair_asymmetric(
117        id_a: FaceId,
118        id_b: FaceId,
119        config_a_to_b: LinkConfig,
120        config_b_to_a: LinkConfig,
121        buffer: usize,
122    ) -> (SimFace, SimFace) {
123        let (tx_a, rx_a) = tokio::sync::mpsc::channel(buffer);
124        let (tx_b, rx_b) = tokio::sync::mpsc::channel(buffer);
125
126        // face_a sends through config_a_to_b into rx_b (received by face_b)
127        // face_b sends through config_b_to_a into rx_a (received by face_a)
128        let face_a = SimFace::new(id_a, tx_b, rx_a, config_a_to_b);
129        let face_b = SimFace::new(id_b, tx_a, rx_b, config_b_to_a);
130
131        (face_a, face_b)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use ndn_transport::Face;
139
140    #[tokio::test]
141    async fn direct_link_delivers_packet() {
142        let (face_a, face_b) = SimLink::pair(FaceId(1), FaceId(2), LinkConfig::direct(), 16);
143
144        let payload = bytes::Bytes::from_static(b"hello");
145        face_a.send(payload.clone()).await.unwrap();
146
147        let received = face_b.recv().await.unwrap();
148        assert_eq!(received, payload);
149    }
150
151    #[tokio::test]
152    async fn bidirectional_delivery() {
153        let (face_a, face_b) = SimLink::pair(FaceId(1), FaceId(2), LinkConfig::direct(), 16);
154
155        face_a
156            .send(bytes::Bytes::from_static(b"ping"))
157            .await
158            .unwrap();
159        face_b
160            .send(bytes::Bytes::from_static(b"pong"))
161            .await
162            .unwrap();
163
164        let at_b = face_b.recv().await.unwrap();
165        let at_a = face_a.recv().await.unwrap();
166        assert_eq!(at_b, &b"ping"[..]);
167        assert_eq!(at_a, &b"pong"[..]);
168    }
169
170    #[tokio::test]
171    async fn delayed_link() {
172        let config = LinkConfig {
173            delay: Duration::from_millis(50),
174            ..Default::default()
175        };
176        let (face_a, face_b) = SimLink::pair(FaceId(1), FaceId(2), config, 16);
177
178        let start = tokio::time::Instant::now();
179        face_a.send(bytes::Bytes::from_static(b"hi")).await.unwrap();
180        let _received = face_b.recv().await.unwrap();
181        let elapsed = start.elapsed();
182
183        assert!(
184            elapsed >= Duration::from_millis(45),
185            "expected ~50ms delay, got {elapsed:?}"
186        );
187    }
188
189    #[tokio::test]
190    async fn lossy_link_drops_some_packets() {
191        let config = LinkConfig {
192            loss_rate: 1.0, // drop everything
193            ..Default::default()
194        };
195        let (face_a, face_b) = SimLink::pair(FaceId(1), FaceId(2), config, 16);
196
197        for _ in 0..10 {
198            face_a.send(bytes::Bytes::from_static(b"x")).await.unwrap();
199        }
200
201        // With 100% loss, nothing should arrive. Use a short timeout.
202        let result = tokio::time::timeout(Duration::from_millis(100), face_b.recv()).await;
203        assert!(result.is_err(), "expected timeout with 100% loss");
204    }
205}