ADR 0117: a machine's uplink is a seat — the mesh configures the manager, never the link #139

Merged
jschoubben merged 4 commits from decision/0117-the-uplink-is-a-seat into main 2026-09-26 22:56:27 +00:00
2 changed files with 109 additions and 0 deletions
Showing only changes of commit 504adef221 - Show all commits
@@ -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.
+1
View File
@@ -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