From c74ea2a4a860619099466167dfb7c2d8949896f6 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 19:38:16 +0200 Subject: [PATCH] Records say what the build does: 0101 names only measured daemons; 0102 adds to lists and keeps what it writes over; 0103 names every held kind, the found-service rule, conflicting found rules, guards, and what of 0100 it replaces; designs 05, 09 and 17 in step; issue 084's diagnosis in its own file --- ...es-own-resolver-does-not-make-it-in-use.md | 5 +-- ...writes-into-a-shared-file-never-over-it.md | 21 +++++++--- ...d-node-holds-and-what-its-guard-refuses.md | 41 +++++++++++++------ 03-DESIGN/01-to-be/05-the-node-host.md | 7 +++- 03-DESIGN/01-to-be/09-the-node-lifecycle.md | 8 +++- 03-DESIGN/01-to-be/17-raising-a-mesh.md | 6 ++- .../00-report.md | 14 +------ .../01-diagnosis.md | 10 +++++ 8 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/01-diagnosis.md diff --git a/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md b/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md index 0e4afe6..79b50c6 100644 --- a/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md +++ b/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md @@ -42,9 +42,8 @@ habit. ## Decision **A listener held by one of the operating system's own network daemons does not make a machine -in use.** The daemons are the name resolver, the network manager, the address-configuration -client and the time client, named in the installer's code beside the measurement that found -them. Everything else in ADR 0100's definition stands: a running container, or any other +in use.** The daemons are the ones the measurement found: the name resolver and the network +manager, named in the installer's code beside that measurement. Everything else in ADR 0100's definition stands: a running container, or any other listener on an address other than loopback that is not ssh's, makes the machine in use, and genesis still names every one it counted. diff --git a/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md b/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md index 12ab4e2..8731e5a 100644 --- a/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md +++ b/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md @@ -42,10 +42,18 @@ the runtime. **A file the mesh shares with software it did not install is written into, never over.** A file resource may say it is written *into* a structured file. The host then reads what is there, sets only the keys the mesh declares, keeps every other key as it found it, and records what each -of its keys held before. Undeclared later, each key goes back to what it held, and a file the -mesh created is removed only if nothing but its own keys is left. A file written into replaces -nothing, so on an adopted node it is never held: it is written whether or not its module has -been taken. +of its keys held before. **A list is added to, never replaced**: where the machine already has a +list under a key the mesh declares, the mesh's members are added to it and the host records +exactly which members it added — setting the key would replace the operator's own list, the harm +this record exists to prevent. Undeclared later, each key goes back to what it held and each +added member is taken out again, and a file the mesh created is removed only if nothing but its +own keys is left. A file written into replaces nothing, so on an adopted node it is never held: +it is written whether or not its module has been taken. + +**Whatever the host writes over without a record of it, it keeps first.** On any node, adopted +or converged, before the host writes a file over one it has no record of making, it keeps the +original once and says where; if it cannot keep it, it does not write. A file the mesh takes over +is then never lost, whatever put it there. **A service that re-reads its configuration on a reload is reloaded, not restarted.** A service resource may name what it must be *reloaded* on, beside what it must be restarted on. The @@ -70,8 +78,9 @@ and stays held until networking is taken; a converge preview names it among the ## How it is checked Unit tests hold the host to setting only the declared keys and keeping the rest, restoring each -key and removing only a file it created when the resource is undeclared, refusing a file that is -not a JSON object rather than overwriting it, never holding a file written into on an adopted +key and removing only a file it created when the resource is undeclared, adding to a list and +removing only the members it added, keeping the original of a file it writes over without a +record, refusing a file that is not a JSON object rather than overwriting it, never holding a file written into on an adopted node, and reloading rather than restarting a service whose reload-on resource changed. A test in the controller holds the networking module to declaring the runtime's file written into and the runtime reloaded. The adoption lab bed gives the machine a runtime file with a setting of its own 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 index 98d4837..0cb2ea1 100644 --- 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 @@ -40,9 +40,14 @@ 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 **service** the machine runs — a unit running or enabled at boot — 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 unit file a package merely ships, which nothing runs, is not found: the private network's own + tunnel unit is one; +- an **archive** whose target is present with no record is not unpacked or re-owned; a + **process** whose unit file is present with no record is not written over or restarted; a + **user** that exists with no record keeps its shell and groups; - 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. @@ -51,23 +56,34 @@ What is held is reported as held and is never removed, as ADR 0100 decides for f 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 +derived from each *taken* module: every machine port it publishes that the filter would admit +from the private network only, and the ports its manifest names under `guards` — which is how +the store's port and the broker's management port are included. Only TCP is guarded. 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. +in their action, log setting or comment as one rule, so adding the mesh's would take over the +operator's. Where the found rule is such a twin but is not a plain allow — a deny, a limit, a +logged allow — the opening is refused, naming that rule, and nothing is added. **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. +**What of ADR 0100 this replaces.** ADR 0100's guard refused two ports, the foundation's own, +checked free at genesis, and could therefore close nothing the machine served. That guard is +replaced by the one above: a guarded port of a taken module may be one the predecessor served +more widely, and taking the module narrows it to the private network. Everything else in ADR 0100 +stands, and its rules for files and containers now cover the other kinds listed here. + ## Consequences +- Taking a module can narrow a port the predecessor served to anyone: the guard then refuses it + from outside the private network, as the module declares. Nothing says so yet + ([issue 086](../04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md)). - 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 @@ -78,14 +94,15 @@ the flag on an adopted machine is refused. ## 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' +Unit tests hold the host to holding a found directory, a found service, a found archive, process +and user, and a container that would mount found data; to not holding a unit nothing runs; to +deferring an action run in a held container; to adding no opening a found rule answers, and to +refusing one a conflicting found rule would absorb. 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. +outside the private network even with the found firewall admitting it, and that an operator's +own rule for a port the mesh opens survives the opening being removed. ## References 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 8788b9d..35300b6 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -158,8 +158,11 @@ keeps its mode and owner, a unit present with no record keeps its state and boot 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 +keeps every other key, adds its members to a list already there rather than replacing it, +records what each of its keys held and which members it added, and gives them back when the +file is undeclared. Such a file replaces nothing, so it is never held. And on any node, before +the host writes over a file it has no record of making, it keeps the original once and names +where; if it cannot keep it, it does not write. 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. diff --git a/03-DESIGN/01-to-be/09-the-node-lifecycle.md b/03-DESIGN/01-to-be/09-the-node-lifecycle.md index fc6d16e..61db9c4 100644 --- a/03-DESIGN/01-to-be/09-the-node-lifecycle.md +++ b/03-DESIGN/01-to-be/09-the-node-lifecycle.md @@ -300,8 +300,12 @@ cutover. The firewall found there stays in force and the mesh opens what it need ([08-connectivity](08-connectivity.md)). **Converging is one act per node, previewed**: it refuses while an assigned module still holds a found container; otherwise it lists what is reachable on the machine now — listening sockets and published ports — whether an assigned module declares each or -it will close, and which modules it will take, then takes them, loads the mesh's own filter in place -of its refusal-only table and disables the found firewall without flushing it. Returning a +it will close, which modules it will take and every held thing each will replace, and ends with a +short digest of all of that. **The flip is made only with that digest** — the operator confirms +the preview they read, and a preview that has changed since, or whose account of the machine is +more than fifteen minutes old, is refused. It then takes the modules, loads the mesh's own filter +in place of its refusal-only table and disables the found firewall without flushing it, putting +back the forwarding policy the found firewall had set. Returning a converged node to adopted unloads the derived filter, restores the refusal-only table, enables the found firewall again and converges the openings through it; what was taken stays taken. *How it is checked:* a lab bed prepares a machine the way a predecessor leaves one and asserts nothing that serves changes until a module is taken or the diff --git a/03-DESIGN/01-to-be/17-raising-a-mesh.md b/03-DESIGN/01-to-be/17-raising-a-mesh.md index 96a4f8a..c7c7e59 100644 --- a/03-DESIGN/01-to-be/17-raising-a-mesh.md +++ b/03-DESIGN/01-to-be/17-raising-a-mesh.md @@ -9,6 +9,7 @@ updated: 2026-09-22 decisions: - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md + - 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md - 02-DECISIONS/0067-genesis-is-a-pivot.md - 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md - 02-DECISIONS/0006-the-substrate-and-the-control-plane.md @@ -117,7 +118,10 @@ refuses ([08-connectivity](08-connectivity.md)). **A converged genesis refuses a a container running, or a port listening on an address other than loopback that is neither ssh's nor held by one of the operating system's own network daemons ([ADR 0101](../../02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md)) — and -names every one it counted, so a forgotten flag cannot close a working machine. *How it is checked:* the adoption bed raises genesis converged on a machine in use and +names every one it counted, so a forgotten flag cannot close a working machine. **A machine +raised adopted stays adopted when genesis is run again** +([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)): the installer reads the mode from what the machine records, +refuses a run without the flag on an adopted machine, and refuses the flag on a converged one. *How it is checked:* the adoption bed raises genesis converged on a machine in use and asserts the refusal, then adopted with the registry's port held and asserts the refusal names its holder, then with another port given asserts the foundation comes up, stays on that port once adopted as modules, and the machine's service is still reachable. 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 cb639de..00977bb 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,9 +1,9 @@ --- status: located opened: 2026-09-22 -located-in: [mesh-control internal/overlay, mesh-host internal/apply] +located-in: [mesh-controller internal/overlay, mesh-host internal/apply] fixed-by: -amended-design: +amended-design: 03-DESIGN/01-to-be/05-the-node-host.md --- # 084 — Taking the networking module on an adopted node restarts every container on the machine @@ -56,13 +56,3 @@ 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? -## Diagnosis - -*2026-09-22.* Worse than reported. The controller's "merge" of the runtime's file merges an -operator's settings into the module's content. The host then writes the result **whole**, so a -machine's own runtime settings, including its data directory, are replaced, not added to. -Measured on a lab machine: the runtime takes a new trusted-registry list on a reload, and a -running container without a restart policy survives it. Decided in -[ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md): the -runtime's file is written into and the runtime reloaded. The hosts file stays a whole file, held -until networking is taken. diff --git a/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/01-diagnosis.md b/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/01-diagnosis.md new file mode 100644 index 0000000..1a36968 --- /dev/null +++ b/04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/01-diagnosis.md @@ -0,0 +1,10 @@ +# 084 — Diagnosis + +*2026-09-22.* Worse than reported. The controller's "merge" of the runtime's file merges an +operator's settings into the module's content. The host then writes the result **whole**, so a +machine's own runtime settings, including its data directory, are replaced, not added to. +Measured on a lab machine: the runtime takes a new trusted-registry list on a reload, and a +running container without a restart policy survives it. Decided in +[ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md): the +runtime's file is written into and the runtime reloaded. The hosts file stays a whole file, held +until networking is taken.