Expand description
The capsule’s own network namespace: the structural half of the mechanism that replaced the
seccomp-notify connect/sendto supervisor.
[crate::egress_proxy] decides which destinations a native subprocess may reach. This module
is what makes that decision the only way out — it puts the subprocess tree somewhere with no
route off the host, so an unlisted destination is not “denied” by a filter that has to be
consulted, it is simply unreachable.
§The syscall sequence
Everything happens in the forked child’s pre_exec window, before the seccomp filter (which
denies unshare, setns and socket(AF_NETLINK) outright) and before crate::sealed’s
composed root:
unshare(CLONE_NEWUSER | CLONE_NEWNET), then the identityuid_map/gid_mapwrites that make the child the owner of both. The user namespace is not incidental:CLONE_NEWNETalone needsCAP_NET_ADMIN, which an unprivileged process does not have, and creating it inside a user namespace it owns is what supplies that capability there and nowhere else. This is the same primitivesealeduses and the same AppArmor restriction governs it — seeEgressNamespaceBlocker.lois brought up ([bring_loopback_up]). It is the namespace’s only interface: there is no veth pair, no bridge and no NAT, so nothing in the namespace has a path to the host’s network or to the outside world.- One route is installed over netlink ([
add_local_default_route]):local default dev lo. Alocal-type default route makes every address locally deliverable, so aconnect()to any address is looped back to a socket in this namespace with the original destination intact — recoverable withgetsockname(2). That is the entire interception mechanism, and it is why no netfilter rule is needed anywhere. It also means a lookup reaches the resolver whatever address/etc/resolv.confnames, and a connection to a port nothing is bound to is refused immediately rather than stalling through the kernel’s full SYN-retry schedule against a blackhole. - The listening sockets are created here, inside the namespace — one TCP listener per
port the allowlist implies, then one wildcard UDP :53 resolver socket — and handed back
to the parent in a single
SCM_RIGHTSmessage. A socket belongs to the namespace it was created in, not the one it is used from, which is the whole reason this inversion works: the runtime serves them while staying in the host’s namespace, with real internet access for the upstream half.
§Why this shape rather than the obvious ones
No veth pair. Wiring a namespace to the host with veth requires CAP_NET_ADMIN in the
host’s network namespace, which means real root. This design needs no host privilege at
all, which is what lets mur install per-user.
No proxy process inside the namespace. setns(2) into the namespace from a thread of the
runtime is not possible: it requires CAP_SYS_ADMIN in the caller’s own user namespace, and
an unprivileged multithreaded process cannot get it (unshare(CLONE_NEWUSER) requires a
single-threaded process). Passing the listening sockets out inverts the problem and needs no
privilege whatsoever. This was established empirically, not assumed — the setns route was
tried first and returned EPERM.
No iptables/nft/TPROXY. Nothing here needs a netfilter rule, so there is no dependency
on netfilter modules being present, no iptables binary to shell out to, and no rule set that
can drift out of step with the manifest.
§What this does not isolate
AF_UNIX is untouched. CLONE_NEWNET does not mediate unix sockets in any way that matters
here — a pathname socket is reached through the filesystem, not the network stack. That is
exactly why the register-level socket(2) domain filter (denied_socket_domains in
[crate::sandbox]) is untouched here and remains the thing that refuses
/var/run/docker.sock.
Enums§
- Egress
Namespace Blocker - Why this host cannot give a capsule its own network namespace.
Functions§
- check_
egress_ namespace - Refuses the launch when a capsule that can spawn native subprocesses is on a host that cannot give them a network namespace.
- detect_
egress_ namespace_ blocker - Which blocker, if any, stands between this host and a capsule network namespace.
- skip_
without_ egress_ namespace - Whether a test that needs a capsule network namespace must stand down on this host, printing the blocker when it must.