ADR 0103: what an adopted node holds and what its guard refuses; the node host and connectivity designs name 0102 and 0103

This commit is contained in:
2026-09-22 17:52:58 +02:00
parent 84761f0600
commit 213ab898d6
4 changed files with 116 additions and 3 deletions
@@ -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)
+1
View File
@@ -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) - **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) - **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) - **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 ### Its tiers, from the bottom up
+14
View File
@@ -5,6 +5,8 @@ code: [mesh-host]
updated: 2026-09-22 updated: 2026-09-22
decisions: decisions:
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 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/0019-how-this-repository-works.md
- 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 02-DECISIONS/0005-the-node-host.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 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. 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 ## Where a declaration comes from
One behaviour, two sources One behaviour, two sources
+8 -3
View File
@@ -9,6 +9,7 @@ code:
- mesh-host internal/apply (the service that reflects a rule set) - mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-22 updated: 2026-09-22
decisions: 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/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/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 - 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 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 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** — 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, passing everything by default and holding nothing but refusals, and the found firewall's reload
checked free at genesis, so it cannot close what the machine serves, and the found firewall's 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
reload does not touch it. It refuses the store's port and the broker's management port except from *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 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 its source address alone — at
the prerouting hook, before the container runtime redirects the packet, so it matches the port the the prerouting hook, before the container runtime redirects the packet, so it matches the port the