Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d01258b67 |
@@ -1,23 +0,0 @@
|
||||
# nftables' runtime: the tool runtime, carrying the packet filter's tools and the binaries they speak.
|
||||
#
|
||||
# Built from this module's own directory and nothing else (novox/hq ADR 0069). Two bases, named in
|
||||
# module.json's `build.on`: the image this is compiled in and the image it runs in.
|
||||
ARG BUILD_BASE
|
||||
ARG RUNTIME_BASE
|
||||
|
||||
FROM ${BUILD_BASE} AS build
|
||||
WORKDIR /app/modules/nftables
|
||||
COPY . .
|
||||
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
|
||||
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
|
||||
|
||||
FROM ${RUNTIME_BASE}
|
||||
# The filter's own tools: nft for the machine's ruleset and the mesh's table, iptables for the
|
||||
# legacy filter and the tables iptables-nft manages — a predecessor's rules live there (ADR 0168).
|
||||
# The container runs on the machine's network with NET_ADMIN (ADR 0170), so these act on the
|
||||
# machine's packet filter, not on a namespace of their own.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nftables iptables \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=build /app/modules/nftables/dist /app/modules/nftables/dist
|
||||
ENV MESH_TOOL_MODULES=/app/modules/nftables/dist/tools/index.js
|
||||
@@ -2,7 +2,8 @@
|
||||
// rule set from every module's `listens` and writes it to the filter file (ADR 0045); the module
|
||||
// loads it through its own unit. This code reads the filter back as the machine enforces it, reloads
|
||||
// the mesh's own table, and removes one thing the mesh did not write when the operator names it
|
||||
// (ADR 0168, ADR 0170) — the seat's three verbs, over the machine's own tools.
|
||||
// (ADR 0168, ADR 0170) — the seat's three verbs, over the machine's own tools, which it runs as root
|
||||
// through sudo when the runtime loading it is not (ADR 0175).
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
@@ -12,9 +13,27 @@ const execFileP = promisify(execFile);
|
||||
/** A command runner, so the acts can be tested without a packet filter. */
|
||||
export type Runner = (cmd: string, args: string[]) => Promise<string>;
|
||||
|
||||
/** The command as it is run: as given when this process is root, else through sudo without a
|
||||
* prompt. The runtime that loads this bundle runs as the node's operator account, which may
|
||||
* escalate as the operator would (novox/hq ADR 0175 §4); the packet filter answers only to root,
|
||||
* listing included. A command sudo refuses fails by name, saying what the account lacks. */
|
||||
export function escalated(cmd: string, args: string[], uid: number | undefined = process.getuid?.()): [string, string[]] {
|
||||
if (uid === 0) return [cmd, args];
|
||||
return ["sudo", ["-n", cmd, ...args]];
|
||||
}
|
||||
|
||||
export const execRunner: Runner = async (cmd, args) => {
|
||||
const { stdout } = await execFileP(cmd, args, { maxBuffer: 16 * 1024 * 1024 });
|
||||
const [program, argv] = escalated(cmd, args);
|
||||
try {
|
||||
const { stdout } = await execFileP(program, argv, { maxBuffer: 16 * 1024 * 1024 });
|
||||
return stdout;
|
||||
} catch (err) {
|
||||
const stderr = String((err as { stderr?: string }).stderr ?? "").trim();
|
||||
if (program === "sudo" && /a password is required|not allowed to execute|not in the sudoers/.test(stderr)) {
|
||||
throw new Error(`${cmd} needs root and the runtime's account may not escalate without a prompt: ${stderr}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/** The mesh's own tables, which `remove` never touches. */
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"module": "nftables",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"firewall",
|
||||
"container-runtime"
|
||||
"firewall"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
@@ -42,7 +41,7 @@
|
||||
"id": "stock-unit-stop",
|
||||
"type": "file",
|
||||
"path": "/etc/systemd/system/nftables.service.d/mesh.conf",
|
||||
"content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) \u2014 a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n",
|
||||
"content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) — a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n",
|
||||
"mode": "0644"
|
||||
},
|
||||
{
|
||||
@@ -64,24 +63,6 @@
|
||||
"type": "package",
|
||||
"package": "ufw",
|
||||
"absent": true
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-nftables",
|
||||
"network": "host",
|
||||
"capabilities": [
|
||||
"NET_ADMIN"
|
||||
],
|
||||
"volumes": [
|
||||
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
|
||||
"/etc/nftables.conf:/etc/nftables.conf:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_FILTER_FILE": "/etc/nftables.conf"
|
||||
},
|
||||
"artifact": "runtime"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
@@ -91,23 +72,14 @@
|
||||
"broker": "${dir:mesh-state}/broker"
|
||||
},
|
||||
"build": {
|
||||
"on": [
|
||||
{
|
||||
"arg": "BUILD_BASE",
|
||||
"module": "mesh-tools",
|
||||
"artifact": "build"
|
||||
},
|
||||
{
|
||||
"arg": "RUNTIME_BASE",
|
||||
"module": "mesh-tools",
|
||||
"artifact": "runtime"
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "runtime",
|
||||
"kind": "image",
|
||||
"from": "Dockerfile"
|
||||
"name": "tools",
|
||||
"kind": "bundle",
|
||||
"language": "typescript",
|
||||
"entrypoints": [
|
||||
"tools/index.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc client.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist",
|
||||
"build": "tsc client.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --rootDir . --outDir dist",
|
||||
"test": "node --test --experimental-strip-types 'test/*.test.ts'"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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 { FirewallClient, chainsJumpingTo, escalated, type Runner } from "../client.ts";
|
||||
|
||||
const legacy = [
|
||||
"-P INPUT ACCEPT", "-P FORWARD DROP", "-P OUTPUT ACCEPT",
|
||||
@@ -72,3 +72,9 @@ 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"]]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user