Air-gapped sandboxes

The E2B SDK accepts an allow_internet_access boolean on sandbox create. Setting it to False means the sandbox must run without the open internet — useful for evaluating an untrusted prompt’s code, or for compliance postures that require egress logging/allowlisting. Coppice implements the flag by extending the per-sandbox pf anchor that Wave A already used for live network policy (eBPF → pf). No new machinery — just a different fragment shape.

The fragment

Every sandbox gets its own pf anchor at cube/sandbox-<short>, where <short> is the first twelve hex chars of the sandbox id. The root ruleset has a single anchor “cube/*” directive installed by coppice-pool-ctl.sh init, so whatever we load into cube/sandbox-<short> takes effect immediately for traffic matching that anchor’s path.

For a broad-allow sandbox (default), the fragment is a no-op:

table <sandbox_allow> persist {}
table <sandbox_deny>  persist {}
block quick from any to <sandbox_deny>
pass  quick from any to <sandbox_allow>

For allow_internet_access=false, the fragment installs a default-deny with loopback and an optional DNS allowlist punched through:

table <sandbox_allow> persist {}
table <sandbox_deny>  persist {}
table <sandbox_dns>   persist { 1.1.1.1, 8.8.8.8 }
pass  quick on lo0
pass  quick proto { tcp, udp } from any to <sandbox_dns> port 53
pass  quick from any to <sandbox_allow>
block quick from any to <sandbox_deny>
block quick all

The <sandbox_dns> table is populated from COPPICE_DNS_ALLOWLIST, a comma-separated env var read by the gateway at startup. Leave it unset and the air-gapped sandbox has no DNS path at all — which is usually what you want when the point is to keep a suspicious prompt from phoning home.

The pf evaluation order matters: quick short-circuits, so the first matching rule wins. Loopback always passes, then DNS to the allowlisted resolvers, then the caller’s allow_out list (via <sandbox_allow>), then the caller’s deny_out list, then the default-deny catches everything else.

The live toggle

PUT /sandboxes/:id/network accepts an optional air_gapped field alongside the existing allow_out / deny_out. Behaviors:

The handler persists the new mode on the in-memory Sandbox record, so GET /sandboxes/:id’s allowInternetAccess field reflects the live state, not the create-time value.

What it does and doesn’t prevent

Prevents outbound TCP/UDP to any destination not explicitly allowed. That’s the whole story pf can tell when the jail uses ip4=inherit and shares the host’s IP: everything the sandbox tries to send to the external interface goes through the cube anchor and gets dropped. Ping / ICMP, DNS, HTTP, HTTPS, arbitrary SOCKS — all blocked unless allowlisted.

Does not prevent three things worth naming:

The receipt

benchmarks/rigs/air-gapped-smoke.sh creates two sandboxes, one air-gapped and one default, and asserts four things:

  1. External reachability is blocked on the air-gapped sandbox within a 5-second fetch timeout.
  2. Loopback still reaches the gateway’s own /health.
  3. PUT /network with air_gapped=false restores outbound to the external target.
  4. A sandbox created with the default (air-gap off) reaches external destinations immediately with no PUT.

Runs green on honor against a gateway built from master with the B3 commit. The rig dumps pfctl -a cube/sandbox-<short> -sr mid-run so the transcript shows exactly which fragment was live.