The sibling of node public-domain, and the worse one: a placement is three facts declared together, so an invocation that said none of them took all three away — the endpoint every other machine dials, the site, and the hub. A mesh whose hub was placed that way has no paths left, at the moment somebody was trying to look at it. --nothing keeps the real case (a machine that roams and opens every path itself) sayable, by name. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
385 lines
14 KiB
Go
385 lines
14 KiB
Go
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 <node> [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 <node> [--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
|
|
}
|