diff --git a/02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md b/02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md index 842f160..75c6606 100644 --- a/02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md +++ b/02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md @@ -98,6 +98,17 @@ nameservers, and systemd-networkd's module declaring the file itself. `resolv-co `node-resolver-config` then retire, and ADR 0220's dependency of `node-resolver-config` on `node-uplink` goes with them. One owner for the file, and it is the program that would otherwise rewrite it. +> **Progressive insight — 2026-10-05.** Built, the managers' own mechanisms turned out not to write +> the mesh's file: NetworkManager's global DNS configuration and dhcpcd's resolv.conf hook each write +> `/etc/resolv.conf` in their own form — their own header, their own options line — and dhcpcd reads +> its configuration only at its next start, so a change of resolvers would wait for one. The record +> said NetworkManager and dhcpcd would be given the resolvers "through its global DNS configuration" +> and "through static nameservers"; instead every one of the three modules declares the file itself, +> from one template, and keeps its manager off it as before (`dns=none`, `nohook resolv.conf`, and +> nothing for systemd-networkd). The decision — the uplink's holder owns the file, and `resolv-conf`, +> `node-resolver-config` and ADR 0220's dependency retire — stands. How parts 2 and 3 are checked is +> in [connectivity §2](../03-DESIGN/01-to-be/08-connectivity.md). + **3. Next, decided and not yet built: a machine's names are one seat's.** `/etc/hosts` and `/etc/hostname` belong to one seat for the machine's identity. The `hosts` module, holding `node-hosts-file` ([ADR 0199](0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)), diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 161a3e3..ed14fcd 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -7,15 +7,15 @@ code: - mesh-controller internal/identity/authority.go - mesh-controller cmd/mesh-controller/plan.go (the names the roster publishes) - mesh-controller internal/catalogue/zones.go (the zones a module answers, ADR 0199) - - mesh-controller internal/catalogue/seats.go (mesh-dns-resolver, node-hosts-file, node-resolver-config needing node-uplink) - - mesh-controller internal/catalogue/seat_dependencies.go (a seat's need checked at assignment, ADR 0220) + - mesh-controller internal/catalogue/seats.go (mesh-dns-resolver, node-hostname, node-uplink) + - mesh-controller internal/catalogue/resolve.go (one owner per path, a rendered fact's included, ADR 0223) - mesh-controller cmd/mesh-controller/holdings.go (the holders of a replicated seat, ADR 0223) - mesh-controller internal/catalogue/roster.go (each replicated seat's holders, for a template) - mesh-catalog modules/dnsmasq (the mesh's resolvers) - - mesh-catalog modules/resolv-conf (what a node asks) - - mesh-catalog modules/hosts (a node's /etc/hosts) + - mesh-catalog modules/networkmanager, modules/systemd-networkd, modules/dhcpcd (what a node asks, written by its uplink's holder) + - mesh-catalog modules/hostname (a node's /etc/hostname and /etc/hosts) - mesh-host internal/identity/serving.go - - mesh-host internal/apply (the service that reflects a rule set) + - mesh-host internal/apply (the service that reflects a rule set; a whole file handed to its new owner) updated: 2026-10-05 decisions: - 02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md @@ -379,40 +379,52 @@ the module not on record stands beside them, eligible and silent, and a seat hel once. *Checked by the controller's resolution tests with two holders on record, with two claimants and nothing on record, and on a seat held once.* -**Every node asks them for everything, and nothing else.** The module holding -`node-resolver-config` writes `/etc/resolv.conf` listing every holder's private address — the holder -on the machine itself first if it is one, then the rest by name — with a short timeout and two -attempts, and **no public resolver**. ADR 0196 listed a public resolver second, for the anchor being -unreachable; a C library that asks every listed server at once and takes the first reply — musl, so -every Alpine container — took the public resolver's "no such name" for a mesh name, and every build on -the home server failed. With only the mesh's resolvers listed, whichever answers first gives the one -answer. The cost is stated: a machine that reaches no mesh resolver has no names until it does, and -with the anchor down the second resolver is reachable only on its own machine and its own LAN. -Containers take the same lines from their machine, the runtime copying non-loopback resolvers into -every container, so the runtime is given no `dns` of its own +**Every node asks them for everything, and nothing else.** `/etc/resolv.conf` lists every holder's +private address — the holder on the machine itself first if it is one, then the rest by name — with a +short timeout and two attempts, and **no public resolver**. ADR 0196 listed a public resolver second, +for the anchor being unreachable; a C library that asks every listed server at once and takes the +first reply — musl, so every Alpine container — took the public resolver's "no such name" for a mesh +name, and every build on the home server failed. With only the mesh's resolvers listed, whichever +answers first gives the one answer. The cost is stated: a machine that reaches no mesh resolver has no +names until it does, and with the anchor down the second resolver is reachable only on its own machine +and its own LAN. Containers take the same lines from their machine, the runtime copying non-loopback +resolvers into every container, so the runtime is given no `dns` of its own ([ADR 0196](../../02-DECISIONS/0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md), -replacing ADR 0194's per-node `systemd-resolved` stub). `resolv-conf` is the one module the catalogue -offers for it; the systemd-resolved split-DNS module that once claimed the same seat is gone -([ADR 0220](../../02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)). -*Checked by the controller's composition tests on both holders and on a third machine, and live by -each machine's `/etc/resolv.conf` and an Alpine container on the home server resolving the anchor's -name every time.* +replacing ADR 0194's per-node `systemd-resolved` stub). *Checked by the controller's composition tests +on both holders and on a third machine, for every uplink module, and live by each machine's +`/etc/resolv.conf` and an Alpine container on the home server resolving the anchor's name every +time.* -**Next, decided and not yet built** ([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)): -`/etc/resolv.conf` becomes the file of the `node-uplink` holder, given the resolvers by the mesh — -NetworkManager through its global DNS configuration, dhcpcd through static nameservers, -systemd-networkd's module declaring the file — and `resolv-conf`, `node-resolver-config` and its need -for the uplink below retire. And a machine's own names become one seat's: the `hosts` module is -renamed `hostname` and holds `node-hostname`, writing `/etc/hostname` and the machine's `127.0.1.1` -line and keeping the operator's lines. +**The file is the uplink's holder's** ([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)). +A network manager rewrites `/etc/resolv.conf` on every connectivity change unless it is told not to +([ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)), so the module holding +`node-uplink` — the one telling it — writes the file itself, and there is one owner for it, the one +whose program would otherwise overwrite it. Each of the catalogue's three managers' modules +(NetworkManager, systemd-networkd, dhcpcd) renders the same template from the resolver's holders and +requires the mesh's resolver, so a machine is refused when nothing in the mesh resolves rather than +given a file listing nothing. The managers' own mechanisms were weighed and not used: NetworkManager's +global DNS and dhcpcd's static nameservers each write the file in their own form — their own header, +their own options line — so neither can write the mesh's file byte for byte, and dhcpcd reads its +configuration only at its next start; each manager is told to keep off the file and the module +declares it. No other module may write that path, as a file or as a rendered fact: two modules on one +node declaring one path are refused. The module that wrote the file before, its seat +`node-resolver-config` and that seat's need of the uplink beside it +([ADR 0220](../../02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)) +retire. The file changes owner in one apply on each machine: the node-engine hands a whole file to the +resource declaring its path now rather than removing it first, so a machine is never without it. +*Checked by the controller's tests that the three modules carry one identical template and that +nothing else in the catalogue writes the path, a resolution test refusing a second writer, and the +node-engine's handover test, in which the file is present at every step of the apply.* -**That file stays the mesh's only beside the uplink.** A network manager rewrites `/etc/resolv.conf` -on every connectivity change unless it is told not to, and the module holding `node-uplink` is what -tells it ([ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)). So -`node-resolver-config` needs `node-uplink` held on the same node: assigning the resolver file to a -machine with no uplink holder is refused, naming the managers' modules that could hold it, and taking -the last uplink holder from beneath it is refused too -([ADR 0220](../../02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)). *Checked by the controller's seat-dependency tests and by `assign` live.* +**A machine's names are one seat's** ([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)). +The `hosts` module is renamed `hostname` and holds `node-hostname`, the seat once named +`node-hosts-file`, whose former name resolves to it. It writes `/etc/hostname` from its `hostname` +setting — with no default: what a machine calls itself is the operator's, and the mesh's name for a +machine and its own need not agree — and the machine's `127.0.1.1` line, keeping the operator's lines. +A new name takes effect at the next boot; nothing sets it live, because a graphical session's X +authority is keyed by the name the session started under. *Checked by a resolution test that a +module claiming the old name and one claiming the new are one seat on one machine, and a composition +test that `/etc/hostname` is the setting, left out naming the key when nothing sets it.* **No node holds a copy of its own.** The two holders hold the same rendering of one roster, never a list anyone edits. The per-node resolver, its zones file and the mesh's region of `/etc/hosts` @@ -428,7 +440,7 @@ setting) and the listen that answers it; the controller hands the `mesh-dns-reso zone with its module's node address and published port, and each holder forwards that zone there and answers nothing in it itself — the lab answers `.incus` for its running scenarios this way. An operator's own names, unrelated to the mesh, live in `/etc/hosts`'s kept region, held per node by -the `node-hosts-file` seat's holder and changed through its tools; the controller holds none of them +the `node-hostname` seat's holder and changed through its tools; the controller holds none of them ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)). *Checked by the holder's configuration carrying one forwarding rule per declared zone, and by a push leaving the hosts file's operator region byte for byte.* diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index 5d6235e..277b5e3 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -13,6 +13,8 @@ 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 + - mesh-controller internal/inventory/migrations/0063-a-machines-names-are-one-seats.sql + - mesh-controller internal/inventory/migrations/0064-the-resolver-file-is-the-uplinks.sql updated: 2026-10-05 decisions: - 02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md @@ -146,11 +148,11 @@ convention, which later seats departed from. | `mesh-build-machine` | `the-build-machine` | node | — | a builder | | `mesh-resolver` | — | mesh | — | the mesh's resolvers, each holding every node's internal domain ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)); replicated, held on the anchor and the home server ([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)) | | ~~`mesh-dns-port`~~ | `the-dns-port` | node | — | retired by [ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md): the local resolver became the mesh's one; deleted from the set, and from the store's table, once nothing claimed it ([ADR 0220](../../02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)) | -| `node-hosts-file` | — | node | — | owns `/etc/hosts`: the machine's own lines and the operator's kept region, changed through its verbs `entries`, `add`, `remove` ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)). Next, decided: renamed `node-hostname`, held by the module renamed `hostname`, owning `/etc/hostname` as well ([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)) | +| `node-hostname` | `node-hosts-file` (renamed by [ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md); the former name an alias) | node | — | owns a machine's names: `/etc/hostname`, written from its holder's `hostname` setting with no default and taking effect at the next boot, and in `/etc/hosts` the machine's own lines and the operator's kept region, changed through its verbs `entries`, `add`, `remove` ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)). Held by the module `hostname`, formerly `hosts` | | `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service | | `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter | | `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over | -| `mesh-resolver-configuration` | `the-resolver-configuration` | node | — | the module writing `/etc/resolv.conf`; its holder needs the uplink's held on the same node ([ADR 0220](../../02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)). Next, decided: retired, the file becoming the uplink holder's ([ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)) | +| ~~`mesh-resolver-configuration`~~ | `the-resolver-configuration` | node | — | retired by [ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md): `/etc/resolv.conf` is written by the holder of `node-uplink`, the program that would otherwise rewrite it, and the need of the uplink beside it ([ADR 0220](../../02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)) goes with it; deleted from the set, and from the store's table, once nothing claims it | | `mesh-showcase` | `the-showcase` | node | — | the showcase module | The controller holds **the mesh's own** entries in code, and a test asserts their size and that