package main import ( "context" "errors" "flag" "fmt" "os" "sort" "strings" "time" "github.com/novox/mesh-control/internal/catalogue" "github.com/novox/mesh-control/internal/inventory" "github.com/novox/mesh-control/internal/overlay" ) // the private network: who is on it, where, and what they are called. // // Split out of main.go, which had reached 2,769 lines because appending was always the // cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636: // nothing in it was wrong, and no one edit was the one that should have been a new file. func overlayCIDR() string { if v := strings.TrimSpace(os.Getenv(OverlayCIDRVar)); v != "" { return v } return "10.42.0.0/16" } func overlayCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("overlay place [flags], or overlay show") } // Answered before anything is opened. A message about which command to use should not need a // database to say so, and needing one turns a redirect into a connection error. if args[0] == "push" { return errors.New("`overlay push` is now `push`, which sends a node its network AND " + "what its assignments resolve to — the two are computed from one picture of the " + "mesh, and sending them separately would let them disagree") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory switch args[0] { case "place": return overlayPlace(ctx, inv, args[1:]) case "show": return overlayShow(ctx, open) default: return fmt.Errorf("overlay has no %q; it has place and show", args[0]) } } func overlayPlace(ctx context.Context, inv *inventory.Inventory, args []string) error { if len(args) == 0 { return errors.New( "overlay place [--endpoint host:port] [--site name] [--hub], or --nothing") } node := args[0] set := flag.NewFlagSet("overlay place", flag.ContinueOnError) endpoint := set.String("endpoint", "", "where this node can be dialled, or empty for nowhere") site := set.String("site", "", "where this machine physically is, or empty if it roams") hub := set.Bool("hub", false, "this node is the hub every other routes through") nothing := set.Bool("nothing", false, "place it with nothing set: not dialable, no site, not the hub") if err := set.Parse(args[1:]); err != nil { return err } // **All three are declared together, so saying nothing took all three away.** The sibling of // `node public-domain`: `overlay place anchor` reads like it places the node it names, and it // silently unset the endpoint every other machine dials, the site it is in, and the hub if it // was the hub — every path through it going with them, at the moment somebody was trying to // look at it. // // A placement with nothing set is a real thing to want — a machine that roams and opens every // path itself is exactly that — so it keeps a way to say so, by name. if set.NFlag() == 0 { return fmt.Errorf("overlay place %s was given nothing to place it with, and all three are "+ "declared together — it would take away the endpoint other machines dial %s at, its "+ "site, and the hub if it is the hub. Say --endpoint/--site/--hub, or --nothing if that "+ "is what you meant", node, node) } if *nothing && (*endpoint != "" || *site != "" || *hub) { return fmt.Errorf("give %s a placement or --nothing, not both: they say opposite things "+ "and the mesh will not choose between them", node) } // Declared, all three. The address is evidence of reachability and is not the fact, and hub // election by address prefix fails silently (novox/hq ADR 0007). if err := inv.SetPlace(ctx, node, *endpoint, *site, *hub, ""); err != nil { return err } found, err := inv.NodeByName(ctx, node) if err != nil { return err } address, err := inv.AssignAddress(ctx, found.ID, overlayCIDR()) if err != nil { return err } fmt.Printf("%s is at %s on the overlay\n", node, address) switch { case *hub: fmt.Println(" the hub — every node not sharing a site routes through it") case *endpoint == "": fmt.Println(" not dialable — it opens every path itself") } if *site != "" { fmt.Printf(" at %s, so it peers directly with anything else there\n", *site) } return nil } // network builds the private network over the machines that resolved the module for it. // // Not over every node the mesh knows. **A machine is on the private network because it was given // the module**, and one that was not is absent from every peer list and from the names — which is // the only thing "not on the network" can mean. Until this, having an address was enough, and // there was no way to keep a machine off. // // Every node at once, which is the whole reason this is the control plane's work: a peer list is // derived from all the others, so no node could compute its own. func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, refused map[string]string) (*overlay.Generator, error) { places, err := inv.Overlays(ctx) if err != nil { return nil, err } nodes := make([]overlay.Node, 0, len(places)) for _, p := range places { if !on[p.Name] { continue } nodes = append(nodes, overlay.Node{ Name: p.Name, Key: p.Key, Endpoint: p.Endpoint, Site: p.Site, Hub: p.Hub, Address: p.Address, }) } if len(nodes) == 0 { // Nobody was given it. An empty network is a legitimate mesh, not a broken one, so this // answers rather than refusing -- Compute would refuse for want of a hub, and reporting // "no hub" to somebody who never asked for a network would be a lie about the cause. return overlay.Empty(), nil } g, err := overlay.From(nodes, overlayCIDR(), "") if err != nil && len(refused) > 0 { // The network is missing something, and some machines could not be resolved at all. Those // are almost always the same fact: a node that does not resolve contributes nothing, so // reporting "no hub" would name a consequence and hide the cause. var who []string for name, why := range refused { who = append(who, fmt.Sprintf(" %s: %s", name, why)) } sort.Strings(who) return nil, fmt.Errorf("%w\n\nand %d node(s) could not be resolved at all, which is "+ "probably why:\n%s", err, len(refused), strings.Join(who, "\n")) } return g, err } // graph is the whole mesh's network, for showing it. func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, error) { inv := open.inventory on, refused, err := whoResolves(ctx, open, overlay.Requirement) if err != nil { return nil, nil, err } g, err := network(ctx, inv, on, refused) if err != nil { return nil, nil, err } return g.Nodes(), g.Graph(), nil } // whoResolves is the machines whose resolution answers a requirement, and why the others did not. // // By what a module **provides**, not by its name. WireGuard is one way to have a private network // and there could be others, so a machine is on the network because something it runs provides // one — asking for a particular module by name would be the mistake this whole mechanism exists // to avoid. // // Resolved rather than read from the assignment table, because a module can arrive by being // required by something else, and a machine that needs the private network to do its job is on it // for the same reason as one that was handed it directly. func whoResolves(ctx context.Context, open *stores, requirement string) ( map[string]bool, map[string]string, error) { inv := open.inventory nodes, err := inv.Nodes(ctx) if err != nil { return nil, nil, err } on := map[string]bool{} // Why a node could not be resolved, kept rather than raised: one broken node must not stop // the rest being described, and whoever is rendering that node will raise it themselves. refused := map[string]string{} for _, n := range nodes { plan, _, err := planFor(ctx, open, n.Name) if err != nil { refused[n.Name] = err.Error() continue } for _, m := range plan.Modules { for _, offered := range m.Offers() { if offered == requirement { on[n.Name] = true } } } } return on, refused, nil } // rendering is everything a declaration needs, computed over the whole mesh. func generators(ctx context.Context, open *stores) ( map[string]catalogue.Generator, error) { inv := open.inventory on, refused, err := whoResolves(ctx, open, overlay.Addressing) if err != nil { return nil, err } net, err := network(ctx, inv, on, refused) if err != nil { return nil, err } // Both generators see the same machines: the ones on the private network. Names for a machine // that is not on it would resolve to addresses it cannot reach, which is worse than no names. return map[string]catalogue.Generator{ overlay.Name: net, overlay.Names: overlay.NamesFor(net.Nodes()), overlay.Resolver: overlay.ResolverFor(net.Nodes()), }, nil } func overlayShow(ctx context.Context, open *stores) error { nodes, computed, err := graph(ctx, open) if err != nil { return err } if len(nodes) == 0 { // Not "this mesh has no nodes", which it said until the network became a module and was // then a lie about the cause: a mesh can have every node it will ever have and nobody on // the private network, because nobody asked for one. fmt.Printf("nobody is on the private network — assign %s to put a machine on it\n", overlay.Name) return nil } for _, n := range nodes { place := n.Address if place == "" { // Said, not skipped. A node with no place is a node with no network, and it should // be visible here rather than quietly absent from a list of who is on it. place = "no address — run `overlay place`" } fmt.Printf("%-16s %-14s", n.Name, place) switch { case n.Hub: fmt.Print(" hub") case !n.Reachable(): fmt.Print(" not dialable") } if n.Site != "" { fmt.Printf(" at %s", n.Site) } fmt.Println() for _, p := range computed[n.Name] { fmt.Printf(" → %-14s %-18s %s\n", p.Name, p.Allowed, p.Why) } } return nil } // SilentFor is how long a node may be quiet before the mesh says so. // // A node speaks every minute, so three of them missed is a gap rather than a slow one. The number // is not the point — being able to say "out of touch" at all is, and nothing could before. const SilentFor = 3 * time.Minute // whereEveryoneIs is each machine's name on the private network, for the ones on it. // // **Resolved without consulting the rest of the mesh**, and that is not an optimisation. Every // other path here answers a question about one node by resolving the others; this one is called // *from* that path, so doing the same would not terminate — which it did not, for two minutes, // until it was run. // // An unchecked resolution is exactly right for the question anyway. Whether a machine is on the // private network depends on what it was assigned and what that requires, both of which are local // facts. What it takes *from* other machines does not change the answer. // // The distinction that matters is kept: a machine absent from the network module's own view is // absent here, so "has an address" is not mistaken for "is reachable" — which it was, before the // network became something a machine is given. func whereEveryoneIs(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest) (map[string]string, error) { if shelf == nil { // Refused rather than answered. Being on the private network is a conclusion about what a // node resolves to, so with no catalogue nothing resolves and the honest answer is // "nobody" — which is wrong, indistinguishable from a mesh with no overlay, and refused // every certificate the mesh was asked for while saying the machine was on no network. return nil, errors.New( "asked where everyone is without the catalogue, which cannot be answered") } places, err := inv.Overlays(ctx) if err != nil { return nil, err } out := map[string]string{} for _, p := range places { if p.Address == "" { continue } assigned, err := inv.Assigned(ctx, p.Name) if err != nil || len(assigned) == 0 { continue } caps, _ := inv.ProfileOf(ctx, p.Name) got, err := catalogue.Resolve(shelf, assigned, catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: caps}, catalogue.World{Unchecked: true}) if err != nil { continue } for _, m := range got.Modules { for _, offered := range m.Offers() { if offered == overlay.Requirement { out[p.Name] = overlay.InternalName(p.Name) } } } } return out, nil } // onThePrivateNetwork is every node's address on the overlay, sorted. // // A node with no address is left out rather than rendered as an empty source: an empty entry in a // source set is a syntax error in the rule file, and a rule file that does not load leaves the // node filtering whatever it was filtering before -- the one outcome worse than a wrong rule, // because nothing reports it. func onThePrivateNetwork(ctx context.Context, inv *inventory.Inventory) ([]string, error) { places, err := inv.Overlays(ctx) if err != nil { return nil, err } var out []string for _, p := range places { if strings.TrimSpace(p.Address) != "" { out = append(out, p.Address) } } sort.Strings(out) return out, nil } // namesInTheMesh is every machine's internal name and the address behind it. // // A machine with no address has no name: writing one that resolves to nothing is worse than not // writing it, because a connection to an address that does not answer hangs where a name that // does not resolve fails at once and says so. That is the rule the hosts file already follows, // and this is the same set read the same way. func namesInTheMesh(ctx context.Context, inv *inventory.Inventory) (map[string]string, error) { places, err := inv.Overlays(ctx) if err != nil { return nil, err } out := map[string]string{} for _, p := range places { if strings.TrimSpace(p.Address) == "" { continue } out[overlay.InternalName(p.Name)] = p.Address } return out, nil }