Networking is a module, and a domain module is how you avoid choosing
Connectivity was code beside the module system doing the module system's
job: every machine with an address was on the private network and there
was no way to keep one off.
A manifest can now say its resources are computed by the control plane,
which is what a peer list needs — it is derived from every machine at
once, so nothing could be written in advance. The network is a module
from there on: assigned, resolved, settled, and absent from a machine
nobody gave it to.
Three modules rather than one, because WireGuard is one VPN of several:
mesh-wireguard provides private-network, mesh-addressing
claims the-private-network, one per node
mesh-names provides name-resolution, requires mesh-addressing
networking requires both, and ships no files of its own
The last is the point. Most people want the network up and do not want
to choose a VPN, so `assign networking` takes the only answer to each
requirement silently. The day the catalogue holds a second one there are
two answers, the resolver refuses and names them, and choosing is
assigning the one you want. No flavor field, nothing to configure.
Names left the WireGuard declaration for their own module. They would be
identical over a different private network, and bundling them made one
module out of two things.
Three faults the walk found:
- choosing tailscale still installed WireGuard, dragged back in by the
names needing the mesh's own addresses. Caught now by a claim: running
two VPNs is fine, being *the* mesh network is singular.
- a requirement wanted by two modules was reported twice, identically.
- "this mesh has no hub" was reported when the real cause was that a
node could not be resolved at all. It now names the node and the why.
And a test that asserts the manifests actually shipped, after the claim
went missing from the real one while every test stayed green.
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package overlay
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// The private network as a module rather than as code beside the module system.
|
||||
//
|
||||
// A machine's peer list is derived from every other machine, so it differs on each one and
|
||||
// changes when any of them changes — there is nothing that could be written in a manifest. So the
|
||||
// module says its resources are computed, and this computes them.
|
||||
//
|
||||
// What that buys, beyond one mechanism instead of two: **a machine is on the private network
|
||||
// because it was assigned the module.** Before this, every machine with a key and an address was
|
||||
// on it, and there was no way to say a machine should stay off.
|
||||
//
|
||||
// It is worth saying what is *not* here, because networking looks like it should be foundational.
|
||||
// The host needs none of this. It has an address and a route to the broker before the mesh exists
|
||||
// — that is the machine's own networking — and the broker's address is carried in the enrolment
|
||||
// token rather than resolved. So the private network is something the mesh installs on top, like
|
||||
// anything else, and a node without it is an ordinary node that nothing reaches directly.
|
||||
|
||||
// What a module asks for when it needs machines to reach each other. **This is the name that
|
||||
// matters** — WireGuard is one way to answer it, and naming the requirement after the answer is
|
||||
// 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"
|
||||
|
||||
// Addressing is the mesh handing out addresses on the private network itself.
|
||||
//
|
||||
// Names are computed from it, which is why they require this rather than a private network in
|
||||
// general. A different VPN that hands out its own addresses would come with its own names — the
|
||||
// mesh has nothing to write about a machine whose address it did not choose. Saying so here is
|
||||
// what keeps a machine from being given a hosts file full of addresses that mean nothing.
|
||||
const Addressing = "mesh-addressing"
|
||||
|
||||
// TheNetwork is what a machine can only have one of.
|
||||
//
|
||||
// Providing a private network is not the singular part — a machine could reasonably run two VPNs
|
||||
// for two different purposes. Being **the** one the mesh runs over is singular, and without
|
||||
// saying so a person who chose a different VPN can still end up with this one dragged back in by
|
||||
// something that needed the mesh's own addresses. Which is exactly what happened, once.
|
||||
const TheNetwork = "the-private-network"
|
||||
|
||||
// Domain is the module for people who want a network and do not want to choose one.
|
||||
//
|
||||
// It has no files of its own — it is requirements and nothing else. Assigning it finds one
|
||||
// answer to each and takes them silently, so getting a mesh onto a private network is one word.
|
||||
// The day the catalogue holds a second VPN there are two answers, the resolver refuses and names
|
||||
// both, and choosing is assigning the one you want. **That is the whole mechanism**: picking an
|
||||
// implementation is assigning a module, and there is no flavor field, no configuration language,
|
||||
// and nothing to learn.
|
||||
const Domain = "networking"
|
||||
|
||||
// Generator answers what one node's network configuration is.
|
||||
type Generator struct {
|
||||
nodes []Node
|
||||
graph Graph
|
||||
// keyPath is where each node keeps the private half it generated. Named rather than carried:
|
||||
// the mesh has never seen it and never will.
|
||||
keyPath string
|
||||
}
|
||||
|
||||
// From builds a generator over the machines that are part of the network.
|
||||
//
|
||||
// The nodes given are the ones assigned the module — not every node the mesh knows. A machine
|
||||
// that was never given it is absent from everybody's peer list and from the names, which is what
|
||||
// "not on the network" has to mean.
|
||||
func From(nodes []Node, cidr, keyPath string) (*Generator, error) {
|
||||
graph, err := Compute(nodes, cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Generator{nodes: nodes, graph: graph, keyPath: keyPath}, nil
|
||||
}
|
||||
|
||||
// Resources is one node's interface, peers and names.
|
||||
func (g *Generator) Resources(node string) ([]map[string]any, bool, error) {
|
||||
peers, part := g.graph[node]
|
||||
if !part {
|
||||
// Assigned and not yet placed on the network. Ordinary and brief, so it is an answer
|
||||
// rather than an error.
|
||||
return nil, false, nil
|
||||
}
|
||||
var self Node
|
||||
for _, n := range g.nodes {
|
||||
if n.Name == node {
|
||||
self = n
|
||||
}
|
||||
}
|
||||
|
||||
raw, err := Declaration(self, peers, g.keyPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var parsed struct {
|
||||
Resources []map[string]any `json:"resources"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
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 }
|
||||
|
||||
// NamesFor builds the name generator over the machines on the private network.
|
||||
func NamesFor(nodes []Node) *NameGenerator { return &NameGenerator{nodes: nodes} }
|
||||
|
||||
// Resources is the one file.
|
||||
func (g *NameGenerator) Resources(node string) ([]map[string]any, bool, error) {
|
||||
var found bool
|
||||
for _, n := range g.nodes {
|
||||
if n.Name == node {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, false, nil
|
||||
}
|
||||
hosts, err := Hosts(g.nodes, node)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return []map[string]any{{
|
||||
"id": "mesh-names", "type": "file", "path": HostsPath,
|
||||
"mode": "0644", "content": hosts,
|
||||
}}, true, nil
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
||||
// Graph is the peer list per node, for showing.
|
||||
func (g *Generator) Graph() Graph { return g.graph }
|
||||
|
||||
// Manifest is the module the mesh provides for itself.
|
||||
//
|
||||
// It ships with the control plane rather than coming from a repository, because the thing that
|
||||
// computes it ships with the control plane. That is the only way it is unusual: it is assigned,
|
||||
// unassigned, resolved and settled exactly like a module somebody wrote.
|
||||
func Manifest() map[string]any {
|
||||
return map[string]any{
|
||||
"module": Name,
|
||||
"version": "1",
|
||||
"computed": Name,
|
||||
"provides": []string{Requirement, Addressing},
|
||||
"claims": []map[string]any{{"name": TheNetwork, "scope": "node"}},
|
||||
}
|
||||
}
|
||||
|
||||
// NamesManifest is the module that gives machines names on the private network.
|
||||
//
|
||||
// It requires the network rather than providing it, which is the whole reason it is separate: a
|
||||
// name resolves to an address on the private wire, so having names without being on it would
|
||||
// point every machine at somewhere it cannot reach.
|
||||
func NamesManifest() map[string]any {
|
||||
return map[string]any{
|
||||
"module": Names,
|
||||
"version": "1",
|
||||
"computed": Names,
|
||||
"provides": []string{Resolution},
|
||||
"requires": []string{Addressing},
|
||||
}
|
||||
}
|
||||
|
||||
// DomainManifest is the module that means "get the network working".
|
||||
func DomainManifest() map[string]any {
|
||||
return map[string]any{
|
||||
"module": Domain,
|
||||
"version": "1",
|
||||
"requires": []string{Requirement, Resolution},
|
||||
}
|
||||
}
|
||||
|
||||
// Empty is a network nobody is on.
|
||||
//
|
||||
// A mesh where no machine was given the module. Legitimate rather than broken — every node still
|
||||
// reaches the broker, which is what being in the mesh is — so it answers "not part of this" for
|
||||
// everyone instead of refusing for want of a hub.
|
||||
func Empty() *Generator { return &Generator{graph: Graph{}} }
|
||||
Reference in New Issue
Block a user