Skip to content

Commit e403eb6

Browse files
thejoeejoeeOpenCode Agent
andauthored
fix(embed): fall back to iptables-legacy when nft backend is unsupported (#74)
* fix(embed): fall back to iptables-legacy when nft backend is unsupported Rancher Desktop with VZ virtualization (no Rosetta) runs a minimal ARM64 kernel that lacks CONFIG_NF_TABLES. The entrypoint now probes iptables-nft first, falls back to iptables-legacy, and aborts if neither works — network isolation is never silently skipped. * docs(installation): add Docker runtime compatibility matrix * fix(entrypoint): include actual error in iptables fallback message * docs: clarify iptables fallback chain in runtime compatibility notes * chore: bump Go to 1.26.2 * chore: revert Go version to 1.26.1 (nixpkgs-unstable lacks 1.26.2) --------- Co-authored-by: OpenCode Agent <agent@opencode>
1 parent 9643d66 commit e403eb6

3 files changed

Lines changed: 68 additions & 13 deletions

File tree

docs/explanation/network-isolation.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,11 @@ Any containers the agent starts through dind inherit the dind daemon's network c
6060
- The root filesystem is not read-only
6161

6262
These gaps are intentional or accepted tradeoffs, not oversights. The goal of jailoc is to protect your internal network and keep the agent's state from bleeding into your host environment — not to prevent the agent from doing its job on the public internet.
63+
64+
## Kernel requirements
65+
66+
The iptables rules require the container runtime's Linux kernel to support **netfilter**. On startup, jailoc probes the default `iptables` (nft backend) first; if that fails, it falls back to `iptables-legacy`. If neither backend works, the container aborts because network isolation cannot be enforced.
67+
68+
Most Docker runtimes ship kernels with full netfilter support, but some minimal hypervisor configurations do not. Rancher Desktop using VZ virtualization without Rosetta on macOS is a known case — its ARM64 kernel lacks netfilter entirely, so both backends fail with `Protocol not supported`.
69+
70+
See [Installation — Docker runtime compatibility](../how-to/installation.md#docker-runtime-compatibility) for a full list of tested runtimes.

docs/how-to/installation.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@ This guide covers installation methods and the prerequisites you need before run
66

77
- **Docker Engine** must be running on your machine. jailoc communicates with the Docker daemon directly.
88
- No `docker compose` CLI plugin is required. jailoc embeds the Compose SDK and manages containers without it.
9+
- The container runtime's Linux kernel must support **netfilter** (iptables). jailoc uses iptables rules inside the container to enforce [network isolation](../explanation/network-isolation.md). Runtimes whose kernel lacks netfilter support cannot run jailoc.
10+
11+
### Docker runtime compatibility
12+
13+
| Runtime | Platform | Status | Notes |
14+
|---------|----------|--------|-------|
15+
| Docker Engine | Linux || Native performance, no VM overhead |
16+
| OrbStack | macOS || Lightweight VM, fast file I/O — recommended on macOS |
17+
| Docker Desktop | macOS / Linux || VirtioFS file sharing; higher memory footprint than OrbStack |
18+
| Colima | macOS || Lima-based VM; performance depends on VM type (`vz` faster than `qemu`) |
19+
| Podman | macOS || VM-based on macOS; comparable to Docker Desktop |
20+
| Rancher Desktop (VZ + Rosetta) | macOS || Rosetta provides a more complete kernel with netfilter support |
21+
| Rancher Desktop (VZ, no Rosetta) | macOS || VZ hypervisor without Rosetta runs a minimal ARM64 kernel that lacks netfilter — jailoc probes both `iptables-nft` and `iptables-legacy` but neither works, so startup is aborted |
22+
| Docker Engine (rootless) | Linux | ⚠️ | Untested — DinD sidecar requires `--privileged`, which rootless mode may not support |
23+
| WSL2 + Docker | Windows | ⚠️ | Untested — the Linux binary may work under WSL2 with Docker Engine installed inside the distribution |
24+
25+
jailoc connects to whichever Docker daemon your current **docker context** points to. If your runtime uses a non-default socket path (common with Colima or Podman), make sure the active context is set correctly:
26+
27+
```bash
28+
# list available contexts
29+
docker context ls
30+
31+
# switch to a specific runtime
32+
docker context use colima
33+
```
934

1035
---
1136

internal/embed/assets/entrypoint.sh

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
#!/bin/bash
22
set -euo pipefail
33

4+
# --- Detect working iptables variant ---
5+
# Ubuntu 24.04 defaults to iptables-nft, which requires kernel nf_tables support.
6+
# Some environments (e.g. Rancher Desktop with VZ virtualization) lack this kernel
7+
# module, so we fall back to iptables-legacy. Abort if neither works — network
8+
# isolation is non-negotiable.
9+
if iptables -L -n >/dev/null 2>&1; then
10+
IPT=iptables
11+
else
12+
IPTABLES_ERROR="$(iptables -L -n 2>&1 || true)"
13+
if command -v iptables-legacy >/dev/null 2>&1 && iptables-legacy -L -n >/dev/null 2>&1; then
14+
IPT=iptables-legacy
15+
if [ -n "$IPTABLES_ERROR" ]; then
16+
echo "jailoc: iptables unusable ($IPTABLES_ERROR), using iptables-legacy" >&2
17+
else
18+
echo "jailoc: iptables unusable, using iptables-legacy" >&2
19+
fi
20+
else
21+
echo "jailoc: FATAL: no working iptables found, cannot enforce network isolation" >&2
22+
exit 1
23+
fi
24+
fi
25+
426
# --- Allow infrastructure targets ---
5-
iptables -I OUTPUT -d dind -j ACCEPT
27+
$IPT -I OUTPUT -d dind -j ACCEPT
628

729
HOST_IP=$(getent hosts host.docker.internal | awk '{print $1}' || true)
830
if [ -n "$HOST_IP" ]; then
9-
iptables -I OUTPUT -d "$HOST_IP" -j ACCEPT
31+
$IPT -I OUTPUT -d "$HOST_IP" -j ACCEPT
1032
fi
1133

1234
for GW in $(awk '$3 != "00000000" && $3 != "Gateway" {print $3}' /proc/net/route | sort -u); do
1335
GW_IP=$(printf '%d.%d.%d.%d' "0x${GW:6:2}" "0x${GW:4:2}" "0x${GW:2:2}" "0x${GW:0:2}")
14-
iptables -I OUTPUT -d "$GW_IP" -j ACCEPT
36+
$IPT -I OUTPUT -d "$GW_IP" -j ACCEPT
1537
done
1638

1739
# --- Allow hostnames from config ---
@@ -25,7 +47,7 @@ if [ -f "$ALLOWED_HOSTS" ]; then
2547
RESOLVED=$(getent hosts "$line" | awk '{print $1}' || true)
2648
if [ -n "$RESOLVED" ]; then
2749
for IP in $RESOLVED; do
28-
iptables -I OUTPUT -d "$IP" -j ACCEPT
50+
$IPT -I OUTPUT -d "$IP" -j ACCEPT
2951
echo "jailoc: allow $line ($IP)"
3052
done
3153
else
@@ -41,8 +63,8 @@ if [ -f /etc/resolv.conf ]; then
4163
if [[ "$value" == *:* ]]; then
4264
continue
4365
fi
44-
iptables -I OUTPUT -p udp -d "$value" --dport 53 -j ACCEPT
45-
iptables -I OUTPUT -p tcp -d "$value" --dport 53 -j ACCEPT
66+
$IPT -I OUTPUT -p udp -d "$value" --dport 53 -j ACCEPT
67+
$IPT -I OUTPUT -p tcp -d "$value" --dport 53 -j ACCEPT
4668
fi
4769
done < /etc/resolv.conf
4870
fi
@@ -55,20 +77,20 @@ if [ -f "$ALLOWED_NETWORKS" ]; then
5577
line="${line// /}"
5678
[ -z "$line" ] && continue
5779

58-
iptables -I OUTPUT -d "$line" -j ACCEPT
80+
$IPT -I OUTPUT -d "$line" -j ACCEPT
5981
echo "jailoc: allow network $line"
6082
done < "$ALLOWED_NETWORKS"
6183
fi
6284

6385
# --- Allow replies to inbound connections on the published service port ---
64-
iptables -A OUTPUT -p tcp --sport 4096 -m conntrack --ctstate ESTABLISHED -j ACCEPT
86+
$IPT -A OUTPUT -p tcp --sport 4096 -m conntrack --ctstate ESTABLISHED -j ACCEPT
6587

6688
# --- Block private/internal networks ---
67-
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
68-
iptables -A OUTPUT -d 172.16.0.0/12 -j DROP
69-
iptables -A OUTPUT -d 192.168.0.0/16 -j DROP
70-
iptables -A OUTPUT -d 169.254.0.0/16 -j DROP
71-
iptables -A OUTPUT -d 100.64.0.0/10 -j DROP
89+
$IPT -A OUTPUT -d 10.0.0.0/8 -j DROP
90+
$IPT -A OUTPUT -d 172.16.0.0/12 -j DROP
91+
$IPT -A OUTPUT -d 192.168.0.0/16 -j DROP
92+
$IPT -A OUTPUT -d 169.254.0.0/16 -j DROP
93+
$IPT -A OUTPUT -d 100.64.0.0/10 -j DROP
7294

7395
chown -R 1000:1000 /home/agent/.local /home/agent/.cache 2>/dev/null || true
7496
chown 1000:1000 /home/agent/.claude 2>/dev/null || true

0 commit comments

Comments
 (0)