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 }