ferroday_cage/netstack/mod.rs
1//! The native userspace network stack.
2//!
3//! The default sandbox network is an isolated namespace whose only interface
4//! is loopback. This module attaches the library's own network stack to
5//! such a sandbox at the [`Pending`] seam: [`NetStack::attach`] creates a
6//! tap device in the sandbox's network namespace, configures its addresses
7//! and routes, and hands the device's descriptor to a pump thread in the
8//! caller's process. The pump terminates the guest's TCP connections and
9//! UDP flows in an in-process TCP/IP implementation and forwards each over
10//! an ordinary, unprivileged host socket, so the sandbox gets outbound
11//! connectivity while the host sees a normal application. The stack is
12//! self-contained, and the privilege it needs is exactly what creating the
13//! sandbox already granted: the caller's user owns the sandbox's user
14//! namespace, and entering it confers the network capabilities inside.
15//!
16//! The stack forwards where the guest could plausibly reach on its own and
17//! nowhere surprising: multicast, broadcast, loopback, and link-local
18//! destinations are refused, with one deliberate exception —
19//! [`host_loopback`](NetStackBuilder::host_loopback) maps connections to
20//! the gateway address onto the host's loopback. A refused TCP connection is
21//! answered with a reset, with one exception: the guest's own subnet is
22//! on-link from the guest's point of view, so it resolves a hardware address
23//! before it sends a segment, and the stack answers address resolution only
24//! for the gateway. Those destinations fail when the guest's own resolution
25//! gives up rather than on a reset. A refused UDP datagram is dropped, and
26//! whether the guest also receives an ICMP port-unreachable depends on
27//! whether the stack happens to hold a socket on that port for some other
28//! destination, so a caller must not read anything into either outcome.
29//!
30//! Only TCP and UDP are forwarded. The stack answers ICMP echo for the
31//! gateway address and for nothing else, so a ping inside the guest tells the
32//! guest that its gateway is up and says nothing at all about the destination
33//! — a reachable address does not answer, and neither does a refused one.
34//! Reachability is what a connection reports.
35//!
36//! Attachment happens in a forked, short-lived helper process that joins
37//! the sandbox's user and network namespaces, creates and configures the
38//! tap, passes its descriptor back, and exits; the caller's own threads
39//! never change namespace ([`tap`] explains why the kernel forces this
40//! shape). The returned [`NetStackHandle`] owns the pump thread. The pump's
41//! open tap descriptor pins the device and its namespace, so the handle
42//! outlives the sandbox gracefully: wait for the command first, then
43//! [`stop`](NetStackHandle::stop) the stack. Dropping the handle stops the
44//! pump the same way and leaves the sandbox running — a sandbox without its
45//! stack merely loses connectivity.
46
47use std::fmt;
48use std::io;
49use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
50use std::os::fd::OwnedFd;
51use std::panic::AssertUnwindSafe;
52use std::sync::Arc;
53use std::sync::atomic::{AtomicBool, Ordering};
54use std::thread::JoinHandle;
55
56use rustix::fs::{Mode, OFlags};
57use rustix::pipe::{self, PipeFlags};
58
59use crate::running::Pending;
60
61mod device;
62mod pump;
63mod sniff;
64mod tap;
65mod tcp;
66mod udp;
67
68/// The guest interface's MAC address: locally administered and stable, so
69/// the guest side of every exchange is deterministic.
70const GUEST_MAC: [u8; 6] = [0x02, 0xfc, 0x0d, 0x00, 0x00, 0x0f];
71
72/// The gateway MAC address the stack's interface answers with.
73const GATEWAY_MAC: [u8; 6] = [0x02, 0xfc, 0x0d, 0x00, 0x00, 0x02];
74
75/// The host part of the guest's IPv4 address within the configured network.
76const GUEST_HOST_V4: u32 = 15;
77/// The host part of the gateway's IPv4 address. The stack's interface owns
78/// this address; the guest's default route points at it.
79const GATEWAY_HOST_V4: u32 = 2;
80/// The host part of the guest's IPv6 address (`::15`).
81const GUEST_HOST_V6: u128 = 0x15;
82/// The host part of the gateway's IPv6 address (`::2`).
83const GATEWAY_HOST_V6: u128 = 0x2;
84
85/// The widest IPv4 prefix that leaves room for the address plan: the
86/// gateway at host 2 and the guest at host 15, below the broadcast address.
87const MAX_PREFIX_V4: u8 = 27;
88/// The widest IPv6 prefix that leaves the address plan a full final byte.
89const MAX_PREFIX_V6: u8 = 120;
90
91/// The smallest usable MTU with IPv4 alone (RFC 791's reassembly minimum).
92const MIN_MTU_V4: u16 = 576;
93/// The smallest usable MTU when IPv6 is enabled (RFC 8200's link minimum).
94const MIN_MTU_V6: u16 = 1280;
95
96/// A frozen network-stack configuration, ready to attach to sandboxes.
97///
98/// Built by [`NetStackBuilder`]; the default configuration is valid and
99/// matches the builder's defaults. One `NetStack` may be attached to any
100/// number of sandboxes — [`attach`](Self::attach) borrows it and every
101/// attachment is independent.
102///
103/// ```no_run
104/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
105/// # let cage = ferroday_cage::Cage::builder().rootfs("/r").command("/bin/x").build()?;
106/// use ferroday_cage::NetStack;
107///
108/// let stack = NetStack::default();
109/// let pending = cage.spawn_pending()?;
110/// let handle = stack.attach(&pending)?;
111/// let mut running = pending.proceed()?;
112/// let status = running.wait()?;
113/// handle.stop()?;
114/// # let _ = status;
115/// # Ok(())
116/// # }
117/// ```
118#[derive(Debug, Clone)]
119pub struct NetStack {
120 plan: Plan,
121}
122
123/// The validated address and interface plan an attachment executes.
124#[derive(Debug, Clone)]
125struct Plan {
126 /// The IPv4 side, when enabled.
127 v4: Option<PlanV4>,
128 /// The IPv6 side, when enabled.
129 v6: Option<PlanV6>,
130 /// The tap interface's MTU.
131 mtu: u16,
132 /// The tap interface's name inside the sandbox.
133 interface: String,
134 /// Whether connections to the gateway address map to host loopback.
135 host_loopback: bool,
136}
137
138/// The IPv4 half of the plan: the guest address the tap carries and the
139/// gateway address the stack's interface answers for.
140#[derive(Debug, Clone)]
141struct PlanV4 {
142 guest: Ipv4Addr,
143 gateway: Ipv4Addr,
144 prefix_len: u8,
145}
146
147/// The IPv6 half of the plan.
148#[derive(Debug, Clone)]
149struct PlanV6 {
150 guest: Ipv6Addr,
151 gateway: Ipv6Addr,
152 prefix_len: u8,
153}
154
155impl NetStack {
156 /// Returns a builder holding the default configuration.
157 pub fn builder() -> NetStackBuilder {
158 NetStackBuilder::default()
159 }
160
161 /// Attaches the stack to a pending sandbox launch.
162 ///
163 /// A forked helper process joins the sandbox's user and network
164 /// namespaces through `/proc/<pid>/ns`, creates the tap device, assigns
165 /// the guest addresses and default routes, brings the interface up, and
166 /// exits. The tap's descriptor comes back to a pump thread in the
167 /// caller's process, owned by the returned handle. On return the
168 /// sandbox's interface is fully configured, so the caller proceeds
169 /// immediately:
170 ///
171 /// ```no_run
172 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
173 /// # let cage = ferroday_cage::Cage::builder().rootfs("/r").command("/bin/x").build()?;
174 /// # let stack = ferroday_cage::NetStack::default();
175 /// let pending = cage.spawn_pending()?;
176 /// let handle = stack.attach(&pending)?;
177 /// let mut running = pending.proceed()?;
178 /// let status = running.wait()?;
179 /// handle.stop()?;
180 /// # let _ = status;
181 /// # Ok(())
182 /// # }
183 /// ```
184 ///
185 /// The sandbox must have a private network namespace —
186 /// [`Network::Isolated`](crate::Network::Isolated) (the default) or
187 /// [`Network::None`](crate::Network::None). A sandbox sharing the host's
188 /// namespace ([`Network::Host`](crate::Network::Host)) is refused with
189 /// [`NetStackError::SharedNamespace`].
190 ///
191 /// When the configuration enables IPv6, the guest address is usable
192 /// immediately where the stack could disable duplicate address detection
193 /// (it tries, through the sandbox's `net.ipv6.conf` sysctls); where it
194 /// could not, the kernel holds the address tentative for about a second
195 /// after launch, and the guest's first IPv6 connection may stall that
196 /// long. IPv4 is never delayed.
197 pub fn attach(&self, pending: &Pending<'_>) -> Result<NetStackHandle, NetStackError> {
198 let pid = pending.netns_pid();
199 let target = format!("/proc/{pid}/ns/net");
200
201 // Refuse a sandbox that shares the caller's namespace: attaching
202 // would create a tap on the host network, which is never what a
203 // caller means. The namespace links compare equal exactly when the
204 // namespaces are the same.
205 let caller = std::fs::read_link("/proc/self/ns/net").map_err(|err| {
206 NetStackError::NamespaceJoin {
207 which: "reading the caller's namespace link",
208 source: err.raw_os_error().is_some().then_some(err),
209 }
210 })?;
211 let sandbox = std::fs::read_link(&target).map_err(|err| NetStackError::NamespaceJoin {
212 which: "reading the sandbox's namespace link",
213 source: err.raw_os_error().is_some().then_some(err),
214 })?;
215 if caller == sandbox {
216 return Err(NetStackError::SharedNamespace);
217 }
218
219 // The Pending handle keeps the supervisor alive, so the pid cannot
220 // be recycled while these descriptors are being opened. The user
221 // namespace is joined for its capabilities; the network namespace
222 // is where the tap is created.
223 let netns = rustix::fs::open(&target, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())
224 .map_err(|errno| NetStackError::NamespaceJoin {
225 which: "opening the sandbox's network namespace",
226 source: Some(errno.into()),
227 })?;
228 let userns = rustix::fs::open(
229 format!("/proc/{pid}/ns/user"),
230 OFlags::RDONLY | OFlags::CLOEXEC,
231 Mode::empty(),
232 )
233 .map_err(|errno| NetStackError::NamespaceJoin {
234 which: "opening the sandbox's user namespace",
235 source: Some(errno.into()),
236 })?;
237
238 let tap = tap::create(userns, netns, &self.plan)?;
239
240 // The stop pipe wakes the pump: stop() writes a byte, and a dropped
241 // handle closes the write end, which the pump sees as a hangup.
242 let (stop_read, stop_write) =
243 pipe::pipe_with(PipeFlags::CLOEXEC).map_err(|errno| NetStackError::Spawn {
244 source: errno.into(),
245 })?;
246 let running = Arc::new(AtomicBool::new(true));
247 let running_pump = Arc::clone(&running);
248 let plan = self.plan.clone();
249 let pump = std::thread::Builder::new()
250 .name("fcage-netstack".to_string())
251 .spawn(move || {
252 // Panics are converted into a failed exit and the running
253 // flag is cleared on every path, so the handle's view of the
254 // pump is always truthful and stop() never rethrows.
255 let exit =
256 std::panic::catch_unwind(AssertUnwindSafe(|| pump::run(tap, stop_read, plan)));
257 running_pump.store(false, Ordering::SeqCst);
258 exit.unwrap_or(PumpExit::Failed {
259 op: "recovering from a panic",
260 source: None,
261 })
262 })
263 .map_err(|err| NetStackError::Spawn { source: err })?;
264
265 Ok(NetStackHandle {
266 pump: Some(pump),
267 stop: Some(stop_write),
268 running,
269 })
270 }
271}
272
273impl Default for NetStack {
274 fn default() -> Self {
275 NetStackBuilder::default()
276 .build()
277 .expect("the default network-stack configuration is valid")
278 }
279}
280
281/// How the pump thread concluded.
282enum PumpExit {
283 /// The pump stopped because the handle asked it to.
284 Clean,
285 /// The pump stopped on an unrecoverable error of its own machinery. The
286 /// error is absent where the pump stopped on something that was not a
287 /// syscall failure — a panic it recovered from.
288 Failed {
289 op: &'static str,
290 source: Option<io::Error>,
291 },
292}
293
294/// A handle to one attached network stack.
295///
296/// Owns the pump thread serving one sandbox's tap device. The intended order
297/// is: wait for the sandbox's command, then [`stop`](Self::stop) the stack —
298/// the pump holds the sandbox's network namespace open, so it never observes
299/// the sandbox's death and does not stop by itself. Dropping the handle stops
300/// the pump the same way, discarding its outcome. Stopping or dropping never
301/// affects the sandbox: a running command merely loses connectivity.
302#[derive(Debug)]
303pub struct NetStackHandle {
304 /// The pump thread; `None` only after `stop` or `drop` takes it.
305 pump: Option<JoinHandle<PumpExit>>,
306 /// Write end of the stop pipe; closing it is itself a stop signal.
307 stop: Option<OwnedFd>,
308 /// Cleared by the pump on every exit path.
309 running: Arc<AtomicBool>,
310}
311
312impl NetStackHandle {
313 /// Stops the stack and reports how the pump fared.
314 ///
315 /// Signals the pump, waits for it to exit, and returns the first error
316 /// the pump stopped on, if any. A pump that failed earlier — leaving
317 /// [`is_running`](Self::is_running) false — still has its outcome
318 /// reported here.
319 pub fn stop(mut self) -> Result<(), NetStackError> {
320 let pump = self
321 .pump
322 .take()
323 .expect("a handle owns its pump thread until stop or drop consumes it");
324 // Closing the stop pipe's write end wakes the pump's poll with a
325 // hangup, which is the whole stop signal. Dropping the descriptor here
326 // closes it. A byte-write would also wake the pump, but a pump that
327 // exited on its own has closed the read end, so the write could raise
328 // SIGPIPE; the close cannot.
329 drop(self.stop.take());
330 match pump.join() {
331 Ok(PumpExit::Clean) => Ok(()),
332 Ok(PumpExit::Failed { op, source }) => Err(NetStackError::Pump { op, source }),
333 // The pump converts its panics into a failed exit; a join error
334 // means that conversion itself was bypassed. Report, not rethrow.
335 Err(_) => Err(NetStackError::Pump {
336 op: "recovering from a panic",
337 source: None,
338 }),
339 }
340 }
341
342 /// Whether the pump thread is still serving the tap device.
343 ///
344 /// False once the stack has stopped — after [`stop`](Self::stop), or on
345 /// its own if the pump hit an unrecoverable error (reported by `stop`).
346 pub fn is_running(&self) -> bool {
347 self.running.load(Ordering::SeqCst)
348 }
349}
350
351impl Drop for NetStackHandle {
352 fn drop(&mut self) {
353 // Closing the stop pipe's write end hangs up the pump's read end,
354 // which the pump treats as a stop request. Join so the tap descriptor
355 // is closed — and the sandbox's namespace released — before drop
356 // returns; the outcome is discarded, stop() being the reporting path.
357 drop(self.stop.take());
358 if let Some(pump) = self.pump.take() {
359 let _ = pump.join();
360 }
361 }
362}
363
364/// Builder for a [`NetStack`].
365///
366/// The defaults follow the established userspace-networking convention:
367/// IPv4 and IPv6 both enabled, the guest at `10.0.2.15/24` and `fd00::15/64`,
368/// the gateway — the stack itself — at `10.0.2.2` and `fd00::2`, MTU 1500,
369/// interface `tap0`. Within a configured network the stack always places the
370/// gateway at host 2 and the guest at host 15.
371///
372/// With the `serde` feature the builder serializes and deserializes as the
373/// stack specification, for embedding in a consumer's own configuration.
374/// Keys are kebab-case; unknown keys are rejected; the CIDR fields read and
375/// write as strings. In TOML:
376///
377/// ```toml
378/// ipv4-cidr = "10.0.2.0/24"
379/// ipv6 = false
380/// mtu = 1500
381/// interface = "tap0"
382/// ```
383#[derive(Debug, Clone)]
384#[cfg_attr(
385 feature = "serde",
386 derive(serde::Serialize, serde::Deserialize),
387 serde(default, rename_all = "kebab-case", deny_unknown_fields)
388)]
389pub struct NetStackBuilder {
390 ipv4: bool,
391 ipv6: bool,
392 #[cfg_attr(feature = "serde", serde(with = "serde_cidr::v4"))]
393 ipv4_cidr: (Ipv4Addr, u8),
394 #[cfg_attr(feature = "serde", serde(with = "serde_cidr::v6"))]
395 ipv6_cidr: (Ipv6Addr, u8),
396 mtu: u16,
397 interface: String,
398 host_loopback: bool,
399}
400
401impl Default for NetStackBuilder {
402 fn default() -> Self {
403 NetStackBuilder {
404 ipv4: true,
405 ipv6: true,
406 ipv4_cidr: (Ipv4Addr::new(10, 0, 2, 0), 24),
407 ipv6_cidr: (Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0), 64),
408 mtu: 1500,
409 interface: "tap0".to_string(),
410 host_loopback: false,
411 }
412 }
413}
414
415impl NetStackBuilder {
416 /// Enables or disables IPv4. Enabled by default.
417 pub fn ipv4(mut self, enabled: bool) -> Self {
418 self.ipv4 = enabled;
419 self
420 }
421
422 /// Enables or disables IPv6. Enabled by default.
423 pub fn ipv6(mut self, enabled: bool) -> Self {
424 self.ipv6 = enabled;
425 self
426 }
427
428 /// Sets the IPv4 network. The default is `10.0.2.0/24`.
429 ///
430 /// `network` must be the network address itself — no bits set past the
431 /// prefix — and the prefix length must be between 1 and 27, leaving room
432 /// for the gateway at host 2 and the guest at host 15. Validated by
433 /// [`build`](Self::build).
434 pub fn ipv4_cidr(mut self, network: Ipv4Addr, prefix_len: u8) -> Self {
435 self.ipv4_cidr = (network, prefix_len);
436 self
437 }
438
439 /// Sets the IPv6 network. The default is `fd00::/64`.
440 ///
441 /// `network` must be the network address itself and the prefix length
442 /// must be between 1 and 120, leaving the address plan the final byte.
443 /// Validated by [`build`](Self::build).
444 pub fn ipv6_cidr(mut self, network: Ipv6Addr, prefix_len: u8) -> Self {
445 self.ipv6_cidr = (network, prefix_len);
446 self
447 }
448
449 /// Sets the tap interface's MTU. The default is 1500.
450 ///
451 /// The minimum is 576, or 1280 while IPv6 is enabled. Validated by
452 /// [`build`](Self::build).
453 pub fn mtu(mut self, mtu: u16) -> Self {
454 self.mtu = mtu;
455 self
456 }
457
458 /// Names the tap interface inside the sandbox. The default is `tap0`.
459 ///
460 /// The name must satisfy the kernel's interface-name rules: 1 to 15
461 /// bytes, not `.` or `..`, and free of `/`, `:`, and whitespace.
462 /// Validated by [`build`](Self::build).
463 pub fn interface(mut self, name: impl Into<String>) -> Self {
464 self.interface = name.into();
465 self
466 }
467
468 /// Maps connections to the gateway address onto host loopback. Off by
469 /// default.
470 ///
471 /// The stack never forwards guest traffic to the host's loopback
472 /// addresses: `127.0.0.0/8` and `::1` destinations are refused
473 /// outright. With this option, a connection to the *gateway* address
474 /// (`10.0.2.2` and `fd00::2` by default) is instead forwarded to
475 /// `127.0.0.1` or `::1` on the host, port preserved — the conventional
476 /// way to reach a host-local service from inside the sandbox, granted
477 /// deliberately rather than by making the whole loopback range
478 /// reachable.
479 pub fn host_loopback(mut self, enabled: bool) -> Self {
480 self.host_loopback = enabled;
481 self
482 }
483
484 /// Validates the configuration and freezes it into a [`NetStack`].
485 pub fn build(self) -> Result<NetStack, NetStackError> {
486 if !self.ipv4 && !self.ipv6 {
487 return Err(NetStackError::NoProtocols);
488 }
489 let v4 = self
490 .ipv4
491 .then(|| {
492 let (network, prefix_len) = self.ipv4_cidr;
493 let invalid = |reason| NetStackError::PrefixInvalid {
494 network: IpAddr::V4(network),
495 prefix_len,
496 reason,
497 };
498 if prefix_len == 0 || prefix_len > MAX_PREFIX_V4 {
499 return Err(invalid("the IPv4 prefix length must be between 1 and 27"));
500 }
501 let base = u32::from(network);
502 if base & !(u32::MAX << (32 - prefix_len)) != 0 {
503 return Err(invalid("the address has bits set past the prefix"));
504 }
505 if network.is_loopback()
506 || network.is_multicast()
507 || network.is_broadcast()
508 || network.is_link_local()
509 || network.octets()[0] == 0
510 {
511 return Err(invalid("the network must be an ordinary unicast range"));
512 }
513 Ok(PlanV4 {
514 guest: Ipv4Addr::from(base | GUEST_HOST_V4),
515 gateway: Ipv4Addr::from(base | GATEWAY_HOST_V4),
516 prefix_len,
517 })
518 })
519 .transpose()?;
520 let v6 = self
521 .ipv6
522 .then(|| {
523 let (network, prefix_len) = self.ipv6_cidr;
524 let invalid = |reason| NetStackError::PrefixInvalid {
525 network: IpAddr::V6(network),
526 prefix_len,
527 reason,
528 };
529 if prefix_len == 0 || prefix_len > MAX_PREFIX_V6 {
530 return Err(invalid("the IPv6 prefix length must be between 1 and 120"));
531 }
532 let base = u128::from(network);
533 if base & !(u128::MAX << (128 - prefix_len)) != 0 {
534 return Err(invalid("the address has bits set past the prefix"));
535 }
536 if base == 0 || network.is_multicast() || (network.segments()[0] & 0xffc0) == 0xfe80
537 {
538 return Err(invalid("the network must be an ordinary unicast range"));
539 }
540 Ok(PlanV6 {
541 guest: Ipv6Addr::from(base | GUEST_HOST_V6),
542 gateway: Ipv6Addr::from(base | GATEWAY_HOST_V6),
543 prefix_len,
544 })
545 })
546 .transpose()?;
547 let minimum = if self.ipv6 { MIN_MTU_V6 } else { MIN_MTU_V4 };
548 if self.mtu < minimum {
549 return Err(NetStackError::MtuInvalid {
550 mtu: self.mtu,
551 minimum,
552 });
553 }
554 if !interface_name_valid(&self.interface) {
555 return Err(NetStackError::InterfaceNameInvalid {
556 name: self.interface,
557 });
558 }
559 Ok(NetStack {
560 plan: Plan {
561 v4,
562 v6,
563 mtu: self.mtu,
564 interface: self.interface,
565 host_loopback: self.host_loopback,
566 },
567 })
568 }
569}
570
571/// Mirrors the kernel's `dev_valid_name`: 1 to 15 bytes (`IFNAMSIZ` minus
572/// the terminator), not `.` or `..`, and free of `/`, `:`, and whitespace.
573/// NUL is additionally rejected so the name embeds cleanly in the
574/// NUL-terminated kernel requests.
575fn interface_name_valid(name: &str) -> bool {
576 !name.is_empty()
577 && name.len() < 16
578 && name != "."
579 && name != ".."
580 && !name
581 .bytes()
582 .any(|byte| byte == b'/' || byte == b':' || byte == 0 || byte.is_ascii_whitespace())
583}
584
585/// An error from building or running a network stack.
586///
587/// Never a [`crate::Error`]: the stack is an attachment to a sandbox, not
588/// part of launching one, and its failures are its own. A failed
589/// [`attach`](NetStack::attach) leaves the pending launch untouched — the
590/// caller may still proceed (without connectivity) or drop it.
591#[derive(Debug)]
592#[non_exhaustive]
593pub enum NetStackError {
594 /// A configured network cannot hold the stack's address plan.
595 #[non_exhaustive]
596 PrefixInvalid {
597 /// The network address as it was given to the builder.
598 network: IpAddr,
599 /// The prefix length as it was given to the builder.
600 prefix_len: u8,
601 /// What makes the pair unusable.
602 reason: &'static str,
603 },
604 /// The MTU is below the minimum for the enabled protocols.
605 #[non_exhaustive]
606 MtuInvalid {
607 /// The MTU as it was given to the builder.
608 mtu: u16,
609 /// The minimum the enabled protocols require.
610 minimum: u16,
611 },
612 /// The interface name does not satisfy the kernel's rules.
613 #[non_exhaustive]
614 InterfaceNameInvalid {
615 /// The name as it was given to the builder.
616 name: String,
617 },
618 /// The configuration enables neither IPv4 nor IPv6.
619 NoProtocols,
620 /// The sandbox shares the caller's network namespace, where a stack
621 /// must not attach. Sandboxes built with
622 /// [`Network::Host`](crate::Network::Host) are refused with this error.
623 SharedNamespace,
624 /// The sandbox's network namespace could not be examined or joined.
625 #[non_exhaustive]
626 NamespaceJoin {
627 /// The step that failed.
628 which: &'static str,
629 /// The underlying error, absent where the failure carried none.
630 source: Option<io::Error>,
631 },
632 /// `/dev/net/tun` could not be opened.
633 ///
634 /// The tun device is compiled into every mainstream kernel but a host
635 /// may omit the module or restrict the device node; the stack cannot
636 /// run there.
637 #[non_exhaustive]
638 TapOpen {
639 /// The underlying error.
640 source: io::Error,
641 },
642 /// The tap interface could not be created.
643 #[non_exhaustive]
644 TapCreate {
645 /// The interface name the creation was asked for.
646 name: String,
647 /// The underlying error.
648 source: io::Error,
649 },
650 /// The tap interface was created but could not be configured.
651 #[non_exhaustive]
652 TapConfigure {
653 /// The configuration step that failed.
654 detail: &'static str,
655 /// The underlying error.
656 source: io::Error,
657 },
658 /// The tap was created and configured, but its descriptor could not be
659 /// handed back to the caller.
660 ///
661 /// Distinct from [`TapConfigure`](NetStackError::TapConfigure): nothing is
662 /// wrong with the interface, only with the handoff, and the tap is
663 /// destroyed with the helper that made it.
664 #[non_exhaustive]
665 TapHandoff {
666 /// The underlying error.
667 source: io::Error,
668 },
669 /// The stack's helper process or pump thread could not be started.
670 #[non_exhaustive]
671 Spawn {
672 /// The underlying error.
673 source: io::Error,
674 },
675 /// The tap helper exited without delivering a result.
676 ///
677 /// The helper always reports an outcome — the tap descriptor or a typed
678 /// failure — before it exits; silence means it was killed from outside
679 /// or its result could not be read.
680 HelperLost,
681 /// The pump stopped on an error of its own machinery, reported by
682 /// [`NetStackHandle::stop`].
683 #[non_exhaustive]
684 Pump {
685 /// The operation the pump failed at.
686 op: &'static str,
687 /// The underlying error, absent where the pump stopped on something
688 /// that was not a syscall failure.
689 source: Option<io::Error>,
690 },
691}
692
693impl fmt::Display for NetStackError {
694 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695 match self {
696 NetStackError::PrefixInvalid {
697 network,
698 prefix_len,
699 reason,
700 } => write!(
701 f,
702 "the network {network}/{prefix_len} is unusable: {reason}"
703 ),
704 NetStackError::MtuInvalid { mtu, minimum } => write!(
705 f,
706 "the MTU {mtu} is below {minimum}, the minimum for the enabled protocols",
707 ),
708 NetStackError::InterfaceNameInvalid { name } => {
709 write!(f, "{name:?} is not a valid interface name")
710 }
711 NetStackError::NoProtocols => {
712 f.write_str("the configuration enables neither IPv4 nor IPv6")
713 }
714 NetStackError::SharedNamespace => f.write_str(
715 "the sandbox shares the caller's network namespace; a network stack \
716 attaches only to a private one",
717 ),
718 NetStackError::NamespaceJoin {
719 which,
720 source: None,
721 } => write!(
722 f,
723 "could not join the sandbox's network namespace while {which}"
724 ),
725 NetStackError::NamespaceJoin {
726 which,
727 source: Some(source),
728 } => write!(
729 f,
730 "could not join the sandbox's network namespace while {which}: {source}"
731 ),
732 NetStackError::TapOpen { source } => {
733 write!(f, "could not open /dev/net/tun: {source}")
734 }
735 NetStackError::TapCreate { name, source } => {
736 write!(f, "could not create the tap interface {name}: {source}")
737 }
738 NetStackError::TapConfigure { detail, source } => write!(
739 f,
740 "could not configure the tap interface while {detail}: {source}"
741 ),
742 NetStackError::TapHandoff { source } => write!(
743 f,
744 "could not send the tap descriptor back from the network stack's helper: {source}"
745 ),
746 NetStackError::Spawn { source } => write!(
747 f,
748 "could not start the network stack's helper or pump: {source}"
749 ),
750 NetStackError::HelperLost => {
751 f.write_str("the network stack's tap helper exited without delivering a result")
752 }
753 NetStackError::Pump { op, source: None } => {
754 write!(f, "the network stack pump failed while {op}")
755 }
756 NetStackError::Pump {
757 op,
758 source: Some(source),
759 } => write!(f, "the network stack pump failed while {op}: {source}"),
760 }
761 }
762}
763
764impl std::error::Error for NetStackError {
765 /// The OS failure underneath, where there is one.
766 ///
767 /// The configuration refusals — an unusable prefix, an MTU below the
768 /// minimum, an invalid interface name, no protocols, a shared namespace —
769 /// carry no inner error: nothing failed, the request was refused.
770 /// [`HelperLost`](Self::HelperLost) carries none either: the helper's
771 /// silence is the failure.
772 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
773 match self {
774 NetStackError::NamespaceJoin { source, .. } | NetStackError::Pump { source, .. } => {
775 source
776 .as_ref()
777 .map(|source| source as &(dyn std::error::Error + 'static))
778 }
779 NetStackError::TapOpen { source }
780 | NetStackError::TapCreate { source, .. }
781 | NetStackError::TapConfigure { source, .. }
782 | NetStackError::TapHandoff { source }
783 | NetStackError::Spawn { source } => Some(source),
784 _ => None,
785 }
786 }
787}
788
789/// Serde adapters presenting the CIDR fields as `"address/len"` strings, the
790/// reviewable form for profile files.
791#[cfg(feature = "serde")]
792mod serde_cidr {
793 use std::fmt;
794 use std::marker::PhantomData;
795 use std::str::FromStr;
796
797 use serde::de::{self, Visitor};
798 use serde::{Deserializer, Serializer};
799
800 /// Serializes an `(address, prefix_len)` pair as `"address/len"`.
801 fn serialize_pair<A: fmt::Display, S: Serializer>(
802 (address, len): &(A, u8),
803 serializer: S,
804 ) -> Result<S::Ok, S::Error> {
805 serializer.serialize_str(&format!("{address}/{len}"))
806 }
807
808 /// Deserializes `"address/len"` into an `(address, prefix_len)` pair.
809 fn deserialize_pair<'de, A, D>(
810 deserializer: D,
811 family: &'static str,
812 ) -> Result<(A, u8), D::Error>
813 where
814 A: FromStr,
815 D: Deserializer<'de>,
816 {
817 struct CidrVisitor<A> {
818 family: &'static str,
819 marker: PhantomData<A>,
820 }
821
822 impl<A: FromStr> Visitor<'_> for CidrVisitor<A> {
823 type Value = (A, u8);
824
825 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
826 write!(f, "an {} network in \"address/len\" form", self.family)
827 }
828
829 fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
830 let (address, len) = value
831 .split_once('/')
832 .ok_or_else(|| E::custom(format!("{value:?} has no '/' separator")))?;
833 let address = address.parse().map_err(|_| {
834 E::custom(format!("{address:?} is not an {} address", self.family))
835 })?;
836 let len = len
837 .parse()
838 .map_err(|_| E::custom(format!("{len:?} is not a prefix length")))?;
839 Ok((address, len))
840 }
841 }
842
843 deserializer.deserialize_str(CidrVisitor {
844 family,
845 marker: PhantomData,
846 })
847 }
848
849 pub(super) mod v4 {
850 use std::net::Ipv4Addr;
851
852 use serde::{Deserializer, Serializer};
853
854 pub(crate) fn serialize<S: Serializer>(
855 pair: &(Ipv4Addr, u8),
856 serializer: S,
857 ) -> Result<S::Ok, S::Error> {
858 super::serialize_pair(pair, serializer)
859 }
860
861 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
862 deserializer: D,
863 ) -> Result<(Ipv4Addr, u8), D::Error> {
864 super::deserialize_pair(deserializer, "IPv4")
865 }
866 }
867
868 pub(super) mod v6 {
869 use std::net::Ipv6Addr;
870
871 use serde::{Deserializer, Serializer};
872
873 pub(crate) fn serialize<S: Serializer>(
874 pair: &(Ipv6Addr, u8),
875 serializer: S,
876 ) -> Result<S::Ok, S::Error> {
877 super::serialize_pair(pair, serializer)
878 }
879
880 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
881 deserializer: D,
882 ) -> Result<(Ipv6Addr, u8), D::Error> {
883 super::deserialize_pair(deserializer, "IPv6")
884 }
885 }
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 #[test]
893 fn the_default_configuration_builds() {
894 let stack = NetStack::builder().build().expect("the defaults are valid");
895 let v4 = stack.plan.v4.expect("IPv4 is on by default");
896 assert_eq!(v4.guest, Ipv4Addr::new(10, 0, 2, 15));
897 assert_eq!(v4.gateway, Ipv4Addr::new(10, 0, 2, 2));
898 assert_eq!(v4.prefix_len, 24);
899 let v6 = stack.plan.v6.expect("IPv6 is on by default");
900 assert_eq!(v6.guest, "fd00::15".parse::<Ipv6Addr>().unwrap());
901 assert_eq!(v6.gateway, "fd00::2".parse::<Ipv6Addr>().unwrap());
902 assert_eq!(v6.prefix_len, 64);
903 assert_eq!(stack.plan.mtu, 1500);
904 assert_eq!(stack.plan.interface, "tap0");
905 }
906
907 #[test]
908 fn a_custom_network_places_the_plan_within_it() {
909 let stack = NetStack::builder()
910 .ipv4_cidr(Ipv4Addr::new(192, 168, 100, 0), 24)
911 .ipv6_cidr("fdab:cd::".parse().unwrap(), 64)
912 .build()
913 .expect("a valid custom network");
914 let v4 = stack.plan.v4.expect("IPv4 is on");
915 assert_eq!(v4.guest, Ipv4Addr::new(192, 168, 100, 15));
916 assert_eq!(v4.gateway, Ipv4Addr::new(192, 168, 100, 2));
917 let v6 = stack.plan.v6.expect("IPv6 is on");
918 assert_eq!(v6.guest, "fdab:cd::15".parse::<Ipv6Addr>().unwrap());
919 assert_eq!(v6.gateway, "fdab:cd::2".parse::<Ipv6Addr>().unwrap());
920 }
921
922 #[test]
923 fn disabling_both_protocols_is_refused() {
924 let err = NetStack::builder()
925 .ipv4(false)
926 .ipv6(false)
927 .build()
928 .expect_err("no protocols is invalid");
929 assert!(matches!(err, NetStackError::NoProtocols), "{err}");
930 }
931
932 #[test]
933 fn a_prefix_too_long_for_the_address_plan_is_refused() {
934 let err = NetStack::builder()
935 .ipv4_cidr(Ipv4Addr::new(10, 0, 2, 0), 28)
936 .build()
937 .expect_err("/28 cannot hold host 15 below broadcast");
938 assert!(matches!(err, NetStackError::PrefixInvalid { .. }), "{err}");
939
940 let err = NetStack::builder()
941 .ipv6_cidr("fd00::".parse().unwrap(), 121)
942 .build()
943 .expect_err("/121 does not leave the final byte");
944 assert!(matches!(err, NetStackError::PrefixInvalid { .. }), "{err}");
945 }
946
947 #[test]
948 fn host_bits_past_the_prefix_are_refused() {
949 let err = NetStack::builder()
950 .ipv4_cidr(Ipv4Addr::new(10, 0, 2, 5), 24)
951 .build()
952 .expect_err("10.0.2.5 is not a /24 network address");
953 assert!(matches!(err, NetStackError::PrefixInvalid { .. }), "{err}");
954
955 let err = NetStack::builder()
956 .ipv6_cidr("fd00::1".parse().unwrap(), 64)
957 .build()
958 .expect_err("fd00::1 is not a /64 network address");
959 assert!(matches!(err, NetStackError::PrefixInvalid { .. }), "{err}");
960 }
961
962 #[test]
963 fn the_mtu_floor_follows_the_enabled_protocols() {
964 let err = NetStack::builder()
965 .mtu(1279)
966 .build()
967 .expect_err("1279 is below the IPv6 minimum");
968 assert!(
969 matches!(err, NetStackError::MtuInvalid { minimum: 1280, .. }),
970 "{err}"
971 );
972
973 NetStack::builder()
974 .ipv6(false)
975 .mtu(576)
976 .build()
977 .expect("576 suffices for IPv4 alone");
978
979 let err = NetStack::builder()
980 .ipv6(false)
981 .mtu(575)
982 .build()
983 .expect_err("575 is below the IPv4 minimum");
984 assert!(
985 matches!(err, NetStackError::MtuInvalid { minimum: 576, .. }),
986 "{err}"
987 );
988 }
989
990 #[test]
991 fn interface_names_follow_the_kernel_rules() {
992 for name in ["", ".", "..", "a/b", "a:b", "a b", "sixteen-byte-name"] {
993 let err = NetStack::builder()
994 .interface(name)
995 .build()
996 .expect_err("an invalid interface name is refused");
997 assert!(
998 matches!(err, NetStackError::InterfaceNameInvalid { .. }),
999 "{name:?}: {err}"
1000 );
1001 }
1002 NetStack::builder()
1003 .interface("fifteen-byte-nm")
1004 .build()
1005 .expect("a 15-byte name is the longest valid one");
1006 }
1007
1008 #[cfg(feature = "serde")]
1009 #[test]
1010 fn the_builder_round_trips_through_serde() {
1011 let toml = "ipv4-cidr = \"192.168.7.0/24\"\nipv6 = false\ninterface = \"net0\"\n";
1012 let builder: NetStackBuilder = toml::from_str(toml).expect("the profile parses");
1013 assert_eq!(builder.ipv4_cidr, (Ipv4Addr::new(192, 168, 7, 0), 24));
1014 assert!(!builder.ipv6);
1015 assert_eq!(builder.interface, "net0");
1016 assert_eq!(builder.mtu, 1500, "unset keys keep their defaults");
1017
1018 let serialized = toml::to_string(&builder).expect("the builder serializes");
1019 let reparsed: NetStackBuilder = toml::from_str(&serialized).expect("and round-trips");
1020 assert_eq!(reparsed.ipv4_cidr, builder.ipv4_cidr);
1021 assert_eq!(reparsed.ipv6_cidr, builder.ipv6_cidr);
1022
1023 let err = toml::from_str::<NetStackBuilder>("ipv4-cidr = \"10.0.2.0\"")
1024 .expect_err("a CIDR without a length is refused");
1025 assert!(err.to_string().contains("separator"), "{err}");
1026
1027 let err = toml::from_str::<NetStackBuilder>("gateway = \"10.0.2.1\"")
1028 .expect_err("unknown keys are refused");
1029 assert!(err.to_string().contains("gateway"), "{err}");
1030 }
1031}