Skip to content

Commit de56e1a

Browse files
author
Ryan Kaufman
committed
modified disable internet function, allows for container specific blockers
1 parent 5552c75 commit de56e1a

3 files changed

Lines changed: 198 additions & 2 deletions

File tree

project/alcatraz/alcatraz/clusters/local.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,97 @@ async def _create_container(
434434
stack.__exit__(*sys.exc_info())
435435
raise
436436

437+
async def _install_firewall_stub(self, container: docker.models.containers.Container) -> None:
438+
"""
439+
On every container installation, we add an initially empty chain (named CTR-<cid>).
440+
Then, we add a jump that only sends packets on the forward chain from this specific container to that currently empty chain.
441+
This allows us to implement container specific internet blocking rules (if desired) without impacting parallel/future containers.
442+
Both are tagged with --comment "alcatraz_block" so we can find/remove them later.
443+
Important note: This does NOT change the internet perms of the container, it only allows us to create unique ones later.
444+
"""
445+
# Dont forget to update _remove_firewall_stub when modifying this!!
446+
container.reload()
447+
cid = container.id[:12]
448+
sandbox_key = container.attrs["NetworkSettings"].get("SandboxKey", "")
449+
veth = Path(sandbox_key).name
450+
ctr_ip = next(iter(container.attrs["NetworkSettings"]["Networks"].values()))["IPAddress"] # e.g. 172.18.0.2
451+
net_info = next(iter(container.attrs["NetworkSettings"]["Networks"].values()))
452+
bridge_id = net_info["NetworkID"][:12]
453+
bridge_if = f"br-{bridge_id}"
454+
455+
# Make a unique chain for the container -- atm silences all errors (such that if chain already exists it gets cleared), but TODO: separate errors
456+
await async_subprocess_run(
457+
["bash", "-c", f"iptables -N CTR-{cid} 2>/dev/null || true"]
458+
)
459+
await async_subprocess_run(["sudo", "iptables", "-F", f"CTR-{cid}"])
460+
461+
# Insert jump from forward to this new container specific chain
462+
await async_subprocess_run(
463+
[
464+
"bash",
465+
"-c",
466+
(
467+
f"iptables -C DOCKER-USER -i {bridge_if} -s {ctr_ip} -j CTR-{cid} "
468+
f"-m comment --comment alcatraz_block 2>/dev/null || "
469+
f"iptables -I DOCKER-USER 1 -i {bridge_if} -s {ctr_ip} -j CTR-{cid} "
470+
f"-m comment --comment alcatraz_block"
471+
),
472+
]
473+
)
474+
logger.info("Added jump for veth %s --> CTR-%s", veth, cid)
475+
# remove container specific jumps for host cleanliness
476+
self._exit_stack.callback(
477+
lambda cid=cid, veth=veth, bridge_if=bridge_if, ctr_ip=ctr_ip: asyncio.create_task(self._remove_firewall_stub(cid, veth, bridge_if, ctr_ip))
478+
)
479+
480+
async def _remove_firewall_stub(self, cid: str, veth: str, bridge_if: str, ctr_ip: str) -> None:
481+
"""
482+
Reverse the work of _install_firewall_stub **and** _ensure_input_block.
483+
If either of those change without mirrored updates here, proper cleanup can't happen.
484+
This function deletes the FORWARD jump and deletes the container specific chain (both done by _install_firewall_stub)
485+
It also removes the INPUT rules done by _ensure_input_block.
486+
"""
487+
# deletes the FORWARD jump
488+
489+
await async_subprocess_run(
490+
[
491+
"bash",
492+
"-c",
493+
(
494+
f"iptables -D DOCKER-USER -i {bridge_if} -s {ctr_ip} -j CTR-{cid} "
495+
f"-m comment --comment alcatraz_block 2>/dev/null || true"
496+
),
497+
]
498+
)
499+
500+
# clears and deletes the container specific chain
501+
await async_subprocess_run(["bash", "-c", f"iptables -F CTR-{cid} 2>/dev/null || true"])
502+
await async_subprocess_run(["bash", "-c", f"iptables -X CTR-{cid} 2>/dev/null || true"])
503+
504+
# deletes the container specific INPUT rules (order independent but exact rule dependent -- must be exactly what is added in _ensure_input_block)
505+
await async_subprocess_run(
506+
[
507+
"bash",
508+
"-c",
509+
(
510+
f"iptables -D INPUT -i {veth} -m conntrack --ctstate RELATED,ESTABLISHED "
511+
f"-j ACCEPT -m comment --comment alcatraz_block 2>/dev/null || true"
512+
),
513+
]
514+
)
515+
await async_subprocess_run(
516+
[
517+
"bash",
518+
"-c",
519+
(
520+
f"iptables -D INPUT -i {veth} -j REJECT "
521+
f"-m comment --comment alcatraz_block 2>/dev/null || true"
522+
),
523+
]
524+
)
525+
526+
logger.info("Removed firewall stub and INPUT rules for veth %s → CTR-%s", veth, cid)
527+
437528
async def _pull_image(self, i: str) -> None:
438529
# Ideally, we would only retry if not docker.errors.NotFound, to crash quickly when the image is misspelled.
439530
# However, since this code is also used by all workers in the cluster, we can overload ACR. This often
@@ -596,6 +687,9 @@ async def _start(self) -> None:
596687
shm_size=self.shm_size if i == 0 else None,
597688
mem_limit=self.mem_limit if i == 0 else None,
598689
)
690+
691+
await self._install_firewall_stub(ctr) # this doesn't yet restrict container access, just allows future places to add container specific restrictions
692+
599693
self.containers.append(ctr)
600694

