From 213ab898d6897a44ea40426095367e7562ed34a0 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 17:52:58 +0200 Subject: [PATCH] ADR 0103: what an adopted node holds and what its guard refuses; the node host and connectivity designs name 0102 and 0103 --- ...d-node-holds-and-what-its-guard-refuses.md | 93 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/05-the-node-host.md | 14 +++ 03-DESIGN/01-to-be/08-connectivity.md | 11 ++- 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md diff --git a/02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md b/02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md new file mode 100644 index 0000000..98d4837 --- /dev/null +++ b/02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md @@ -0,0 +1,93 @@ +--- +topic: the mesh +status: accepted +date: 2026-09-22 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md +--- + +# 103. What an adopted node holds, and what its guard refuses + +## Context + +[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) holds a found file or +container until its module is taken. It also has the mesh guard the store's port and the +broker's management port with a table that only refuses. Two independent reviews of the build +found both rules drawn too narrowly and, for the guard, in the wrong place: + +1. **Other kinds of resource reach what was found.** An untaken module's directory is re-owned + and re-moded if a predecessor's directory is already at its path, and a database refuses to + start on a data directory whose mode changed. A service of the same name as a predecessor's + unit is started, stopped or re-enabled. An action run *in* a held container runs inside the + predecessor's service. A container that is not found under its own name is created, and can + mount the predecessor's data beside the predecessor's own container. +2. **The guard's two ports are not the only ones a found firewall misses.** A published + container port is forwarded, not received, and a firewall that filters only incoming traffic + never sees it (ADR 0100's own Context). The broker's plaintext port is published on every + interface and admitted by the filter from the private network only. So on an adopted node it + is reachable from anywhere. The same holds for any published port a module restricts to the + private network. +3. **The guard refuses by port alone.** On a machine that routes for others, such as a + predecessor's private-network hub, a packet for another machine's database port is refused as + well. And a port the guard refuses for a module that is assigned but not taken may still be + the predecessor's own, serving the predecessor's other machines. + +## Decision + +**Found covers every kind that can reach what the machine already has.** On an adopted node, +for a module not yet taken: + +- a **directory** present with no record is held: its mode and owner are left, and nothing in it + is touched; +- a **service** whose unit is present with no record is held: its state and whether it starts at + boot are left. A reload named by the module still happens, since a reload stops nothing + ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)); +- a **container** not found by its name is still held if it would mount a path or a volume that + is present with no record, because it would share the predecessor's data; +- an **action** or one-off step run *in* a held container is held until the module is taken. + +What is held is reported as held and is never removed, as ADR 0100 decides for files and +containers. + +**The guard refuses what the found firewall may miss, for taken modules only.** Its ports are +derived, not listed. They are every machine port a *taken* module publishes that the filter +would admit from the private network only, together with the store's port and the broker's +management port. A port of a module that is assigned but not taken is not guarded: it may still +be the predecessor's. The guard matches only packets addressed to this machine. Traffic the +machine routes for others is never its business. + +**An opening already answered by a found rule is not added.** If the found firewall already +admits what an opening says, the opening is reported as satisfied by the found rule, and the +mesh adds nothing and later removes nothing. The found firewall treats two rules differing only +in their comment as one rule, so adding the mesh's would take over the operator's. + +**A machine raised adopted stays adopted if genesis is run again.** The installer reads the +node's mode from what the machine records, not only from the operator's flag. A run without +the flag on an adopted machine is refused. + +## Consequences + +- An untaken module on an adopted node can come up only beside what was found, never on top of + it: whatever would share the predecessor's data waits for the cutover. +- The guard grows with the modules taken, and a module's published private-network port is + protected on an adopted node the way the derived filter protects it on a converged one. +- Guarding only taken modules means a foundation port on a node joining adopted is guarded once + its module is taken, not when it is assigned. Genesis takes the foundation's modules, so the + control-node's store is guarded from the first push. + +## How it is checked + +Unit tests hold the host to holding a found directory, a found service and a container that +would mount found data, to deferring an action run in a held container, and to adding no +opening a found rule answers. They hold the controller to deriving the guard from taken modules' +private-network published ports, and the guard's text to matching only this machine's +addresses. The installer's tests hold a re-run without the flag on an adopted machine to a +refusal. The adoption lab bed asserts that the broker's plaintext port is unreachable from +outside the private network, and that an operator's own rule for a port the mesh opens +survives the opening being removed. + +## References + +- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), + [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index a56570c..c94167e 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -91,6 +91,7 @@ python3 00-META/checks/index.py fail if stale - **0100** — [A node in use is adopted before it is converged](0100-a-node-in-use-is-adopted-before-it-is-converged.md) - **0101** — [A machine's own resolver does not make it in use](0101-a-machines-own-resolver-does-not-make-it-in-use.md) - **0102** — [The mesh writes into a shared file, never over it](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) +- **0103** — [What an adopted node holds, and what its guard refuses](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.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 b78d4f3..8788b9d 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -5,6 +5,8 @@ code: [mesh-host] updated: 2026-09-22 decisions: - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md + - 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md + - 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md - 02-DECISIONS/0019-how-this-repository-works.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0005-the-node-host.md @@ -150,6 +152,18 @@ checked:* unit tests hold the host to keeping a found file and container, conver taken, never removing a held file and reporting one that changed; the adoption bed asserts a found file byte for byte unchanged until its module is taken. +**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 +container that would mount found data is not created, and an action run in a held container +waits for the cutover. **A file the machine shares is written into, never over** +([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)): the host sets the mesh's keys in the object already there, +keeps every other key, records what each of its keys held before and gives them back when the +file is undeclared. Such a file replaces nothing, so it is never held. A service that re-reads +its configuration is **reloaded** for what it names in `reload-on`, never restarted. *How it is +checked:* unit tests hold the host to each of these, and the adoption bed asserts the runtime's +own settings survive adoption and a container without a restart policy keeps running. + ## Where a declaration comes from One behaviour, two sources diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 0ecc114..2d3c85d 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-09-22 decisions: + - 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md - 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md @@ -550,9 +551,13 @@ the found firewall in that firewall's own terms, marks it as the mesh's, removes marked, and re-checks every opening on each reconcile so a reload or a reboot does not lose it for longer than one reconcile. An opening is state, not a command, so it travels over the link like any other resource. **The mesh guards its own ports in a table of its own that only refuses** — -passing everything by default and holding nothing but refusals of the foundation's own two ports, -checked free at genesis, so it cannot close what the machine serves, and the found firewall's -reload does not touch it. It refuses the store's port and the broker's management port except from +passing everything by default and holding nothing but refusals, and the found firewall's reload +does not touch it. Its ports are derived ([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)): every machine port a +*taken* module publishes that the filter admits from the private network only, with the store's +port and the broker's management port. A port of a module not yet taken may still be the +predecessor's, so it is not guarded; and only packets addressed to this machine are matched, so +traffic it routes for others passes. An opening a found rule already answers is not added, so +removing it never removes the operator's rule. It refuses those ports except from the private network and the machine itself — loopback and the container runtime's networks, known by the interface a packet arrives on, never by its source address alone — at the prerouting hook, before the container runtime redirects the packet, so it matches the port the