Skip to main content

turmoil_net/
lib.rs

1//! Deterministic network simulation for turmoil.
2//!
3//! `turmoil-net` is a simulated socket stack. Production code imports
4//! [`tokio::net`]; tests swap the import for [`shim::tokio::net`] and
5//! run the same code against a fully deterministic network.
6//!
7//! The crate README has the motivation and code examples. The module
8//! list below is a map of the code:
9//!
10//! - [`shim`] — drop-in replacements for `tokio::net` types.
11//!   Production code changes one import and runs unchanged.
12//! - [`fixture`] — batteries-included scheduler + runtime for the
13//!   common shapes ([`fixture::lo`] single-host, [`fixture::ClientServer`]
14//!   multi-host). Start here; drop to the primitives when you outgrow
15//!   them.
16//! - [`Net`] / [`EnterGuard`] — the primitives the fixtures are built
17//!   on. Build a topology with [`Net::add_host`], install it with
18//!   [`Net::enter`], drive the fabric with [`EnterGuard::egress_all`] /
19//!   [`EnterGuard::evaluate`] / [`EnterGuard::deliver`].
20//! - [`Rule`] / [`Verdict`] / [`rule`] — packet-level fault injection.
21//!   Rules see every non-loopback packet and decide Pass / Deliver
22//!   (with optional delay) / Drop.
23//! - [`netstat`] — Linux-style socket snapshot for debugging a test.
24//!
25//! [`tokio::net`]: https://docs.rs/tokio/latest/tokio/net/index.html
26
27use std::cell::RefCell;
28
29use indexmap::IndexMap;
30
31mod dns;
32mod fabric;
33pub mod fixture;
34mod kernel;
35mod netstat;
36mod rule;
37pub mod shim;
38
39use crate::dns::Dns;
40pub use crate::dns::{ToIpAddr, ToIpAddrs};
41use crate::fabric::Fabric;
42pub use crate::fabric::HostId;
43use crate::kernel::Kernel;
44pub use crate::kernel::{KernelConfig, Packet, TcpFlags, TcpSegment, Transport, UdpDatagram};
45pub use crate::netstat::{Netstat, NetstatEntry, NetstatState, Proto};
46pub use crate::rule::{Latency, Rule, RuleGuard, RuleId, Verdict};
47
48thread_local! {
49    static CURRENT: RefCell<Option<Net>> = const { RefCell::new(None) };
50}
51
52pub struct Net {
53    fabric: Fabric,
54    dns: Dns,
55    current: Option<HostId>,
56    /// Installed rules, consulted in insertion order.
57    rules: IndexMap<RuleId, Box<dyn Rule>>,
58    next_rule_id: u64,
59}
60
61impl Net {
62    pub fn new() -> Self {
63        Self::with_config(KernelConfig::default())
64    }
65
66    /// `cfg` is applied to every host added later.
67    pub fn with_config(cfg: KernelConfig) -> Self {
68        Self {
69            fabric: Fabric::new(cfg),
70            dns: Dns::new(),
71            current: None,
72            rules: IndexMap::new(),
73            next_rule_id: 1,
74        }
75    }
76
77    /// Register a host. `addrs` accepts hostnames (auto-allocated to
78    /// 192.168.x.x on first sight, idempotent on reuse) or literal
79    /// IPs. Loopback (127.0.0.1, ::1) is implicit — do not pass it.
80    /// Panics if an address is already claimed by another host, or
81    /// if loopback is passed explicitly. The first host added becomes
82    /// current.
83    pub fn add_host<A: ToIpAddrs>(&mut self, addrs: A) -> HostId {
84        let ips = addrs.to_ip_addrs(&mut self.dns);
85        let id = self.fabric.add_host(ips);
86        if self.current.is_none() {
87            self.current = Some(id);
88        }
89        id
90    }
91
92    /// Resolve `name` to its registered IP, allocating if unseen.
93    /// Mirrors the name resolution used by [`Net::add_host`] and the
94    /// shim's hostname-aware socket addrs.
95    pub fn lookup(&mut self, name: &str) -> std::net::IpAddr {
96        self.dns.resolve(name)
97    }
98
99    pub fn host_ids(&self) -> impl Iterator<Item = HostId> + '_ {
100        self.fabric.host_ids()
101    }
102
103    /// Install a rule for the life of the `Net`. Use this when the
104    /// rule is part of the test's fixed setup (symmetric latency,
105    /// permanent packet filter, etc). For rules that only apply to
106    /// a phase of the test, use the guard-returning [`rule`] free
107    /// function from inside the sim instead.
108    pub fn rule(&mut self, rule: impl Rule) {
109        self.install_rule(Box::new(rule));
110    }
111
112    fn install_rule(&mut self, rule: Box<dyn Rule>) -> RuleId {
113        let id = RuleId(self.next_rule_id);
114        self.next_rule_id += 1;
115        self.rules.insert(id, rule);
116        id
117    }
118
119    fn uninstall_rule(&mut self, id: RuleId) {
120        self.rules.shift_remove(&id);
121    }
122
123    /// Walk the installed rules in insertion order; first non-`Pass`
124    /// wins. Harness code calls this once per outbound packet to let
125    /// rules interpose.
126    fn evaluate(&mut self, pkt: &Packet) -> Verdict {
127        for rule in self.rules.values_mut() {
128            match rule.on_packet(pkt) {
129                Verdict::Pass => continue,
130                v => return v,
131            }
132        }
133        Verdict::Pass
134    }
135
136    /// Panics if another `Net` is already installed on this thread.
137    pub fn enter(self) -> EnterGuard {
138        CURRENT.with(|c| {
139            let mut slot = c.borrow_mut();
140            assert!(slot.is_none(), "another Net is already installed");
141            *slot = Some(self);
142        });
143        EnterGuard { _priv: () }
144    }
145}
146
147impl std::fmt::Debug for Net {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("Net")
150            .field("fabric", &self.fabric)
151            .field("dns", &self.dns)
152            .field("current", &self.current)
153            .field("rules", &format!("{} installed", self.rules.len()))
154            .finish()
155    }
156}
157
158impl Default for Net {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164#[must_use = "a Net is only active while the guard is held"]
165pub struct EnterGuard {
166    _priv: (),
167}
168
169impl EnterGuard {
170    /// Drain every host's outbound queue into `out`. The caller decides
171    /// what happens next — typically: consult rules via
172    /// [`EnterGuard::evaluate`] for each packet and then
173    /// [`EnterGuard::deliver`] (or schedule for later). The buffer is
174    /// passed in so the caller can reuse its allocation across ticks.
175    /// See [`fixture`] for the default tokio-driven loop.
176    pub fn egress_all(&self, out: &mut Vec<Packet>) {
177        CURRENT.with(|c| {
178            c.borrow_mut()
179                .as_mut()
180                .expect("guard is live")
181                .fabric
182                .egress_all(out)
183        });
184    }
185
186    /// Route a packet to the host owning its destination IP. Drops
187    /// silently if no host is registered for that IP.
188    pub fn deliver(&self, pkt: Packet) {
189        CURRENT.with(|c| {
190            c.borrow_mut()
191                .as_mut()
192                .expect("guard is live")
193                .fabric
194                .deliver(pkt)
195        });
196    }
197
198    /// Walk installed rules for `pkt`. First non-`Pass` verdict wins;
199    /// empty rule chain returns `Verdict::Pass`.
200    pub fn evaluate(&self, pkt: &Packet) -> Verdict {
201        CURRENT.with(|c| {
202            c.borrow_mut()
203                .as_mut()
204                .expect("guard is live")
205                .evaluate(pkt)
206        })
207    }
208
209    /// Pin which host subsequent `sys()` calls (i.e. socket syscalls
210    /// from any task spawned inside this guard) talk to.
211    pub fn set_current(&self, id: HostId) {
212        CURRENT.with(|c| {
213            c.borrow_mut().as_mut().expect("guard is live").current = Some(id);
214        });
215    }
216
217    /// Install a rule, vending a [`RuleGuard`] that uninstalls it on
218    /// drop. Useful in schedulers or fixture code that owns the
219    /// guard's lifetime explicitly — async tasks should call the
220    /// free [`rule`] function instead.
221    pub fn rule(&self, r: impl Rule) -> RuleGuard {
222        RuleGuard::new(install_rule(Box::new(r)))
223    }
224}
225
226impl Drop for EnterGuard {
227    fn drop(&mut self) {
228        CURRENT.with(|c| *c.borrow_mut() = None);
229    }
230}
231
232pub(crate) fn sys<R>(f: impl FnOnce(&mut Kernel) -> R) -> R {
233    CURRENT.with(|c| {
234        let mut cell = c.borrow_mut();
235        let net = cell
236            .as_mut()
237            .expect("no Net installed — call Net::enter() first");
238        let id = net
239            .current
240            .expect("no current host — register one with Net::add_host()");
241        f(net.fabric.kernel_mut(id))
242    })
243}
244
245/// Resolve `name` against the installed `Net`'s DNS. Returns `None`
246/// if there is no `Net` installed, or if the name isn't registered
247/// and can't be parsed as an IP literal.
248pub fn lookup_host(name: &str) -> Option<std::net::IpAddr> {
249    CURRENT.with(|c| c.borrow().as_ref().and_then(|net| net.dns.lookup(name)))
250}
251
252/// Pin which host subsequent [`sys`](crate) calls (i.e. socket
253/// syscalls from the caller's task) talk to.
254///
255/// This is the free-function form of [`EnterGuard::set_current`], for
256/// use where the guard isn't in scope — typically inside a future
257/// wrapper that rescopes every poll. Harnesses with shared-runtime
258/// fixtures (see [`fixture::ClientServer`] for the canonical pattern)
259/// call this on entry to each task's poll so `sys()` lookups land in
260/// the right kernel.
261///
262/// Panics if no `Net` is installed.
263pub fn set_current(id: HostId) {
264    CURRENT.with(|c| {
265        c.borrow_mut()
266            .as_mut()
267            .expect("no Net installed — call Net::enter() first")
268            .current = Some(id);
269    });
270}
271
272/// Install a rule and return a guard that uninstalls it on drop.
273/// Callable from any task inside an installed `Net`. Panics if no
274/// `Net` is installed.
275///
276/// For rules that should live for the entire simulation, use
277/// [`Net::rule`] before calling [`Net::enter`].
278pub fn rule(r: impl Rule) -> RuleGuard {
279    RuleGuard::new(install_rule(Box::new(r)))
280}
281
282fn install_rule(r: Box<dyn Rule>) -> RuleId {
283    CURRENT.with(|c| {
284        c.borrow_mut()
285            .as_mut()
286            .expect("no Net installed — call Net::enter() first")
287            .install_rule(r)
288    })
289}
290
291fn uninstall_rule(id: RuleId) {
292    CURRENT.with(|c| {
293        // Tolerant of the Net already being gone — drop order during
294        // teardown isn't guaranteed.
295        if let Some(net) = c.borrow_mut().as_mut() {
296            net.uninstall_rule(id);
297        }
298    });
299}
300
301/// Snapshot a host's socket table, Linux `netstat`-style. `host`
302/// accepts a hostname (resolved via DNS like [`Net::add_host`]) or a
303/// literal IP. Panics if no `Net` is installed, or if the address
304/// doesn't match a registered host. Loopback isn't routable on its
305/// own — pass a hostname or the host's configured IP.
306pub fn netstat<H: ToIpAddr>(host: H) -> Netstat {
307    CURRENT.with(|c| {
308        let cell = c.borrow();
309        let net = cell
310            .as_ref()
311            .expect("no Net installed — call Net::enter() first");
312        let ip = host
313            .try_to_ip_addr(&net.dns)
314            .expect("hostname not registered");
315        let id = net
316            .fabric
317            .host_for_ip(ip)
318            .unwrap_or_else(|| panic!("no host registered for {ip}"));
319        netstat::snapshot(net.fabric.kernel(id))
320    })
321}