The machine that most needed a firewall was the one that could not have one. A hub is dialled by every node at other sites and needs its port open; a machine that is not a hub dials out and needs nothing open. They are the same module, and `listens` in a manifest is one answer for every machine that runs it — so the machine a static answer gets wrong is the one facing the public internet. A generator can now say what it opens, in a second interface rather than a method on every generator: most have nothing to say here, and requiring an empty method of each would be a cost paid everywhere for one caller. The port is the one in the endpoint, which is where the interface takes its ListenPort from. One source, so a rule set cannot open a port the interface is not on. Open to everywhere and deliberately: a node at another site is not on the private network until this port lets it on, so restricting it to the mesh would be a rule that can never be satisfied by the thing it exists for. And a generator that cannot say is refused rather than read as silence. Closing a port on the evidence of a failure to look is how a machine is severed by a fault somewhere else — and the machine it would sever is the hub, whose only route to being fixed is the network it just closed.
247 lines
10 KiB
Go
247 lines
10 KiB
Go
package overlay
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/novox/mesh-control/internal/catalogue"
|
|
)
|
|
|
|
// 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{}} }
|
|
|
|
// Listens is the port this node accepts the private network on, which only a hub has.
|
|
//
|
|
// **A fact about this machine's place in the mesh, not about the module.** Every machine on the
|
|
// network runs the same module; a hub is dialled by every node at other sites and needs its port
|
|
// open, and a machine that is not a hub dials out and needs nothing open at all. A static field in
|
|
// a manifest is one answer for every machine that runs it, so it cannot say this — and the machine
|
|
// it would get wrong is the one facing the public internet, which is the machine that most needs
|
|
// filtering.
|
|
//
|
|
// The port is the one in the endpoint, which is also where the interface takes its ListenPort
|
|
// from. One source, so a rule set cannot open a port the interface is not on.
|
|
func (g *Generator) Listens(node string) ([]catalogue.Listening, error) {
|
|
for _, n := range g.nodes {
|
|
if n.Name != node {
|
|
continue
|
|
}
|
|
if !n.Reachable() {
|
|
// It dials out and nothing dials it. Opening a port here would be opening one on a
|
|
// machine nothing connects to, which is not harmless — it is a rule with no source
|
|
// that somebody later has to work out the reason for.
|
|
return nil, nil
|
|
}
|
|
port := portOf(n.Endpoint)
|
|
if port == "" {
|
|
return nil, fmt.Errorf(
|
|
"%s is reachable at %q and no port can be read from it, so what it must accept "+
|
|
"the private network on is unknown", node, n.Endpoint)
|
|
}
|
|
number, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s is reachable at %q, and %q is not a port", node, n.Endpoint, port)
|
|
}
|
|
return []catalogue.Listening{{
|
|
Port: number, Protocol: "udp", From: catalogue.FromEverywhere,
|
|
// From everywhere, and deliberately: a node at another site is not on the private
|
|
// network until this port lets it on, so restricting this to the mesh would be a
|
|
// rule that can never be satisfied by the thing it exists for.
|
|
Why: "the private network — a node at another site has no other way in",
|
|
}}, nil
|
|
}
|
|
return nil, nil
|
|
}
|