// `remove` acts on one rule set the mesh did not write, named as the host reports it (novox/hq ADR // 0168, 0169), over the shapes two machines of the first mesh reported live: a predecessor's chain in // the legacy filter, the runtime's user chain in the IPv6 legacy filter, a leftover front-end chain, // and the same in an iptables-nft table. It refuses what is not the operator's to remove. import { test } from "node:test"; import assert from "node:assert/strict"; import { FirewallClient, chainsJumpingTo, type Runner } from "../client.ts"; const legacy = [ "-P INPUT ACCEPT", "-P FORWARD DROP", "-P OUTPUT ACCEPT", "-N DOCKER", "-N DOCKER-USER", "-N HAL-MESH-ONLY", "-A FORWARD -j DOCKER-USER", "-A DOCKER-USER -i enp6s0 -p tcp -m conntrack --ctstate NEW -j HAL-MESH-ONLY", "-A HAL-MESH-ONLY -m conntrack --ctorigdstport 80 -j RETURN", "-A HAL-MESH-ONLY -m comment --comment \"HAL: not public -> mesh only\" -j DROP", ].join("\n") + "\n"; function fake(ufwActive = false): { run: Runner; asked: string[] } { const asked: string[] = []; const run: Runner = async (cmd, args) => { asked.push([cmd, ...args].join(" ")); if (cmd === "ufw") return ufwActive ? "Status: active\n" : "Status: inactive\n"; if (args.join(" ") === "-S") return legacy; if (cmd === "nft" && args[0] === "list" && args[1] === "table") { return "table ip6 own {\n\tchain forward {\n\t\ttype filter hook forward priority filter; policy accept;\n\t\tjump deny\n\t}\n\tchain deny {\n\t\tdrop\n\t}\n}\n"; } if (cmd === "nft" && args[0] === "-a") { return "table ip6 own {\n\tchain forward {\n\t\ttype filter hook forward priority filter; policy accept;\n\t\tjump deny # handle 7\n\t}\n}\n"; } return ""; }; return { run, asked }; } test("a predecessor's chain in the legacy filter loses its jumps, is flushed and deleted", async () => { const f = fake(); const out = await new FirewallClient(f.run).remove("chain HAL-MESH-ONLY (iptables-legacy)"); assert.deepEqual(out.did, [ "iptables-legacy -D DOCKER-USER -i enp6s0 -p tcp -m conntrack --ctstate NEW -j HAL-MESH-ONLY", "iptables-legacy -F HAL-MESH-ONLY", "iptables-legacy -X HAL-MESH-ONLY", ]); }); test("the runtime's user chain is emptied back to its one return, never deleted", async () => { const f = fake(); const out = await new FirewallClient(f.run).remove("chain DOCKER-USER (ip6tables-legacy)"); assert.deepEqual(out.did, ["ip6tables-legacy -F DOCKER-USER", "ip6tables-legacy -A DOCKER-USER -j RETURN"]); const nft = await new FirewallClient(fake().run).remove("table ip6 filter, chain DOCKER-USER"); assert.deepEqual(nft.did, ["ip6tables -F DOCKER-USER", "ip6tables -A DOCKER-USER -j RETURN"]); }); test("a chain of the machine's own nftables table goes with the rules that reach it", async () => { const f = fake(); const out = await new FirewallClient(f.run).remove("table ip6 own, chain deny"); assert.deepEqual(out.did, ["nft delete rule ip6 own forward handle 7", "nft delete chain ip6 own deny"]); }); test("what is not the operator's to remove is refused by name", async () => { const c = new FirewallClient(fake(true).run); await assert.rejects(c.remove("table inet mesh, chain forward"), /the mesh's own table/); await assert.rejects(c.remove("chain DOCKER (iptables-legacy)"), /container runtime's own/); await assert.rejects(c.remove("chain FORWARD (iptables-legacy)"), /built in/); await assert.rejects(c.remove("chain ufw6-docker-logging-deny (ip6tables-legacy)"), /found firewall, which is in force/); await assert.rejects(c.remove("something else"), /not a rule set as the host reports one/); // Retired, a front end's leftover is nobody's and goes. const retired = await new FirewallClient(fake(false).run).remove("chain ufw6-docker-logging-deny (ip6tables-legacy)"); assert.ok(retired.did.includes("ip6tables-legacy -X ufw6-docker-logging-deny")); }); test("which chains jump to a target is read from a listing", () => { const listing = "table ip6 own {\n\tchain a {\n\t\tjump deny\n\t}\n\tchain b {\n\t\tgoto deny\n\t}\n\tchain deny {\n\t\tdrop\n\t}\n}\n"; assert.deepEqual(chainsJumpingTo(listing, "deny"), ["a", "b"]); });