From b8cacbaf4d4e0658f234a023b105355144ff333d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 15 Sep 2026 21:33:20 +0200 Subject: [PATCH] What remains of names.go is the naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hosts-file writing moved to the node-names fact; what stays is the suffix (configurable, MESH_INTERNAL_SUFFIX, defaulting to .internal which IANA reserved for exactly this), a node's internal name, and the rule for what a node may be called. Several things compose an internal name, and one of them writing the suffix differently would be a name nothing answers to. First attempt at this rewrote the file from memory and silently dropped the configurable suffix. Restored from the original instead — deleting most of a file is git surgery, not paraphrase. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- internal/overlay/generator.go | 28 +++------ internal/overlay/names.go | 56 ++---------------- internal/overlay/names_test.go | 105 --------------------------------- 3 files changed, 12 insertions(+), 177 deletions(-) delete mode 100644 internal/overlay/names_test.go diff --git a/internal/overlay/generator.go b/internal/overlay/generator.go index d065f73..ac6f88b 100644 --- a/internal/overlay/generator.go +++ b/internal/overlay/generator.go @@ -29,19 +29,14 @@ import ( // how a mesh ends up unable to have a second one. const Requirement = "private-network" -// Name is the module that answers it with WireGuard, and Names is the one that gives the machines -// names. Two modules rather than one, because they are two different things: names would be the -// same over any private network, and they are only bundled here by an accident of both being -// computed. -const ( - Name = "mesh-wireguard" - Names = "mesh-names" -) - -// Resolution is what a module asks for when it needs to reach other machines by name. Separate -// from Requirement because they are separate jobs: one is whether packets arrive, the other is -// whether a name means anything. A machine can want the first without the second. -const Resolution = "name-resolution" +// 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. +const Name = "mesh-wireguard" // Addressing is the mesh handing out addresses on the private network itself. // @@ -131,13 +126,6 @@ func (g *Generator) Resources(node string) ([]map[string]any, bool, error) { return parsed.Resources, true, nil } -// NameGenerator answers what one node's hosts file is. -// -// Separate from the interface and the peers because it is a separate concern. A machine's names -// come from the mesh knowing every machine, not from how the packets travel — over a different -// private network the peers would be written by something else and this would be unchanged. -type NameGenerator struct{ nodes []Node } - // Nodes are the machines this generator was built over, so a caller can say who is on the network. func (g *Generator) Nodes() []Node { return g.nodes } diff --git a/internal/overlay/names.go b/internal/overlay/names.go index a495f56..a3b0b03 100644 --- a/internal/overlay/names.go +++ b/internal/overlay/names.go @@ -1,10 +1,8 @@ package overlay import ( - "fmt" "os" "regexp" - "sort" "strings" ) @@ -49,53 +47,7 @@ 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 -} +// **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 +// writing the suffix differently would be a name nothing answers to. diff --git a/internal/overlay/names_test.go b/internal/overlay/names_test.go deleted file mode 100644 index 00103f7..0000000 --- a/internal/overlay/names_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package overlay - -import ( - "strings" - "testing" -) - -func hostsFor(t *testing.T, self string, nodes ...Node) string { - t.Helper() - out, err := Hosts(nodes, self) - if err != nil { - t.Fatal(err) - } - return out -} - -func TestEveryNodeWithAPlaceGetsAName(t *testing.T) { - got := hostsFor(t, "laptop", - Node{Name: "anchor", Address: "10.42.0.1"}, - Node{Name: "laptop", Address: "10.42.0.2"}) - - for _, want := range []string{"10.42.0.1\tanchor.internal\tanchor", "10.42.0.2\tlaptop.internal\tlaptop"} { - if !strings.Contains(got, want) { - t.Errorf("no entry for %q in:\n%s", want, got) - } - } -} - -func TestANodeSeesItselfAtItsOverlayAddress(t *testing.T) { - // Not at a loopback. A service that binds to the name the machine was given would otherwise - // listen somewhere nothing else can reach, and the failure appears on every other node rather - // than this one. - got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}) - if !strings.Contains(got, "10.42.0.2\tlaptop.internal") { - t.Error("a node does not resolve its own mesh name to its overlay address") - } -} - -func TestTheMachinesOwnLoopbackSurvives(t *testing.T) { - // This file is generated whole, so anything left out is removed. Dropping localhost would - // break things that have nothing to do with the mesh, on a machine the mesh was asked to - // improve. - got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}) - if !strings.Contains(got, "127.0.0.1\tlocalhost") { - t.Error("localhost is missing; this file replaces the machine's own") - } - if !strings.Contains(got, "::1") { - t.Error("the IPv6 loopback is missing") - } -} - -func TestANodeWithNoAddressGetsNoName(t *testing.T) { - // A name resolving to nothing is worse than no name: a connection to an address that does not - // answer hangs, where a name that does not resolve fails at once and says which name it was. - got := hostsFor(t, "laptop", - Node{Name: "laptop", Address: "10.42.0.2"}, - Node{Name: "newcomer", Address: ""}) - if strings.Contains(got, "newcomer") { - t.Error("a node with no address on the network was given a name") - } -} - -func TestANameThatCannotBeAHostnameIsRefused(t *testing.T) { - // Refused here, where a person is looking, rather than written into a file that every - // machine then reads and disagrees about. - for _, bad := range []string{"Anchor", "my node", "under_score", "-leading", "trailing-"} { - if _, err := Hosts([]Node{{Name: bad, Address: "10.42.0.1"}}, ""); err == nil { - t.Errorf("%q was accepted as a mesh name", bad) - } - } -} - -func TestTheOrderIsStable(t *testing.T) { - // The file is rewritten whenever anything changes, and a file whose lines move for no reason - // makes every reconcile look like a change — which means a service that reflects it restarts - // for ever. - a := hostsFor(t, "", Node{Name: "b", Address: "10.42.0.2"}, Node{Name: "a", Address: "10.42.0.1"}) - b := hostsFor(t, "", Node{Name: "a", Address: "10.42.0.1"}, Node{Name: "b", Address: "10.42.0.2"}) - if a != b { - t.Error("the same mesh produced two different files depending on the order it was read in") - } -} - -func TestTheSuffixIsReservedForThis(t *testing.T) { - // `.internal` was reserved by IANA in 2024 for exactly this. 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. - if InternalName("anchor") != "anchor.internal" { - t.Errorf("internal names end in %q", Suffix()) - } -} - -func TestTheSuffixCanBeChosen(t *testing.T) { - t.Setenv(SuffixVar, ".mesh") - if InternalName("anchor") != "anchor.mesh" { - t.Errorf("got %q", InternalName("anchor")) - } -} - -func TestTheFileSaysItIsGenerated(t *testing.T) { - got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}) - if !strings.Contains(got, "Do not edit") { - t.Error("a generated file does not say so") - } -}