nftables drops its container, NET_ADMIN, the container-runtime capability, the runtime base images and the Dockerfile; its tools are declared as a TypeScript bundle the toolchain compiles and node-tools loads on every node. 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); the filter file is the path the manifest's filtering names, no container env carrying it.
81 lines
4.6 KiB
TypeScript
81 lines
4.6 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 { FirewallClient, chainsJumpingTo, escalated, 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"]);
|
|
});
|
|
|
|
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"]]);
|
|
});
|