The controller writes no /etc/hosts (hq ADR 0199)

/etc/hosts is the file of the node-hosts-file seat's holder; the controller writes into no file
another seat's holder owns, and asks that holder if it ever needs a line there. The private
network's module stops asking for the node-names fact; every machine already asks the mesh's
one resolver for these names, and the host gives the region back at the next push.
This commit is contained in:
jochen
2026-10-05 21:23:25 +02:00
parent 4b382ceffd
commit 7fa2568ce7
4 changed files with 20 additions and 163 deletions
+11 -35
View File
@@ -31,19 +31,18 @@ const Requirement = "private-network"
// Name is the module that answers it with WireGuard.
//
// **The names went with it.** A mesh-names module used to sit beside this — it wrote /etc/hosts
// and ran nothing, which is not a module. Being on the private network is what gives a machine a
// name, so this module asks for the `node-names` fact and the mesh writes the file. The
// name-resolution provision went the same way: names are facts the mesh computes, not something a
// module that runs nowhere can provide.
// **It writes no names.** A machine's mesh names are answered by the mesh's one resolver (novox/hq
// ADR 0194), and /etc/hosts is the file of one module, the holder of `node-hosts-file` (ADR 0199): the
// controller writes into no file another seat's holder owns. If the mesh ever needs a line there, it
// asks that holder to register it. This module asked for a `node-names` fact written into /etc/hosts
// until 2026-10-05; the host gives that region back at the first push without it.
const Name = "mesh-wireguard"
// Addressing is the mesh handing out addresses on the private network itself.
//
// Names are computed from it, which is why they require this rather than a private network in
// general. A different VPN that hands out its own addresses would come with its own names — the
// mesh has nothing to write about a machine whose address it did not choose. Saying so here is
// what keeps a machine from being given a hosts file full of addresses that mean nothing.
// The mesh's resolver answers names from it, which is why it requires this rather than a private
// network in general. A different VPN that hands out its own addresses would come with its own
// names — the mesh has nothing to answer about a machine whose address it did not choose.
const Addressing = "mesh-addressing"
// TheNetwork is what a machine can only have one of.
@@ -82,7 +81,8 @@ type Generator struct {
// registry is the mesh's artifact store as the network reaches it (host:port), or empty when
// the mesh has none. Being on the network is what grants a machine the right to pull from it
// (novox/hq ADR 0082), so the module that puts a machine on the network is what writes the
// runtime's trust — the same reasoning that has it write /etc/hosts.
// runtime's trust. (That is still a write into another module's file, the container runtime's;
// novox/hq issue 190 has it handed to that module as a value.)
registry string
}
@@ -161,20 +161,6 @@ func (g *Generator) Nodes() []Node { return g.nodes }
// Graph is the peer list per node, for showing.
func (g *Generator) Graph() Graph { return g.graph }
// hostsTemplate is the mesh's region of `/etc/hosts` — every machine's mesh name at its private
// address, written into a marked region and merged (RosterFile.Shared → `into: block`), so the rest
// of the file (localhost, the machine's own name, other tools' blocks) is kept byte for byte
// (novox/hq issue 128). It is a roster template like any module's: the mesh owns the data, this owns
// the format, and the control plane holds no formatter.
//
// - No floor: no header, no localhost, no `127.0.1.1` — those are the machine's, above the region.
// - A machine's own line is marked, and its mesh name resolves to its mesh address, not loopback.
// - `.Names` is the machines (novox/hq ADR 0191): a route's internal name is under its node's
// internal domain and the resolver answers it by wildcard, and a public name is public DNS's.
// Machines with no address yet are already left out of the set.
const hostsTemplate = "# The mesh's names. This region is replaced whenever a machine joins or leaves.\n" +
"{{range .Names}}{{.Address}}\t{{.FQDN}}{{if ne .Name .FQDN}}\t{{.Name}}{{end}}{{if eq .Name $.Node}}\t# this machine{{end}}\n{{end}}"
// Manifest is the module the mesh provides for itself.
//
// It ships with the control plane rather than coming from a repository, because the thing that
@@ -186,17 +172,7 @@ func Manifest() map[string]any {
"version": "1",
"computed": Name,
"provides": []string{Requirement, Addressing},
// Being on the private network is what gives a machine a name, so the module that puts it
// there is what writes them. Asked for rather than generated by a module of its own: the
// mesh knows which machines exist and where; writing that into a hosts file is not a thing
// that needs a module to run nowhere. The format is a template like any other roster fact —
// the mesh's own module owns the `/etc/hosts` layout the way dnsmasq owns its zones, and the
// control plane holds no formatter (see catalogue.RosterFile). `shared`: the mesh owns only
// its region of the file and keeps the rest (novox/hq issue 128).
"facts": map[string]any{
"node-names": map[string]any{"path": "/etc/hosts", "template": hostsTemplate, "shared": true},
},
"claims": []map[string]any{{"name": TheNetwork, "scope": "node"}},
"claims": []map[string]any{{"name": TheNetwork, "scope": "node"}},
}
}
+2 -17
View File
@@ -20,20 +20,6 @@ const SuffixVar = "MESH_INTERNAL_SUFFIX"
// rather than reaching a stranger's machine.
const DefaultSuffix = "internal"
// HostsPath is where the names go.
//
// This is not the `/etc/hosts` floor the design removes. That floor existed because a node had to
// reach the mesh's database before its own DNS worked — a fallback for a circularity, and the
// circularity is gone. This is the mechanism itself: the complete set of names in this mesh
// (novox/hq ADR 0011). Written as a marked region *into* the file rather than as the file: the
// rest of it — `localhost`, the machine's own name, other tools' blocks — is the machine's, and
// writing it whole replaced all of that (novox/hq issue 128).
//
// A file rather than a resolver daemon, deliberately, for now: it works on every Linux, needs no
// package, and has no failure mode of its own. A daemon becomes necessary when names are wanted
// that are not one-per-node — service names, wildcards — and that is not yet true.
const HostsPath = "/etc/hosts"
// Suffix is what internal names end in.
func Suffix() string {
if v := strings.TrimSpace(os.Getenv(SuffixVar)); v != "" {
@@ -48,7 +34,6 @@ var nodeName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`)
// InternalName is a node's name inside the mesh.
func InternalName(node string) string { return node + "." + Suffix() }
// **What remains of a larger file.** The rest wrote /etc/hosts — that is the `node-names` fact now
// (catalogue.FactsInto), computed where the graph lives instead of by a module that ran nothing.
// The naming stays here, because several things compose a node's internal name and one of them
// **What remains of a larger file.** The rest wrote /etc/hosts, which the controller no longer
// writes at all (novox/hq ADR 0199): the mesh's resolver answers these names. The naming stays here, because several things compose a node's internal name and one of them
// writing the suffix differently would be a name nothing answers to.