The packet filter's tools are a bundle the node's runtime serves; its container goes (hq to-be 38 WP4)

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.
This commit is contained in:
jochen
2026-10-03 12:59:07 +02:00
parent 853ace3828
commit db5e7c80cf
6 changed files with 127 additions and 90 deletions
+41 -7
View File
@@ -4,7 +4,8 @@
// 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";
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",
@@ -34,7 +35,7 @@ function fake(ufwActive = false): { run: Runner; asked: string[] } {
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)");
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",
@@ -44,27 +45,27 @@ test("a predecessor's chain in the legacy filter loses its jumps, is flushed and
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)");
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).remove("table ip6 filter, chain DOCKER-USER");
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).remove("table ip6 own, chain deny");
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);
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).remove("chain ufw6-docker-logging-deny (ip6tables-legacy)");
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"));
});
@@ -72,3 +73,36 @@ 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/,
);
});