From 0e102dd350e0cc33d07a9628a2b3327318143259 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 20:52:26 +0200 Subject: [PATCH] firewall: the module that applies the mesh-computed packet filter (ADR 0050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The missing applier. mesh-control already derives a node's whole nftables rule set from the union of its modules' listens and writes it to /etc/nftables.conf; this module declares filtering:{into} to receive it and loads it — the nftables service, reloaded on 'filtering' whenever the rules change. A firewall_rules tool reads the live table so a declared scope can be checked against what is really enforced. Closes the loop from listens.from to a packet actually dropped. Manifest parses; tool typechecks. --- modules/firewall/client.ts | 21 +++++++++++++++++++++ modules/firewall/module.json | 33 +++++++++++++++++++++++++++++++++ modules/firewall/package.json | 14 ++++++++++++++ modules/firewall/tools/index.ts | 19 +++++++++++++++++++ modules/firewall/tsconfig.json | 15 +++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 modules/firewall/client.ts create mode 100644 modules/firewall/module.json create mode 100644 modules/firewall/package.json create mode 100644 modules/firewall/tools/index.ts create mode 100644 modules/firewall/tsconfig.json diff --git a/modules/firewall/client.ts b/modules/firewall/client.ts new file mode 100644 index 0000000..5b71df0 --- /dev/null +++ b/modules/firewall/client.ts @@ -0,0 +1,21 @@ +// The firewall's own code, in the module (novox/hq ADR 0044). The mesh computes this node's whole +// rule set from every module's `listens` and writes it to /etc/nftables.conf (novox/hq ADR 0050); +// the module loads it (the nftables service, reloaded whenever the rules change). This code exists +// only to read back what is actually enforced — the enforcement itself is declarative. + +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const run = promisify(execFile); + +export class FirewallClient { + static fromEnv(_env: NodeJS.ProcessEnv = process.env): FirewallClient { + return new FirewallClient(); + } + + /** The mesh's live table — exactly what is dropping and accepting on this node right now. */ + async ruleset(): Promise { + const { stdout } = await run("nft", ["list", "table", "inet", "mesh"]); + return stdout; + } +} diff --git a/modules/firewall/module.json b/modules/firewall/module.json new file mode 100644 index 0000000..e8331a2 --- /dev/null +++ b/modules/firewall/module.json @@ -0,0 +1,33 @@ +{ + "module": "firewall", + "version": "1", + "capabilities": [ + "firewall" + ], + "claims": [ + { + "name": "the-packet-filter", + "scope": "node" + } + ], + "filtering": { + "into": "/etc/nftables.conf" + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "nftables" + }, + { + "id": "load", + "type": "service", + "unit": "nftables.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "filtering" + ] + } + ] +} diff --git a/modules/firewall/package.json b/modules/firewall/package.json new file mode 100644 index 0000000..c80274f --- /dev/null +++ b/modules/firewall/package.json @@ -0,0 +1,14 @@ +{ + "name": "@novox/module-firewall", + "version": "0.1.0", + "description": "firewall — applies the mesh-computed packet filter (ADR 0050). Its diagnostic tool lives here. + "type": "module", + "private": true, + "dependencies": { + "@novox/mesh-sdk": "^0.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/modules/firewall/tools/index.ts b/modules/firewall/tools/index.ts new file mode 100644 index 0000000..198e5de --- /dev/null +++ b/modules/firewall/tools/index.ts @@ -0,0 +1,19 @@ +// firewall's tools — one, and the useful one: what is actually enforced. The rules are the mesh's, +// computed from every module's listens; this reads the live table so a declared scope can be checked +// against what the packet filter is really doing. + +import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; +import { FirewallClient } from "../client.js"; + +export function getFirewallTools(firewall: FirewallClient): ToolDefinition[] { + return [ + { + name: "firewall_rules", + description: "The mesh's live nftables rules on this node — what is actually accepting and dropping.", + input: {}, + run: async () => ({ ruleset: await firewall.ruleset() }), + }, + ]; +} + +registerModuleTools("firewall", () => getFirewallTools(FirewallClient.fromEnv())); diff --git a/modules/firewall/tsconfig.json b/modules/firewall/tsconfig.json new file mode 100644 index 0000000..91d5b91 --- /dev/null +++ b/modules/firewall/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "client.ts", + "tools/index.ts" + ] +} \ No newline at end of file