Files
mesh-controller/cmd/mesh-controller/network.go
T
jschoubben 79a9e17df0 The broker credential resolves the mesh-broker seat, not the hub (issue 059)
An adversarial review of the 055 fix found it encoded the wrong invariants, latent while
every mesh keeps its broker on the hub. Now: the address is the overlay name of the node
ASSIGNED a module claiming the mesh-broker seat (the hub stands in only while nothing holds
the seat — genesis); "on the overlay" is what whereEveryoneIs answers (resolved the
networking module), not "has an address"; a portless genesis address defaults to 5671
instead of silently disabling the path; a second `overlay place --hub` is refused rather
than last-write-wins; and `overlay place` says that earlier credentials keep their old
address. A test now binds the controller's own module.json to its seat, so deleting the
claim fails the suite.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-18 01:02:26 +02:00

464 lines
17 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"sort"
"strings"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/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)
}
// One hub per mesh, refused rather than last-write-wins: with two flagged, which one the
// graph and the broker address pick is order-dependent — the silent-election fault ADR 0007
// exists to avoid, one flag over (novox/hq issue 059). Moving the hub is explicit: re-place
// the old one without --hub first.
if *hub {
placed, err := inv.Overlays(ctx)
if err != nil {
return err
}
for _, p := range placed {
if p.Hub && p.Name != node {
return fmt.Errorf("%s is already the hub; a mesh has one. Re-place %s without "+
"--hub first if the hub is moving", p.Name, p.Name)
}
}
}
// 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)
fmt.Println(" credentials issued for it before this placement keep their old broker address —" +
" `module issue` them again and push (novox/hq issue 059)")
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 g != nil {
// The artifact store, as this network reaches it. Found rather than configured: the
// provider is whichever module offers it, on whichever machine holds that module — and if
// nothing does yet (genesis raises the registry before the catalogue knows it), there is
// no trust to write and nothing is written (novox/hq ADR 0082).
//
// Refused rather than composed without it when the question could not be answered: a
// declaration missing the trust because a lookup failed is a machine that cannot pull,
// delivered by a push that reported success — and nothing recomposes it until the next
// push (the shape of novox/hq issues 042/048, reappearing as a race).
at, port, found, storeErr := artifactStoreOnNetwork(ctx, inv, on)
if storeErr != nil {
return nil, fmt.Errorf("finding the artifact store this network reaches: %w", storeErr)
}
if found {
g.TrustRegistry(overlay.InternalName(at) + ":" + port)
}
}
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
}
// **One generator now.** Two more used to sit beside it — the names and a resolver's zone
// file — as modules that ran nothing. Both are facts a module asks for in its manifest
// (`facts:` — catalogue.FactsInto), computed from the same machines this sees: the ones on the
// private network, because a name for a machine not on it would resolve to an address nothing
// can reach.
return map[string]catalogue.Generator{
overlay.Name: net,
}, 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
}
// artifactStoreOnNetwork is the machine and port the mesh's artifact store answers on, when a
// module providing it is assigned to a machine that is on the private network.
//
// A lookup failure is an error, never "not found": collapsing the two composed a declaration
// without the trust whenever the inventory hiccuped, delivered by a push that reported success —
// and nothing recomposed the machine until the next push. "No store" must mean the mesh has none,
// not that the question went unanswered.
func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory,
on map[string]bool) (node, port string, found bool, err error) {
shelf, err := inv.Catalogue(ctx)
if err != nil {
return "", "", false, fmt.Errorf("reading the catalogue: %w", err)
}
providers := map[string]string{} // module -> served port
for name, m := range shelf {
served, offers := m.Serves[catalogue.ArtifactStoreProvision]
if !offers {
continue
}
if p, ok := served["port"]; ok {
providers[name] = fmt.Sprintf("%v", p)
}
}
if len(providers) == 0 {
return "", "", false, nil
}
for machine := range on {
assigned, err := inv.Assigned(ctx, machine)
if err != nil {
return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err)
}
for _, a := range assigned {
if p, ok := providers[a]; ok {
return machine, p, true, nil
}
}
}
return "", "", false, nil
}