Skip to content

Feature: egress allowlist via slirp4netns + iptables in sandbox netns #27

@haard

Description

@haard

Problem

Network isolation today is binary: unshare_net = true gives the sandbox zero
network, unshare_net = false (default) gives it the full host network.
Nothing in between. A common real-world need is:

  • Allow outbound to api.anthropic.com (Claude Code), github.com (git),
    the company VPN range, PyPI, etc.
  • Block everything else, so a compromised tool or dependency can't exfiltrate
    to an arbitrary host.

Where the privilege boundary actually sits

When bwrap --unshare-user --unshare-net maps the host uid to uid 0 inside
a new user namespace
, the new net namespace is owned by that user ns, and
uid 0 in the owning user ns holds CAP_NET_ADMIN over that netns. Inside
the sandbox we can legitimately run iptables -A OUTPUT ... on the sandbox's
own chains.

What we cannot do unprivileged is touch the host netns (create veth pairs,
set up host-side NAT). That's where slirp4netns comes in.

Architecture

sandbox (new userns, uid 0, new netns)
  │
  ├── tap0 (only interface) ←──────────────────┐
  │        │                                   │
  │        iptables OUTPUT chain (the filter)  │
  │        │                                   │
  │        v                                   │
  └── slirp4netns userspace TCP/IP stack  ←── plumbing
           │
           v
      host socket() as real uid → internet

Two layers, cleanly separated:

  1. slirp4netns — plumbing. Userspace TCP/IP stack (forked from QEMU
    SLIRP). Creates a tap device inside the sandbox netns, parses packets
    in userspace, relays connections via socket() on the host. No root
    needed. Packaged on every major distro, used by podman/buildah for
    rootless containers. Does essentially no filtering — flags like
    --disable-host-loopback, --enable-sandbox, --enable-seccomp
    harden slirp4netns itself, not the sandbox's egress.
  2. iptables in the sandbox netns — policy. Installed inside the
    sandbox before the shell exec, generated from the TOML allowlist. This
    is what firejail's --netfilter does.

Proposed TOML

[sandbox]
unshare_net = true                # now meaningful default-on when net_allow is set

[sandbox.net_allow]
  hosts = [
      "api.anthropic.com",
      "github.com",
      "*.pypi.org",
      "files.pythonhosted.org",
  ]
  # cidrs = ["10.0.0.0/8"]          # optional: raw CIDR allow
  # log_denials = true              # optional: log dropped connections

Implementation sketch

  1. Add slirp4netns to optional deps (pwrap --check-deps detects).
  2. When [sandbox.net_allow] is present:
    • Build bwrap args with --unshare-user --unshare-net (+ --uid 0 if
      not already mapped by vault code).
    • Pre-shell setup step:
      a. Resolve each hosts entry to IPs (DNS at rule-install time).
      b. Generate iptables-restore input: default OUTPUT DROP, then
      ACCEPT for each resolved IP + established/related, plus DNS
      allow to slirp4netns's forwarder.
      c. Apply via iptables-restore (inside the sandbox netns, using the
      CAP_NET_ADMIN we hold there).
  3. Launch slirp4netns with --configure --mtu=65520 --disable-host-loopback; pass tap fd to bwrap via --net-socket.
  4. Write /etc/resolv.conf pointing at slirp4netns's DNS (default 10.0.2.3).

Caveats

  • IP staleness: CDN-hosted hosts rotate IPs. Options, ranked:
    1. Re-resolve periodically inside the sandbox and refresh rules.
    2. ipset referenced by iptables + DNS-driven set updates.
  • Hardcoded-IP bypass: attacker with a baked-in exfil IP skips DNS.
    Mitigation: filtering DNS stub in the netns that returns NXDOMAIN for
    non-allowlisted names, combined with default-drop iptables.
  • Shared IPs / SNI: IP-only rules can't distinguish tenants on shared
    CDN IPs. True hostname filtering needs an SNI-aware proxy — out of scope.
  • ICMP / non-TCP: slirp4netns handles ICMP via host SOCK_DGRAM;
    filter like any other protocol in the rules.

Non-goals

Prior art

  • firejail --netfilter — ships iptables templates, loads inside the
    sandbox netns. Same mechanics; pwrap would generate rules from TOML.
  • podman / buildah rootless — slirp4netns as plumbing (no egress
    filter, containers trusted).
  • flatpak network sandbox — bwrap + slirp4netns in production.

Why now

Issue #23 closed the at-rest leak surface (deny-by-default home,
clean_env = true). The remaining exfil surface is the network: code
running in the sandbox can still reach arbitrary hosts. Egress control
closes that loop.

Earlier (incorrect) framing

An earlier draft of this issue claimed iptables was unreachable because
bwrap drops CAP_NET_ADMIN. That's wrong for the relevant case: when
--unshare-user maps uid to 0 in a new user ns, the new netns is owned
by that user ns and we hold CAP_NET_ADMIN on the sandbox netns, just
not on the host's. The proposal above relies on exactly that.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions