The mesh computes a private network it cannot impersonate
The first thing the control plane decides rather than relays. Every node's peer list is derived from every node at once, which is what makes this control-plane work by definition: no node has that view. A hub, with direct peering between nodes at the same site. Not a full mesh, and the reason is a property of WireGuard rather than a preference -- there is no failover, so a more specific route to a dead endpoint blackholes instead of falling back. A node gets exactly one path to any peer, because two would mean one of them silently swallowing traffic. A roaming node is hub-only for the same reason. Reachability and the hub are declared, never inferred from an address. The address is evidence and is not the fact: carrier-grade NAT looks public and is not, a routable address behind a closed firewall looks public and is not, and the regular expression that used to decide it got the lab wrong too. Hub election by address prefix failed silently when nobody knew the convention. No private key travels, and that is the whole design. The node generated its own keypair and kept the private half; the configuration points at a file the node wrote, using WireGuard's own PostUp. So the control plane composes a complete configuration for a node it cannot pretend to be -- it knows every public key and holds none of the private ones. Delivered as an ordinary declaration: a package, a file and a service. The host does not know what a private network is and does not learn one. There is a test holding that line, because the moment connectivity needs a new shape in tier 0 is the moment the host stops being small enough to trust. The generated file is written to be read: each peer says why it is there, a peer with no endpoint says why it has none, and the header says not to edit it -- an edit survives until the graph next changes and then vanishes, which is worse than never being applied, because the machine works and then stops and nothing changed that anybody remembers. Fault injection found one weak test. The keepalive rule was asserted only against the hub, whose peer entries happen not to set the field at all, so it was testing an absence rather than the rule. It now checks two direct peers where one is reachable and one is not.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The overlay is delivered as an ordinary declaration.
|
||||
//
|
||||
// novox/hq 08-connectivity: what the host receives is an interface configuration and a peer list,
|
||||
// as files. It does not compute them and it does not know what a private network is — the shapes
|
||||
// it already has are enough, which is why connectivity needs nothing new from tier 0.
|
||||
//
|
||||
// **No private key travels.** The configuration points at a file the node wrote from a key the
|
||||
// mesh has never seen, using WireGuard's own ability to set one after the interface is up. So the
|
||||
// control plane composes a complete configuration for a node it cannot impersonate.
|
||||
|
||||
// Interface is what the private network is called on a machine, and where the node's own key
|
||||
// lives. A constant rather than a setting: two nodes disagreeing about the name would produce a
|
||||
// mesh where each is configured correctly and nothing meets.
|
||||
const (
|
||||
Interface = "mesh0"
|
||||
ConfigPath = "/etc/wireguard/" + Interface + ".conf"
|
||||
Unit = "wg-quick@" + Interface
|
||||
DefaultKeyPath = "/var/lib/mesh-host/overlay.key"
|
||||
)
|
||||
|
||||
// Resource is one entry in a declaration, built here and read by the host.
|
||||
type Resource map[string]any
|
||||
|
||||
// Declaration is what the mesh sends a node to put it on the network.
|
||||
//
|
||||
// Three resources and nothing clever: the tools, the configuration, and the interface running. A
|
||||
// person can read it, which is the point — this is the first thing a node is ever told, and if it
|
||||
// is wrong the node is unreachable and the mistake has to be findable by eye.
|
||||
func Declaration(node Node, peers []Peer, keyPath string) ([]byte, error) {
|
||||
if node.Address == "" {
|
||||
return nil, fmt.Errorf("%s has no address on the overlay, so there is nothing to configure",
|
||||
node.Name)
|
||||
}
|
||||
if keyPath == "" {
|
||||
keyPath = DefaultKeyPath
|
||||
}
|
||||
|
||||
resources := []Resource{
|
||||
{
|
||||
"id": "overlay-tools", "type": "package", "package": "wireguard-tools",
|
||||
},
|
||||
{
|
||||
"id": "overlay-config", "type": "file", "path": ConfigPath,
|
||||
// Readable only by root: it lists every peer's key and endpoint, which is a map of
|
||||
// the mesh. Not secret in the way a private key is, and not something to leave
|
||||
// world-readable on a machine somebody else also uses.
|
||||
"mode": "0600",
|
||||
"content": config(node, peers, keyPath),
|
||||
},
|
||||
{
|
||||
"id": "overlay-up", "type": "service", "unit": Unit,
|
||||
"state": "running",
|
||||
// Enabled, so the node comes back onto the network after a reboot without waiting to
|
||||
// be told again. A node whose overlay only exists while something is watching is not
|
||||
// a node that survives being switched off and on.
|
||||
"boot": "enabled",
|
||||
},
|
||||
}
|
||||
|
||||
return json.Marshal(map[string]any{"declaration": 1, "resources": resources})
|
||||
}
|
||||
|
||||
// config writes the interface file.
|
||||
//
|
||||
// Deliberately in the order a person would read it: who I am, then who I talk to, each with a
|
||||
// line saying why it is there. A generated file that cannot be understood by the person it
|
||||
// confuses is a generated file that gets edited by hand.
|
||||
func config(node Node, peers []Peer, keyPath string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever the\n")
|
||||
b.WriteString("# peer graph changes, and an edit would survive until the next change and\n")
|
||||
b.WriteString("# then vanish, which is worse than not being applied at all.\n")
|
||||
fmt.Fprintf(&b, "#\n# node %s", node.Name)
|
||||
if node.Site != "" {
|
||||
fmt.Fprintf(&b, ", at %s", node.Site)
|
||||
}
|
||||
if !node.Reachable() {
|
||||
b.WriteString(", not dialable — it opens every path itself")
|
||||
}
|
||||
b.WriteString("\n\n[Interface]\n")
|
||||
fmt.Fprintf(&b, "Address = %s/32\n", node.Address)
|
||||
if node.Reachable() {
|
||||
if port := portOf(node.Endpoint); port != "" {
|
||||
fmt.Fprintf(&b, "ListenPort = %s\n", port)
|
||||
}
|
||||
}
|
||||
// The private key is set from a file the node wrote, so it never appears here and never
|
||||
// travelled. Everything else in this file came from the mesh; this one line is the node's.
|
||||
fmt.Fprintf(&b, "PostUp = wg set %%i private-key %s\n", keyPath)
|
||||
|
||||
for _, p := range peers {
|
||||
fmt.Fprintf(&b, "\n# %s — %s\n[Peer]\n", p.Name, p.Why)
|
||||
fmt.Fprintf(&b, "PublicKey = %s\n", p.Key)
|
||||
fmt.Fprintf(&b, "AllowedIPs = %s\n", p.Allowed)
|
||||
if p.Endpoint != "" {
|
||||
fmt.Fprintf(&b, "Endpoint = %s\n", p.Endpoint)
|
||||
} else {
|
||||
b.WriteString("# no endpoint: this peer cannot be dialled and opens the path itself\n")
|
||||
}
|
||||
if p.Keepalive {
|
||||
b.WriteString("PersistentKeepalive = 25\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func portOf(endpoint string) string {
|
||||
if i := strings.LastIndex(endpoint, ":"); i >= 0 {
|
||||
return endpoint[i+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user