nftables drops its container, NET_ADMIN, the container-runtime capability, the runtime base images, the Dockerfile, and the bus credential and state directory only the container read; its tools are declared as a TypeScript bundle the toolchain compiles and node-tools loads on every node, and the iptables package the image used to carry is declared on the host. The runtime runs as the operator's account, so the tool runs the filter's commands through sudo without a prompt when it is not root (ADR 0175 §4, to-be 38 WP4), naming sudo's absence or refusal by how it failed; the filter file is the path the manifest's filtering names, held to it by a test; a found firewall that is present but will not answer stops a removal rather than passing for inactive; a legacy tool that is present but fails is said, not swallowed.
109 lines
6.2 KiB
TypeScript
109 lines
6.2 KiB
TypeScript
// `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/,
|
|
);
|
|
});
|