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