package overlay import ( "fmt" "sort" "strings" ) // A resolver answers every name under a node, not just the node. // // **Services are named under the machine they run on** — `postgres.novox.internal`, // `plex.ace.internal`. The first label is the service and the rest is the node, so what has to // resolve is *anything* under a node's name, going to that node's address. A reverse proxy there // routes by the name it was asked for, which is a separate concern and stays separate. // // **This is what a hosts file cannot do.** It answers exact names; a wildcard would mean writing // down every service name in advance, which is the enumeration the arrangement exists to avoid. // novox/hq 08-connectivity named exactly this as the trigger for needing a resolver rather than a // file, and it is the first thing to meet it. // // What is generated is the data, not the daemon's configuration language. One line per node, // in the form dnsmasq reads because that is what the module runs — and if a mesh runs something // else, this is the shape it translates from rather than a second thing to compute. // ResolverPath is where the mesh writes what a node must answer. const ResolverPath = "/etc/mesh-resolver/nodes.conf" // Wildcards is one line per node: everything under its name, and the name itself. // // A machine with no address is left out. A wildcard pointing at nothing is worse than no wildcard: // every name under it would resolve and then hang, where an unresolvable name fails at once and // says which name it was. func Wildcards(nodes []Node) string { var b strings.Builder b.WriteString("# Generated by the mesh. Do not edit — it is replaced whenever a machine\n") b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n") b.WriteString("#\n") b.WriteString("# Each line answers the node's own name AND everything under it, so a service\n") b.WriteString("# is reached at .." + Suffix() + " without the mesh being told\n") b.WriteString("# the service exists. What routes it there once it arrives is the proxy's.\n\n") named := make([]Node, 0, len(nodes)) for _, n := range nodes { if strings.TrimSpace(n.Address) == "" { continue } named = append(named, n) } sort.Slice(named, func(i, j int) bool { return named[i].Name < named[j].Name }) for _, n := range named { fmt.Fprintf(&b, "address=/%s/%s\n", InternalName(n.Name), n.Address) } if len(named) == 0 { b.WriteString("# No machine in this mesh has an address on the private network.\n") } return b.String() } // ResolverGenerator answers what one node's resolver must know. type ResolverGenerator struct{ nodes []Node } // ResolverFor builds it over the machines on the private network. func ResolverFor(nodes []Node) *ResolverGenerator { return &ResolverGenerator{nodes: nodes} } // Resources is the one file. The daemon that reads it is the module's, not the mesh's. func (g *ResolverGenerator) Resources(node string) ([]map[string]any, bool, error) { var here bool for _, n := range g.nodes { if n.Name == node { here = true } } if !here { // Assigned and not yet on the network. Ordinary and brief. return nil, false, nil } return []map[string]any{{ "id": "nodes", "type": "file", "path": ResolverPath, "content": Wildcards(g.nodes), "mode": "0644", }}, true, nil }