Compare commits

...
Author SHA1 Message Date
jschoubben 3ed55a3420 connectivity: name the code that builds the one resolver, zones and the hosts file 2026-10-04 00:23:43 +02:00
jschoubben 8184585213 ADR 0199: a module that answers names declares its zone, and a node's hosts file is one module's
The per-node resolvers 0194 retires held two kinds of names that are neither nodes nor routes: the
lab's scenario machines and an operator's own lines. A zone a module declares is forwarded by the
mesh's resolver to that module; /etc/hosts is held per node through node-hosts-file, the operator's
lines in its kept region. Research 023 parks seats that define what their holder owns.
2026-10-03 23:19:36 +02:00
5 changed files with 185 additions and 0 deletions
@@ -0,0 +1,37 @@
---
status: active
initiated: 2026-10-03
touches: [the seats, the seat protocol, the controller's ownership check, 03-DESIGN/01-to-be/26-the-seats.md]
---
# 023 — A seat protocol that defines what its holder owns
## What is investigated
**A seat is a definition — a protocol — and a module occupies it by implementing that protocol.**
Today the protocol is what the holder accepts, emits and serves (ADR 0118, 0129, 0132): its verbs, as MCP
tool definitions. This asks whether the protocol should also name the **files and directories the
holder owns**, so that occupying the seat means owning them: `node-resolver-config` owns
`/etc/resolv.conf`, `node-hosts-file` owns `/etc/hosts`, the intrusion prevention owns its jail file.
The direction is the protocol's, not the holder's: the seat states what any holder must own; a module
that wants the seat must declare those paths among its resources, or the controller refuses the claim
as not implementing the seat. Two seats may not name one path.
## Why
Who owns a singular file is today answered by reading every manifest, and enforced only after the fact,
when two modules on one machine both declare the same path. The question *which module owns
`/etc/resolv.conf`?* came up on 2026-10-03 with no place to look it up. A seat that names the path answers
it from the seat table, before any module is written, and makes "implements the seat" checkable.
## What it touches
- The seat definition and its table (ADR 0122) — a new part of the protocol.
- The controller's ownership check (`checkResources`), which already refuses two modules owning one path.
- Every node seat that is really about a file: `node-resolver-config`, `node-hosts-file`
([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)),
`node-intrusion-prevention`, `node-packet-filter`.
Raised by the operator during the resolver work of ADRs 0194–0199 and parked there so that work was not
widened by it.
@@ -0,0 +1,127 @@
---
topic: the tiers
status: accepted
date: 2026-10-03
deciders: jochen
reconstructed: false
extends: 0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
---
# 199. A module that answers names declares its zone, and a node's hosts file is one module's
## Context
**[ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md) and
[ADR 0196](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) leave
one resolver holding the nodes' internal domains, and retire the resolver every node ran.** Two kinds
of names lived in those per-node resolvers that are neither a node nor a route, and both were found on
the workstation on 2026-10-03:
- **Names a module answers.** The lab raises scenario machines and gives them addresses from its
scenario files — the anchor's stand-in at a documentation address, the home server's on the LAN —
and the workstation resolved `<machine>.incus` through two wildcard lines in a drop-in file its
resolver read. The lines were written by hand; the addresses are the lab's, known only while a
scenario runs.
- **The operator's own names, unrelated to the mesh.** Twelve `<loopback> <name>` lines for a
client's development hosts, kept in `/etc/hosts` and again in `/etc/hosts.local`, which the per-node
resolver read as additional hosts.
**A manifest never names an address, a node or a domain** ([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)).
So the lab cannot list `<machine>.incus → <address>` in its definition, and the operator's twelve lines
are not any module's to define.
## Considered Options
**For a module's names:**
1. **The manifest lists its records.** Refused by ADR 0112: the addresses are the lab's runtime facts
and the scenario's choice.
2. **The module reports its records at runtime to the mesh's resolver**, which writes them into its
configuration. It works, and it makes the resolver hold every module's runtime state and decide,
per call, whether the caller may write the name it sent — authorisation for a write, on the one
server every node depends on.
3. **The module declares the zone it answers and the listen that answers it; the mesh's resolver
forwards that zone there.** The definition names a zone (from a setting) and one of its own listens,
which ADR 0112 allows; the address and the port are the mesh's facts. The records stay where they
are known — in the module, at runtime. Chosen.
**For the operator's names:**
1. **Records the controller holds, served by the mesh's resolver.** They are not the mesh's: a client's
development hosts on one machine are nothing any other node should resolve, and the controller would
become the keeper of a workstation's private notes.
2. **A node-scoped module owns `/etc/hosts`, and the operator's lines live in its kept region**
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), changed
through that module's tools on that machine. Chosen.
## Decision
**1. A module that answers names declares a zone.** Its definition names the zone — a single label or a
dotted name, from a setting, never a domain the mesh knows — and the listen that answers DNS for it.
The controller refuses two modules in the mesh declaring one zone, and a zone that is the mesh's suffix,
under it, or one of a node's public domains: a module may not shadow names the mesh or the public DNS
answers.
**2. The mesh's resolver forwards each zone to the module that declared it.** The controller hands the
holder of `mesh-dns-resolver` every declared zone with the private address of the node its module runs
on and the port that listen is published on; the holder places one forwarding rule per zone into its
configuration and answers nothing in that zone itself. What names exist in the zone, and their
addresses, are the module's — answered by its own long-running code
([ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)),
from its own state, as they change. Whether an answered address is reachable from the asking node is
the module's matter, not the resolver's.
**3. A node's `/etc/hosts` is held by one module, through a node seat, `node-hosts-file`.** The seat is
the definition: its holder owns `/etc/hosts`, and implements three verbs — MCP tool definitions served
as `<node>/node-hosts-file.<verb>` ([ADR 0195](0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)):
**`entries`** (the file's lines, the module's and the operator's, each marked whose), **`add`** (one
address and its names, into the operator's region) and **`remove`** (one name or address from it). The
module writes the machine's own lines — loopback and the machine's name — and keeps a region for the
operator, which survives every push and is given back when the module goes. Its tools change that
region on that machine, escalating as the packet filter's do
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §4). **The
controller holds none of it:** an operator's line is the machine's, not a record.
**4. No other module writes `/etc/hosts`.** The private network's region goes, as ADR 0194 already has
it; a module that once wrote a line there asks the mesh's resolver instead.
## Consequences
- **The lab's names follow its scenarios.** A scenario raised is resolvable from every node at once; a
scenario torn down is gone, with no line left behind in any file.
- **The mesh's resolver holds no module's state.** It holds the nodes' domains and a table of who
answers which zone, both composed by the controller; nothing writes to it at runtime.
- **A module answering a zone needs a DNS answerer of its own** — a long-running bundle, or a resolver
it runs. The lab gains one.
- **The operator's names reach the machine's own programs, not its containers.** A container does not
read the machine's `/etc/hosts`. For names unrelated to the mesh that is the right boundary; a name a
container needs belongs in a zone.
- **Taking `/etc/hosts` keeps what is there.** The first time the module writes the file, every line
that is not the machine's own goes into the operator's region, so a workstation's twelve lines survive
the take — the same adoption [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) gives
every shared file.
**How each is checked:**
- **Zones:** the controller's catalogue tests refuse a second module declaring a zone, a zone under the
mesh suffix, and a zone equal to a node's public domain.
- **Forwarding:** on the holder, the resolver's configuration carries one forwarding rule per declared
zone, at the declaring node's private address and published port; asking any node's resolver for a
name in the lab's zone while a scenario runs returns the scenario's address.
- **The hosts file:** a push leaves the operator's region byte for byte; `add` followed by `entries`
shows the line as the operator's; unassigning the module gives the region back.
## References
- [ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md),
[ADR 0196](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) —
the one resolver and how nodes ask it.
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why a definition names no address.
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
[ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) — kept regions and shared files.
- [ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md) —
where a zone's answerer runs.
- [Connectivity §2](../03-DESIGN/01-to-be/08-connectivity.md) and [the seats](../03-DESIGN/01-to-be/26-the-seats.md),
amended alongside.
- [Research 023](../01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md) —
the general form of decision 3's "the holder owns `/etc/hosts`".
+1
View File
@@ -225,6 +225,7 @@ python3 00-META/checks/index.py fail if stale
- **0191** — [The mesh's resolver holds only the mesh's own names; a public name resolves publicly](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)
- **0194** — [The mesh has one resolver, and every node asks it for the mesh's names](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)
- **0196** — [A node asks the mesh's resolver first, and a public one only when it is silent](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md)
- **0199** — [A module that answers names declares its zone, and a node's hosts file is one module's](0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)
### What runs on them, and how it gets there
+18
View File
@@ -6,10 +6,16 @@ code:
- mesh-controller examples/route-proxy
- 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)
- mesh-catalog modules/dnsmasq (the mesh's one resolver)
- mesh-catalog modules/resolv-conf (what a node asks)
- mesh-catalog modules/hosts (a node's /etc/hosts)
- mesh-host internal/identity/serving.go
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-10-03
decisions:
- 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md
- 02-DECISIONS/0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md
- 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
- 02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md
@@ -377,6 +383,16 @@ member's resolver answers a LAN; a router pointing at one is moved first. *Check
`/etc/resolv.conf` naming `mesh-resolver` then a public resolver, by no node but the holder answering
DNS on any address, and by the router's DHCP DNS option naming the router.*
**Names that are neither a node nor a route.** A module that answers names declares a zone (a
setting) and the listen that answers it; the controller hands the `mesh-dns-resolver` holder every
zone with its module's node address and published port, and the 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
([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.*
*What follows describes the per-node resolver this replaces — how it was built and why the roles were
split. The split stands; the serving role's scope is what moved.*
@@ -1021,6 +1037,8 @@ The list is worth having in one place, because it is most of the argument:
- **One resolver ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)).** Not built: every node still runs
`node-dns-resolver`. The migration's four steps are in the record, in order.
Nor are zones or the hosts file's holder ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)): the
workstation moves to the one resolver only once both exist, its lab and operator names depending on them.
- ~~**What happens when the hub is down.**~~ **Resolved** by
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md), together with `06`'s
+2
View File
@@ -12,6 +12,7 @@ code:
- mesh-catalog modules/gitea/module.json
updated: 2026-10-03
decisions:
- 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md
- 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
@@ -129,6 +130,7 @@ convention, which later seats departed from.
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
| `mesh-resolver` | — | mesh | — | the mesh's one resolver, 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)) |
| ~~`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 |
| `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)) |
| `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 |