From 5395c7196f8106377ffdd6d71ae6948b1f28bc1e Mon Sep 17 00:00:00 2001 From: didericis Date: Thu, 16 Jul 2026 12:54:46 -0400 Subject: [PATCH] feat(firecracker): route agent VMs to the gateway VM (Stage B, 3/n) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent VMs must reach the shared gateway that now runs in the infra VM (egress:9099 / supervise:9100 / git-http:9420 at the orchestrator link's guest IP). Add a PREROUTING DNAT: agents keep addressing their own host-side TAP IP on the gateway ports, and the rule redirects that to the infra VM. The isolation table's existing `ct status dnat accept` forward rule lets the DNAT'd traffic through; every other agent egress stays dropped, so a bottle still reaches only the gateway and nothing else. Source IP is deliberately NOT masqueraded: the gateway attributes each request to the originating bottle by its guest IP, which the /31 TAP + the bot_bottle_fc nft table make unspoofable. Keeping the agent addressed at its own host TAP IP means no per-bottle config change vs the docker-DNAT path it replaces. - scripts/firecracker-netpool.sh: `_install_gateway_route` adds `table ip _gw` (prerouting dstnat -> orch_guest on the gateway ports); wired into up/down/status. The nix module needs no change — it runs this script, and the ports are baked in. Verified on a KVM host: an agent VM's `curl -x http://:9099` reaches mitmproxy in the infra VM and gets a 403 (correct policy denial for an unregistered bottle) — i.e. the route lands end-to-end. Persist with a nixos-rebuild; the imperative rule holds until then. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck --- scripts/firecracker-netpool.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/firecracker-netpool.sh b/scripts/firecracker-netpool.sh index 43161e1..bcf201c 100755 --- a/scripts/firecracker-netpool.sh +++ b/scripts/firecracker-netpool.sh @@ -146,6 +146,8 @@ cmd_up() { echo "nftables table inet $TABLE installed (fail-closed boundary)" _install_orch_egress echo "orchestrator egress installed ($ORCH_IFACE -> NAT out)" + _install_gateway_route + echo "agent->gateway route installed (${PREFIX}* :$GATEWAY_PORTS -> $(orch_guest))" echo "done." } @@ -228,9 +230,29 @@ _docker_user_orch() { done } +# Agent -> gateway VM routing. The shared gateway (egress / supervise / +# git-http) runs in the orchestrator/infra VM at $(orch_guest). Agents keep +# addressing their own host-side TAP IP on the gateway ports; a PREROUTING +# DNAT redirects that to the infra VM, and the isolation table's +# `ct status dnat accept` forward rule lets it through — every other agent +# egress stays dropped. Source IP is deliberately NOT masqueraded: the +# gateway attributes each request to the originating bottle by its (nft + +# /31 unspoofable) guest IP. +_install_gateway_route() { + nft -f - </dev/null || true nft delete table inet "${TABLE}_nat" 2>/dev/null || true if ip link show "$ORCH_IFACE" >/dev/null 2>&1; then ip link set "$ORCH_IFACE" down 2>/dev/null || true @@ -254,6 +276,8 @@ cmd_status() { nft list table inet "$TABLE" 2>/dev/null || echo " (absent)" echo "table inet ${TABLE}_nat (orchestrator egress):" nft list table inet "${TABLE}_nat" 2>/dev/null || echo " (absent)" + echo "table ip ${TABLE}_gw (agent->gateway route):" + nft list table ip "${TABLE}_gw" 2>/dev/null || echo " (absent)" echo "taps:" for i in $(seq 0 $((POOL_SIZE-1))); do local dev ; dev="$(iface "$i")"