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}