The firewall module ships the unit that loads its rules

The distribution's nftables.service is Type=oneshot with no RemainAfterExit: it
loads the rules and goes inactive. A host asked for a service that is "running"
then reports, quite correctly, that it is stopped — every packet filtered as
declared, and the machine marked as not doing what it was told.

There is no state in the vocabulary for "ran and exited having done its job",
so a module that needs one brings a unit that stays. That is also the right
shape: how a machine enforces rules is a fact about the machine, and the mesh
has no business depending on what a distribution happens to package.

And when the builder's build times out, dump the builder's own account of
itself. "Nothing consumed the queue" names no cause and is the same sentence
whether the credential was refused, the queue was never declared, or the
process died three seconds in.
This commit is contained in:
2026-08-31 01:20:46 +02:00
parent 6bd7833ae9
commit 83727099eb
+25 -4
View File
@@ -578,11 +578,24 @@ test("a machine filters exactly what its modules declared, and nothing else", {
`"resources":[]}' > /tmp/talker.json`); `"resources":[]}' > /tmp/talker.json`);
// The rule set goes where this machine's nftables unit reads from, and the unit is declared to // The rule set goes where this machine's nftables unit reads from, and the unit is declared to
// reflect it. No command anywhere. // reflect it. No command anywhere.
// The module ships the unit that loads its rules, rather than using the one the distribution's
// nftables package provides. That unit is `Type=oneshot` with no `RemainAfterExit`, so it does
// its work and goes inactive — and a host asked for a service that is "running" reports, quite
// correctly, that it is stopped. There is no state in the vocabulary for "ran and exited having
// done its job", so a module that wants one brings a unit that stays.
//
// Which is also the right shape: how a machine enforces rules is a fact about the machine, and
// the mesh has no business depending on what a distribution happens to package.
await must("anchor", `printf %s '{"module":"firewall","version":"1",` + await must("anchor", `printf %s '{"module":"firewall","version":"1",` +
`"capabilities":["firewall"],` + `"capabilities":["firewall"],` +
`"filtering":{"into":"/etc/nftables.conf"},` + `"filtering":{"into":"/etc/mesh/filter.nft"},` +
`"resources":[{"id":"nftables","type":"package","package":"nftables"},` + `"resources":[{"id":"nftables","type":"package","package":"nftables"},` +
`{"id":"filter","type":"service","unit":"nftables.service","state":"running",` + `{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"},` +
`{"id":"unit","type":"file","path":"/etc/systemd/system/mesh-filter.service",` +
`"mode":"0644","content":"[Unit]\\nDescription=What the mesh computed for this machine\\n` +
`[Service]\\nType=oneshot\\nRemainAfterExit=yes\\n` +
`ExecStart=/usr/bin/nft -f /etc/mesh/filter.nft\\n[Install]\\nWantedBy=multi-user.target\\n"},` +
`{"id":"filter","type":"service","unit":"mesh-filter.service","state":"running",` +
`"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`); `"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`);
for (const f of ["talker", "firewall"]) { for (const f of ["talker", "firewall"]) {
await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`); await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`);
@@ -593,7 +606,7 @@ test("a machine filters exactly what its modules declared, and nothing else", {
await mesh("push laptop"); await mesh("push laptop");
await new Promise((r) => setTimeout(r, 20_000)); await new Promise((r) => setTimeout(r, 20_000));
const written = await must("laptop", `cat /etc/nftables.conf`); const written = await must("laptop", `cat /etc/mesh/filter.nft`);
// A rule names its source. Not decoration: it is the only thing that answers "why is this open". // A rule names its source. Not decoration: it is the only thing that answers "why is this open".
assert.match(written, /# talker . the thing this test is about/, assert.match(written, /# talker . the thing this test is about/,
`the rule does not name what caused it:\n${written}`); `the rule does not name what caused it:\n${written}`);
@@ -719,7 +732,15 @@ test("the builder is a module the mesh assigns, with a credential the mesh deliv
`> /root/built/module.json`); `> /root/built/module.json`);
await must("anchor", `cd /root/built && git init -q . && git add -A && ` + await must("anchor", `cd /root/built && git init -q . && git add -A && ` +
`git -c user.email=lab -c user.name=lab commit -qm built`); `git -c user.email=lab -c user.name=lab commit -qm built`);
await mesh("build /root/built --wait 300s", 420_000); try {
await mesh("build /root/built --wait 300s", 420_000);
} catch (why) {
// The builder's own account of itself. Without it the failure is "nothing consumed the
// queue", which names no cause and is the same sentence whether the credential was refused,
// the queue was never declared, or the process died three seconds in.
const said = (await on("anchor", `docker logs mesh-builder 2>&1 | tail -40`)).out;
throw new Error(`${(why as Error).message}\n\nwhat the builder said:\n${said}`);
}
// Naming the module, and not merely containing its name: `builds` says "nothing has been built // Naming the module, and not merely containing its name: `builds` says "nothing has been built
// yet" when there is nothing, and that sentence contains the word this was matching on. // yet" when there is nothing, and that sentence contains the word this was matching on.