From ee31f9f7618d82a90ae638b58dc106a640fe5f00 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:38:08 +0200 Subject: [PATCH 1/2] issue 128: the machine's hosts file is written whole, and on a workstation it is shared --- .../00-report.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md diff --git a/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md b/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md new file mode 100644 index 0000000..8e5b9f5 --- /dev/null +++ b/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md @@ -0,0 +1,59 @@ +--- +status: open +opened: 2026-09-26 +located-in: [mesh-controller internal/catalogue/facts.go, mesh-host internal/apply] +--- + +# 128 — the machine's hosts file is written whole, and on a workstation it is shared + +## What was observed + +The private network asks for the `node-names` fact, and the mesh delivers it as +`/etc/hosts`. `nodeNames` composes a **complete** file — its own header, `localhost`, the +machine's own name, and every name in the mesh — and the host writes it over whatever is there. + +On an adopted workstation the file the mesh holds contains, besides the predecessor's block of +mesh names: + +- the distribution's own lines (`localhost`, the machine's `.localdomain` name); +- two marked blocks (`# BEGIN … # END …`) maintained by a local-development tool, pointing a + dozen development hostnames at `127.0.0.1` — rewritten by that tool whenever its project + list changes; +- hand-added entries of the operator's. + +Today the file is only **held** ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)): +the private network was assigned, not yet taken, so nothing was lost. Taking it — or +converging the node, which takes everything — replaces the file. The development tool's +entries disappear, its projects stop resolving, and every later write it makes is overwritten +at the next change to the mesh's names (a machine joins, a route is contributed), silently and +without a failure anywhere: the development tool thinks it wrote its block, and the mesh thinks +it owns the file. + +This is [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)'s +failure exactly — a file the mesh shares with software it did not install, written over — in a +file 0102 did not name, because its merge verb is structured (`into: json`) and a hosts file is +not JSON. + +A second, smaller finding from the same reading: the fact's contents depend on which machines +hold the private network. A machine that is enrolled but not yet assigned the private network +is in neither `node-names` nor `node-zones`; its name resolves on the others only for as long +as a predecessor's hosts block survives. Taking the hosts file before every machine is on the +private network loses that name too. + +## What would have prevented it + +- A **marked-region** merge in the host's vocabulary: `into: "block"` (or similar) — the host + owns only the lines between its own begin and end markers, keeps everything outside them + byte for byte, records what the region held before, and on undeclare removes the region and + nothing else. The shape local tools already use for this very file. +- The `node-names` fact written as that region — no header of its own, no `localhost`, no + machine name — so the distribution's lines and every other tool's stay where they are. +- A converge preview that names a held file the take would replace *whole*, with its line + count before and after, so a person sees "hosts: 31 lines → 12" before the flip. + +## Evidence to carry into diagnosis + +- `internal/catalogue/facts.go`, `nodeNames`: the complete file is built here. +- The host's file resource supports `into: "json"` only; anything else is a whole write. +- `node show ` on the adopted workstation: `holds file /etc/hosts + mesh-wireguard.fact-node-names`, original kept. From a865fc7d7913f7c55f26c4571eca39e25437cf67 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 00:03:30 +0200 Subject: [PATCH 2/2] 128 review: located; the fix as built (block, at, never held, order) --- .../00-report.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md b/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md index 8e5b9f5..fa02a00 100644 --- a/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md +++ b/04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md @@ -1,5 +1,5 @@ --- -status: open +status: located opened: 2026-09-26 located-in: [mesh-controller internal/catalogue/facts.go, mesh-host internal/apply] --- @@ -51,6 +51,20 @@ private network loses that name too. - A converge preview that names a held file the take would replace *whole*, with its line count before and after, so a person sees "hosts: 31 lines → 12" before the flip. +## The fix, as built (in review) + +- **Host:** a file resource may say `"into": "block"`. The host owns only the lines between + `# BEGIN mesh ` and `# END mesh ` and keeps everything outside them byte for byte. A + new region goes at the `end` by default, or at the `start` (`"at": "start"`) for files where a + line's meaning depends on what stands above it; a region already present is never moved. + Undeclared, what the region held before is put back, or the region is removed and nothing + else. Replacing nothing, it is written on an adopted node without being held — so a machine + gets the mesh's names before its private network is taken. +- **Controller:** `node-names` is a fact written into a shared file, emitted as that region: the + mesh's names only, no header, no `localhost`, no `127.0.1.1` line. +- **Order:** a host older than the block mode refuses the whole declaration on an unknown + `into`, so hosts are upgraded before the controller that emits it. + ## Evidence to carry into diagnosis - `internal/catalogue/facts.go`, `nodeNames`: the complete file is built here.