Skip to content

feat: implement AllowNet filtering on Linux via slirp4netns#3

Merged
machado144 merged 11 commits intomainfrom
feat/allownet-linux-slirp4netns
Feb 12, 2026
Merged

feat: implement AllowNet filtering on Linux via slirp4netns#3
machado144 merged 11 commits intomainfrom
feat/allownet-linux-slirp4netns

Conversation

@machado144
Copy link
Contributor

Summary

  • Network filtering: When allow_net is configured, sandboxed processes now get real but restricted connectivity on Linux. Uses slirp4netns for user-mode networking into a unshare --net namespace, then iptables inside the namespace to allow only DNS + resolved AllowNet IPs (all others rejected).
  • Prerequisite fixes: init is now idempotent (safe to re-run), and RunPassthrough properly wires stdin/stdout/stderr so sandboxed commands can use the terminal.
  • Graceful fallback: If slirp4netns is not installed, logs a warning and runs without network restrictions (instead of the previous broken --net that killed all connectivity).

How it works

  1. RunSandboxed dispatches: AllowNet set + slirp4netns available → runWithNetFilter; otherwise → runUnshare
  2. runWithNetFilter starts two processes:
    • Sandbox (unshare --user --net --mount --pid --fork): waits for tap0, mounts resolv.conf pointing to slirp4netns DNS (10.0.2.3), applies iptables OUTPUT rules, then execs the target command
    • slirp4netns: attaches to the sandbox PID's network namespace providing connectivity
  3. DNS servers are read from systemd-resolved or /etc/resolv.conf, filtering out 127.* stubs
  4. Only IPv4 addresses are used in iptables rules (IPv6 filtered out)

Test plan

  • go build ./... compiles
  • go test ./... — all 48 tests pass (new: TestResolveAllowedIPs, TestBuildNetFilterScript, TestRunSandboxedDispatch, TestGetSystemDNS, TestParseDNSFromFile)
  • Manual: aigate run -- curl https://api.anthropic.com works (allowed domain)
  • Manual: aigate run -- curl https://google.com is rejected (not in AllowNet)
  • Manual: rename slirp4netns → verify warning logged and command runs unrestricted

init now skips group/user creation when they already exist instead of
erroring out, so re-running `sudo aigate init` is safe.

RunPassthrough now connects stdin/stdout/stderr to the parent process
so sandboxed commands can interact with the terminal.
When AllowNet is configured, RunSandboxed now creates a network-filtered
namespace using slirp4netns + iptables instead of a fully isolated one.

Architecture:
- Process 1: unshare --net sandbox waits for tap0, sets up iptables
  rules allowing only DNS + resolved AllowNet IPs, then execs the target
- Process 2: slirp4netns attaches to process 1's netns providing
  user-mode networking (no root required)

DNS resolution uses the system's upstream nameservers (from
systemd-resolved or /etc/resolv.conf), falling back to 8.8.8.8/1.1.1.1.
Only IPv4 addresses are used in iptables rules.

Falls back to unrestricted networking with a warning when slirp4netns is
not installed.
Host-side DNS resolution returned different IPs than slirp4netns DNS
inside the sandbox (CDN anycast / Cloudflare load balancing), causing
iptables to REJECT connections that should have been allowed.

Now hostnames are resolved inside the namespace via `getent ahostsv4`
after the resolv.conf mount is in place, so the iptables ACCEPT rules
match exactly what the sandboxed process will connect to.

Also: wire slirp4netns stderr to os.Stderr so errors are visible,
and add a DNS readiness wait loop before resolving hosts.
slirp4netns needs CAP_SYS_ADMIN in both the target's owning user
namespace AND its own current user namespace to call
setns(CLONE_NEWNET). Launching it from the host (init user namespace)
as an unprivileged user fails the second check.

Restructured to two-layer unshare:
- Outer: unshare --user --map-root-user (user namespace only, host net)
- Inner: unshare --net --mount --pid --fork (sandbox in new net ns)
- slirp4netns runs inside the user ns (has caps) with host networking

The orchestration script preserves terminal stdin for the backgrounded
sandbox via fd 3, uses base64-encoded inner script to avoid quoting
issues, and waits for the net namespace inode to change before starting
slirp4netns.
The DNS readiness check used `getent ahostsv4 localhost` which resolves
from /etc/hosts, not DNS. This passed immediately while slirp4netns DNS
(10.0.2.3) was still starting, causing subsequent getent calls for
remote hosts to silently fail — no iptables ACCEPT rules got created.

Now the readiness check queries the first AllowNet host (a real remote
DNS query), and each host resolution retries up to 3 times.

Also suppress slirp4netns stdout (verbose protocol debug) and remount
/proc in PID namespace to fix glibc "fatal library error, lookup self".
Add PlantUML diagrams for each isolation layer:
- file-isolation: dual-layer ACLs + runtime mount overrides
- linux-network: slirp4netns + iptables in network namespace
- macos-network: sandbox-exec Seatbelt profiles
- linux-process: user/mount/PID namespace architecture

Embed rendered PNG images in user README and replace ASCII art
in root README with the process isolation diagram.
Print active restrictions to stderr when sandbox starts so AI agents
can understand what is enforced (deny_read, deny_exec, allow_net).
Use icmp-admin-prohibited for iptables REJECT to give a clearer signal
than generic connection-refused.
Replace /dev/null bind-mounts with a marker file containing
"[aigate] access denied: this file is protected by sandbox policy"
so AI agents understand why content is unavailable instead of seeing
empty files. Directories get a .aigate-denied marker file inside a
read-only tmpfs.
Echo allowed hosts to stderr from inside the namespace so AI agents
see an explicit "[aigate] network restricted: only X, Y are reachable"
message instead of just inferring from connection-refused errors.
Write /tmp/.aigate-policy inside the sandbox with all active deny_read,
deny_exec, and allow_net rules. Deny markers in files and directories
now point to this file ("Run cat /tmp/.aigate-policy to see all active
restrictions"), giving AI agents a discoverable path to understand
the full sandbox policy including network restrictions.
Change "Run 'cat /tmp/.aigate-policy'" to "See /tmp/.aigate-policy"
so AI agents don't flag the marker content as a prompt injection
attempt.
@machado144 machado144 merged commit 72c6be1 into main Feb 12, 2026
2 checks passed
murilopmachado pushed a commit that referenced this pull request Mar 5, 2026
Implement network egress filtering for sandboxed processes on Linux using
slirp4netns + iptables, with no root required.

## Changes

- **Network filtering**: Two-layer unshare architecture (user ns + net/mount/pid ns)
  with slirp4netns providing user-mode networking and iptables OUTPUT chain restricting
  egress to allowed hosts only. DNS resolved inside the namespace to avoid CDN/anycast
  IP mismatches.
- **Sandbox banner**: Print active restrictions (deny_read, deny_exec, allow_net) to
  stderr at startup so users see what's enforced.
- **Explicit deny markers**: Denied files show "[aigate] access denied" message instead
  of appearing empty. Policy file at /tmp/.aigate-policy inside the sandbox lists all
  active restrictions for AI agent discoverability.
- **Architecture diagrams**: PlantUML sources + rendered PNGs for file isolation,
  Linux/macOS network isolation, and Linux process isolation.
- **Documentation**: Rewritten user guide with prerequisites, slirp4netns install
  instructions, and troubleshooting.

Falls back gracefully: if slirp4netns is not installed, logs a warning and runs
without network restrictions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant