Skip to content

Commit 400c94e

Browse files
eligottsclaude
andcommitted
feat(v1): DockerConfig.network_access to block the workload's internet
Adds `network_access: bool = True` to DockerConfig (mirroring prime/modal, so `--harness.runtime.network-access false` now works uniformly across all three runtimes). When false, the container's DNS resolver is disabled so every hostname-based fetch (pip install, git clone, curl <host>) fails — while the interception model tunnel keeps working, because it egresses from the HOST, not the container: the container only reaches host.docker.internal / host loopback, which needs no DNS. Platform-specific (matching the existing host_url() split): - Docker Desktop (macOS/Windows): drop --network host for the bridge, add --dns 0.0.0.0 and --add-host=host.docker.internal:host-gateway (host route pinned in /etc/hosts, resolves without DNS). - Linux: keep --network host (interception is reachable at 127.0.0.1) and add --dns 0.0.0.0. --dns writes a container-private resolv.conf, so the host's own DNS is untouched. A DNS-level block, not an IP-level airgap: a hardcoded raw IP could still egress; use an iptables egress allowlist if that matters. Verified end-to-end: a local-docker eval with --harness.runtime.network-access false solves a task (reward 1.0, 59 model calls over the tunnel) while pip/git to public hosts fail from inside the container. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 13eed1f commit 400c94e

2 files changed

Lines changed: 65 additions & 3 deletions

File tree

tests/v1/test_docker_runtime.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""DockerRuntime network-arg construction: the `network_access=False` isolation is
2+
platform-specific, so pin both branches (pure logic, no docker daemon needed)."""
3+
4+
import verifiers.v1.runtimes.docker as docker_mod
5+
from verifiers.v1.runtimes.docker import DockerConfig, DockerRuntime
6+
7+
8+
def _args(network_access: bool, platform: str, monkeypatch) -> list[str]:
9+
monkeypatch.setattr(docker_mod.sys, "platform", platform)
10+
return DockerRuntime(DockerConfig(network_access=network_access))._network_args()
11+
12+
13+
def test_network_access_default_is_on() -> None:
14+
assert DockerConfig().network_access is True
15+
16+
17+
def test_access_on_uses_host_networking(monkeypatch) -> None:
18+
for platform in ("linux", "darwin", "win32"):
19+
assert _args(True, platform, monkeypatch) == ["--network", "host"]
20+
21+
22+
def test_blocked_linux_keeps_host_net_and_kills_dns(monkeypatch) -> None:
23+
# host netns already reaches the interception at 127.0.0.1; only DNS is broken
24+
assert _args(False, "linux", monkeypatch) == [
25+
"--network",
26+
"host",
27+
"--dns",
28+
"0.0.0.0",
29+
]
30+
31+
32+
def test_blocked_docker_desktop_uses_bridge_and_pins_host(monkeypatch) -> None:
33+
# Docker Desktop VM: bridge + dead DNS + host.docker.internal pinned (resolves w/o DNS)
34+
for platform in ("darwin", "win32"):
35+
assert _args(False, platform, monkeypatch) == [
36+
"--dns",
37+
"0.0.0.0",
38+
"--add-host",
39+
"host.docker.internal:host-gateway",
40+
]

verifiers/v1/runtimes/docker.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ class DockerConfig(BaseConfig):
3838
disk: float | None = None
3939
"""Advisory disk request in GB. Docker has no portable per-container size limit, so
4040
this is accepted (so a task can declare it without a warning) but not enforced."""
41+
network_access: bool = True
42+
"""Whether the workload can reach the internet. When False, the container's DNS resolver
43+
is disabled, so every hostname-based fetch (pip, git clone, curl <host>) fails — while the
44+
interception model tunnel keeps working, because it egresses from the HOST, not the
45+
container: the container only reaches `host.docker.internal` / host loopback, which needs
46+
no DNS. This is a DNS-level block, not an IP-level airgap (a hardcoded raw IP could still
47+
egress). Mirrors prime/modal's `network_access`, except those sever the tunnel too."""
4148

4249

4350
class DockerRuntimeInfo(DockerConfig, BaseRuntimeInfo):
@@ -98,8 +105,7 @@ async def start(self) -> None:
98105
run = await docker(
99106
"run",
100107
"--detach",
101-
"--network",
102-
"host",
108+
*self._network_args(),
103109
*limits,
104110
"--workdir",
105111
self.config.workdir,
@@ -115,11 +121,27 @@ async def start(self) -> None:
115121
:12
116122
] # `docker run -d` prints the container id
117123
logger.info(
118-
"docker: started container %s (image=%s)",
124+
"docker: started container %s (image=%s%s)",
119125
self._container,
120126
self.config.image,
127+
"" if self.config.network_access else ", internet blocked",
121128
)
122129

130+
def _network_args(self) -> list[str]:
131+
"""`docker run` networking args. Default: host networking. With network_access=False,
132+
block the workload's internet with a dead DNS resolver (hostname fetches fail) while
133+
keeping the host route the model tunnel needs — DNS-less, so it survives the block."""
134+
if self.config.network_access:
135+
return ["--network", "host"]
136+
if sys.platform == "linux":
137+
# Host netns already reaches the interception at 127.0.0.1; only break DNS. --dns
138+
# writes a container-private resolv.conf, so the host's own DNS is untouched.
139+
return ["--network", "host", "--dns", "0.0.0.0"]
140+
# Docker Desktop VM: --network host can't reach host loopback, so use the bridge and
141+
# pin host.docker.internal into /etc/hosts (resolves without DNS); host_url() already
142+
# points the tunnel there on non-Linux.
143+
return ["--dns", "0.0.0.0", "--add-host", "host.docker.internal:host-gateway"]
144+
123145
def host_url(self, url: str) -> str:
124146
# Docker Desktop (macOS/Windows) runs containers in a VM, so `--network host`
125147
# doesn't reach the host's loopback; `host.docker.internal` does.

0 commit comments

Comments
 (0)