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:
@@ -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)),
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user