Skip to main content

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}