601695
if self.docker_compose_yaml:
@@ -980,6 +1074,105 @@ async def get_container_net_config(self) -> ContainerNetConfig:
9801074
"vm_private_ip": vm_private_ip,
9811075
}
9821076

1077+
async def add_container_network_block_via_ip_tables(self, cid_prefix: str) -> None:
1078+
"""
1079+
Instead of the universal blocks added by add_weak_network_block_via_ip_tables, this function distinguishes between containers.
1080+
Specifically, we add on startup a jump chain from DOCKER-USER to a unqiue container ID chain.
1081+
Then, we add the same rules added to DOCKER-USER in the below add_weak_network_block_via_ip_tables, but to this specific chain.
1082+
This way internet blocking rules apply only to this container, and not parallel/future containers.
1083+
The same logic applies for INPUT-chain rules (instead of jumping to the container specific chain we add container specific rules directly to the INPUT-chain)
1084+
"""
1085+
# Find the specific container to apply the rules to its chain, and then apply them
1086+
try:
1087+
container = next(c for c in self.containers if c.id.startswith(cid_prefix))
1088+
except StopIteration:
1089+
raise RuntimeError(f"No container with id prefix {cid_prefix} in this cluster")
1090+
1091+
await self._populate_ctr_chain(container)
1092+
1093+
await self._ensure_input_block(container)
1094+
1095+
try:
1096+
await self._check_shell_command(
1097+
"python -c \"import urllib.request; urllib.request.urlopen('http://example.com', timeout=5)\""
1098+
)
1099+
except Exception:
1100+
pass
1101+
else:
1102+
assert False, "Setting up network block with IP tables failed"
1103+
1104+
async def _populate_ctr_chain(self, container: docker.models.containers.Container) -> None:
1105+
"""
1106+
Add the three rules to the container specific chain.
1107+
"""
1108+
# Note: even if changed, _remove_firewall_stub does not necessarily need to be updated.
1109+
# The function _remove_firewall_stub currently just clears and deletes this chain entirely, so modifying what happens inside of the chain is okay.
1110+
# If something major is changed, like adding a jump to a new chain, make sure things are properly cleaned up by _remove_firewall_stub.
1111+
await asyncio.to_thread(container.reload)
1112+
1113+
cid = container.id[:12]
1114+
attrs = container.attrs
1115+
1116+
ctr_ip = next(iter(container.attrs["NetworkSettings"]["Networks"].values()))["IPAddress"] # e.g. 172.18.0.2
1117+
net_config = await self.get_container_net_config()
1118+
subnet = net_config["subnet"]
1119+
1120+
# Allow already established connections into the container
1121+
await async_subprocess_run(
1122+
["bash", "-c",
1123+
f"iptables -C CTR-{cid} -m conntrack --ctstate RELATED,ESTABLISHED "
1124+
f"-j ACCEPT -m comment --comment alcatraz_block 2>/dev/null || "
1125+
f"iptables -A CTR-{cid} -m conntrack --ctstate RELATED,ESTABLISHED "
1126+
f"-j ACCEPT -m comment --comment alcatraz_block"]
1127+
)
1128+
1129+
# Allow container to communicate within the Docker network (ctr -> ctr2)
1130+
await async_subprocess_run(
1131+
["bash", "-c",
1132+
f"iptables -C CTR-{cid} -s {subnet} -d {subnet} -j ACCEPT "
1133+
f"-m comment --comment alcatraz_block 2>/dev/null || "
1134+
f"iptables -A CTR-{cid} -s {subnet} -d {subnet} -j ACCEPT "
1135+
f"-m comment --comment alcatraz_block"]
1136+
)
1137+
1138+
# Reject all other outgoing connections from the container to the world
1139+
await async_subprocess_run(
1140+
["bash", "-c",
1141+
f"iptables -C CTR-{cid} -s {ctr_ip} -j REJECT "
1142+
f"-m comment --comment alcatraz_block 2>/dev/null || "
1143+
f"iptables -A CTR-{cid} -s {ctr_ip} -j REJECT "
1144+
f"-m comment --comment alcatraz_block"]
1145+
)
1146+
1147+
async def _ensure_input_block(self, container: docker.models.containers.Container) -> None:
1148+
"""
1149+
This adds two rules. One allows preexisting connections (ctr --> host okay if host initiated them). This is container specific -- we match by the veth as usual.
1150+
The second rule is also a container specific rule (matching the veth) which prevents the container from accessing the host.
1151+
"""
1152+
# Important note: dont forget to update _remove_firewall_stub when modifying this!!
1153+
# These rules need to be cleaned up on container shutdown, which is handled by _remove_firewall_stub
1154+
# If the added rules are modified, the cleanup needs to be edited too.
1155+
cid = container.id[:12]
1156+
veth = Path(container.attrs["NetworkSettings"]["SandboxKey"]).name
1157+
1158+
# Allow preexisting connections (so ctr -> host comms are allowed if the host initiated them). This is also separated by container.
1159+
await async_subprocess_run(
1160+
["bash", "-c",
1161+
f"iptables -C INPUT -i {veth} -m conntrack --ctstate RELATED,ESTABLISHED "
1162+
f"-m comment --comment alcatraz_block -j ACCEPT 2>/dev/null || "
1163+
f"iptables -I INPUT 1 -i {veth} -m conntrack --ctstate RELATED,ESTABLISHED "
1164+
f"-m comment --comment alcatraz_block -j ACCEPT"]
1165+
)
1166+
1167+
# Prevent container from accessing host. (INPUT = host input)
1168+
await async_subprocess_run(
1169+
["bash", "-c",
1170+
f"iptables -C INPUT -i {veth} -j REJECT "
1171+
f"-m comment --comment alcatraz_block 2>/dev/null || "
1172+
f"iptables -I INPUT 2 -i {veth} -j REJECT "
1173+
f"-m comment --comment alcatraz_block"]
1174+
)
1175+
9831176
async def add_weak_network_block_via_ip_tables(self) -> None:
9841177
"""
9851178
Blocking internet access with IP tables isn't super secure tbh. Model can escape container to change the VM level IP table.

project/nanoeval_alcatraz/nanoeval_alcatraz/alcatraz_computer_interface.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ def cluster(self) -> BaseAlcatrazCluster:
4545
pass
4646

4747
async def disable_internet(self) -> None:
48-
await self.cluster.add_weak_network_block_via_ip_tables()
48+
res = await self.send_shell_command("hostname")
49+
cid_prefix = res.output.decode().strip()
50+
51+
await self.cluster.add_container_network_block_via_ip_tables(cid_prefix)
4952

5053
# Verify
5154
logger.info("Post-setup network access disabled")

project/swelancer/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ gpt-4o:
148148
uv run python swelancer/run_swelancer.py \
149149
swelancer.split=diamond \
150150
swelancer.task_type=ic_swe \
151-
swelancer.taskset="['39848_1045']" \
151+
swelancer.taskset="['28565_1001']" \
152152
swelancer.solver=swelancer.solvers.swelancer_agent.solver:SimpleAgentSolver \
153153
swelancer.solver.model=openai/gpt-4o \
154154
swelancer.solver.computer_runtime=nanoeval_alcatraz.alcatraz_computer_interface:AlcatrazComputerRuntime \

0 commit comments

Comments
 (0)