refactor(firecracker): non-invasive NixOS module (no firewall switch)

The module used `networking.nftables.tables.*` (which forces
`networking.nftables.enable = true`, flipping the host firewall backend)
and `systemd.network.enable` (handing interfaces to systemd-networkd) —
disruptive on an iptables + Docker daily driver.

Rewrite it to a single systemd oneshot that brings the pool up: idempotent
`ip tuntap` for the TAPs + `nft -f` for the independent `inet bot_bottle_fc`
table (its own hooks at priority -10), with ExecStop teardown. Same as the
imperative script, but declarative. It touches neither the firewall backend
nor networkd, so it coexists with iptables/Docker/ufw/firewalld. Verified
through the NixOS module system (service present, firewall untouched).

Drop the now-redundant `render_nixos_module()` paste generator (the module
is a real importable file) and point `backend setup` at importing it (flake
output or the file path), noting it's non-invasive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-11 17:25:50 -04:00
parent 949b001464
commit 480269b116
4 changed files with 98 additions and 125 deletions
+6 -68
View File
@@ -6,8 +6,9 @@ a pool of point-to-point TAP devices (owned by the invoking user, so
`./cli.py start` never needs root) and a dedicated nftables table that
isolates every VM. This module is the single source of truth for the
pool parameters — the shell script (`scripts/firecracker-netpool.sh`),
the printed NixOS snippet, and the backend's fail-closed preflight all
derive from the constants here so they can't drift.
the NixOS module (`nix/firecracker-netpool.nix`), and the backend's
fail-closed preflight all derive from these constants so they can't
drift.
Topology (per slot i):
* TAP ``bbfc{i}`` — no shared bridge, so no docker0 / virbr0 / cni0
@@ -237,72 +238,9 @@ def render_shell_setup() -> str:
return f"sudo {prefix}./scripts/firecracker-netpool.sh up"
def render_nixos_module() -> str:
"""A paste-ready NixOS snippet, derived from the live constants.
Uses systemd-networkd tap netdevs (so TAPs are only reconfigured
when this config changes, not torn down on every rebuild — which
would drop running VMs) and `networking.nftables.tables.<name>`
(an independent table that coexists with Docker's iptables rules
rather than replacing the whole ruleset)."""
slots = all_slots()
owner = os.environ.get("USER", "youruser")
netdevs = "\n".join(
f''' "10-{s.iface}" = {{
netdevConfig = {{ Name = "{s.iface}"; Kind = "tap"; }};
tapConfig = {{ User = "{owner}"; }};
}};'''
for s in slots
)
networks = "\n".join(
f''' "10-{s.iface}" = {{
matchConfig.Name = "{s.iface}";
address = [ "{s.host_ip}/31" ];
networkConfig.ConfigureWithoutCarrier = true;
}};'''
for s in slots
)
return f"""# bot-bottle Firecracker network pool ({len(slots)} slots, base {ip_base()}).
# Generated by `./cli.py backend setup --backend=firecracker`; owner user = {owner!r}.
{{ ... }}:
{{
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
systemd.network.enable = true;
systemd.network.netdevs = {{
{netdevs}
}};
systemd.network.networks = {{
{networks}
}};
# Independent table — does not touch Docker/ufw/firewalld rules.
# A bottle VM (traffic from a bbfc* TAP) may reach only its own
# sidecar (DNAT'd from the host TAP IP); everything else is dropped,
# so there is no egress except through the sidecar proxy.
networking.nftables.enable = true;
networking.nftables.tables."{NFT_TABLE}" = {{
family = "inet";
content = ''
chain forward {{
type filter hook forward priority -10; policy accept;
iifname != "{IFACE_PREFIX}*" return
ct state established,related accept
ct status dnat accept
drop
}}
chain input {{
type filter hook input priority -10; policy accept;
iifname != "{IFACE_PREFIX}*" return
ct state established,related accept
drop
}}
'';
}};
}}
"""
# The NixOS setup is a real, importable module (nix/firecracker-netpool.nix,
# exposed as the flake output nixosModules.firecracker-netpool) rather than a
# generated paste — see `backend setup` output.
def _nondefault_env() -> str: