Skip to content

How network isolation actually works

The agent sandbox has no default route. Its only next hop is the Squid proxy,
which checks network.allowed_hosts by dstdomain match, not by IP. api.groq.com is
CONNECT ALLOWED — the TLS tunnel opens and the server replies. evil.example.com is
CONNECT REFUSED with a 403; the tunnel is never opened. A note reads: matching is by
name, but blocking isn't — a separate rule denies destinations given as raw IPs,
including the real IP of an allowed host, so resolving a hostname yourself and
connecting straight to the address is not a way around the allowlist. Both attempts
appear in internal/audit/egress-probe-2026-08-08.jsonl as a network_allowed and a
network_blocked event. The agent sandbox has no default route. Its only next hop is the Squid proxy,
which checks network.allowed_hosts by dstdomain match, not by IP. api.groq.com is
CONNECT ALLOWED — the TLS tunnel opens and the server replies. evil.example.com is
CONNECT REFUSED with a 403; the tunnel is never opened. A note reads: matching is by
name, but blocking isn't — a separate rule denies destinations given as raw IPs,
including the real IP of an allowed host, so resolving a hostname yourself and
connecting straight to the address is not a way around the allowlist. Both attempts
appear in internal/audit/egress-probe-2026-08-08.jsonl as a network_allowed and a
network_blocked event.

The agent process has no route to the internet. The only reachable next hop is the proxy, which checks each CONNECT against allowed_hosts. Both backends render this policy from the same function (buildSquidConfig, internal/sandbox/docker.go), so Docker and Firecracker enforce the same ruleset.

Resolving a hostname inside the sandbox and connecting to the resulting address does not get around it — the raw IP of an allowed host is denied along with every other IP literal, because the allowlist is a dstdomain ACL that an address can never match:

declared hostname              https://api.groq.com/      CONNECT allowed
raw IP OF THE DECLARED HOST    https://172.64.149.20/     Tunnel connection failed: 403 Forbidden
raw IP, undeclared             https://1.1.1.1/           Tunnel connection failed: 403 Forbidden
undeclared hostname            https://evil.example.com/  Tunnel connection failed: 403 Forbidden

Ignoring the proxy isn't an option either

Everything above assumes the agent goes through the proxy. It can't do otherwise: the sandbox has no route that reaches anything else. On Docker the agent's network is created --internal, its routing table holds a single on-link entry and no default route at all, and the only address family present is IPv4. Attempting to dial out directly, with the proxy environment cleared:

IPv6  2606:4700:4700::1111   OSError: [Errno 101] Network is unreachable
IPv4  1.1.1.1                OSError: [Errno 101] Network is unreachable

DNS doesn't resolve in there either — the sandbox cannot look up an address, let alone route to one.

IPv6 in particular is closed on both backends, and closed deterministically rather than environment-dependently: the internal network is created with an explicit --ipv6=false instead of inheriting whatever the operator's Docker daemon defaults to, so this guarantee is a property of the code and not of the host it runs on. The Firecracker guest gets only a kernel-generated link-local fe80:: address, never a global one or a ::/0 route, and its per-run nftables table drops the tap interface in the dual-family inet table — so the same rule covers both families.

One caveat, stated because it's the kind of thing that rots quietly: the proxy's raw-IP ACL (ip_only) is IPv4-only, with no ::/0 counterpart. Nothing rides on it today — an IPv6 literal is refused by the config's trailing deny all, and nothing can reach the proxy over IPv6 anyway — but it would become a real gap if a future change gave a sandbox an IPv6 route without adding the matching rule. It's flagged in the code at buildSquidConfig.

What this does not cover

The proxy trusts DNS. If a declared hostname resolves to an address an attacker controls, the allowlist will let it through — name-based allowlisting is only as good as the name resolution behind it.

If a document the agent reads contains a hidden instruction to exfiltrate data to an undeclared host, that instruction has no path to succeed. The block happens at the network layer, below the model, whether or not the agent "knows" it is compromised — and the attempt lands in the audit log as a network_blocked event, which is how you find out it happened.