// `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 { readFileSync } from "node:fs"; import { FILTER_FILE, FirewallClient, chainsJumpingTo, escalated, installed, 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, undefined, () => true).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, undefined, () => true).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, undefined, () => true).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, undefined, () => true).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, undefined, () => true); 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, undefined, () => true).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"]); }); test("the filter's commands run as given by root and through sudo without a prompt by anyone else", () => { assert.deepEqual(escalated("nft", ["list", "ruleset"], 0), ["nft", ["list", "ruleset"]]); assert.deepEqual(escalated("nft", ["-f", "/etc/nftables.conf"], 1000), ["sudo", ["-n", "nft", "-f", "/etc/nftables.conf"]]); assert.deepEqual(escalated("iptables-legacy", ["-S"], undefined), ["sudo", ["-n", "iptables-legacy", "-S"]]); }); test("the filter file is the one the manifest's filtering names", () => { const manifest = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8")) as { filtering: { into: string } }; assert.equal(FILTER_FILE, manifest.filtering.into); }); test("a tool is installed when an executable of its name is on the path, and not otherwise", () => { assert.equal(installed("sh"), true); assert.equal(installed("no-such-tool-of-the-mesh"), false); }); test("a found firewall that is absent guards nothing; one that will not answer stops the removal", async () => { // Absent: its leftover chain is nobody's and goes, without asking it. const absent = fake(true); const out = await new FirewallClient(absent.run, undefined, () => false).remove("chain ufw6-docker-logging-deny (ip6tables-legacy)"); assert.ok(out.did.includes("ip6tables-legacy -X ufw6-docker-logging-deny")); assert.ok(!absent.asked.some((a) => a.startsWith("ufw "))); // Present and failing — refused by sudo, say — nothing is removed on a guess. const refusing: Runner = async (cmd, args) => { if (cmd === "ufw") throw new Error("ufw needs root and the runtime's account may not run it without a prompt"); return fake().run(cmd, args); }; await assert.rejects( new FirewallClient(refusing, undefined, () => true).remove("chain ufw6-docker-logging-deny (ip6tables-legacy)"), /cannot tell whether the found firewall is in force/, ); });