From 504adef22158e2e8d7e1341a299b693a59be502a Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:37:48 +0200 Subject: [PATCH 1/4] =?UTF-8?q?ADR=200117:=20a=20machine's=20uplink=20is?= =?UTF-8?q?=20a=20seat=20=E2=80=94=20the=20mesh=20configures=20the=20manag?= =?UTF-8?q?er,=20never=20the=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0117-a-machines-uplink-is-a-seat.md | 108 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 2 files changed, 109 insertions(+) create mode 100644 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md diff --git a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md new file mode 100644 index 0000000..0103e8c --- /dev/null +++ b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md @@ -0,0 +1,108 @@ +--- +topic: what runs on it +status: proposed +date: 2026-09-26 +deciders: jochen +reconstructed: false +extends: 0110-a-seat-is-a-module-assignment-from-a-closed-set.md +--- + +# 117. A machine's uplink is a seat: the mesh configures the manager, never the link + +## Context + +The mesh installs on top of a machine's own networking. The private network's generator says +so in as many words: a machine has an address and a route to the broker *before* the mesh +exists, the broker's address travels in the enrolment token rather than being resolved, and the +private network is something the mesh installs on top, like anything else. Nothing in the mesh +says who manages that uplink, or what the mesh needs from whoever does. + +Adopting the first workstations showed that the mesh does need something from it, and gets it +by accident: + +- **The resolver the mesh owns depends on a file the mesh does not.** `resolv-conf` writes + `/etc/resolv.conf` and names the mesh's resolver. On a machine running NetworkManager, the + manager rewrites that file on every connectivity change unless it is told `dns=none`; on one + running dhcpcd, every lease renewal rewrites it unless it is told `nohook resolv.conf`. On + the adopted machines both settings exist only because the predecessor wrote them. No module + declares them. Remove the predecessor's file and the mesh's resolver is silently replaced the + next time a laptop changes network, while every surface of the mesh still reads green. +- **`resolv-conf` cannot declare them itself.** Which setting is needed depends on which + manager runs, and a `service` resource for a manager that is not installed fails the + declaration. A resolver module that knew about network managers would be the wrong module + knowing the wrong thing. +- **The private network's interface is exposed to the manager.** A manager that considers + every interface its own may try to configure `mesh0`, or tear it down on a profile change. + Nothing tells it not to. +- **Two managers on one machine go unnoticed.** Of the four machines adopted so far, one runs + systemd-networkd, two run NetworkManager, and one runs NetworkManager *and* dhcpcd at once: + two programs that each believe they own the machine's addresses and its resolver file. + Nothing detected it, because nothing in the mesh knows the role exists. + +The machines differ in a way that matters: servers are wired and never move, while +workstations join wireless networks, captive portals and phone hotspots wherever they are. + +## Considered Options + +**1. The mesh manages the uplink: links, addressing, wireless networks and their +credentials.** Rejected. The mesh reaches a machine only over that link. A declaration that +gets it wrong — a mistyped network, a stale credential, a manager that fails to start — takes +the machine off the network, and with it the only channel a fix could arrive on. That is the +one failure the sshd module's `listens` rule forbids the firewall to arrange; a mesh that owned +the link could arrange it with any push. And a wireless network is joined at the machine, by +the person using it, in the moment. A declaration composed elsewhere cannot answer a captive +portal. + +**2. Leave the uplink unmanaged; accept the implicit dependency.** Rejected. It keeps the +resolver working only for as long as a predecessor's file survives, and it leaves two managers +on one machine undetectable. + +**3. The uplink is a seat. The module holding it configures the manager's relationship to the +mesh, and never the link.** Chosen. + +## Decision + +**`the-uplink` is a node-scoped seat** in the closed set ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). +It delivers no provision. It is held by the module for the program that manages the machine's +own network, one per manager: `networkmanager`, `systemd-networkd`, and `dhcpcd` for a machine +with nothing more. Assigning a second is refused, naming the first. + +**What a holder declares** — only what keeps the manager and the mesh from contradicting each +other: + +- the manager's package, and its service running and enabled at boot; +- the manager's own configuration that leaves the resolver file to the mesh (`dns=none` for + NetworkManager, `nohook resolv.conf` for dhcpcd, and for systemd-networkd the equivalent + that stops it handing DNS to a resolver the machine does not use); +- the manager's own configuration that leaves the private network's interface alone + (NetworkManager's `unmanaged-devices` naming `mesh0`; for systemd-networkd, no network file + of the module's matches it); +- each as a drop-in beside the manager's main file where the manager reads one, and written + *into* a shared file otherwise ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)); +- the service **reloaded** when a drop-in changes, never restarted — a restart drops the link, + and the link is the mesh's own channel to the machine. Whether each manager applies these + settings on a reload is measured in the lab before its module is written, not assumed. + +**What a holder never declares:** a link, an address, a route, a connection profile, a +wireless network or its credentials. Those are the operator's, in the sense of +[ADR 0051](0051-shared-data-is-the-operators.md): the mesh does not create, change or delete +them, and the module's `access`, if it needs one, is read-only. + +## Consequences + +- `resolv-conf` stays generic. The condition it could not express — "only if NetworkManager + runs" — is expressed by assigning the module for the manager that does. +- The dependency on the predecessor's `dns=none` file becomes a declared resource. On an + adopted node the holder's drop-in arrives beside the predecessor's; both say the same thing, + and the predecessor's is retired by hand after the take, like any other file the mesh + replaced under another name. +- A machine running two managers is found at assignment: the second holder is refused, and the + operator decides which manager the machine keeps before either module is taken. +- Workstations keep joining networks the way they always have. The host already cooperates + with the manager — its dispatcher hook wakes it on every connectivity change — and nothing + here changes that. +- The seat table gains one entry: `the-uplink`, node scope, delivering nothing, decided here. +- **Not decided here:** whether the mesh should ever *offer* known networks to a machine — a + sealed, add-only list the operator curates once for all workstations. That is a different + question (the mesh holding credentials for links it must never be able to break) and gets its + own record if it is wanted. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 67eb0fe..1f26d55 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -199,6 +199,7 @@ python3 00-META/checks/index.py fail if stale - **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)* - **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md) *(proposed)* - **0115** — [One assignment of a module per node: the module's name is the assignment's identity](0115-one-assignment-of-a-module-per-node.md) *(proposed)* +- **0117** — [A machine's uplink is a seat: the mesh configures the manager, never the link](0117-a-machines-uplink-is-a-seat.md) *(proposed)* ### How it is built -- 2.54.0 From 0e066473b37eb1523e2f60e0b0705553f7f1be8c Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:44:50 +0200 Subject: [PATCH 2/4] ADR 0117 accepted; a manager that cannot reload takes the setting at its next start (dhcpcd, measured) --- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md | 10 +++++++--- 02-DECISIONS/README.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md index 0103e8c..3cbbbbb 100644 --- a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md +++ b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md @@ -1,6 +1,6 @@ --- topic: what runs on it -status: proposed +status: accepted date: 2026-09-26 deciders: jochen reconstructed: false @@ -80,8 +80,12 @@ other: - each as a drop-in beside the manager's main file where the manager reads one, and written *into* a shared file otherwise ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)); - the service **reloaded** when a drop-in changes, never restarted — a restart drops the link, - and the link is the mesh's own channel to the machine. Whether each manager applies these - settings on a reload is measured in the lab before its module is written, not assumed. + and the link is the mesh's own channel to the machine. **A manager that cannot reload is not + restarted instead:** its setting takes effect at the manager's next start. Measured on the + adopted machines: NetworkManager (1.58) and systemd-networkd (systemd 261) both report + `CanReload=yes`; dhcpcd (10.3) reports `CanReload=no`, so its module declares no trigger at + all. Whether each setting is actually *applied* by a reload is confirmed on a machine before + the module is taken there, not assumed. **What a holder never declares:** a link, an address, a route, a connection profile, a wireless network or its credentials. Those are the operator's, in the sense of diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 1f26d55..de2b056 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -199,7 +199,7 @@ python3 00-META/checks/index.py fail if stale - **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)* - **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md) *(proposed)* - **0115** — [One assignment of a module per node: the module's name is the assignment's identity](0115-one-assignment-of-a-module-per-node.md) *(proposed)* -- **0117** — [A machine's uplink is a seat: the mesh configures the manager, never the link](0117-a-machines-uplink-is-a-seat.md) *(proposed)* +- **0117** — [A machine's uplink is a seat: the mesh configures the manager, never the link](0117-a-machines-uplink-is-a-seat.md) ### How it is built -- 2.54.0 From c3313f6e1779f641165776a2e7baa504d20c9570 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 00:03:15 +0200 Subject: [PATCH 3/4] 0117 review: seat table row + decision cited, networkd/dhcpcd lines match the modules, block placement, dispatcher scope, references --- .../0117-a-machines-uplink-is-a-seat.md | 37 +++++++++++++------ 03-DESIGN/01-to-be/26-the-seats.md | 4 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md index 3cbbbbb..79a2f57 100644 --- a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md +++ b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md @@ -34,9 +34,9 @@ by accident: - **The private network's interface is exposed to the manager.** A manager that considers every interface its own may try to configure `mesh0`, or tear it down on a profile change. Nothing tells it not to. -- **Two managers on one machine go unnoticed.** Of the four machines adopted so far, one runs - systemd-networkd, two run NetworkManager, and one runs NetworkManager *and* dhcpcd at once: - two programs that each believe they own the machine's addresses and its resolver file. +- **Two managers on one machine go unnoticed.** Among the machines adopted so far, one runs + NetworkManager *and* dhcpcd at once: two programs that each believe they own the machine's + addresses and its resolver file. Nothing detected it, because nothing in the mesh knows the role exists. The machines differ in a way that matters: servers are wired and never move, while @@ -72,13 +72,16 @@ other: - the manager's package, and its service running and enabled at boot; - the manager's own configuration that leaves the resolver file to the mesh (`dns=none` for - NetworkManager, `nohook resolv.conf` for dhcpcd, and for systemd-networkd the equivalent - that stops it handing DNS to a resolver the machine does not use); + NetworkManager, `nohook resolv.conf` for dhcpcd, and nothing for systemd-networkd, which + never writes the resolver file); - the manager's own configuration that leaves the private network's interface alone - (NetworkManager's `unmanaged-devices` naming `mesh0`; for systemd-networkd, no network file - of the module's matches it); + (NetworkManager's `unmanaged-devices` naming `mesh0`; dhcpcd's `denyinterfaces mesh0`; for + systemd-networkd a network file of the module's matching `mesh0` as `Unmanaged=yes`); - each as a drop-in beside the manager's main file where the manager reads one, and written - *into* a shared file otherwise ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)); + *into* a shared file otherwise, as a marked region the host owns + ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)'s idea for text files), + placed where the manager reads it as global — at the start of `dhcpcd.conf`, above any + `interface` line, because every line after one belongs to that interface; - the service **reloaded** when a drop-in changes, never restarted — a restart drops the link, and the link is the mesh's own channel to the machine. **A manager that cannot reload is not restarted instead:** its setting takes effect at the manager's next start. Measured on the @@ -102,11 +105,23 @@ them, and the module's `access`, if it needs one, is read-only. replaced under another name. - A machine running two managers is found at assignment: the second holder is refused, and the operator decides which manager the machine keeps before either module is taken. -- Workstations keep joining networks the way they always have. The host already cooperates - with the manager — its dispatcher hook wakes it on every connectivity change — and nothing - here changes that. +- Workstations keep joining networks the way they always have. Under NetworkManager and + systemd-networkd the host already cooperates with the manager — its dispatcher hook wakes it + on every connectivity change — and nothing here changes that. A dhcpcd-only machine has no + such hook, and nothing here adds one. - The seat table gains one entry: `the-uplink`, node scope, delivering nothing, decided here. - **Not decided here:** whether the mesh should ever *offer* known networks to a machine — a sealed, add-only list the operator curates once for all workstations. That is a different question (the mesh holding credentials for links it must never be able to break) and gets its own record if it is wanted. + +## References + +- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): the closed set this seat joins; + [to-be 26](../03-DESIGN/01-to-be/26-the-seats.md): the seat table +- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md): written into, never over +- [ADR 0051](0051-shared-data-is-the-operators.md): what is the operator's stays the operator's +- mesh-controller `internal/catalogue/seats.go` (the seat), `internal/overlay/generator.go` (the + mesh installs on top of the machine's own networking) +- mesh-catalog `modules/networkmanager`, `modules/systemd-networkd`, `modules/dhcpcd` +- mesh-host `internal/apply/block.go` (a file written into a marked region, `at` start or end) diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index 65dc1b9..bf2317d 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -8,11 +8,12 @@ code: - mesh-controller cmd/mesh-controller/source.go - mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql - mesh-catalog modules/gitea/module.json -updated: 2026-09-26 +updated: 2026-09-27 decisions: - 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md - 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md - 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md + - 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md --- # 26 — The seats @@ -64,6 +65,7 @@ nobody argued for is an entry nobody can explain. | `the-private-network` | node | — | the private network the mesh runs over | | `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen | | `the-showcase` | node | — | the showcase module | +| `the-uplink` | node | — | the program that manages the machine's own network ([ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)) | The controller holds this set in code, and a test asserts both its size and that every entry names the record that made it a seat. **This table and [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) -- 2.54.0 From df4a3538c3decbc7024fca9ddad4ae1db1dc1f85 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 00:07:42 +0200 Subject: [PATCH 4/4] =?UTF-8?q?0117=20review:=20a=20holder's=20service=20d?= =?UTF-8?q?eclares=20no=20state=20=E2=80=94=20the=20manager's=20lifecycle?= =?UTF-8?q?=20is=20the=20machine's;=20a=20start-only=20setting's=20gap=20o?= =?UTF-8?q?n=20a=20fresh=20machine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md index 79a2f57..7fbc5da 100644 --- a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md +++ b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md @@ -70,7 +70,11 @@ with nothing more. Assigning a second is refused, naming the first. **What a holder declares** — only what keeps the manager and the mesh from contradicting each other: -- the manager's package, and its service running and enabled at boot; +- the manager's package, present — and its service **with no state**: the manager's lifecycle is + the machine's. The mesh never starts, stops, enables or disables it, because stopping it takes + the link down, and a holder unassigned by mistake — or the wrong holder assigned — must not be + able to do that, nor start a second manager beside the one the machine runs. The service is + declared only so a change to the holder's settings reaches a *running* manager; - the manager's own configuration that leaves the resolver file to the mesh (`dns=none` for NetworkManager, `nohook resolv.conf` for dhcpcd, and nothing for systemd-networkd, which never writes the resolver file); @@ -103,6 +107,10 @@ them, and the module's `access`, if it needs one, is read-only. adopted node the holder's drop-in arrives beside the predecessor's; both say the same thing, and the predecessor's is retired by hand after the take, like any other file the mesh replaced under another name. +- A setting a manager reads only at its start is not in force until then. On an adopted machine + the predecessor's identical line normally already is; on a machine that was not adopted, + dhcpcd's resolver hook keeps rewriting the resolver file until dhcpcd next starts, and the + operator restarts it once, in a window of their choosing. - A machine running two managers is found at assignment: the second holder is refused, and the operator decides which manager the machine keeps before either module is taken. - Workstations keep joining networks the way they always have. Under NetworkManager and -- 2.54.0