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.
102 lines
3.9 KiB
Go
102 lines
3.9 KiB
Go
package overlay
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Names are how one node reaches another by name rather than by address.
|
|
//
|
|
// novox/hq 08-connectivity: what the host receives is the resolver's configuration, as files,
|
|
// listing every peer's internal name and overlay address. Computed centrally for the same reason
|
|
// the peer graph is — it needs every node at once.
|
|
|
|
// SuffixVar lets a mesh choose what its internal names end in.
|
|
const SuffixVar = "MESH_INTERNAL_SUFFIX"
|
|
|
|
// DefaultSuffix is `.internal`, which IANA reserved for exactly this in 2024. A name under it 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.
|
|
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,
|
|
// generated whole and owned by the mesh (novox/hq ADR 0011), rather than a patch written
|
|
// underneath something else.
|
|
//
|
|
// 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 != "" {
|
|
return strings.TrimPrefix(v, ".")
|
|
}
|
|
return DefaultSuffix
|
|
}
|
|
|
|
// nodeName is what a node may be called, so that it can also be a hostname.
|
|
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() }
|
|
|
|
// Hosts writes the name file for one node.
|
|
//
|
|
// Every node in the mesh, including this one. Including itself because a machine referring to
|
|
// itself by its mesh name should get its overlay address rather than a loopback — otherwise a
|
|
// service that binds to the name it was given ends up unreachable from everywhere else.
|
|
//
|
|
// The machine's own loopback lines come first and are not the mesh's to have an opinion about,
|
|
// but they have to be here: this file is generated whole, so anything left out is removed.
|
|
func Hosts(nodes []Node, self string) (string, error) {
|
|
var b strings.Builder
|
|
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a node\n")
|
|
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
|
|
|
// The floor every Linux expects, and which removing would break things that have nothing to
|
|
// do with the mesh.
|
|
b.WriteString("127.0.0.1\tlocalhost\n")
|
|
b.WriteString("::1\t\tlocalhost ip6-localhost ip6-loopback\n")
|
|
if self != "" {
|
|
fmt.Fprintf(&b, "127.0.1.1\t%s\n", self)
|
|
}
|
|
|
|
named := make([]Node, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
if n.Address == "" {
|
|
// A node with no address on the network has no name here. Writing one that resolves
|
|
// to nothing is worse than not writing it: a connection to an address that does not
|
|
// answer hangs, where a name that does not resolve fails at once and says so.
|
|
continue
|
|
}
|
|
if !nodeName.MatchString(n.Name) {
|
|
return "", fmt.Errorf(
|
|
"%q cannot be a mesh name: it becomes a hostname, so it is lower-case letters, "+
|
|
"digits and dashes", n.Name)
|
|
}
|
|
named = append(named, n)
|
|
}
|
|
sort.Slice(named, func(i, j int) bool { return named[i].Name < named[j].Name })
|
|
|
|
if len(named) > 0 {
|
|
fmt.Fprintf(&b, "\n# the mesh, %d node(s)\n", len(named))
|
|
}
|
|
for _, n := range named {
|
|
line := fmt.Sprintf("%s\t%s\t%s", n.Address, InternalName(n.Name), n.Name)
|
|
if n.Name == self {
|
|
line += "\t# this machine"
|
|
}
|
|
b.WriteString(line + "\n")
|
|
}
|
|
return b.String(), nil
|
|
}
|