Names, from the same graph as the network

Step 5 of the connectivity order. Every node's internal name resolves to its
overlay address, on every node, computed centrally because it needs every node
at once.

Under `.internal`, which IANA reserved for exactly this in 2024 -- a name there
can never collide with a public one, so an internal name that leaks into a
public resolver fails rather than reaching a stranger's machine. The suffix is
settable for a mesh that wants its own.

Delivered in the same declaration as the peer list rather than a second one. A
node holding the peers and not the names, or the reverse, is half on the
network for as long as that lasts.

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 that is now gone. This is the mechanism: the complete set of
names, generated whole and owned by the mesh, rather than a patch written
underneath something else. A resolver daemon becomes necessary when names are
wanted that are not one-per-node, and that is not yet true.

A node resolves its own name to its overlay address rather than a loopback,
because a service binding to the name it was given would otherwise listen
somewhere nothing else can reach -- and the failure would appear on every other
machine rather than that one.

A node with no address gets no name. A name resolving to nothing is worse than
no name: connecting to an address that does not answer hangs, where a name that
does not resolve fails at once and says which name it was.

Found while writing it: a test asserting every file in the declaration is mode
0600 would have forced /etc/hosts to 0600 and broken every lookup on the
machine, to protect a file that is not secret.

Verified in the lab: three machines, nine name lookups, each resolving to the
right overlay address and reaching it.
This commit is contained in:
2026-08-29 19:58:01 +02:00
parent 8b974deb42
commit fc1417be72
5 changed files with 229 additions and 5 deletions
+13 -1
View File
@@ -34,7 +34,7 @@ type Resource map[string]any
// Three resources and nothing clever: the tools, the configuration, and the interface running. A
// person can read it, which is the point — this is the first thing a node is ever told, and if it
// is wrong the node is unreachable and the mistake has to be findable by eye.
func Declaration(node Node, peers []Peer, keyPath string) ([]byte, error) {
func Declaration(node Node, peers []Peer, everyone []Node, keyPath string) ([]byte, error) {
if node.Address == "" {
return nil, fmt.Errorf("%s has no address on the overlay, so there is nothing to configure",
node.Name)
@@ -78,6 +78,18 @@ func Declaration(node Node, peers []Peer, keyPath string) ([]byte, error) {
},
}
// And the names, which come from the same graph and arrive in the same declaration. Separate
// steps in the design and one delivery in practice: a node that had the peers and not the
// names, or the reverse, would be half on the network for as long as that lasted.
names, err := Hosts(everyone, node.Name)
if err != nil {
return nil, err
}
resources = append(resources, Resource{
"id": "mesh-names", "type": "file", "path": HostsPath,
"mode": "0644", "content": names,
})
return json.Marshal(map[string]any{"declaration": 1, "resources": resources})
}