From 1aeb4fe8d89090c7f2795ac9fee501593839d0a8 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 23:31:50 +0200 Subject: [PATCH] ADR 0140: the filter constrains what arrives from outside, and says nothing about a machine's own guests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading a converged machine's rendered rules showed the cause: the chain blocks everything passing through and then allows the machine's own containers back by listing their address ranges. 0137 made that list typeable and 0139 tried to generate it; both refined a list that should not exist, because the mesh has no position on a container reaching outward. Constrain what arrives from outside, allow what did not, and let the machine report which links face outside — one fact instead of a list. Ports keep following the modules unchanged. The records check now allows one record to supersede several, and stops requiring a withdrawn record's own citations to be live. --- 00-META/checks/records.py | 19 ++- ...a-machine-says-which-networks-it-routes.md | 3 +- ...-forwarded-because-a-module-declared-it.md | 3 +- ...er-constrains-what-arrives-from-outside.md | 146 ++++++++++++++++++ 02-DECISIONS/README.md | 5 +- 03-DESIGN/01-to-be/08-connectivity.md | 62 ++++---- .../00-report.md | 12 ++ 7 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md diff --git a/00-META/checks/records.py b/00-META/checks/records.py index 3f12a2e..9c235eb 100644 --- a/00-META/checks/records.py +++ b/00-META/checks/records.py @@ -164,6 +164,12 @@ def check_rests_on(failures, records): # decision is exactly what as-is is for." if rel(path).startswith("03-DESIGN/00-as-is/"): continue + # A withdrawn record's citations are history. It instructs nobody -- every reader + # is sent to its superseder -- so what it was built on may itself be withdrawn. + # Refusing that would mean rewriting the lineage of a record whose reasoning is + # the thing the immutability rule protects. + if frontmatter(read(path)).get("status") == "superseded": + continue # An extension that supersedes legitimately names what it replaced. this = ADR_FILE.match(os.path.basename(path)) supersedes = records[number]["front"].get("superseded-by", "") @@ -241,13 +247,20 @@ def check_supersession_symmetry(failures, records): failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}") continue other = records[match.group(1)] - claims = os.path.basename(str(other["front"].get("supersedes", ""))) - if claims != record["name"]: + # `supersedes:` may name one record or several. One decision replacing two is a real + # situation -- two records that built and refined the same wrong mechanism are withdrawn + # by the one record that removes it -- and a check that allows only one would force + # either a chain of pro-forma records or an unmarked supersession. + claimed = other["front"].get("supersedes", "") + if isinstance(claimed, str): + claimed = [claimed] if claimed else [] + claims = [os.path.basename(str(entry)) for entry in claimed] + if record["name"] not in claims: failures.add( "supersession", rel(other["path"]), f"ADR {number} says this supersedes it; this record does not say so " - f"(supersedes: {claims or 'absent'})", + f"(supersedes: {', '.join(claims) or 'absent'})", ) diff --git a/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md b/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md index 2c3904e..7500266 100644 --- a/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md +++ b/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md @@ -1,10 +1,11 @@ --- topic: what runs on it -status: accepted +status: superseded date: 2026-09-28 deciders: jochen extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md reconstructed: false +superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md --- # 137. A machine says which networks it routes diff --git a/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md b/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md index 6b39398..00a9e6c 100644 --- a/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md +++ b/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md @@ -1,10 +1,11 @@ --- topic: what runs on it -status: accepted +status: superseded date: 2026-09-28 deciders: jochen reconstructed: false extends: 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md +superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md --- # 139. A network is forwarded because a module declared it diff --git a/02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md b/02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md new file mode 100644 index 0000000..e2028f2 --- /dev/null +++ b/02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md @@ -0,0 +1,146 @@ +--- +topic: what runs on it +status: accepted +date: 2026-09-28 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md +supersedes: + - 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md + - 02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md +--- + +# 140. The filter constrains what arrives from outside, and says nothing about a machine's own guests + +## Context + +The filter the mesh derives blocks traffic passing *through* a machine unless something allows it, +because a container's published port is traffic passing through rather than traffic arriving at the +machine itself ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), +[issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md)). +Having blocked all of it, the filter then had to let the machine's own containers reach outward again. +It does that by listing the address ranges those containers sit on. + +As rendered on a converged workstation today: + +``` +policy drop +ct state established,related accept +ip saddr 172.16.0.0/12 accept +ip saddr 192.168.128.0/17 accept +ip saddr 10.0.0.0/8 accept +ip saddr 192.168.16.0/20 accept +... four more +``` + +Two of those ranges were constants in the control plane's source. The rest were typed by the operator +after [ADR 0137](0137-a-machine-says-which-networks-it-routes.md), which existed to make the typing +possible, because converging that workstation had cut every one of its containers off from the +internet and nothing reported a fault +([issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md)). + +**The list is the mistake, not its contents.** Every attempt to make it correct fails the same way. +A constant describes one machine. A typed range goes stale, and cannot tell a network the mesh made +from one a predecessor left behind — measured on the control-node, where six such ranges fall outside +the constants and two of the six belong to services the mesh does not run +([issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)). +[ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) tried to generate the same +list from the modules and put half the rule set on the machine to do it. Three records, one list, and +the list should not exist. + +**Because the mesh has no policy about a container reaching outward.** What the filter is for is +stated in [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md): which port is open, +and to whom. That is about what arrives. A container of this machine's own opening a connection to +something else is not a port being opened to anybody, and enumerating the addresses it might do so +from is bookkeeping about the machine's internal plumbing, which the mesh neither owns nor can know. + +**The system being replaced never had this fault, and its rule says why.** The chain still protecting +the control-node applies only to traffic arriving on that machine's outward link, and leaves +everything else alone. The mesh's filter dropped that distinction and replaced it with a list of +addresses. + +## Considered Options + +1. **Keep the list and generate it better** — from the modules' declared networks, or from what the + machine reports. Rejected: [ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) + is that, and it puts part of the rule set on the machine, which makes the rule set partly the + machine's and the derivation advisory. +2. **Name the guest links instead of their addresses, and allow only those.** Rejected as more than is + needed: it fails in the safe direction, but it is still a list that has to keep up with the + machine, and the thing it protects against — a container reaching outward — is not a thing the mesh + has a position on. +3. **Do not block traffic passing through at all.** Rejected: that is + [issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md), + where a published port was reachable from anywhere because no rule mentioned it. +4. **Constrain what arrives from outside, and nothing else.** Adopted. + +## Decision + +**The filter constrains traffic arriving from outside the machine, and says nothing about traffic that +did not.** Traffic passing through the machine is allowed unless it arrived on one of the machine's +outward links, in which case it is allowed only where a declared endpoint's reach admits it +([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). A container of this +machine's own reaching anywhere is not filtered, because the mesh has no position on it. + +**A machine says which of its links face outside.** One node-level fact, reported by the machine the +way it already reports the kind of firewall it found and the tunnel it carries — not a setting, not a +list of addresses, and not something anybody types. It does not change when a module is added or +removed, which is what separates it from the list it replaces. + +**A machine that has reported no outward link is sent no filter.** Rendering a rule around a link +whose name is not known produces a rule set that does not load, which is a machine filtering nothing +while its unit reports success. The refusal happens in the control plane, where a person reads it, and +the machine keeps the filter it already has. + +**No addresses of the machine's own networks appear in the filter.** The two constants are removed and +`node networks` is removed with them, along with everything any machine was told to say through it. +Ports continue to follow the modules exactly as before: a module assigned to a machine opens the port +its assignment says it reaches on, and nothing about a network is said anywhere. + +## Consequences + +- **Three records collapse into one rule.** 0137 and 0139 are superseded. What 0137 was right about — + that converging a machine had silently cut off its own containers, and that nothing previewed it — is + answered by removing the cause rather than by giving the operator a way to compensate for it. +- **Every machine already converged loses its declared ranges and keeps working**, because the traffic + those ranges allowed is now allowed by not having arrived from outside. The workstation's five ranges + and the laptop's one are deleted rather than migrated. +- **A machine's test beds stop being a special case.** A bed's network is created while the machine + runs and was the case no list could cover; it is now covered by not being mentioned. +- **A new fact travels in the report**, and the control plane refuses to compose a filter without it, + so the order of the roll-out matters: the machines report before the control plane depends on it. +- **A machine with more than one outward link says so**, and a machine that acquires one while the mesh + is not looking is treated as internal until its next report. That window is the cost of this shape; + it is bounded by the report interval, and it exists on machines whose outward link changes, which + are the machines with nothing published to the outside. +- **What got harder:** nothing in the declaration, and one more thing a machine must be able to work + out about itself. A machine that cannot say which link faces outside cannot be given a filter. + +## How it is checked + +- **A machine's own container reaches outward with no network named anywhere.** A bed converges a + machine carrying containers on several networks, none of them mentioned in any setting, and each + reaches out afterwards. This fails against the previous behaviour, where the same flip cut them off, + and that is how it is written. +- **A port declared reachable from outside is reachable; one that is not, is not.** Probed from off the + machine's private network, for a published port and for an undeclared one, before and after the flip. +- **A network created after the filter was composed needs no new filter.** A network is made on the + machine after its last declaration and a container on it reaches out, with nothing re-sent. +- **No address of a machine's own networks appears in a rendered filter**, asserted on the text so a + range cannot creep back in. +- **A machine that reports no outward link is sent no filter, and the refusal names it** — asserted in + the control plane, and that the machine's existing filter is left alone. +- **A machine reporting two outward links has both constrained**, asserted per chain body so a rule + covering one and not the other cannot pass. + +## References + +- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — what the filter is for +- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — what admits traffic + arriving from outside +- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — why traffic passing through is + filtered at all +- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md), + [ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) — superseded here +- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md), + [issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 768aa53..b79b54e 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -217,9 +217,10 @@ python3 00-META/checks/index.py fail if stale - **0133** — [A module owns its migrations, and the mesh owns when they run](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) *(superseded)* - **0135** — [A module version prepares its state before it runs](0135-a-module-version-prepares-its-state-before-it-runs.md) - **0136** — [A step gates its module, not the machine](0136-a-step-gates-its-module-not-the-machine.md) -- **0137** — [A machine says which networks it routes](0137-a-machine-says-which-networks-it-routes.md) +- **0137** — [A machine says which networks it routes](0137-a-machine-says-which-networks-it-routes.md) *(superseded)* - **0138** — [An assignment binds an endpoint and says how far it reaches](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) -- **0139** — [A network is forwarded because a module declared it](0139-a-network-is-forwarded-because-a-module-declared-it.md) +- **0139** — [A network is forwarded because a module declared it](0139-a-network-is-forwarded-because-a-module-declared-it.md) *(superseded)* +- **0140** — [The filter constrains what arrives from outside, and says nothing about a machine's own guests](0140-the-filter-constrains-what-arrives-from-outside.md) ### How it is built diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 75f6f94..297aee8 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -10,7 +10,7 @@ code: updated: 2026-09-28 decisions: - 02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md - - 02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md + - 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md - 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md - 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md @@ -627,41 +627,49 @@ the found firewall reloads and reachable from a container on the node, that a ma enrols through the openings before and after a reload and a reboot, and that after the flip the declared port is open and the undeclared one closed. -### The networks it forwards are the ones its modules declared +### It filters what arrives from outside, and not what the machine's own guests send *2026-09-28, preparing the control-node's convergence — [issue 141](../../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md), settled by +[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), which replaces +[ADR 0137](../../02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md) and [ADR 0139](../../02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md).* -The forward chain denies by default, so a machine's own guests have to be allowed back in. Until now -that was two ranges named in the control plane and, since -[ADR 0137](../../02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md), a list a machine could -add to. On the machine measured here, six of its container networks fell outside those ranges — and -four of the six were networks the mesh's own modules had declared and the host had created, while two -were the predecessor's leftovers. A range wide enough to keep the four keeps the two: a filter widened -by hand to protect what should not be there. +Traffic passing through a machine is filtered, because a container's published port is traffic passing +through rather than traffic arriving at the machine itself. Having blocked it, the filter then had to +let the machine's own containers reach outward again — and it did that by listing the address ranges +they sit on. Two of those ranges were constants in this repository's code, and the rest were typed by an +operator after the flip had already cut a workstation's containers off from everything. -**A network is forwarded because a module declared it.** The forward chain forwards the networks of the -modules assigned to that node and, by default, nothing else; a module unassigned stops being forwarded -at the next reconcile. The control plane says which networks, by name, and the host — which created -them — renders their addresses, because the runtime allocates the subnet and the host is what knows it. -That keeps §4's shape and this document's: the mesh decides, the host applies. +**The list was the mistake, not its contents.** A constant describes one machine. A typed range goes +stale and cannot tell a network the mesh made from one a predecessor left behind — on the control-node, +six ranges fall outside the constants and two of the six belong to services the mesh does not run. The +attempt to generate the list from the modules put half the rule set on the machine and made the +derivation advisory. Three records, one list. -**This is derivation from the declaration, not from the machine.** 0137 rejected reading the machine, -for two reasons that do not apply here: a network created between two declarations is already named in -the one that asked for it, and a network nobody declared is never forwarded however it appeared. What -0137's mechanism keeps is the case it was right for — guests no module declares, a test bed's range — -added to the derived set and never replacing it. +**And the mesh has no position on a container reaching outward.** §4 exists to say which port is open +and to whom, which is about what arrives. A container of this machine's own opening a connection +somewhere is not a port opened to anybody, and the addresses it might do that from are the machine's +internal plumbing, which the mesh neither owns nor can know. -The runtime's own default bridge, which a container attaches to when it names no module network, is the -runtime's and not a module's, so the host renders it from what the runtime reports. With that, no range -is named in the control plane at all. +So the filter constrains what arrives from **outside** the machine and says nothing about what did not. +Traffic passing through is allowed unless it came in on one of the machine's outward links, and then +only where a declared endpoint's reach admits it (§6). The machine says which of its links face +outside — one fact it reports, like the kind of firewall it found and the tunnel it carries, not a +setting and not a list of addresses. It does not change when a module is added or removed, which is the +whole difference from what it replaces. A machine that has reported no outward link is sent no filter +at all, and keeps the one it has, because a rule written around a link with no name is a rule set that +does not load — a machine filtering nothing while its unit reports success. -*How it is checked:* a node with two modules declaring networks renders rules for exactly those two and -none for a third present on the machine that nothing declared — which fails against forwarding by -range, and is how it was written; unassigning one removes its rule at the next reconcile; the default -bridge is asserted for a runtime whose bridge is somewhere other than the old constant named; and the -guests of a declared network keep address and name service, per chain body. +Ports go on following the modules exactly as before: assign a module to a machine and the port its +assignment says it reaches on opens. Nothing about a network is said anywhere, by anybody. + +*How it is checked:* a bed converges a machine carrying containers on several networks, none of them +named in any setting, and each reaches outward afterwards — which fails against the previous behaviour, +where the same flip cut them off, and is how it was written; a network made *after* the last declaration +needs no new filter; a declared port is reachable from off the private network and an undeclared one is +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. ## 5 — Certificates 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 51260c5..92f0ee4 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 @@ -61,6 +61,18 @@ module declares — a test bed's pool — which is a much smaller residue than t has to know the runtime's allocations to tell whether that line is sufficient. On the machine measured here it read as though nothing needed saying. +## What was decided + +*2026-09-28, later the same day.* The answer is not a better list. The question in the first open +item below — should the chain be derived from the networks the modules declare — was answered *no*, +after a converged machine's rendered rules were read: the chain blocks everything passing through the +machine and then allows its own guests back by listing their addresses. Every route to a correct list +fails, because the mesh has no position on a container reaching outward in the first place. The filter +now constrains what arrives from **outside** the machine and says nothing about what did not, and a +machine says which of its links face outside — one reported fact instead of a list. See +[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), which +supersedes both 0137 and the first attempt at answering this. + ## Open questions - Should the forward chain be derived from the network resources the node's modules declare, with the