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
|
// 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
|
// 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
|
// 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 { execFile } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
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. */
|
/** A command runner, so the acts can be tested without a packet filter. */
|
||||||
export type Runner = (cmd: string, args: string[]) => Promise<string>;
|
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) => {
|
export const execRunner: Runner = async (cmd, args) => {
|
||||||
const { stdout } = await execFileP(cmd, args, { maxBuffer: 16 * 1024 * 1024 });
|
const [program, argv] = escalated(cmd, args);
|
||||||
return stdout;
|
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. */
|
/** The mesh's own tables, which `remove` never touches. */
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
"module": "nftables",
|
"module": "nftables",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"firewall",
|
"firewall"
|
||||||
"container-runtime"
|
|
||||||
],
|
],
|
||||||
"claims": [
|
"claims": [
|
||||||
{
|
{
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
"id": "stock-unit-stop",
|
"id": "stock-unit-stop",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "/etc/systemd/system/nftables.service.d/mesh.conf",
|
"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"
|
"mode": "0644"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -64,24 +63,6 @@
|
|||||||
"type": "package",
|
"type": "package",
|
||||||
"package": "ufw",
|
"package": "ufw",
|
||||||
"absent": true
|
"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": [
|
"tools": [
|
||||||
@@ -91,23 +72,14 @@
|
|||||||
"broker": "${dir:mesh-state}/broker"
|
"broker": "${dir:mesh-state}/broker"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"on": [
|
|
||||||
{
|
|
||||||
"arg": "BUILD_BASE",
|
|
||||||
"module": "mesh-tools",
|
|
||||||
"artifact": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"arg": "RUNTIME_BASE",
|
|
||||||
"module": "mesh-tools",
|
|
||||||
"artifact": "runtime"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
{
|
{
|
||||||
"name": "runtime",
|
"name": "tools",
|
||||||
"kind": "image",
|
"kind": "bundle",
|
||||||
"from": "Dockerfile"
|
"language": "typescript",
|
||||||
|
"entrypoints": [
|
||||||
|
"tools/index.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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'"
|
"test": "node --test --experimental-strip-types 'test/*.test.ts'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// and the same in an iptables-nft table. It refuses what is not the operator's to remove.
|
// and the same in an iptables-nft table. It refuses what is not the operator's to remove.
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
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 = [
|
const legacy = [
|
||||||
"-P INPUT ACCEPT", "-P FORWARD DROP", "-P OUTPUT ACCEPT",
|
"-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";
|
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"]);
|
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