From 1bd13446d4c11bb82c3f45a343d7b8373f671a77 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 2 Oct 2026 11:57:38 +0200 Subject: [PATCH] ADR 0168: a converged machine is filtered by the mesh alone, and the host says what else refuses (group 7) Designs 08 and 05 revised; 141 resolved by ADR 0140 and 084 by ADR 0102 and issue 128, both by reading; 143 and 144 decided, built on the matching branches in mesh-host and mesh-controller, resolved when the home server's record names the predecessor's chain. --- ...d-machine-is-filtered-by-the-mesh-alone.md | 120 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/05-the-node-host.md | 9 ++ 03-DESIGN/01-to-be/08-connectivity.md | 33 +++++ .../00-report.md | 13 +- .../00-report.md | 12 +- .../00-report.md | 8 ++ .../00-report.md | 9 ++ 8 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md diff --git a/02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md b/02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md new file mode 100644 index 0000000..bdb6032 --- /dev/null +++ b/02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md @@ -0,0 +1,120 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-02 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md +--- + +# 168. A converged machine is filtered by the mesh alone, and the host says what else refuses + +## Context + +[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) says what converging does to +the firewall a machine was found with: the mesh's derived filter is loaded in place of the +refusal-only guard, and the found firewall is retired — disabled, never flushed. Four issues from +the first two convergences are four ways that sentence was not the machine: + +- the flip reported the found firewall retired and it was active two minutes later; fifty minutes + on, a reconcile found it disabled by hand and recorded that the mesh had done it + ([143](../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md)); +- "the firewall found" named one front end, and what filtered the forwarded path on that machine + was a chain a predecessor had installed in the container runtime's user hook — invisible to the + mesh, refusing two ports the mesh declared open, and when it was removed, carrying an allowance + every module reaching another by the machine's own name had been relying on + ([144](../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md), + [145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)); +- the forward chain listed address ranges that followed neither the modules nor the machine + ([141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)), answered by + [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md) before this record; +- the networking module wrote two machine-wide files whole, so taking it restarted every + container ([084](../04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md)), + answered by [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) and the hosts + file's marked region ([issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)). + +Read on the four machines of this mesh on 2026-10-02, after every one had converged: on both +machines that had a front end it is inactive, and the host's record says the mesh retired it on +both — true of one, false of the other. On the home server the predecessor's chain is still in +force on the forwarded path, in the legacy packet filter the mesh's reader of rules does not +consult once a machine is converged, so that machine is filtered by two things and the mesh says +one. The host's reader already knows how to tell a table that refuses traffic from the runtime's +own plumbing and from a ban list; it is asked once, at adoption, and only to refuse a machine +whose firewall nobody speaks. Nothing asks it afterwards, and nothing reports what it saw. + +The group's exit is one sentence: *a converged machine has exactly one thing filtering it, and +the mesh says truthfully which.* The first half the mesh can enforce only for what it owns; the +second half it can always do, and it is the half that was missing. + +## Decision + +**1. Convergence is a state the host keeps, not a step it takes once.** Every apply of a converged +declaration reads whether the found firewall is in force. Active — enabled again by a package, a +boot, a hand — it is retired again and said. The record distinguishes *the mesh disabled it* from +*it was found inactive*, and a reconcile that finds it inactive never records that the mesh did +it. When the step is skipped because the apply had failures, the report says the found firewall +was left in force and why; a step that does nothing is never silent. + +**2. The host reports what filters the machine, with every apply, adopted or converged.** Every +table of the packet filter, and every chain of the legacy filter, that refuses traffic — a drop or +a reject, or a base chain whose policy drops — with an owner: the *mesh's*, the *found firewall's*, +the *container runtime's own*, a *ban* (a refusal that names the sources it refuses, in a chain +that accepts nothing), or *other*. The runtime's own is its plumbing — its chains, the forward +policy it sets when it turns forwarding on, its guard against reaching a container's address from +off its bridge. The user chain the runtime leaves for an administrator is not the runtime's: +anything refusing in it is *other*, which is where both predecessors' chains lived. Each entry +says in one line what it refuses. The mesh removes none of it: a rule it did not write is the +operator's to remove, now that they can see it. + +**3. The mesh says which.** `node show` lists the filters with their owners. `status` names every +converged machine that something other than the mesh's table, the runtime's plumbing and a ban +list filters, the way it names strays and untaken modules, and such a machine is not "all well". +The converge preview lists the filters found and the fate of each: the found firewall retired, the +runtime's and the bans left, *other* left and named — so a person knows before the flip that the +machine will not be filtered by the mesh alone until they remove it, and what they would be +removing. *A converged machine is filtered by the mesh alone* when its list holds nothing but the +mesh's, the runtime's own and bans. + +**4. Adoption's threshold does not move.** A machine whose front end nobody speaks is still refused +adoption; a refusing rule in the runtime's user chain still does not refuse it — on both machines +of this mesh it would have, and the migration would not have happened. It is reported instead, +from the first report on. + +**5. Two of the group's issues are settled by records already accepted.** The forward chain follows +the machine's outward links and says nothing about networks ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md)), +which answers 141 whole. The runtime's file is written into and reloaded, and the hosts file's +region is the mesh's alone ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), +[issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)), which answers 084. One +machine-wide file the mesh still writes whole is its own filter, at the path the distribution's +packet filter reads; an operator's own rules at that path would be contested, and are held as +found until the filter module is taken ([ADR 0163](0163-taking-a-module-over-is-a-comparison.md)). +That is a difference a take shows, not a fault, and is decided when it bites. + +## Consequences + +- The host's report grows by the filters it found and, for a converged machine, the state of its + found firewall and who retired it; the controller keeps both on the node's record. +- `retireFirewall` runs on every converged apply and can disable the found firewall more than + once; the record's *disabled by the mesh* means exactly that. +- The reader of rules gains an owner per table and chain; what it refuses adoption for does not + change. A ban stays what it was: not a firewall. +- Issues 143 and 144 close on rules 1 to 3 once a machine's record names the predecessor's chain; + 141 closes on ADR 0140 and 084 on ADR 0102, both by reading. +- Removing what is reported is the operator's act, by hand, with the preview's words in front of + them. The mesh never flushes and never deletes a rule it did not mark. + +## How this is checked + +| Rule | Checked by | +|---|---| +| Every refusing table and chain is classified, the user chain's refusals as *other* | host tests over rulesets captured from three machines of this mesh: a predecessor's chain in the legacy filter, a ban list and empty front-end chains beside the runtime's, a virtualisation host and an endpoint agent that refuse nothing | +| The found firewall active again on a converged machine is retired again and said; found inactive is recorded as found, not done; a skipped step is said | host tests over a fake front end | +| The report carries the filters and the found firewall's state for a converged machine | a host test reading the report | +| `node show` lists filters with owners; `status` names a converged machine something else filters and is not well; the preview lists filters and fates | controller tests over a fixture report | +| Live | the home server's record names the predecessor's chain in the runtime's user chain as *other*; `status` names the machine; after the operator removes the chain, the next report drops it and `status` is well | + +## References + +- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0163](0163-taking-a-module-over-is-a-comparison.md) +- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md) +- Issues 084, 141, 143, 144, 145 diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 2ec3c13..d1b924a 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -178,6 +178,7 @@ python3 00-META/checks/index.py fail if stale - **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) - **0163** — [Taking a module over is a comparison: what it compares, what it refuses, and what it carries](0163-taking-a-module-over-is-a-comparison.md) - **0167** — [A membership carries what its module receives, and who the mesh is](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md) +- **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/05-the-node-host.md b/03-DESIGN/01-to-be/05-the-node-host.md index 9e5bc82..36da208 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -4,6 +4,7 @@ status: in-progress code: [mesh-host] updated: 2026-10-02 decisions: + - 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md - 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md - 02-DECISIONS/0141-the-host-delivers-its-own-successor.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md @@ -176,6 +177,14 @@ has left to say. *How it is checked:* a host test joins a kept network and refus host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test holds the installer's constants to the module's manifest where the catalogue is checked out beside it. +**What filters the machine, and the found firewall kept retired** — revision, 2026-10-02 +([ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)). The host +reports, with every apply, every table and legacy chain that refuses traffic and whose it reads it as — +the mesh's, the found firewall's, the container runtime's own, a ban, or other — and, converged, whether +the firewall it was found with is in force and who retired it. It retires that firewall on every +converged apply, not once, records *found inactive* apart from *disabled by the mesh*, and says when the +step was skipped. *How it is checked:* ADR 0168's table. + **Found reaches every kind that can touch what the machine has** ([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record keeps its mode and owner, a unit present with no record keeps its state and boot setting, a diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index c1a7ea5..78830f5 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -9,6 +9,7 @@ code: - mesh-host internal/apply (the service that reflects a rule set) updated: 2026-10-02 decisions: + - 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md - 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md - 02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md - 02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md @@ -710,6 +711,38 @@ needs no new filter; a declared port is reachable from off the private network a not; no address of a machine's own networks appears in a rendered filter, asserted on the text; and a machine reporting no outward link is refused in the control plane with its existing filter left alone. +### A converged machine is filtered by the mesh alone, and the host says what else refuses + +*2026-10-02, [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), +from [issues 143](../../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md) and +[144](../../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md).* + +Retiring the found firewall was a step the flip took once, and said it had taken whatever happened; +on the first machine with one it did not take, and a hand's work fifty minutes later was recorded as +the mesh's. And "the firewall found" named one front end while a predecessor's chain in the container +runtime's user hook — legacy iptables on one machine, invisible to a reader of nftables — filtered the +forwarded path, refused ports the mesh declared open, and carried an allowance every module reaching +another by the machine's own name relied on. + +**Convergence is a state the host keeps.** Every converged apply reads whether the found firewall is in +force; enabled again, it is retired again and said; the record says whether the mesh disabled it or +found it inactive, and a skipped step is said. **The host reports what filters the machine**, every +apply, adopted or converged: every table and legacy chain that refuses, with an owner — the mesh's, +the found firewall's, the runtime's own plumbing, a ban, or *other*, which is where the runtime's user +chain's refusals go. **The mesh says which:** `node show` lists them; `status` names a converged machine +anything *other* filters and is not well; the converge preview lists what filters the machine and the +fate of each — retired with the front end, left as the runtime's, left as a ban, or *left in force and +not the mesh's*. The mesh removes none of it; adoption's threshold does not move. + +*How it is checked:* host tests over rulesets captured from three machines of this mesh classify every +refusing chain (a predecessor's chain in the legacy filter as *other*, a ban list reached through the +user chain as a ban, a leftover front-end chain as *other*); a fake front end enabled again on a +converged machine is retired again and said, found inactive is recorded as found; the report carries +the filters and the found firewall's state and a change in them is worth an unasked report; controller +tests over a fixture report check the recording, the preview's fates, the status JSON and the well +predicate. Live: the home server's record names the predecessor's chain as *other* and `status` +names the machine until the chain is removed by hand. + ## 5 — Certificates **Two authorities, kept separate on purpose.** diff --git a/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md b/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md index 00977bb..e04aa9b 100644 --- a/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md +++ b/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-09-22 located-in: [mesh-controller internal/overlay, mesh-host internal/apply] -fixed-by: +fixed-by: ADR 0102 (mesh-controller internal/overlay: the runtime file written into, reloaded), issue 128 (the hosts file as a region) amended-design: 03-DESIGN/01-to-be/05-the-node-host.md --- @@ -56,3 +56,12 @@ and nothing checks for it today. - Should an adopted node that cannot trust the registry be refused a module that needs to pull? Or should the refusal come earlier, when the node joins? +## Resolved, 2026-10-02 + +The runtime's file is written into and the runtime reloaded, never restarted +([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md), the +diagnosis above); the hosts file is a marked region the mesh owns alone +([issue 128](../128-the-hosts-file-is-written-whole/00-report.md)). Neither whole file remains. Read +into [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rule 5, +which names the one whole machine-wide file the mesh still writes — its own filter at the +distribution's path — as a difference a take shows, not a fault. diff --git a/04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md b/04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md index 92f0ee4..7f014f5 100644 --- a/04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md +++ b/04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md @@ -1,10 +1,10 @@ --- -status: located +status: resolved opened: 2026-09-28 located-in: - mesh-controller internal/catalogue/filtering.go - mesh-host internal/apply -fixed-by: +fixed-by: ADR 0140 — mesh-controller (the filter around outward links; no network ranges anywhere) amended-design: 03-DESIGN/01-to-be/08-connectivity.md --- @@ -86,3 +86,11 @@ supersedes both 0137 and the first attempt at answering this. runtime, or left as the one constant? - Should the preview say which of a machine's networks are the mesh's and which are not, so a range that exists to protect a leftover is visible as such? + +## Resolved, 2026-10-02 + +By [ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), built and +live since 2026-09-29: the forward chain constrains what arrives on the machine's outward links and +says nothing about networks, so there is no list to derive and nothing for a preview to tell apart. +Read into [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), +rule 5, which closes it. diff --git a/04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md b/04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md index b940789..7377bf5 100644 --- a/04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md +++ b/04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md @@ -100,3 +100,11 @@ harmless, but the mesh's belief about which firewall is in force has been wrong so". Should it? - Why do the host's own detail lines not reach the journal? Everything it decided during the flip is unrecoverable, which is why this account has candidates instead of a cause. + +## Decided, 2026-10-02 + +[ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rule 1: +convergence is a state the host keeps — the found firewall active again is retired again and said, a +reconcile that finds it inactive records *found so* and never *done by the mesh*, and a step skipped +after a failed apply is said. Built in mesh-host on `feat/one-thing-filters-a-converged-machine`; the +record of both machines of this mesh is corrected by the first report under it. diff --git a/04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md b/04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md index 3a9dd56..435d060 100644 --- a/04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md +++ b/04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md @@ -82,3 +82,12 @@ everything reached from within. - Is the bus and the registry being reachable from anywhere still what the mesh wants on a machine that faces the internet? The design says yes, for enrolment. It deserves asking on its own rather than being answered by a leftover. + +## Decided, 2026-10-02 + +[ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rules 2 and +3: the host reports every table and legacy chain that refuses, with an owner, and the runtime's user +chain's refusals as *other*; `node show`, `status` and the converge preview say it. Built on +`feat/one-thing-filters-a-converged-machine` in mesh-host and mesh-controller. On 2026-10-02 the home +server still carries the predecessor's chain in its legacy filter; the record's live row is reading it +there.