Merge pull request 'Design: resolv.conf is the uplink's holder's; a machine's names are node-hostname's (ADR 0223 parts 2–3)' (#117) from design/0223-uplink-resolv-and-hostname into main

This commit was merged in pull request #117.
This commit is contained in:
2026-10-05 22:20:32 +00:00
3 changed files with 64 additions and 39 deletions
@@ -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)),
+49 -37
View File
@@ -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 `<machine>.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.*
+4 -2
View File
@@ -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