/** * `inbound: deny` — a host firewall on the machine itself. * * Distinct from NAT and behaving differently: a machine can be perfectly routable and * still refuse everything unsolicited, which is the normal state of a v6-addressed * machine. Without this, v6 addressing would silently imply reachability, and a scenario * that said a machine refuses traffic would produce one that accepts it. * * Established and related traffic is accepted, so the machine can still dial out. That is * what a host firewall does; a machine that could not reach anything would be reproducing * a disconnected machine rather than a defended one. */ import type { Scenario } from "../declaration/types.ts"; import { incus, succeeds } from "../incus/client.ts"; const RULESET = `flush ruleset table inet mlab { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif lo accept ct state invalid drop } } `; export async function applyHostFirewalls( scenario: Scenario, machineNames: Map, log: (message: string) => void = () => {}, ): Promise { for (const [machine, spec] of Object.entries(scenario.machines)) { if (spec.inbound !== "deny") continue; const name = machineNames.get(machine); if (!name) continue; await incus( ["exec", name, "--", "sh", "-c", `cat > /tmp/mlab-host.nft <<'MLABNFT'\n${RULESET}MLABNFT\nnft -f /tmp/mlab-host.nft`], 60_000, ); // Read back. A declared refusal that silently did not apply is the fault this lab // exists to catch, and a ruleset that failed to load leaves the machine wide open — // which looks exactly like a machine that is working. const check = await incus( ["exec", name, "--", "sh", "-c", "nft list table inet mlab >/dev/null 2>&1 && echo present || echo absent"], 20_000, ); if (check.stdout.trim() !== "present") { throw new Error( `${machine}: inbound: deny was declared but the ruleset is not loaded — the machine ` + `would accept traffic the scenario says it refuses`, ); } // Recorded only after the read-back proved it loaded. A tag written before the check // would be a claim rather than a record, and anything reading the instance back would // report a defended machine that is in fact wide open. await succeeds(["config", "set", name, "user.mesh-lab.inbound", "deny"], 20_000); log(` ${machine} refuses unsolicited inbound`); } }