// 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 }