Skip to main content

capsule_runtime/
network_namespace.rs

1//! The capsule's own network namespace: the structural half of the mechanism that replaced the
2//! seccomp-notify `connect`/`sendto` supervisor.
3//!
4//! [`crate::egress_proxy`] decides *which* destinations a native subprocess may reach. This module
5//! is what makes that decision the only way out — it puts the subprocess tree somewhere with no
6//! route off the host, so an unlisted destination is not "denied" by a filter that has to be
7//! consulted, it is simply unreachable.
8//!
9//! ## One namespace per session
10//!
11//! A session has exactly one network namespace, [`SessionNetns`], created the first time one of
12//! its commands is spawned and shared by every command after that. A server one shell call starts
13//! on `127.0.0.1:8080` is therefore reachable from the session's next call, and from nothing
14//! outside the session: two sessions are two `mur` processes, each with its own `SessionNetns`,
15//! and no interface hands one across a process boundary. A plan's shell steps run in the
16//! namespace of the session that submitted the plan.
17//!
18//! The namespace is built once by a dedicated *creator* child, forked for nothing else, which
19//! hands the parent the namespace's listening sockets and a descriptor for each of its two
20//! namespaces in one `SCM_RIGHTS` message and then exits. The namespace outlives the creator
21//! because the runtime holds those descriptors. Every command then enters it with
22//! [`join_session_netns`] — `setns(2)` into the user namespace, then into the network namespace —
23//! in its own `pre_exec` window, where it is single-threaded and still unfiltered. The runtime's
24//! own threads never call `setns` or `unshare`. The namespace is released when the session ends:
25//! [`SessionNetns::end`] stops the session's egress proxy and closes the descriptors, after
26//! `ShellEnforcement::end_session` has killed every process the session started.
27//!
28//! ## Whether a host can create one
29//!
30//! Answered by creating one: [`crate::namespace_probe`] runs the same creator a session does, with
31//! an empty allowlist, and drops what it hands over. A staged session's staging and launch read
32//! the answer off that session's one [`crate::sandbox::HostProbe`], and a session's first command
33//! gets the same refusal from its own creation ([`SessionNetns::establish`]). AppArmor's
34//! unprivileged-userns restriction and the profile confining `mur` are read only to explain a
35//! failure: a container whose own profile permits the namespace is not refused because the host's
36//! restriction reads `1`.
37//!
38//! ## The syscall sequence
39//!
40//! Everything happens in the creator child, before any capsule code runs, and the join happens in
41//! each command's forked child before the seccomp filter (which denies `unshare`, `setns` and
42//! `socket(AF_NETLINK)` outright) and before [`crate::sealed`]'s composed root:
43//!
44//!   1. `unshare(CLONE_NEWUSER | CLONE_NEWNET)`, then the identity `uid_map`/`gid_map` writes that
45//!      make the child the owner of both. The user namespace is not incidental: `CLONE_NEWNET`
46//!      alone needs `CAP_NET_ADMIN`, which an unprivileged process does not have, and creating it
47//!      inside a user namespace it owns is what supplies that capability *there and nowhere else*.
48//!      This is the same primitive `sealed` uses, and AppArmor's unprivileged-userns
49//!      restriction can refuse it the same way.
50//!   2. `lo` is brought up ([`bring_loopback_up`]). It is the namespace's only interface: there is
51//!      no veth pair, no bridge and no NAT, so nothing in the namespace has a path to the host's
52//!      network or to the outside world.
53//!   3. One route is installed over netlink ([`add_local_default_route`]): `local default dev lo`.
54//!      A `local`-type default route makes every address *locally deliverable*, so a `connect()`
55//!      to any address is looped back to a socket in this namespace with the original destination
56//!      intact — recoverable with `getsockname(2)`. That is the entire interception mechanism,
57//!      and it is why no netfilter rule is needed anywhere. It also means a lookup reaches the
58//!      resolver whatever address `/etc/resolv.conf` names, and a connection to a port nothing is
59//!      bound to is refused immediately rather than stalling through the kernel's full SYN-retry
60//!      schedule against a blackhole.
61//!   4. The listening sockets are created here, *inside* the namespace — one TCP listener per
62//!      port the allowlist implies, then one wildcard UDP :53 resolver socket — and handed back
63//!      to the parent in a single `SCM_RIGHTS` message, followed by the `/proc/self/ns/user` and
64//!      `/proc/self/ns/net` descriptors later commands join through. A socket belongs to the
65//!      namespace it was **created** in, not the one it is used from, which is the whole reason
66//!      this inversion works: the runtime serves them while staying in the host's namespace, with
67//!      real internet access for the upstream half. The listeners are held for the whole session,
68//!      so a server a command starts on one of those ports gets `EADDRINUSE`.
69//!
70//! ## Why this shape rather than the obvious ones
71//!
72//! *No veth pair.* Wiring a namespace to the host with veth requires `CAP_NET_ADMIN` in the
73//! **host's** network namespace, which means real root. This design needs no host privilege at
74//! all, which is what lets `mur` install per-user.
75//!
76//! *No proxy process inside the namespace, and no holder process.* A multithreaded process cannot
77//! enter a user namespace: `setns(user_fd, CLONE_NEWUSER)` from the runtime fails (`EINVAL`, or
78//! `EPERM` from one of its threads). A freshly forked child is single-threaded, and the euid that
79//! owns the session's user namespace holds every capability in it, so the same `setns` from a
80//! command's `pre_exec` window succeeds, followed by `setns(net_fd, CLONE_NEWNET)`. Passing the
81//! listening sockets *out* is what lets the proxy serve the namespace from the host's side with no
82//! privilege at all.
83//!
84//! A long-lived single-threaded holder that forked each command on request was the alternative.
85//! It would make every command its child rather than the runtime's, so every `Child` the shell
86//! path keys on would need a proxy layer, and it would re-implement `std::process::Command`'s spawn
87//! over a request protocol. Joining in `pre_exec` changes one step of the existing spawn instead.
88//!
89//! *No `iptables`/`nft`/`TPROXY`.* Nothing here needs a netfilter rule, so there is no dependency
90//! on netfilter modules being present, no `iptables` binary to shell out to, and no rule set that
91//! can drift out of step with the manifest.
92//!
93//! ## What this does not isolate
94//!
95//! `AF_UNIX` is untouched. `CLONE_NEWNET` does not mediate unix sockets in any way that matters
96//! here — a pathname socket is reached through the filesystem, not the network stack. That is
97//! exactly why the register-level `socket(2)` domain filter (`denied_socket_domains` in
98//! [`crate::sandbox`]) is untouched here and remains the thing that refuses
99//! `/var/run/docker.sock`.
100
101// The creation steps and the namespace plan stay defined everywhere so the decisions over them are
102// unit-tested on every OS, but outside tests only Linux code builds or reads them, so off Linux
103// they are dead by construction.
104#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
105
106/// One TCP listener per allowlisted port, plus the single wildcard UDP :53 resolver socket.
107pub(crate) const MAX_NAMESPACE_SOCKETS: usize = crate::egress_proxy::MAX_EGRESS_TCP_PORTS + 1;
108
109/// Every descriptor the creator child sends: the namespace sockets, then the user and network
110/// namespace handles.
111///
112/// Bounds the one `SCM_RIGHTS` message, and therefore the fixed stack buffers the creator builds
113/// it in.
114pub(crate) const MAX_SESSION_NETNS_FDS: usize = MAX_NAMESPACE_SOCKETS + 2;
115
116// ---------------------------------------------------------------- the creation's steps
117
118/// One step of the namespace's construction, as the creator reports it when that step fails.
119///
120/// Defined outside the Linux module so [`egress_namespace_failure`], the decision over a failed
121/// step, compiles and is tested on every OS.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum CreationStep {
124    Unshare = 1,
125    IdentityMaps = 2,
126    Loopback = 3,
127    LocalRoute = 4,
128    TcpListener = 5,
129    Resolver = 6,
130    NamespaceHandles = 7,
131    HandOff = 8,
132}
133
134impl CreationStep {
135    const ALL: [CreationStep; 8] = [
136        Self::Unshare,
137        Self::IdentityMaps,
138        Self::Loopback,
139        Self::LocalRoute,
140        Self::TcpListener,
141        Self::Resolver,
142        Self::NamespaceHandles,
143        Self::HandOff,
144    ];
145
146    /// The step a failure record's code names, or `None` for a code no step carries.
147    pub(crate) fn from_code(code: u32) -> Option<Self> {
148        Self::ALL.into_iter().find(|step| *step as u32 == code)
149    }
150
151    /// The call or operation, as a refusal names it.
152    #[must_use]
153    pub fn describe(self) -> &'static str {
154        match self {
155            Self::Unshare => "unshare(CLONE_NEWUSER | CLONE_NEWNET)",
156            Self::IdentityMaps => "writing uid_map/gid_map",
157            Self::Loopback => "bringing lo up",
158            Self::LocalRoute => "installing the local default route",
159            Self::TcpListener => "binding a TCP listener inside the namespace",
160            Self::Resolver => "binding UDP 53 inside the namespace",
161            Self::NamespaceHandles => "opening /proc/self/ns/user and /proc/self/ns/net",
162            Self::HandOff => "handing the namespace to the runtime",
163        }
164    }
165}
166
167/// Why an attempt to create a session's network namespace produced no namespace.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub enum NamespaceAttemptError {
170    /// The creator ran, and `step` failed with `errno`.
171    Step { step: CreationStep, errno: i32 },
172    /// The creator could not run, or exited without handing anything over; the runtime's own
173    /// account of it (`fork() for the namespace creator failed: …`).
174    Creator(String),
175}
176
177// ---------------------------------------------------------------- the failure
178
179/// Which remediation an [`EgressNamespaceFailure`] calls for.
180///
181/// Not an `EnforcementTier` variant and not tied to `ContainmentClass`: a network
182/// namespace is required for every Linux capsule that can spawn a subprocess, at every
183/// containment class, so hanging it off the class ladder would make `advisory` capsules silently
184/// exempt from this boundary.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
186#[serde(rename_all = "snake_case")]
187pub enum EgressNamespaceBlocker {
188    /// The kernel implements unprivileged user namespaces and something on this host refused one
189    /// to `mur`: AppArmor's unprivileged-userns restriction, another AppArmor profile confining
190    /// `mur`, a container runtime's seccomp filter or capabilities, or another security module.
191    /// The refusal came at `unshare`, or after it while the namespace was owned or configured.
192    CapabilityGrantMissing,
193    /// The kernel does not provide the mechanism: `unshare` failed with `ENOSPC`
194    /// (`user.max_user_namespaces`), `EINVAL` or `ENOSYS` (`CONFIG_USER_NS=n`), or `EUSERS` (the
195    /// nesting limit).
196    KernelSupportMissing,
197    /// The runtime could not run the namespace creator, or the creator stopped without reporting
198    /// a step — a process or descriptor limit, memory, or a policy that killed it — so the host's
199    /// answer is unknown. Refused like the other two: a namespace nobody could create is a
200    /// namespace the capsule does not get.
201    CreatorUnavailable,
202}
203
204/// A creation of the capsule network namespace that failed, with what its refusal names.
205///
206/// Every refusal of `E-CAP-005` — at staging, at launch, at a session's first command, in
207/// `mur doctor` — renders [`Self::reason`], so the places that ask cannot explain one host two
208/// ways.
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct EgressNamespaceFailure {
211    /// Decided by the failed call and its `errno` alone; the readings below never change it.
212    pub blocker: EgressNamespaceBlocker,
213    /// The call that failed, in [`CreationStep::describe`]'s words (`unshare(CLONE_NEWUSER |
214    /// CLONE_NEWNET)`, `writing uid_map/gid_map`, …), or, for
215    /// [`EgressNamespaceBlocker::CreatorUnavailable`], the runtime's account of why the creator
216    /// could not run.
217    pub failed_call: String,
218    /// The `errno` the failed call left. `None` exactly for
219    /// [`EgressNamespaceBlocker::CreatorUnavailable`], where no creation step ran.
220    pub errno: Option<i32>,
221    /// AppArmor's unprivileged-userns restriction as this process read it when the failure was
222    /// observed. Explains the failure; never decides whether there is one.
223    pub userns_grant: crate::sealed::UsernsGrant,
224    /// The profile the kernel reports as confining this process, read at the same moment.
225    pub attachment: crate::sealed::ProfileAttachment,
226}
227
228impl EgressNamespaceFailure {
229    /// `<failed call> failed with <ERRNO>`, or the creator's account: the one-line answer
230    /// `mur doctor` prints.
231    #[must_use]
232    pub fn summary(&self) -> String {
233        match self.errno {
234            Some(errno) => format!("{} failed with {}", self.failed_call, errno_label(errno)),
235            None => self.failed_call.clone(),
236        }
237    }
238
239    /// The text an operator sees under `E-CAP-005`: what failed, with its `errno` symbolically
240    /// and in words; the likely cause and its remedy; and, last, the host readings the cause was
241    /// chosen from.
242    #[must_use]
243    pub fn reason(&self) -> String {
244        let what = match self.errno {
245            Some(errno) => format!(
246                "{} failed with {} ({})",
247                self.failed_call,
248                errno_label(errno),
249                errno_words(errno)
250            ),
251            None => self.failed_call.clone(),
252        };
253        format!("{what}. {} {}", self.cause(), self.readings())
254    }
255
256    /// The step `failed_call` names, when it names one.
257    fn step(&self) -> Option<CreationStep> {
258        self.errno?;
259        CreationStep::ALL
260            .into_iter()
261            .find(|step| step.describe() == self.failed_call)
262    }
263
264    fn cause(&self) -> String {
265        match self.blocker {
266            EgressNamespaceBlocker::KernelSupportMissing => kernel_cause(self.errno).to_string(),
267            EgressNamespaceBlocker::CreatorUnavailable => {
268                "Likely cause: the namespace creator could not run or stopped before reporting — \
269                 a process or descriptor limit, or memory, ran out, or a policy killed it (its \
270                 account then reads `killed by signal`) — so whether this host can create \
271                 the namespace is unknown, and the capsule is refused rather than run without \
272                 it. Re-run once the limit is relieved, or allow the creator's syscalls."
273                    .to_string()
274            }
275            EgressNamespaceBlocker::CapabilityGrantMissing => {
276                let cause = grant_cause(self.userns_grant, &self.attachment);
277                match self.step().and_then(step_context) {
278                    Some(context) => format!("{context} {cause}"),
279                    None => cause,
280                }
281            }
282        }
283    }
284
285    fn readings(&self) -> String {
286        host_readings(self.userns_grant, &self.attachment)
287    }
288}
289
290/// The readings line every namespace refusal ends with — `E-CAP-005`'s, and `E-CAP-003`'s for
291/// `sealed`. AppArmor's restriction knob reads `1` for both grants decided with it on, and `0` for
292/// the one decided with it off.
293pub(crate) fn host_readings(
294    userns_grant: crate::sealed::UsernsGrant,
295    attachment: &crate::sealed::ProfileAttachment,
296) -> String {
297    use crate::sealed::{ProfileAttachment, UsernsGrant};
298    let restriction = match userns_grant {
299        UsernsGrant::ApparmorAbsent => "AppArmor not enabled",
300        UsernsGrant::RestrictionDisabledHostWide => {
301            "kernel.apparmor_restrict_unprivileged_userns=0"
302        }
303        UsernsGrant::ProfileConfining | UsernsGrant::Withheld => {
304            "kernel.apparmor_restrict_unprivileged_userns=1"
305        }
306    };
307    let profile = match attachment {
308        ProfileAttachment::Sealed { profile } | ProfileAttachment::Other { profile } => {
309            profile.as_str()
310        }
311        ProfileAttachment::Unconfined => "unconfined",
312        ProfileAttachment::NotReported => "not reported",
313    };
314    format!("Host readings: {restriction}; AppArmor profile confining mur: {profile}.")
315}
316
317/// What a step after `unshare` failing says about the namespace, ahead of the cause.
318fn step_context(step: CreationStep) -> Option<&'static str> {
319    match step {
320        CreationStep::Unshare => None,
321        CreationStep::IdentityMaps => Some(
322            "The namespace was created and could not be owned: writing its identity map was \
323             refused.",
324        ),
325        CreationStep::Loopback | CreationStep::LocalRoute => Some(
326            "The namespace was created and owned, and configuring it was refused — the mark of a \
327             confinement that withholds CAP_NET_ADMIN inside the namespace.",
328        ),
329        CreationStep::TcpListener
330        | CreationStep::Resolver
331        | CreationStep::NamespaceHandles
332        | CreationStep::HandOff => {
333            Some("The namespace was created and configured, and handing it to the runtime failed.")
334        }
335    }
336}
337
338/// The likely cause of a refused namespace on a kernel that provides one, chosen from the
339/// readings.
340///
341/// AppArmor absent comes first because `/proc/self/attr/current` then reports another security
342/// module's label, which is no AppArmor profile. A process another profile confines is next: the
343/// host's restriction knob does not govern it, whatever it reads.
344fn grant_cause(
345    userns_grant: crate::sealed::UsernsGrant,
346    attachment: &crate::sealed::ProfileAttachment,
347) -> String {
348    use crate::sealed::{
349        ProfileAttachment, UsernsGrant, SEALED_APPARMOR_PROFILE_NAME, SEALED_APPARMOR_PROFILE_PATH,
350    };
351    const CONTAINER_REMEDY: &str = "Inside a container, the container runtime's seccomp \
352         profile is the usual blocker: it refuses unshare(CLONE_NEWUSER) and setns whatever \
353         capabilities the container holds. Run the container with a seccomp profile that permits \
354         them (Docker: `--security-opt seccomp=<file>`; Kubernetes: `seccompProfile: \
355         Localhost`), or create the network namespace outside the container and run mur inside \
356         it.";
357    match (userns_grant, attachment) {
358        (UsernsGrant::ApparmorAbsent, _) => format!(
359            "Likely cause: not AppArmor, which is not enabled on this host — a container \
360             runtime's seccomp profile, or another security module's policy. \
361             {CONTAINER_REMEDY} On a bare host, check the policy of the security module in use."
362        ),
363        (_, ProfileAttachment::Other { profile }) => format!(
364            "Likely cause: the AppArmor profile `{profile}` confines mur. \
365             kernel.apparmor_restrict_unprivileged_userns does not govern a process another \
366             profile confines: that profile, and a container runtime's seccomp profile, decide \
367             whether it may create a user namespace. Run the container with an AppArmor profile \
368             and a seccomp profile that permit unshare(CLONE_NEWUSER) and setns (Docker: \
369             `--security-opt apparmor=<profile> --security-opt seccomp=<file>`; Kubernetes: \
370             `appArmorProfile` and `seccompProfile: Localhost`), or create the network namespace \
371             outside the container and run mur inside it."
372        ),
373        (UsernsGrant::ProfileConfining, _) | (_, ProfileAttachment::Sealed { .. }) => format!(
374            "Likely cause: the {SEALED_APPARMOR_PROFILE_NAME} AppArmor profile confines mur and \
375             the creation still failed, so the profile the kernel has loaded may be stale. Reload \
376             it with `sudo apparmor_parser -r {SEALED_APPARMOR_PROFILE_PATH}` (for a checkout \
377             build, re-run `scripts/install-dev-apparmor.sh`), and check `mur doctor`'s \
378             comparison of the installed profile with the one this build ships."
379        ),
380        (UsernsGrant::RestrictionDisabledHostWide, _) => format!(
381            "Likely cause: not AppArmor, whose unprivileged-userns restriction is off on this host \
382             — a container runtime's seccomp profile, or another security module's policy. \
383             {CONTAINER_REMEDY}"
384        ),
385        (UsernsGrant::Withheld, _) => format!(
386            "Likely cause: AppArmor's unprivileged-userns restriction \
387             (kernel.apparmor_restrict_unprivileged_userns=1) applies to this binary, which no \
388             {SEALED_APPARMOR_PROFILE_NAME} profile confines. Install and load the profile shipped \
389             with mur, `sudo install -m 644 packaging/apparmor/{SEALED_APPARMOR_PROFILE_NAME} \
390             {SEALED_APPARMOR_PROFILE_PATH} && sudo apparmor_parser -r \
391             {SEALED_APPARMOR_PROFILE_PATH}` (or re-run the mur installer as root), then re-run. \
392             Building out of a checkout, where the binary sits at ./target/{{debug,release}}/mur \
393             and no shipped profile attaches to it: run `scripts/install-dev-apparmor.sh`, which \
394             generates and loads the same grant for those two paths. LAST RESORT, only where a \
395             profile genuinely cannot be loaded: `sudo sysctl -w \
396             kernel.apparmor_restrict_unprivileged_userns=0` — this removes unprivileged-userns \
397             hardening from every program on the machine, not just from mur, and is not the \
398             configuration murmur ships."
399        ),
400    }
401}
402
403/// The likely cause of an `unshare` the kernel could not honour, by its `errno`.
404fn kernel_cause(errno: Option<i32>) -> &'static str {
405    match errno {
406        Some(libc::ENOSPC) => {
407            "Likely cause: the kernel's limit on user namespaces — user.max_user_namespaces is 0, \
408             or every namespace it allows is in use. Raise it where it is set, on the host or in \
409             the user namespace mur runs in (`sudo sysctl -w user.max_user_namespaces=10000`), \
410             then re-run."
411        }
412        Some(libc::EUSERS) => {
413            "Likely cause: the kernel's nesting limit for user namespaces — mur already runs \
414             inside as many nested user namespaces as the kernel allows. Run it fewer levels \
415             deep."
416        }
417        _ => {
418            "Likely cause: this kernel does not provide user namespaces (built with \
419             CONFIG_USER_NS=n). Run mur on a kernel built with CONFIG_USER_NS=y."
420        }
421    }
422}
423
424/// The symbolic name of the `errno` values a namespace creation can leave.
425pub(crate) fn errno_name(errno: i32) -> Option<&'static str> {
426    Some(match errno {
427        libc::EPERM => "EPERM",
428        libc::ENOENT => "ENOENT",
429        libc::EACCES => "EACCES",
430        libc::EINVAL => "EINVAL",
431        libc::ENOSPC => "ENOSPC",
432        libc::ENOSYS => "ENOSYS",
433        libc::EUSERS => "EUSERS",
434        libc::ENOMEM => "ENOMEM",
435        libc::EAGAIN => "EAGAIN",
436        libc::EADDRINUSE => "EADDRINUSE",
437        libc::EMFILE => "EMFILE",
438        libc::ENFILE => "ENFILE",
439        libc::EROFS => "EROFS",
440        libc::ENOTDIR => "ENOTDIR",
441        libc::EBUSY => "EBUSY",
442        _ => return None,
443    })
444}
445
446/// [`errno_name`], or `errno <n>` for a value outside it.
447pub(crate) fn errno_label(errno: i32) -> String {
448    errno_name(errno).map_or_else(|| format!("errno {errno}"), str::to_string)
449}
450
451/// The C library's description of `errno`, without the `(os error <n>)` suffix `std` appends.
452pub(crate) fn errno_words(errno: i32) -> String {
453    let rendered = std::io::Error::from_raw_os_error(errno).to_string();
454    rendered
455        .strip_suffix(&format!(" (os error {errno})"))
456        .map_or_else(|| rendered.clone(), str::to_string)
457}
458
459/// Whether a namespace attempt is an `E-CAP-005` failure, and which. Pure, and unit-testable on
460/// any OS.
461///
462/// The attempt alone decides: `None` exactly when it succeeded. The readings only choose the
463/// words of the refusal.
464pub(crate) fn egress_namespace_failure(
465    is_linux: bool,
466    attempt: Result<(), NamespaceAttemptError>,
467    userns_grant: crate::sealed::UsernsGrant,
468    attachment: crate::sealed::ProfileAttachment,
469) -> Option<EgressNamespaceFailure> {
470    // Not Linux: there is no network namespace here. A non-Linux host resolves to
471    // `EnforcementTier::EnvironmentOnly`, installs no kernel subprocess sandbox at all, and says
472    // so loudly (`W_SEC_001`). Refusing here would turn every macOS run into an error about a
473    // mechanism macOS has never had.
474    if !is_linux {
475        return None;
476    }
477    attempt
478        .err()
479        .map(|error| failure_of(error, userns_grant, attachment))
480}
481
482/// The failure a failed attempt is, classified by its step and `errno`.
483fn failure_of(
484    error: NamespaceAttemptError,
485    userns_grant: crate::sealed::UsernsGrant,
486    attachment: crate::sealed::ProfileAttachment,
487) -> EgressNamespaceFailure {
488    let (blocker, failed_call, errno) = match error {
489        NamespaceAttemptError::Step { step, errno } => {
490            let blocker = match (step, errno) {
491                (
492                    CreationStep::Unshare,
493                    libc::ENOSPC | libc::EINVAL | libc::ENOSYS | libc::EUSERS,
494                ) => EgressNamespaceBlocker::KernelSupportMissing,
495                _ => EgressNamespaceBlocker::CapabilityGrantMissing,
496            };
497            (blocker, step.describe().to_string(), Some(errno))
498        }
499        NamespaceAttemptError::Creator(detail) => {
500            (EgressNamespaceBlocker::CreatorUnavailable, detail, None)
501        }
502    };
503    EgressNamespaceFailure {
504        blocker,
505        failed_call,
506        errno,
507        userns_grant,
508        attachment,
509    }
510}
511
512/// Whether this host can give a capsule its own network namespace, answered by creating one.
513///
514/// A fresh [`crate::sandbox::HostProbe`] each call, so it re-reads the host: the attempt is the
515/// launch's own construction ([`linux::create_session_netns`] with an empty allowlist, the
516/// namespace it hands over dropped at once), and it cannot pass where a launch would fail, or fail
517/// where a launch would pass. A session does not call this: it reads the failure off its own
518/// `HostProbe`, so staging and launch judge one reading.
519#[must_use]
520pub fn detect_egress_namespace_failure() -> Option<EgressNamespaceFailure> {
521    crate::sandbox::HostProbe::probe().egress_namespace_failure()
522}
523
524/// Whether a test that needs a capsule network namespace must stand down on this host, printing
525/// the failure when it must.
526///
527/// Test support, not a runtime code path. Asked through [`detect_egress_namespace_failure`], the
528/// same probe `stage_session` consults, so the gate and the staging refusal cannot reach different
529/// conclusions about the host.
530///
531/// Narrower than [`crate::skip_without_host_support`], which also requires a delegated cgroup
532/// scope: a test driving `execute_shell` directly needs the namespace and nothing else, and
533/// standing down on the cgroup half would skip it on hosts where it runs perfectly well.
534pub fn skip_without_egress_namespace(test_name: &str) -> bool {
535    match detect_egress_namespace_failure() {
536        Some(failure) => {
537            crate::runtime_err!("[SKIP-HOST] {test_name}: {}", failure.reason());
538            true
539        }
540        None => false,
541    }
542}
543
544/// Refuses the launch when a capsule that can spawn native subprocesses is on a host that cannot
545/// give them a network namespace.
546///
547/// Pure — the host answer is passed in — so the decision is testable on any OS at any tier.
548///
549/// `can_spawn_subprocess` gates the whole check rather than
550/// `capabilities.network.allow` being non-empty, and the asymmetry is deliberate: an empty
551/// allowlist means *no* egress, and without the namespace to deliver that, the capsule would get
552/// unrestricted egress instead. Refusing only capsules that asked for some network would let
553/// exactly the capsules that asked for none run with the most.
554pub fn check_egress_namespace(
555    can_spawn_subprocess: bool,
556    failure: Option<&EgressNamespaceFailure>,
557) -> Result<(), crate::errors::RuntimeError> {
558    match failure {
559        Some(failure) if can_spawn_subprocess => Err(unavailable(failure)),
560        _ => Ok(()),
561    }
562}
563
564/// The `E-CAP-005` refusal of `failure`.
565fn unavailable(failure: &EgressNamespaceFailure) -> crate::errors::RuntimeError {
566    crate::errors::RuntimeError::EgressNamespaceUnavailable {
567        blocker: failure.blocker,
568        reason: failure.reason(),
569        required_by: Vec::new(),
570    }
571}
572
573/// What [`SessionNetns::establish`] reports when the session's namespace cannot be created.
574///
575/// A failed step is the `E-CAP-005` refusal staging would have rendered for it, with the readings
576/// passed in. A creator that could not run is the runtime's own account: that spawn fails, and
577/// the next one tries again.
578fn session_netns_creation_error(
579    error: NamespaceAttemptError,
580    userns_grant: crate::sealed::UsernsGrant,
581    attachment: crate::sealed::ProfileAttachment,
582) -> String {
583    match error {
584        NamespaceAttemptError::Creator(detail) => {
585            format!("sandbox: could not create this session's network namespace: {detail}")
586        }
587        step => format!(
588            "sandbox: E-CAP-005: {}",
589            unavailable(&failure_of(step, userns_grant, attachment))
590        ),
591    }
592}
593
594// ---------------------------------------------------------------- the plan
595
596/// The fully-resolved, allocation-free description of the namespace the forked child will build.
597///
598/// Precomputed in the **parent**, before `fork()`, for the reason [`crate::sealed`] documents at
599/// length: the `pre_exec` window permits only async-signal-safe work, and allocating there can
600/// deadlock on an allocator lock a different thread of the parent held at `fork()` time. The
601/// child reads this and issues syscalls; it computes nothing and allocates nothing.
602#[derive(Debug, Clone)]
603pub(crate) struct CapsuleNetnsPlan {
604    /// TCP ports to bind and listen on inside the namespace, ascending — exactly
605    /// `egress_proxy::egress_listen_ports` for this capsule's allowlist.
606    ///
607    /// May be empty, which is a capsule with no TCP egress at all. The UDP :53 socket is bound
608    /// regardless, so even that capsule gets `REFUSED` *answers* to its lookups rather than the
609    /// multi-second resolver stall a dropped packet produces.
610    pub(crate) tcp_ports: Vec<u16>,
611    /// The real uid/gid, read in the parent.
612    ///
613    /// Load-bearing ordering, exactly as in `namespace_probe`'s rehearsals: inside a fresh user
614    /// namespace with no map written yet, `getuid()` reports the overflow id (65534), and writing
615    /// that into `uid_map` is refused on every host.
616    pub(crate) uid: u32,
617    pub(crate) gid: u32,
618}
619
620impl CapsuleNetnsPlan {
621    /// Resolves the plan for one session's namespace.
622    #[cfg(target_os = "linux")]
623    #[allow(unsafe_code)]
624    pub(crate) fn resolve(tcp_ports: Vec<u16>) -> Self {
625        // SAFETY: `getuid`/`getgid` take no arguments, dereference nothing and cannot fail.
626        let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
627        Self {
628            tcp_ports,
629            uid,
630            gid,
631        }
632    }
633
634    /// How many descriptors the child will send: one per TCP port, plus the resolver socket.
635    pub(crate) fn socket_count(&self) -> usize {
636        self.tcp_ports.len() + 1
637    }
638}
639
640// ---------------------------------------------------------------- the session's namespace
641
642/// One session's network namespace, created on first use and shared by every command after it.
643///
644/// Always held as `Arc<SessionNetns>` on [`crate::sandbox::ShellEnforcement`], so every clone of a
645/// session's enforcement — each tool call's, each demoted command's, a plan's — reaches the same
646/// namespace. Its state moves one way: not yet created, established, ended. An ended namespace is
647/// never recreated, so a command spawned after its session ended is refused rather than given a
648/// fresh namespace nobody will tear down.
649///
650/// Off Linux there is no namespace to create: the type exists so the session-end path needs no
651/// `cfg`, and it only ever moves from not yet created to ended.
652#[derive(Debug, Default)]
653pub(crate) struct SessionNetns {
654    state: std::sync::Mutex<SessionNetnsState>,
655    /// Successful creations, which a test compares against the commands it ran.
656    #[cfg(test)]
657    creations: std::sync::atomic::AtomicUsize,
658}
659
660#[derive(Debug, Default)]
661enum SessionNetnsState {
662    #[default]
663    NotYetCreated,
664    #[cfg(target_os = "linux")]
665    Established(linux::EstablishedNetns),
666    Ended,
667}
668
669/// What a command's `pre_exec` window needs to join its session's namespace: the user and network
670/// namespace descriptors.
671///
672/// Shares the session's descriptors rather than duplicating them, so they stay open for as long as
673/// the `Command` holding this does, even if the session ends between `establish` and the spawn: a
674/// join then enters a namespace whose processes are being killed, never a reused descriptor number.
675#[cfg(target_os = "linux")]
676#[derive(Debug, Clone)]
677pub(crate) struct SessionNetnsJoin {
678    user_ns: std::sync::Arc<std::os::fd::OwnedFd>,
679    net_ns: std::sync::Arc<std::os::fd::OwnedFd>,
680}
681
682#[cfg(target_os = "linux")]
683impl SessionNetnsJoin {
684    /// `(user_ns, net_ns)`, for [`join_session_netns`].
685    pub(crate) fn raw_fds(&self) -> (std::os::fd::RawFd, std::os::fd::RawFd) {
686        use std::os::fd::AsRawFd;
687        (self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
688    }
689}
690
691impl SessionNetns {
692    /// A session whose namespace does not exist yet. Creates nothing.
693    pub(crate) fn new() -> Self {
694        Self::default()
695    }
696
697    /// The handles a command joins the session's namespace through, creating the namespace and
698    /// starting the session's egress proxy on the first call.
699    ///
700    /// A creation step that fails is refused with `E-CAP-005` in the words staging uses, so a
701    /// native tool pulled mid-session, whose first call is the first time anything asks, is
702    /// refused the way a launch would have been.
703    ///
704    /// Serialised on one mutex, so concurrent first commands create exactly one namespace. A
705    /// failed creation leaves the namespace not yet created and leaks nothing, so that command's
706    /// spawn fails and the next one tries again. `tcp_ports` and `policy` are read on the call
707    /// that creates the namespace; every later call is handed the namespace that call created.
708    #[cfg(target_os = "linux")]
709    pub(crate) fn establish(
710        &self,
711        tcp_ports: &[u16],
712        policy: crate::egress_proxy::EgressPolicy,
713    ) -> Result<SessionNetnsJoin, String> {
714        let mut state = self
715            .state
716            .lock()
717            .unwrap_or_else(std::sync::PoisonError::into_inner);
718        match &*state {
719            SessionNetnsState::Established(established) => return Ok(established.join()),
720            SessionNetnsState::Ended => {
721                return Err(
722                    "sandbox: this session has ended, and its network namespace was released \
723                     with it; no further command runs in it"
724                        .to_string(),
725                )
726            }
727            SessionNetnsState::NotYetCreated => {}
728        }
729        let plan = CapsuleNetnsPlan::resolve(tcp_ports.to_vec());
730        // The readings are taken here, in the runtime, when a creation has failed: the creator
731        // child exits as soon as it reports.
732        let created = linux::create_session_netns(&plan).map_err(|error| {
733            session_netns_creation_error(
734                error,
735                crate::sealed::userns_grant(),
736                crate::sealed::inspect_profile_attachment(),
737            )
738        })?;
739        let established = created.serve(policy).map_err(|detail| {
740            format!("sandbox: could not create this session's network namespace: {detail}")
741        })?;
742        let join = established.join();
743        *state = SessionNetnsState::Established(established);
744        #[cfg(test)]
745        self.creations
746            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
747        Ok(join)
748    }
749
750    /// Releases the namespace: stops the session's egress proxy and closes the namespace
751    /// descriptors. Idempotent, and final — [`Self::establish`] refuses every call after it.
752    ///
753    /// The namespace itself is gone once its last process is: the caller kills the session's
754    /// processes first (`ShellEnforcement::end_session`).
755    pub(crate) fn end(&self) {
756        let previous = std::mem::replace(
757            &mut *self
758                .state
759                .lock()
760                .unwrap_or_else(std::sync::PoisonError::into_inner),
761            SessionNetnsState::Ended,
762        );
763        #[cfg(target_os = "linux")]
764        if let SessionNetnsState::Established(established) = previous {
765            established.release();
766        }
767        #[cfg(not(target_os = "linux"))]
768        drop(previous);
769    }
770
771    /// How many namespaces this session has created: `0` before its first command, `1` after.
772    #[cfg(test)]
773    pub(crate) fn creations(&self) -> usize {
774        self.creations.load(std::sync::atomic::Ordering::SeqCst)
775    }
776}
777
778impl Drop for SessionNetns {
779    /// The backstop for a session that never reached `end_session`: the proxy stops and the
780    /// descriptors close when the last enforcement holding this goes.
781    fn drop(&mut self) {
782        self.end();
783    }
784}
785
786/// Bytes in a forked child's step report: a `u32` step code, then the `errno` as an `i32`, both
787/// native-endian. The namespace creator and the namespace probe's rehearsals both report this way.
788pub(crate) const STEP_RECORD_LEN: usize = 8;
789
790/// A child's step report. Integers only, so a forked child builds it without allocating.
791pub(crate) fn encode_step_record(code: u32, errno: i32) -> [u8; STEP_RECORD_LEN] {
792    let mut record = [0u8; STEP_RECORD_LEN];
793    record[..4].copy_from_slice(&code.to_ne_bytes());
794    record[4..].copy_from_slice(&errno.to_ne_bytes());
795    record
796}
797
798/// `(code, errno)` from a child's step report.
799pub(crate) fn decode_step_record(record: &[u8; STEP_RECORD_LEN]) -> (u32, i32) {
800    (
801        u32::from_ne_bytes([record[0], record[1], record[2], record[3]]),
802        i32::from_ne_bytes([record[4], record[5], record[6], record[7]]),
803    )
804}
805
806#[cfg(target_os = "linux")]
807pub(crate) use linux::{create_session_netns, join_session_netns, reap_child};
808
809#[cfg(target_os = "linux")]
810#[allow(unsafe_code)]
811mod linux {
812    use std::io;
813    use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
814    use std::sync::Arc;
815
816    use super::{
817        decode_step_record, encode_step_record, CapsuleNetnsPlan, CreationStep,
818        NamespaceAttemptError, SessionNetnsJoin, MAX_NAMESPACE_SOCKETS, MAX_SESSION_NETNS_FDS,
819        STEP_RECORD_LEN,
820    };
821
822    // ------------------------------------------------------------ netlink constants
823    //
824    // Spelled out here rather than taken from `libc`, whose rtnetlink coverage varies by target
825    // and libc flavour. These are uapi ABI constants — fixed for the lifetime of the interface —
826    // and a wrong one fails loudly at the netlink ACK rather than silently.
827
828    const RTM_NEWROUTE: u16 = 24;
829    const NLM_F_REQUEST: u16 = 0x001;
830    const NLM_F_ACK: u16 = 0x004;
831    const NLM_F_EXCL: u16 = 0x200;
832    const NLM_F_CREATE: u16 = 0x400;
833    const NLMSG_ERROR: u16 = 2;
834    /// Table 255, `local` — the table whose routes cause *input* delivery. A default route here
835    /// is what makes every address locally deliverable.
836    const RT_TABLE_LOCAL: u8 = 255;
837    const RTPROT_BOOT: u8 = 3;
838    const RT_SCOPE_HOST: u8 = 254;
839    const RTN_LOCAL: u8 = 2;
840    const RTA_OIF: u16 = 4;
841    /// `lo` is always interface index 1 in a freshly created network namespace: it is the only
842    /// interface there, and the kernel creates it first.
843    const LOOPBACK_IFINDEX: u32 = 1;
844
845    const NLMSGHDR_LEN: usize = 16;
846    const RTMSG_LEN: usize = 12;
847    const RTATTR_OIF_LEN: usize = 8;
848    const ROUTE_REQUEST_LEN: usize = NLMSGHDR_LEN + RTMSG_LEN + RTATTR_OIF_LEN;
849
850    // ------------------------------------------------------------ the creator child
851
852    /// A failed step and the `errno` it left, carried out of the creator as a fixed-size record.
853    ///
854    /// `Copy` and built from integers only, so the creator reports a failure without allocating.
855    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
856    pub(super) struct CreationFailure {
857        step: CreationStep,
858        errno: i32,
859    }
860
861    impl CreationFailure {
862        /// The failure of `step`, with the `errno` the failing call just left.
863        ///
864        /// # Safety
865        /// Reads the calling thread's `errno`; async-signal-safe.
866        unsafe fn at(step: CreationStep) -> Self {
867            // SAFETY: `__errno_location` returns the calling thread's own errno slot, which is
868            // always valid to read.
869            let errno = unsafe { *libc::__errno_location() };
870            Self { step, errno }
871        }
872
873        fn encode(self) -> [u8; STEP_RECORD_LEN] {
874            encode_step_record(self.step as u32, self.errno)
875        }
876
877        fn decode(record: &[u8; STEP_RECORD_LEN]) -> Option<Self> {
878            let (code, errno) = decode_step_record(record);
879            let step = CreationStep::from_code(code)?;
880            Some(Self { step, errno })
881        }
882    }
883
884    impl From<CreationFailure> for NamespaceAttemptError {
885        fn from(failure: CreationFailure) -> Self {
886            NamespaceAttemptError::Step {
887                step: failure.step,
888                errno: failure.errno,
889            }
890        }
891    }
892
893    /// Builds the session's network namespace and hands it to the parent: its listening sockets,
894    /// then descriptors for its user and network namespaces, in one `SCM_RIGHTS` message ordered
895    /// `[tcp listeners…, udp53, user_ns, net_ns]`.
896    ///
897    /// Runs only in the creator child [`create_session_netns`] forks, which runs no capsule code,
898    /// installs no seccomp filter or Landlock domain and joins no cgroup scope; it `_exit`s as
899    /// soon as this returns. Commands join what this builds through [`join_session_netns`]. A
900    /// `sealed` command goes on to `unshare(CLONE_NEWUSER | CLONE_NEWNS)` a *nested* user
901    /// namespace, and a process in a descendant user namespace holds no capabilities in the
902    /// ancestor that owns this network namespace, so a `sealed` capsule cannot reconfigure the
903    /// namespace confining it: `SIOCSIFFLAGS` returns `EPERM` from inside the nested namespace.
904    ///
905    /// # Safety
906    /// Post-`fork()` child context only. Every call below is async-signal-safe and nothing here
907    /// allocates: `plan` was fully resolved in the parent, and a failure is a `Copy` record.
908    unsafe fn create_capsule_netns(
909        plan: &CapsuleNetnsPlan,
910        sock_fd: RawFd,
911    ) -> Result<(), CreationFailure> {
912        // SAFETY: forked-child context; the caller's `unsafe` contract covers this whole body.
913        unsafe {
914            if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNET) != 0 {
915                return Err(CreationFailure::at(CreationStep::Unshare));
916            }
917            if write_identity_maps(plan.uid, plan.gid).is_err() {
918                return Err(CreationFailure::at(CreationStep::IdentityMaps));
919            }
920            if bring_loopback_up().is_err() {
921                return Err(CreationFailure::at(CreationStep::Loopback));
922            }
923            if add_local_default_route().is_err() {
924                return Err(CreationFailure::at(CreationStep::LocalRoute));
925            }
926
927            // One TCP listener per allowlisted port, then the resolver socket — the order the
928            // parent's `start_egress_proxy` splits them back apart in — then the two namespace
929            // handles, which the parent takes off the end.
930            let mut fds = [-1 as RawFd; MAX_SESSION_NETNS_FDS];
931            let mut count = 0usize;
932
933            for port in &plan.tcp_ports {
934                let fd = bind_tcp_listener(*port);
935                if fd < 0 {
936                    let failure = CreationFailure::at(CreationStep::TcpListener);
937                    close_all(&fds[..count]);
938                    return Err(failure);
939                }
940                fds[count] = fd;
941                count += 1;
942            }
943
944            let resolver = bind_dns_socket();
945            if resolver < 0 {
946                let failure = CreationFailure::at(CreationStep::Resolver);
947                close_all(&fds[..count]);
948                return Err(failure);
949            }
950            fds[count] = resolver;
951            count += 1;
952
953            // Opened after the maps are written, so both name the namespaces this child now owns.
954            for namespace in [c"/proc/self/ns/user", c"/proc/self/ns/net"] {
955                let fd = libc::open(namespace.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC);
956                if fd < 0 {
957                    let failure = CreationFailure::at(CreationStep::NamespaceHandles);
958                    close_all(&fds[..count]);
959                    return Err(failure);
960                }
961                fds[count] = fd;
962                count += 1;
963            }
964
965            let sent = send_fds(sock_fd, &fds[..count]);
966            let failure = CreationFailure::at(CreationStep::HandOff);
967            close_all(&fds[..count]);
968            sent.map_err(|()| failure)
969        }
970    }
971
972    /// Enters the session's namespaces: `setns(user_fd, CLONE_NEWUSER)`, then
973    /// `setns(net_fd, CLONE_NEWNET)`. Returns the `errno` of whichever failed.
974    ///
975    /// The order is required. Joining a network namespace needs `CAP_SYS_ADMIN` in the user
976    /// namespace that owns it, which the caller holds only once it is inside that user namespace;
977    /// joining the user namespace needs the caller to be single-threaded and its euid to own the
978    /// namespace, which a command's forked child is and does.
979    ///
980    /// # Safety
981    /// Post-`fork()`, pre-exec child context only. Both descriptors must be open; the
982    /// [`SessionNetnsJoin`] they came from keeps them so.
983    pub(crate) unsafe fn join_session_netns(user_fd: RawFd, net_fd: RawFd) -> io::Result<()> {
984        // SAFETY: `setns` takes two integers and dereferences nothing; `last_os_error` reads
985        // errno and does not allocate.
986        unsafe {
987            if libc::setns(user_fd, libc::CLONE_NEWUSER) != 0 {
988                return Err(io::Error::last_os_error());
989            }
990            if libc::setns(net_fd, libc::CLONE_NEWNET) != 0 {
991                return Err(io::Error::last_os_error());
992            }
993        }
994        Ok(())
995    }
996
997    /// What the creator handed over: the namespace sockets, ready for the egress proxy, and the
998    /// two namespace handles.
999    pub(crate) struct CreatedNetns {
1000        sockets: Vec<OwnedFd>,
1001        user_ns: OwnedFd,
1002        net_ns: OwnedFd,
1003    }
1004
1005    impl CreatedNetns {
1006        /// `(user_ns, net_ns)`, for [`join_session_netns`] in a child forked while this is held.
1007        pub(crate) fn namespace_handles(&self) -> (RawFd, RawFd) {
1008            (self.user_ns.as_raw_fd(), self.net_ns.as_raw_fd())
1009        }
1010
1011        /// Starts the session's egress proxy on the namespace sockets.
1012        ///
1013        /// On failure the sockets and the namespace handles close here, which leaves no process
1014        /// and no descriptor holding the namespace.
1015        pub(super) fn serve(
1016            self,
1017            policy: crate::egress_proxy::EgressPolicy,
1018        ) -> Result<EstablishedNetns, String> {
1019            let proxy = crate::egress_proxy::start_egress_proxy(self.sockets, policy)
1020                .map_err(|error| format!("starting the session's egress proxy failed: {error}"))?;
1021            Ok(EstablishedNetns {
1022                user_ns: Arc::new(self.user_ns),
1023                net_ns: Arc::new(self.net_ns),
1024                proxy,
1025            })
1026        }
1027    }
1028
1029    /// An established session namespace: the two handles commands join through, and the proxy
1030    /// serving its listeners.
1031    #[derive(Debug)]
1032    pub(super) struct EstablishedNetns {
1033        user_ns: Arc<OwnedFd>,
1034        net_ns: Arc<OwnedFd>,
1035        proxy: crate::egress_proxy::EgressProxyHandle,
1036    }
1037
1038    impl EstablishedNetns {
1039        pub(super) fn join(&self) -> SessionNetnsJoin {
1040            SessionNetnsJoin {
1041                user_ns: Arc::clone(&self.user_ns),
1042                net_ns: Arc::clone(&self.net_ns),
1043            }
1044        }
1045
1046        /// Stops the proxy and drops this session's hold on the namespace handles.
1047        pub(super) fn release(self) {
1048            self.proxy.shutdown();
1049        }
1050    }
1051
1052    /// Forks the creator child, waits for it, and takes what it handed over.
1053    ///
1054    /// The creator is a child of its own rather than a command's `pre_exec` step, so no command is
1055    /// special: a command that fails cannot take the session's namespace with it, and creation
1056    /// never depends on what any command is.
1057    ///
1058    /// A failed step is [`NamespaceAttemptError::Step`] with the step and the `errno` the creator
1059    /// reported; every way the creator itself could not run or report is
1060    /// [`NamespaceAttemptError::Creator`].
1061    pub(crate) fn create_session_netns(
1062        plan: &CapsuleNetnsPlan,
1063    ) -> Result<CreatedNetns, NamespaceAttemptError> {
1064        // Checked here because the creator indexes a fixed buffer of this size and must not be the
1065        // place an oversized plan is discovered.
1066        if plan.socket_count() > MAX_NAMESPACE_SOCKETS {
1067            return Err(NamespaceAttemptError::Creator(format!(
1068                "{} namespace sockets requested, more than the {MAX_NAMESPACE_SOCKETS} one \
1069                 hand-off carries",
1070                plan.socket_count()
1071            )));
1072        }
1073
1074        let mut pair = [0i32; 2];
1075        // SAFETY: `pair` is a 2-element stack array, exactly the size `socketpair` writes into.
1076        let rc = unsafe {
1077            libc::socketpair(
1078                libc::AF_UNIX,
1079                libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
1080                0,
1081                pair.as_mut_ptr(),
1082            )
1083        };
1084        if rc != 0 {
1085            return Err(NamespaceAttemptError::Creator(format!(
1086                "socketpair() for the namespace hand-off failed: {}",
1087                io::Error::last_os_error()
1088            )));
1089        }
1090        // SAFETY: both fds were just returned by the successful `socketpair` above and nothing
1091        // else owns them.
1092        let (parent_end, creator_end) =
1093            unsafe { (OwnedFd::from_raw_fd(pair[0]), OwnedFd::from_raw_fd(pair[1])) };
1094        let creator_fd = creator_end.as_raw_fd();
1095
1096        // SAFETY: `fork()` from a possibly-multithreaded process is sound as long as the child
1097        // touches nothing but async-signal-safe primitives. The child runs only
1098        // `create_capsule_netns` — `unshare`, `prctl`, `open`/`write`/`close`, `socket`, `ioctl`,
1099        // `bind`, `listen`, `setsockopt`, `sendto`/`recv`/`sendmsg` — then `send` and `_exit`,
1100        // with no allocation, no locks and no stdio.
1101        let pid = unsafe { libc::fork() };
1102        if pid < 0 {
1103            return Err(NamespaceAttemptError::Creator(format!(
1104                "fork() for the namespace creator failed: {}",
1105                io::Error::last_os_error()
1106            )));
1107        }
1108        if pid == 0 {
1109            // SAFETY: forked-child context, syscalls only; both branches end in `_exit`, so no
1110            // destructor of the parent's state runs here.
1111            unsafe {
1112                match create_capsule_netns(plan, creator_fd) {
1113                    Ok(()) => libc::_exit(0),
1114                    Err(failure) => {
1115                        let record = failure.encode();
1116                        libc::send(creator_fd, record.as_ptr().cast(), record.len(), 0);
1117                        libc::_exit(1);
1118                    }
1119                }
1120            }
1121        }
1122        drop(creator_end);
1123
1124        // The hand-off is read only after the creator is reaped: a `SOCK_DGRAM` pair reports no
1125        // end-of-file when its peer closes, so reading first would block forever on a creator that
1126        // died before sending, while the message a creator did send stays queued after it exits.
1127        let status = reap_child(pid).map_err(|error| {
1128            NamespaceAttemptError::Creator(format!(
1129                "waiting for the namespace creator failed: {error}"
1130            ))
1131        })?;
1132        match receive_session_netns(parent_end.as_raw_fd(), plan.socket_count())
1133            .map_err(NamespaceAttemptError::Creator)?
1134        {
1135            Received::Handles(mut fds) => {
1136                let net_ns = fds.pop().expect("the count was checked on receipt");
1137                let user_ns = fds.pop().expect("the count was checked on receipt");
1138                Ok(CreatedNetns {
1139                    sockets: fds,
1140                    user_ns,
1141                    net_ns,
1142                })
1143            }
1144            Received::Failure(failure) => Err(failure.into()),
1145            Received::Nothing => Err(NamespaceAttemptError::Creator(format!(
1146                "the namespace creator exited without handing anything over ({status})"
1147            ))),
1148        }
1149    }
1150
1151    /// Reaps the child `pid`, retrying on `EINTR`, and renders how it ended.
1152    pub(crate) fn reap_child(pid: libc::pid_t) -> io::Result<String> {
1153        let mut status: libc::c_int = 0;
1154        loop {
1155            // SAFETY: `pid` is a child this process forked; `status` is a live local.
1156            let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
1157            if waited == pid {
1158                break;
1159            }
1160            let error = io::Error::last_os_error();
1161            if error.kind() != io::ErrorKind::Interrupted {
1162                return Err(error);
1163            }
1164        }
1165        Ok(if libc::WIFEXITED(status) {
1166            format!("exit status {}", libc::WEXITSTATUS(status))
1167        } else if libc::WIFSIGNALED(status) {
1168            format!("killed by signal {}", libc::WTERMSIG(status))
1169        } else {
1170            format!("wait status {status}")
1171        })
1172    }
1173
1174    /// Encodes and decodes a failure record as the creator and the parent do, as the error the
1175    /// parent returns for it.
1176    #[cfg(test)]
1177    pub(super) fn decode_failure_record(step: u32, errno: i32) -> Option<NamespaceAttemptError> {
1178        let record = encode_step_record(step, errno);
1179        CreationFailure::decode(&record).map(|failure| {
1180            assert_eq!(failure.encode(), record);
1181            failure.into()
1182        })
1183    }
1184
1185    /// The creator's one message.
1186    enum Received {
1187        /// `[tcp listeners…, udp53, user_ns, net_ns]`, every one `CLOEXEC`.
1188        Handles(Vec<OwnedFd>),
1189        Failure(CreationFailure),
1190        Nothing,
1191    }
1192
1193    /// Writes `setgroups=deny` and the identity `uid_map`/`gid_map`, reopening the dumpable
1194    /// window around them.
1195    ///
1196    /// The dumpable dance is not optional and is `sealed`'s hardest-won lesson, reused here
1197    /// rather than rediscovered: `mur` marks itself non-dumpable at startup, the flag is inherited
1198    /// across `fork()`, and the kernel reassigns every `/proc/<pid>/*` entry of a non-dumpable
1199    /// task to root — including `uid_map`. Without this the namespace is created and can never be
1200    /// owned, and the failure reads as a host id-mapping policy problem on a host whose policy is
1201    /// perfectly fine.
1202    ///
1203    /// # Safety
1204    /// Post-`fork()`, pre-exec child context only.
1205    unsafe fn write_identity_maps(uid: u32, gid: u32) -> Result<(), ()> {
1206        // SAFETY: caller's contract; every call below is async-signal-safe.
1207        unsafe {
1208            let previous = crate::sealed::make_dumpable_for_map_writes();
1209            let _ = crate::sealed::write_decimal_map(c"/proc/self/setgroups", None, 0);
1210            let mapped = crate::sealed::write_decimal_map(c"/proc/self/uid_map", Some(uid), uid)
1211                .is_ok()
1212                && crate::sealed::write_decimal_map(c"/proc/self/gid_map", Some(gid), gid).is_ok();
1213            crate::sealed::restore_dumpable(previous);
1214            if mapped {
1215                Ok(())
1216            } else {
1217                Err(())
1218            }
1219        }
1220    }
1221
1222    /// `ip link set lo up`, via the legacy `ifreq` ioctl rather than netlink.
1223    ///
1224    /// The ioctl is a fixed-size struct with no message framing, so it is markedly simpler to do
1225    /// allocation-free inside `pre_exec` than the equivalent `RTM_NEWLINK`. It is not deprecated
1226    /// for this purpose, and every distribution's `ifconfig` still uses it.
1227    ///
1228    /// # Safety
1229    /// Post-`fork()`, pre-exec child context only.
1230    unsafe fn bring_loopback_up() -> Result<(), ()> {
1231        // SAFETY: `ifr` is a live, zeroed `ifreq` whose name field is set to "lo"; both ioctls
1232        // read and write only within it, and the socket is closed on every path.
1233        unsafe {
1234            let sock = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
1235            if sock < 0 {
1236                return Err(());
1237            }
1238            let mut ifr: libc::ifreq = std::mem::zeroed();
1239            ifr.ifr_name[0] = b'l' as libc::c_char;
1240            ifr.ifr_name[1] = b'o' as libc::c_char;
1241            if libc::ioctl(sock, libc::SIOCGIFFLAGS, &mut ifr) < 0 {
1242                libc::close(sock);
1243                return Err(());
1244            }
1245            ifr.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
1246            let rc = libc::ioctl(sock, libc::SIOCSIFFLAGS, &ifr);
1247            libc::close(sock);
1248            if rc < 0 {
1249                return Err(());
1250            }
1251            Ok(())
1252        }
1253    }
1254
1255    /// `ip route add local default dev lo`, over raw netlink.
1256    ///
1257    /// A route of type `RTN_LOCAL` in table `local` tells the kernel that the destination is one
1258    /// of *its own* addresses, so a `connect()` to any address is looped back and delivered to a
1259    /// listening socket in this namespace — with the original destination preserved and
1260    /// recoverable by `getsockname(2)`. That is the whole interception mechanism: no netfilter, no
1261    /// `iptables` binary, and no rule set that can drift out of step with the manifest. A port
1262    /// nothing is bound to is refused immediately by the namespace instead of stalling through the
1263    /// kernel's full SYN-retry schedule against a blackhole.
1264    ///
1265    /// IPv4 only, deliberately. A capsule's IPv6 destinations have no route at all and fail with
1266    /// `ENETUNREACH`, and the proxy reaches every upstream over the host's own stack, so nothing
1267    /// a capsule can legitimately ask for depends on IPv6 inside the namespace.
1268    ///
1269    /// # Safety
1270    /// Post-`fork()`, pre-exec child context only. The request is built in a fixed stack buffer.
1271    unsafe fn add_local_default_route() -> Result<(), ()> {
1272        // SAFETY: `request` and `address` are live, correctly-sized stack locals; the socket is
1273        // closed on every path.
1274        unsafe {
1275            let mut request = [0u8; ROUTE_REQUEST_LEN];
1276            request[0..4].copy_from_slice(&(ROUTE_REQUEST_LEN as u32).to_ne_bytes());
1277            request[4..6].copy_from_slice(&RTM_NEWROUTE.to_ne_bytes());
1278            request[6..8].copy_from_slice(
1279                &(NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL).to_ne_bytes(),
1280            );
1281            request[8..12].copy_from_slice(&1u32.to_ne_bytes()); // sequence
1282            request[12..16].copy_from_slice(&0u32.to_ne_bytes()); // pid: the kernel assigns
1283
1284            let rtm = NLMSGHDR_LEN;
1285            request[rtm] = libc::AF_INET as u8;
1286            request[rtm + 1] = 0; // dst_len 0 — a default route
1287            request[rtm + 2] = 0; // src_len
1288            request[rtm + 3] = 0; // tos
1289            request[rtm + 4] = RT_TABLE_LOCAL;
1290            request[rtm + 5] = RTPROT_BOOT;
1291            request[rtm + 6] = RT_SCOPE_HOST;
1292            request[rtm + 7] = RTN_LOCAL;
1293            request[rtm + 8..rtm + 12].copy_from_slice(&0u32.to_ne_bytes()); // rtm_flags
1294
1295            let attr = rtm + RTMSG_LEN;
1296            request[attr..attr + 2].copy_from_slice(&(RTATTR_OIF_LEN as u16).to_ne_bytes());
1297            request[attr + 2..attr + 4].copy_from_slice(&RTA_OIF.to_ne_bytes());
1298            request[attr + 4..attr + 8].copy_from_slice(&LOOPBACK_IFINDEX.to_ne_bytes());
1299
1300            let sock = libc::socket(
1301                libc::AF_NETLINK,
1302                libc::SOCK_RAW | libc::SOCK_CLOEXEC,
1303                libc::NETLINK_ROUTE,
1304            );
1305            if sock < 0 {
1306                return Err(());
1307            }
1308            let mut address: libc::sockaddr_nl = std::mem::zeroed();
1309            address.nl_family = libc::AF_NETLINK as u16;
1310            let sent = libc::sendto(
1311                sock,
1312                request.as_ptr().cast(),
1313                ROUTE_REQUEST_LEN,
1314                0,
1315                std::ptr::addr_of!(address).cast(),
1316                std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
1317            );
1318            if sent < 0 {
1319                libc::close(sock);
1320                return Err(());
1321            }
1322
1323            // Reading the ACK is not optional: netlink reports the real outcome here, and a
1324            // request that was accepted for delivery but rejected by the kernel would otherwise
1325            // look like success and leave the capsule with a namespace it cannot resolve through.
1326            let mut response = [0u8; 256];
1327            let received = libc::recv(sock, response.as_mut_ptr().cast(), response.len(), 0);
1328            libc::close(sock);
1329            if received < (NLMSGHDR_LEN + 4) as isize {
1330                return Err(());
1331            }
1332            if u16::from_ne_bytes([response[4], response[5]]) == NLMSG_ERROR {
1333                let code = i32::from_ne_bytes([
1334                    response[NLMSGHDR_LEN],
1335                    response[NLMSGHDR_LEN + 1],
1336                    response[NLMSGHDR_LEN + 2],
1337                    response[NLMSGHDR_LEN + 3],
1338                ]);
1339                // Netlink spells a plain ACK as an error message carrying code 0.
1340                if code != 0 {
1341                    return Err(());
1342                }
1343            }
1344            Ok(())
1345        }
1346    }
1347
1348    /// A listening TCP socket on `0.0.0.0:port` inside the namespace, or `-1`.
1349    ///
1350    /// The wildcard address is what pairs with the local default route: every address is locally
1351    /// deliverable, so this one socket receives connections addressed to *any* host on this port,
1352    /// and `getsockname(2)` on each accepted connection recovers which one the capsule dialled.
1353    ///
1354    /// # Safety
1355    /// Post-`fork()`, pre-exec child context only.
1356    unsafe fn bind_tcp_listener(port: u16) -> RawFd {
1357        // SAFETY: `address` is a live, zeroed `sockaddr_in`; every failure path closes the fd.
1358        unsafe {
1359            let fd = libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0);
1360            if fd < 0 {
1361                return -1;
1362            }
1363            let mut address: libc::sockaddr_in = std::mem::zeroed();
1364            address.sin_family = libc::AF_INET as libc::sa_family_t;
1365            address.sin_port = port.to_be();
1366            address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
1367            if libc::bind(
1368                fd,
1369                std::ptr::addr_of!(address).cast(),
1370                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
1371            ) != 0
1372                || libc::listen(fd, 128) != 0
1373            {
1374                libc::close(fd);
1375                return -1;
1376            }
1377            fd
1378        }
1379    }
1380
1381    /// A UDP socket bound to `0.0.0.0:53` inside the namespace with `IP_PKTINFO` enabled, or
1382    /// `-1`.
1383    ///
1384    /// The wildcard bind pairs with the local default route: whatever address `/etc/resolv.conf`
1385    /// names, the query is delivered to this one socket. `IP_PKTINFO` is then required rather
1386    /// than a nicety — it is how the proxy recovers the address the query was *sent to*, so its
1387    /// reply can carry that same address as the source. A resolver discards an answer that
1388    /// appears to come from a server it never asked, and the symptom of getting this wrong is
1389    /// "DNS silently does not work".
1390    ///
1391    /// # Safety
1392    /// Post-`fork()`, pre-exec child context only.
1393    unsafe fn bind_dns_socket() -> RawFd {
1394        // SAFETY: as `bind_tcp_listener`.
1395        unsafe {
1396            let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
1397            if fd < 0 {
1398                return -1;
1399            }
1400            let enable: libc::c_int = 1;
1401            if libc::setsockopt(
1402                fd,
1403                libc::IPPROTO_IP,
1404                libc::IP_PKTINFO,
1405                std::ptr::addr_of!(enable).cast(),
1406                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
1407            ) != 0
1408            {
1409                libc::close(fd);
1410                return -1;
1411            }
1412            let mut address: libc::sockaddr_in = std::mem::zeroed();
1413            address.sin_family = libc::AF_INET as libc::sa_family_t;
1414            address.sin_port = super::DNS_PORT_BE;
1415            address.sin_addr.s_addr = libc::INADDR_ANY.to_be();
1416            if libc::bind(
1417                fd,
1418                std::ptr::addr_of!(address).cast(),
1419                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
1420            ) != 0
1421            {
1422                libc::close(fd);
1423                return -1;
1424            }
1425            fd
1426        }
1427    }
1428
1429    /// # Safety
1430    /// Post-`fork()`, pre-exec child context only.
1431    unsafe fn close_all(fds: &[RawFd]) {
1432        for fd in fds {
1433            if *fd >= 0 {
1434                // SAFETY: each entry was returned by a successful `socket` or `open` call above.
1435                unsafe { libc::close(*fd) };
1436            }
1437        }
1438    }
1439
1440    /// Enough room for [`MAX_SESSION_NETNS_FDS`] descriptors plus the `cmsghdr`.
1441    const CMSG_BUFFER_LEN: usize = 128;
1442
1443    // `send_fds`/`receive_session_netns` size `msg_controllen` from
1444    // `libc::CMSG_SPACE(descriptor_bytes)`, independently of `CMSG_BUFFER_LEN`, so a bump of
1445    // `MAX_SESSION_NETNS_FDS` (via `egress_proxy::MAX_EGRESS_TCP_PORTS`) could otherwise make that
1446    // computed length exceed the fixed-size `CmsgBuffer` below, and the CMSG macros would then
1447    // write past its end. Pinned at compile time instead.
1448    const _: () = assert!(
1449        // SAFETY: `CMSG_SPACE` is pure arithmetic on its argument (see libc's definition); it is
1450        // marked `unsafe` only for macro-generation consistency with pointer-taking neighbours
1451        // like `CMSG_DATA`, not because this call touches memory.
1452        unsafe {
1453            libc::CMSG_SPACE((MAX_SESSION_NETNS_FDS * std::mem::size_of::<RawFd>()) as u32) as usize
1454        } <= CMSG_BUFFER_LEN,
1455        "CMSG_BUFFER_LEN is too small to hold MAX_SESSION_NETNS_FDS descriptors plus the \
1456         cmsghdr; raise it to match"
1457    );
1458
1459    /// A `cmsghdr` buffer with the alignment the CMSG macros require.
1460    ///
1461    /// A bare `[u8; N]` is only 1-byte aligned and `CMSG_FIRSTHDR` casts it straight to
1462    /// `*mut cmsghdr`: on a debug build that is a misaligned-pointer panic, and on a release build
1463    /// it is undefined behaviour. Found the hard way while prototyping this hand-off.
1464    #[repr(C, align(8))]
1465    struct CmsgBuffer([u8; CMSG_BUFFER_LEN]);
1466
1467    /// Sends every namespace socket and both namespace handles to the parent in one
1468    /// `SCM_RIGHTS` message.
1469    ///
1470    /// One message rather than one per fd, so the parent's receive is a single unambiguous read:
1471    /// a partial hand-off would leave the proxy serving the endpoint but not the resolver, which
1472    /// presents as "DNS mysteriously does not work" rather than as a failure.
1473    ///
1474    /// # Safety
1475    /// Post-`fork()`, pre-exec child context only; `sendmsg` is async-signal-safe and every
1476    /// buffer is a stack local.
1477    unsafe fn send_fds(sock_fd: RawFd, fds: &[RawFd]) -> Result<(), ()> {
1478        // SAFETY: all buffers are live locals outliving the single `sendmsg`; the control buffer
1479        // is 8-byte aligned, which the CMSG macros require.
1480        unsafe {
1481            let mut payload = [0u8; 1];
1482            let mut iov = libc::iovec {
1483                iov_base: payload.as_mut_ptr().cast(),
1484                iov_len: payload.len(),
1485            };
1486            let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
1487            let bytes = std::mem::size_of_val(fds) as u32;
1488
1489            let mut msg: libc::msghdr = std::mem::zeroed();
1490            msg.msg_iov = &mut iov;
1491            msg.msg_iovlen = 1;
1492            msg.msg_control = control.0.as_mut_ptr().cast();
1493            msg.msg_controllen = libc::CMSG_SPACE(bytes) as _;
1494
1495            let cmsg = libc::CMSG_FIRSTHDR(&msg);
1496            if cmsg.is_null() {
1497                return Err(());
1498            }
1499            (*cmsg).cmsg_level = libc::SOL_SOCKET;
1500            (*cmsg).cmsg_type = libc::SCM_RIGHTS;
1501            (*cmsg).cmsg_len = libc::CMSG_LEN(bytes) as _;
1502            std::ptr::copy_nonoverlapping(
1503                fds.as_ptr().cast::<u8>(),
1504                libc::CMSG_DATA(cmsg),
1505                bytes as usize,
1506            );
1507
1508            if libc::sendmsg(sock_fd, &msg, 0) < 0 {
1509                return Err(());
1510            }
1511            Ok(())
1512        }
1513    }
1514
1515    /// Parent-side counterpart of [`send_fds`]: takes the creator's one message off the queue.
1516    ///
1517    /// Non-blocking, because the creator has already been reaped by the time this runs: whatever
1518    /// it sent is queued, and an empty queue means it sent nothing. A descriptor count other than
1519    /// `expected_sockets + 2` is an error rather than a best-effort start, because a proxy missing
1520    /// one of its sockets is a capsule whose network half-works in a way nothing downstream would
1521    /// report.
1522    fn receive_session_netns(sock_fd: RawFd, expected_sockets: usize) -> Result<Received, String> {
1523        // SAFETY: `sock_fd` is a valid unix socket; all buffers are live locals sized for the
1524        // largest message the creator can send, and the control buffer is 8-byte aligned.
1525        unsafe {
1526            let mut payload = [0u8; STEP_RECORD_LEN];
1527            let mut iov = libc::iovec {
1528                iov_base: payload.as_mut_ptr().cast(),
1529                iov_len: payload.len(),
1530            };
1531            let mut control = CmsgBuffer([0u8; CMSG_BUFFER_LEN]);
1532            let mut msg: libc::msghdr = std::mem::zeroed();
1533            msg.msg_iov = &mut iov;
1534            msg.msg_iovlen = 1;
1535            msg.msg_control = control.0.as_mut_ptr().cast();
1536            msg.msg_controllen = control.0.len() as _;
1537
1538            let received = libc::recvmsg(
1539                sock_fd,
1540                &mut msg,
1541                libc::MSG_DONTWAIT | libc::MSG_CMSG_CLOEXEC,
1542            );
1543            if received < 0 {
1544                let error = io::Error::last_os_error();
1545                if error.kind() == io::ErrorKind::WouldBlock {
1546                    return Ok(Received::Nothing);
1547                }
1548                return Err(format!("receiving the namespace hand-off failed: {error}"));
1549            }
1550
1551            let cmsg = libc::CMSG_FIRSTHDR(&msg);
1552            if cmsg.is_null() {
1553                if received as usize == STEP_RECORD_LEN {
1554                    if let Some(failure) = CreationFailure::decode(&payload) {
1555                        return Ok(Received::Failure(failure));
1556                    }
1557                }
1558                return Ok(Received::Nothing);
1559            }
1560            if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
1561                return Err(
1562                    "the namespace hand-off did not arrive as an SCM_RIGHTS message".to_string(),
1563                );
1564            }
1565            let bytes = (*cmsg).cmsg_len - libc::CMSG_LEN(0) as usize;
1566            let count = bytes / std::mem::size_of::<RawFd>();
1567            let data = libc::CMSG_DATA(cmsg).cast::<RawFd>();
1568            // Wrapped in `OwnedFd` before anything is checked, so a mismatched or truncated
1569            // message closes its descriptors on the error path instead of leaking them.
1570            let mut fds = Vec::with_capacity(count);
1571            for index in 0..count {
1572                fds.push(OwnedFd::from_raw_fd(std::ptr::read_unaligned(
1573                    data.add(index),
1574                )));
1575            }
1576            if msg.msg_flags & libc::MSG_CTRUNC != 0 {
1577                return Err("the namespace hand-off was truncated".to_string());
1578            }
1579            if count != expected_sockets + 2 {
1580                return Err(format!(
1581                    "the namespace creator handed over {count} descriptors; {} were expected \
1582                     ({expected_sockets} sockets and 2 namespace handles)",
1583                    expected_sockets + 2
1584                ));
1585            }
1586            Ok(Received::Handles(fds))
1587        }
1588    }
1589}
1590
1591/// [`crate::egress_proxy::EGRESS_DNS_PORT`] in network byte order, so the `pre_exec` window does
1592/// no arithmetic the parent could have done for it.
1593#[cfg(target_os = "linux")]
1594const DNS_PORT_BE: u16 = crate::egress_proxy::EGRESS_DNS_PORT.to_be();
1595
1596#[cfg(test)]
1597mod tests {
1598    use super::*;
1599    use crate::sealed::{ProfileAttachment, UsernsGrant};
1600
1601    fn attachments() -> [ProfileAttachment; 4] {
1602        [
1603            ProfileAttachment::Sealed {
1604                profile: "mur-sealed".to_string(),
1605            },
1606            ProfileAttachment::Other {
1607                profile: "docker-default".to_string(),
1608            },
1609            ProfileAttachment::Unconfined,
1610            ProfileAttachment::NotReported,
1611        ]
1612    }
1613
1614    fn step(step: CreationStep, errno: i32) -> Result<(), NamespaceAttemptError> {
1615        Err(NamespaceAttemptError::Step { step, errno })
1616    }
1617
1618    fn failure(
1619        attempt: Result<(), NamespaceAttemptError>,
1620        grant: UsernsGrant,
1621        attachment: ProfileAttachment,
1622    ) -> EgressNamespaceFailure {
1623        egress_namespace_failure(true, attempt, grant, attachment)
1624            .expect("a failed attempt is a failure")
1625    }
1626
1627    // ---- the decision ----------------------------------------------------------------------
1628
1629    /// A namespace that was created is never refused, whatever AppArmor reads: a container whose
1630    /// own profile permits it, on a host whose restriction reads `1`, gets its namespace.
1631    #[test]
1632    fn a_created_namespace_is_never_refused_whatever_the_readings() {
1633        for grant in UsernsGrant::ALL {
1634            for attachment in attachments() {
1635                assert_eq!(
1636                    egress_namespace_failure(true, Ok(()), *grant, attachment.clone()),
1637                    None,
1638                    "{grant:?} / {attachment:?}"
1639                );
1640            }
1641        }
1642    }
1643
1644    /// A namespace that was not created is refused, whatever AppArmor reads, and the blocker is
1645    /// the same for every reading.
1646    #[test]
1647    fn a_failed_creation_is_refused_whatever_the_readings() {
1648        let attempts = [
1649            (
1650                step(CreationStep::Unshare, libc::EPERM),
1651                EgressNamespaceBlocker::CapabilityGrantMissing,
1652            ),
1653            (
1654                step(CreationStep::Unshare, libc::ENOSPC),
1655                EgressNamespaceBlocker::KernelSupportMissing,
1656            ),
1657            (
1658                Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
1659                EgressNamespaceBlocker::CreatorUnavailable,
1660            ),
1661        ];
1662        for (attempt, blocker) in attempts {
1663            for grant in UsernsGrant::ALL {
1664                for attachment in attachments() {
1665                    let failure =
1666                        egress_namespace_failure(true, attempt.clone(), *grant, attachment.clone())
1667                            .unwrap_or_else(|| {
1668                                panic!("{attempt:?} under {grant:?} / {attachment:?}")
1669                            });
1670                    assert_eq!(failure.blocker, blocker, "{grant:?} / {attachment:?}");
1671                }
1672            }
1673        }
1674    }
1675
1676    #[test]
1677    fn only_an_unshare_the_kernel_cannot_honour_is_missing_kernel_support() {
1678        for errno in [libc::ENOSPC, libc::EINVAL, libc::ENOSYS, libc::EUSERS] {
1679            assert_eq!(
1680                failure(
1681                    step(CreationStep::Unshare, errno),
1682                    UsernsGrant::Withheld,
1683                    ProfileAttachment::Unconfined
1684                )
1685                .blocker,
1686                EgressNamespaceBlocker::KernelSupportMissing,
1687                "errno {errno}"
1688            );
1689        }
1690        for (failed, errno) in [
1691            (CreationStep::Unshare, libc::EPERM),
1692            (CreationStep::Unshare, libc::EACCES),
1693            (CreationStep::IdentityMaps, libc::EPERM),
1694            (CreationStep::Loopback, libc::EPERM),
1695            (CreationStep::LocalRoute, libc::EINVAL),
1696            (CreationStep::Resolver, libc::ENOSPC),
1697        ] {
1698            assert_eq!(
1699                failure(
1700                    step(failed, errno),
1701                    UsernsGrant::Withheld,
1702                    ProfileAttachment::Unconfined
1703                )
1704                .blocker,
1705                EgressNamespaceBlocker::CapabilityGrantMissing,
1706                "{failed:?} errno {errno}"
1707            );
1708        }
1709    }
1710
1711    #[test]
1712    fn a_creator_that_could_not_run_is_unavailable_and_keeps_its_detail() {
1713        let detail = "fork() for the namespace creator failed: Resource temporarily unavailable \
1714                      (os error 11)";
1715        let failure = failure(
1716            Err(NamespaceAttemptError::Creator(detail.to_string())),
1717            UsernsGrant::Withheld,
1718            ProfileAttachment::Unconfined,
1719        );
1720        assert_eq!(failure.blocker, EgressNamespaceBlocker::CreatorUnavailable);
1721        assert_eq!(failure.errno, None);
1722        let reason = failure.reason();
1723        assert!(reason.starts_with(detail), "{reason}");
1724        assert!(!reason.contains("apparmor_parser"), "{reason}");
1725        assert!(!reason.contains("CONFIG_USER_NS"), "{reason}");
1726        assert!(!reason.contains("user.max_user_namespaces"), "{reason}");
1727    }
1728
1729    #[test]
1730    fn non_linux_is_never_refused_for_a_mechanism_it_never_had() {
1731        for attempt in [
1732            Ok(()),
1733            step(CreationStep::Unshare, libc::EPERM),
1734            Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
1735        ] {
1736            assert_eq!(
1737                egress_namespace_failure(
1738                    false,
1739                    attempt,
1740                    UsernsGrant::Withheld,
1741                    ProfileAttachment::NotReported
1742                ),
1743                None
1744            );
1745        }
1746        #[cfg(not(target_os = "linux"))]
1747        assert_eq!(detect_egress_namespace_failure(), None);
1748    }
1749
1750    #[test]
1751    fn the_check_refuses_exactly_a_capsule_that_can_spawn_on_a_failing_host() {
1752        let failure = failure(
1753            step(CreationStep::Unshare, libc::EPERM),
1754            UsernsGrant::Withheld,
1755            ProfileAttachment::Unconfined,
1756        );
1757        match check_egress_namespace(true, Some(&failure)) {
1758            Err(crate::errors::RuntimeError::EgressNamespaceUnavailable {
1759                blocker,
1760                reason,
1761                ..
1762            }) => {
1763                assert_eq!(blocker, failure.blocker);
1764                assert_eq!(reason, failure.reason());
1765            }
1766            other => panic!("expected the E-CAP-005 refusal, got {other:?}"),
1767        }
1768        assert!(check_egress_namespace(false, Some(&failure)).is_ok());
1769        assert!(check_egress_namespace(true, None).is_ok());
1770        assert!(check_egress_namespace(false, None).is_ok());
1771    }
1772
1773    // ---- the reason ------------------------------------------------------------------------
1774
1775    #[test]
1776    fn an_unconfined_binary_on_a_restricted_host_is_told_the_profile_before_the_sysctl() {
1777        let reason = failure(
1778            step(CreationStep::Unshare, libc::EPERM),
1779            UsernsGrant::Withheld,
1780            ProfileAttachment::Unconfined,
1781        )
1782        .reason();
1783        assert!(
1784            reason.starts_with(
1785                "unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM (Operation not \
1786                 permitted)."
1787            ),
1788            "{reason}"
1789        );
1790        assert!(
1791            reason.contains("kernel.apparmor_restrict_unprivileged_userns=1"),
1792            "{reason}"
1793        );
1794        let profile = reason
1795            .find("apparmor_parser -r")
1796            .expect("the profile install command must be named");
1797        let sysctl = reason
1798            .find("kernel.apparmor_restrict_unprivileged_userns=0")
1799            .expect("the fallback must still be named for hosts that cannot load a profile");
1800        assert!(
1801            profile < sysctl,
1802            "the profile install must come first: {reason}"
1803        );
1804        assert!(reason.contains("LAST RESORT"), "{reason}");
1805        assert!(reason.contains("every program on the machine"), "{reason}");
1806        assert!(
1807            reason.contains("scripts/install-dev-apparmor.sh"),
1808            "{reason}"
1809        );
1810        assert!(
1811            reason.ends_with(
1812                "Host readings: kernel.apparmor_restrict_unprivileged_userns=1; AppArmor profile \
1813                 confining mur: unconfined."
1814            ),
1815            "{reason}"
1816        );
1817    }
1818
1819    #[test]
1820    fn a_binary_another_profile_confines_is_given_the_container_remedy_not_the_sysctl() {
1821        let reason = failure(
1822            step(CreationStep::Unshare, libc::EPERM),
1823            UsernsGrant::Withheld,
1824            ProfileAttachment::Other {
1825                profile: "docker-default".to_string(),
1826            },
1827        )
1828        .reason();
1829        assert!(reason.contains("`docker-default`"), "{reason}");
1830        assert!(!reason.contains("--cap-add"), "{reason}");
1831        assert!(reason.contains("seccomp profile"), "{reason}");
1832        assert!(
1833            reason.contains("unshare(CLONE_NEWUSER) and setns"),
1834            "{reason}"
1835        );
1836        assert!(reason.contains("seccompProfile: Localhost"), "{reason}");
1837        assert!(
1838            !reason.contains("kernel.apparmor_restrict_unprivileged_userns=0"),
1839            "{reason}"
1840        );
1841        assert!(!reason.contains("apparmor_parser"), "{reason}");
1842        assert!(
1843            reason.ends_with("AppArmor profile confining mur: docker-default."),
1844            "{reason}"
1845        );
1846    }
1847
1848    #[test]
1849    fn a_failure_under_the_sealed_profile_points_at_a_stale_load() {
1850        let reason = failure(
1851            step(CreationStep::Unshare, libc::EPERM),
1852            UsernsGrant::ProfileConfining,
1853            ProfileAttachment::Sealed {
1854                profile: "mur-sealed".to_string(),
1855            },
1856        )
1857        .reason();
1858        assert!(
1859            reason.contains("sudo apparmor_parser -r /etc/apparmor.d/mur-sealed"),
1860            "{reason}"
1861        );
1862        assert!(reason.contains("stale"), "{reason}");
1863        assert!(!reason.contains("LAST RESORT"), "{reason}");
1864    }
1865
1866    #[test]
1867    fn without_the_restriction_the_cause_is_not_apparmor() {
1868        for (grant, readings) in [
1869            (
1870                UsernsGrant::RestrictionDisabledHostWide,
1871                "kernel.apparmor_restrict_unprivileged_userns=0",
1872            ),
1873            (UsernsGrant::ApparmorAbsent, "AppArmor not enabled"),
1874        ] {
1875            for attachment in [
1876                ProfileAttachment::Unconfined,
1877                ProfileAttachment::NotReported,
1878            ] {
1879                let reason =
1880                    failure(step(CreationStep::Unshare, libc::EPERM), grant, attachment).reason();
1881                assert!(
1882                    !reason.contains("apparmor_restrict_unprivileged_userns=1"),
1883                    "{reason}"
1884                );
1885                assert!(!reason.contains("apparmor_parser"), "{reason}");
1886                assert!(!reason.contains("--cap-add"), "{reason}");
1887                assert!(!reason.contains("CAP_SYS_ADMIN"), "{reason}");
1888                assert!(
1889                    reason.contains("seccomp profile is the usual blocker"),
1890                    "{reason}"
1891                );
1892                assert!(
1893                    reason.contains("unshare(CLONE_NEWUSER) and setns"),
1894                    "{reason}"
1895                );
1896                assert!(reason.contains("--security-opt seccomp=<file>"), "{reason}");
1897                assert!(
1898                    reason.contains(&format!("Host readings: {readings};")),
1899                    "{reason}"
1900                );
1901            }
1902        }
1903    }
1904
1905    /// On a host without AppArmor `/proc/self/attr/current` carries another module's label, which
1906    /// is no AppArmor profile to blame.
1907    #[test]
1908    fn another_security_modules_label_is_not_blamed_as_an_apparmor_profile() {
1909        let reason = failure(
1910            step(CreationStep::Unshare, libc::EPERM),
1911            UsernsGrant::ApparmorAbsent,
1912            ProfileAttachment::Other {
1913                profile: "unconfined_u:unconfined_r:unconfined_t:s0".to_string(),
1914            },
1915        )
1916        .reason();
1917        assert!(reason.contains("not AppArmor"), "{reason}");
1918        assert!(!reason.contains("the AppArmor profile `"), "{reason}");
1919    }
1920
1921    #[test]
1922    fn a_namespace_created_and_then_refused_says_which_half_failed() {
1923        let configure = failure(
1924            step(CreationStep::Loopback, libc::EPERM),
1925            UsernsGrant::RestrictionDisabledHostWide,
1926            ProfileAttachment::Unconfined,
1927        )
1928        .reason();
1929        assert!(
1930            configure.starts_with("bringing lo up failed with EPERM"),
1931            "{configure}"
1932        );
1933        assert!(configure.contains("CAP_NET_ADMIN"), "{configure}");
1934
1935        let owned = failure(
1936            step(CreationStep::IdentityMaps, libc::EACCES),
1937            UsernsGrant::RestrictionDisabledHostWide,
1938            ProfileAttachment::Unconfined,
1939        )
1940        .reason();
1941        assert!(
1942            owned.starts_with("writing uid_map/gid_map failed with EACCES (Permission denied)."),
1943            "{owned}"
1944        );
1945        assert!(owned.contains("could not be owned"), "{owned}");
1946    }
1947
1948    #[test]
1949    fn missing_kernel_support_names_the_limit_or_the_config_symbol_and_no_apparmor() {
1950        for (errno, named) in [
1951            (libc::ENOSPC, "user.max_user_namespaces"),
1952            (libc::EINVAL, "CONFIG_USER_NS"),
1953            (libc::ENOSYS, "CONFIG_USER_NS"),
1954            (libc::EUSERS, "nesting limit"),
1955        ] {
1956            let reason = failure(
1957                step(CreationStep::Unshare, errno),
1958                UsernsGrant::Withheld,
1959                ProfileAttachment::Unconfined,
1960            )
1961            .reason();
1962            assert!(reason.contains(named), "{reason}");
1963            assert!(
1964                reason.contains(errno_name(errno).expect("a named errno")),
1965                "{reason}"
1966            );
1967            assert!(!reason.contains("apparmor_parser"), "{reason}");
1968            assert!(!reason.contains("LAST RESORT"), "{reason}");
1969        }
1970    }
1971
1972    #[test]
1973    fn an_unnamed_errno_is_rendered_by_number() {
1974        assert_eq!(errno_name(libc::EPERM), Some("EPERM"));
1975        assert_eq!(errno_name(-7), None);
1976        let failure = failure(
1977            step(CreationStep::HandOff, 4095),
1978            UsernsGrant::Withheld,
1979            ProfileAttachment::Unconfined,
1980        );
1981        assert_eq!(
1982            failure.summary(),
1983            "handing the namespace to the runtime failed with errno 4095"
1984        );
1985    }
1986
1987    #[test]
1988    fn the_summary_names_the_call_and_the_errno() {
1989        let failed = failure(
1990            step(CreationStep::Unshare, libc::EPERM),
1991            UsernsGrant::Withheld,
1992            ProfileAttachment::Unconfined,
1993        );
1994        assert_eq!(
1995            failed.summary(),
1996            "unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM"
1997        );
1998        let creator = failure(
1999            Err(NamespaceAttemptError::Creator("fork() failed".to_string())),
2000            UsernsGrant::Withheld,
2001            ProfileAttachment::Unconfined,
2002        );
2003        assert_eq!(creator.summary(), "fork() failed");
2004    }
2005
2006    #[test]
2007    fn every_step_round_trips_through_its_code() {
2008        for step in CreationStep::ALL {
2009            assert_eq!(CreationStep::from_code(step as u32), Some(step));
2010        }
2011        assert_eq!(CreationStep::from_code(0), None);
2012        assert_eq!(CreationStep::from_code(99), None);
2013    }
2014
2015    // ---- one probe, one reason -------------------------------------------------------------
2016
2017    /// A step that fails in a session's own creation is refused with exactly the text staging
2018    /// renders for the same step, `errno` and readings.
2019    #[test]
2020    fn a_session_creation_failure_is_refused_in_the_staging_words() {
2021        for (failed, errno, grant, attachment) in [
2022            (
2023                CreationStep::Unshare,
2024                libc::EPERM,
2025                UsernsGrant::Withheld,
2026                ProfileAttachment::Unconfined,
2027            ),
2028            (
2029                CreationStep::LocalRoute,
2030                libc::EPERM,
2031                UsernsGrant::Withheld,
2032                ProfileAttachment::Other {
2033                    profile: "docker-default".to_string(),
2034                },
2035            ),
2036            (
2037                CreationStep::Unshare,
2038                libc::ENOSPC,
2039                UsernsGrant::RestrictionDisabledHostWide,
2040                ProfileAttachment::Unconfined,
2041            ),
2042        ] {
2043            let staging =
2044                egress_namespace_failure(true, step(failed, errno), grant, attachment.clone());
2045            let staging_refusal = check_egress_namespace(true, staging.as_ref())
2046                .expect_err("staging refuses")
2047                .to_string();
2048            let establish = session_netns_creation_error(
2049                NamespaceAttemptError::Step {
2050                    step: failed,
2051                    errno,
2052                },
2053                grant,
2054                attachment,
2055            );
2056            assert_eq!(establish, format!("sandbox: E-CAP-005: {staging_refusal}"));
2057            assert!(
2058                establish.contains(&staging.expect("a failure").reason()),
2059                "{establish}"
2060            );
2061        }
2062    }
2063
2064    #[test]
2065    fn a_session_creator_that_could_not_run_keeps_the_runtime_account() {
2066        let error = session_netns_creation_error(
2067            NamespaceAttemptError::Creator("fork() for the namespace creator failed".to_string()),
2068            UsernsGrant::Withheld,
2069            ProfileAttachment::Unconfined,
2070        );
2071        assert_eq!(
2072            error,
2073            "sandbox: could not create this session's network namespace: fork() for the \
2074             namespace creator failed"
2075        );
2076    }
2077
2078    // ---- the socket plan --------------------------------------------------------------------
2079
2080    #[test]
2081    fn the_socket_count_is_one_per_port_plus_the_resolver() {
2082        let plan = CapsuleNetnsPlan {
2083            tcp_ports: vec![80, 443],
2084            uid: 1000,
2085            gid: 1000,
2086        };
2087        assert_eq!(plan.socket_count(), 3);
2088    }
2089
2090    #[test]
2091    fn a_capsule_with_no_tcp_egress_still_binds_the_resolver() {
2092        // Load-bearing: with nothing bound on 53 a lookup would be a dropped packet, and the
2093        // capsule would stall through glibc's whole retry schedule instead of being told no.
2094        let plan = CapsuleNetnsPlan {
2095            tcp_ports: Vec::new(),
2096            uid: 1000,
2097            gid: 1000,
2098        };
2099        assert_eq!(plan.socket_count(), 1);
2100    }
2101
2102    #[test]
2103    fn the_widest_allowlist_still_fits_one_scm_rights_message() {
2104        let plan = CapsuleNetnsPlan {
2105            tcp_ports: (0..crate::egress_proxy::MAX_EGRESS_TCP_PORTS as u16).collect(),
2106            uid: 1000,
2107            gid: 1000,
2108        };
2109        assert!(plan.socket_count() <= MAX_NAMESPACE_SOCKETS);
2110        // The two namespace handles ride in the same message.
2111        assert_eq!(MAX_SESSION_NETNS_FDS, MAX_NAMESPACE_SOCKETS + 2);
2112    }
2113
2114    // ---- the session's namespace ---------------------------------------------------------
2115
2116    /// An ended session is never given a namespace, including one that never created one.
2117    #[cfg(target_os = "linux")]
2118    #[test]
2119    fn an_ended_session_refuses_to_create_a_namespace() {
2120        let netns = SessionNetns::new();
2121        netns.end();
2122        let refusal = netns
2123            .establish(
2124                &[],
2125                crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new()),
2126            )
2127            .expect_err("an ended session creates nothing");
2128        assert!(refusal.contains("session has ended"), "{refusal}");
2129        assert_eq!(netns.creations(), 0);
2130    }
2131
2132    /// One creation serves every later call, and the namespace it made is the one each call
2133    /// is handed; ending it is final and repeatable.
2134    #[cfg(target_os = "linux")]
2135    #[test]
2136    fn a_session_creates_its_namespace_once_and_ends_it_for_good() {
2137        if skip_without_egress_namespace(
2138            "a_session_creates_its_namespace_once_and_ends_it_for_good",
2139        ) {
2140            return;
2141        }
2142        let policy = || crate::egress_proxy::EgressPolicy::new(Vec::new(), Vec::new());
2143        let netns = SessionNetns::new();
2144        assert_eq!(
2145            netns.creations(),
2146            0,
2147            "nothing is created before it is needed"
2148        );
2149
2150        let first = netns
2151            .establish(&[], policy())
2152            .expect("the namespace is created");
2153        let second = netns
2154            .establish(&[], policy())
2155            .expect("the namespace is reused");
2156        assert_eq!(netns.creations(), 1);
2157        let inode = |fd: std::os::fd::RawFd| {
2158            std::fs::metadata(format!("/proc/self/fd/{fd}"))
2159                .map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
2160                .unwrap()
2161        };
2162        assert_eq!(inode(first.raw_fds().1), inode(second.raw_fds().1));
2163        let host_netns = std::fs::metadata("/proc/self/ns/net")
2164            .map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
2165            .unwrap();
2166        assert_ne!(
2167            inode(first.raw_fds().1),
2168            host_netns,
2169            "the session's namespace is not the runtime's own"
2170        );
2171
2172        netns.end();
2173        netns.end();
2174        assert!(netns.establish(&[], policy()).is_err());
2175        assert_eq!(
2176            netns.creations(),
2177            1,
2178            "an ended namespace is never recreated"
2179        );
2180    }
2181
2182    /// The creator's failure record survives the round trip, so the parent names the step and
2183    /// the errno rather than a bare exit status.
2184    #[cfg(target_os = "linux")]
2185    #[test]
2186    fn a_creation_failure_names_its_step_and_errno() {
2187        assert_eq!(
2188            linux::decode_failure_record(4, libc::EPERM),
2189            Some(NamespaceAttemptError::Step {
2190                step: CreationStep::LocalRoute,
2191                errno: libc::EPERM,
2192            })
2193        );
2194        assert_eq!(linux::decode_failure_record(99, 0), None);
2195    }
2196
2197    /// The probe is the launch's own construction, so on a host where it reports no failure a
2198    /// session creates its namespace, and on one where it reports a failure the session's
2199    /// creation fails at the same step.
2200    #[cfg(target_os = "linux")]
2201    #[test]
2202    fn the_probe_and_a_session_creation_agree_about_this_host() {
2203        let probe = detect_egress_namespace_failure();
2204        let session = linux::create_session_netns(&CapsuleNetnsPlan::resolve(Vec::new())).map(drop);
2205        match (&probe, &session) {
2206            (None, Ok(())) => {}
2207            (Some(failure), Err(NamespaceAttemptError::Step { step, errno })) => {
2208                assert_eq!(failure.failed_call, step.describe());
2209                assert_eq!(failure.errno, Some(*errno));
2210            }
2211            other => panic!("the probe and a session creation disagree: {other:?}"),
2212        }
2213    }
2214}