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,144 @@
|
||||
// Package overlay computes the private network every node runs on.
|
||||
//
|
||||
// novox/hq 08-connectivity. This is control-plane work by definition: a peer list is derived from
|
||||
// every node at once, and no node has that. A node computes nothing about the mesh — it generates
|
||||
// a keypair, publishes the public half, and receives the rest.
|
||||
//
|
||||
// The shape is 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. A more
|
||||
// specific route to a dead endpoint blackholes; it does not fall back to the general one. So a
|
||||
// node gets exactly one path to any peer, because two would mean one of them silently swallowing
|
||||
// traffic.
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Node is one machine's place on the network, as the mesh holds it.
|
||||
type Node struct {
|
||||
Name string
|
||||
Key string
|
||||
Endpoint string
|
||||
Site string
|
||||
Hub bool
|
||||
Address string
|
||||
}
|
||||
|
||||
// Reachable reports whether other nodes can dial this one. Declared, never inferred.
|
||||
func (n Node) Reachable() bool { return strings.TrimSpace(n.Endpoint) != "" }
|
||||
|
||||
// Peer is one entry in a node's peer list.
|
||||
type Peer struct {
|
||||
Name string
|
||||
Key string
|
||||
// Endpoint is empty when this peer cannot be dialled — it must dial us instead.
|
||||
Endpoint string
|
||||
// Allowed is what traffic goes down this tunnel. A single address for a direct peer; the
|
||||
// whole overlay for the hub, which is what makes it the route of last resort.
|
||||
Allowed string
|
||||
// Keepalive matters only on the side behind NAT: a node that cannot be dialled has to keep
|
||||
// the path open from its end, or the peer's first packet arrives at a mapping that has
|
||||
// already expired.
|
||||
Keepalive bool
|
||||
// Why this peer is in the list, for a person reading a generated file and wondering.
|
||||
Why string
|
||||
}
|
||||
|
||||
// Graph is every node's peer list.
|
||||
type Graph map[string][]Peer
|
||||
|
||||
// ErrNoHub means nobody has said which node is the hub.
|
||||
//
|
||||
// Its own error rather than an empty graph: a mesh with no hub has no path between sites, and
|
||||
// answering with "no peers" would look like a working mesh where nothing can reach anything.
|
||||
var ErrNoHub = errors.New("this mesh has no hub, so there is no path between sites")
|
||||
|
||||
// Compute derives every node's peer list.
|
||||
//
|
||||
// Nodes without a key or an address are skipped rather than refused: a node that has enrolled and
|
||||
// not yet been given a place on the network is an ordinary in-between state, and failing the whole
|
||||
// graph because one node is half-configured would mean no node gets a network.
|
||||
func Compute(nodes []Node, overlayCIDR string) (Graph, error) {
|
||||
var hub *Node
|
||||
usable := make([]Node, 0, len(nodes))
|
||||
for i := range nodes {
|
||||
n := nodes[i]
|
||||
if n.Key == "" || n.Address == "" {
|
||||
continue
|
||||
}
|
||||
usable = append(usable, n)
|
||||
if n.Hub {
|
||||
hub = &usable[len(usable)-1]
|
||||
}
|
||||
}
|
||||
if len(usable) == 0 {
|
||||
return Graph{}, nil
|
||||
}
|
||||
if hub == nil {
|
||||
return nil, ErrNoHub
|
||||
}
|
||||
if !hub.Reachable() {
|
||||
return nil, fmt.Errorf(
|
||||
"%s is the hub and has no endpoint, so nothing can dial it. The hub is the one node "+
|
||||
"that must be reachable from wherever the others are", hub.Name)
|
||||
}
|
||||
|
||||
graph := Graph{}
|
||||
for _, self := range usable {
|
||||
var peers []Peer
|
||||
|
||||
for _, other := range usable {
|
||||
if other.Name == self.Name {
|
||||
continue
|
||||
}
|
||||
// Two nodes at the same site peer directly. A site is where a machine physically is,
|
||||
// and machines that share one have a path that does not need the hub — so using it
|
||||
// keeps their traffic off a link that may be somewhere else entirely.
|
||||
if self.Site != "" && self.Site == other.Site {
|
||||
peers = append(peers, Peer{
|
||||
Name: other.Name, Key: other.Key,
|
||||
Endpoint: other.Endpoint,
|
||||
Allowed: other.Address + "/32",
|
||||
Keepalive: !self.Reachable(),
|
||||
Why: "at the same site",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !self.Hub {
|
||||
// Everything else goes through the hub, including a node that roams. AllowedIPs is
|
||||
// the whole overlay, so this is the route of last resort — and because direct peers
|
||||
// above are single addresses, they win on specificity without either being ambiguous.
|
||||
peers = append(peers, Peer{
|
||||
Name: hub.Name, Key: hub.Key,
|
||||
Endpoint: hub.Endpoint,
|
||||
Allowed: overlayCIDR,
|
||||
Keepalive: !self.Reachable(),
|
||||
Why: "the hub — everything not at this site",
|
||||
})
|
||||
} else {
|
||||
// The hub holds every node that does not share a site with it, because those nodes
|
||||
// route through it and it must know where to send the replies. Ones it cannot dial
|
||||
// will dial it.
|
||||
for _, other := range usable {
|
||||
if other.Name == self.Name || (self.Site != "" && self.Site == other.Site) {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, Peer{
|
||||
Name: other.Name, Key: other.Key,
|
||||
Endpoint: other.Endpoint,
|
||||
Allowed: other.Address + "/32",
|
||||
Why: "routes through this hub",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].Name < peers[j].Name })
|
||||
graph[self.Name] = peers
|
||||
}
|
||||
return graph, nil
|
||||
}
|
||||
Reference in New Issue
Block a user