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.
225 lines
7.4 KiB
Go
225 lines
7.4 KiB
Go
package overlay
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const cidr = "10.42.0.0/16"
|
|
|
|
func at(name, site, address, endpoint string, hub bool) Node {
|
|
return Node{Name: name, Key: "key-" + name, Site: site, Address: address,
|
|
Endpoint: endpoint, Hub: hub}
|
|
}
|
|
|
|
func peersOf(t *testing.T, g Graph, node string) map[string]Peer {
|
|
t.Helper()
|
|
out := map[string]Peer{}
|
|
for _, p := range g[node] {
|
|
out[p.Name] = p
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestEveryNodeReachesTheHub(t *testing.T) {
|
|
// The property that makes this a network at all. Without it a node has no route to anything
|
|
// it does not share a site with.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
at("home", "house", "10.42.0.3", "", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, node := range []string{"laptop", "home"} {
|
|
hub, ok := peersOf(t, g, node)["anchor"]
|
|
if !ok {
|
|
t.Fatalf("%s has no route to the hub", node)
|
|
}
|
|
if hub.Allowed != cidr {
|
|
t.Errorf("%s routes %s through the hub; it must be the whole overlay or the hub is "+
|
|
"not a route of last resort", node, hub.Allowed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodesAtOneSitePeerDirectly(t *testing.T) {
|
|
// Machines that share a site have a path that does not need the hub, and using it keeps their
|
|
// traffic off a link that may be on another continent.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("desk", "house", "10.42.0.2", "192.0.2.2:51820", false),
|
|
at("server", "house", "10.42.0.3", "192.0.2.3:51820", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
direct, ok := peersOf(t, g, "desk")["server"]
|
|
if !ok {
|
|
t.Fatal("two machines at one site do not peer directly")
|
|
}
|
|
if direct.Allowed != "10.42.0.3/32" {
|
|
t.Errorf("the direct peer allows %s; a single address is what makes it win on "+
|
|
"specificity over the hub's whole-overlay route", direct.Allowed)
|
|
}
|
|
}
|
|
|
|
func TestARoamingNodeGetsExactlyOnePath(t *testing.T) {
|
|
// Hub-only, and not as a simplification. WireGuard has no failover: a more specific route to
|
|
// a dead endpoint blackholes rather than falling back, so two paths would mean one of them
|
|
// silently swallowing traffic.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
at("other", "", "10.42.0.3", "", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := len(g["laptop"]); got != 1 {
|
|
t.Fatalf("a roaming node has %d peers; it gets exactly one path", got)
|
|
}
|
|
if g["laptop"][0].Name != "anchor" {
|
|
t.Errorf("a roaming node's single path is %s, not the hub", g["laptop"][0].Name)
|
|
}
|
|
}
|
|
|
|
func TestTwoRoamingNodesDoNotPeerWithEachOther(t *testing.T) {
|
|
// An empty site is not a site. Nodes that roam have no shared location, and treating "" as
|
|
// one would have every roaming machine try to dial every other, none of which can be dialled.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
at("phone", "", "10.42.0.3", "", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := peersOf(t, g, "laptop")["phone"]; ok {
|
|
t.Error("two nodes with no site peered as though they shared one")
|
|
}
|
|
}
|
|
|
|
func TestOnlyTheSideThatCannotBeDialledKeepsThePathOpen(t *testing.T) {
|
|
// Keepalive matters exactly once: on the node behind NAT. Without it the peer's first packet
|
|
// arrives at a mapping that has already expired. On the reachable side it is pointless
|
|
// traffic for ever.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !peersOf(t, g, "laptop")["anchor"].Keepalive {
|
|
t.Error("a node that cannot be dialled does not keep its path open")
|
|
}
|
|
if peersOf(t, g, "anchor")["laptop"].Keepalive {
|
|
t.Error("a reachable node sends keepalives it does not need")
|
|
}
|
|
|
|
// And between two direct peers at one site, where whether to keep the path open depends on
|
|
// which end you are. Checked here because the hub's own peer list happens not to set the
|
|
// field at all, so asserting only against the hub tests an absence rather than the rule.
|
|
same, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("desk", "house", "10.42.0.2", "192.0.2.2:51820", false),
|
|
at("server", "house", "10.42.0.3", "", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !peersOf(t, same, "server")["desk"].Keepalive {
|
|
t.Error("the peer that cannot be dialled does not keep the path open")
|
|
}
|
|
if peersOf(t, same, "desk")["server"].Keepalive {
|
|
t.Error("the peer that can be dialled sends keepalives it does not need")
|
|
}
|
|
}
|
|
|
|
func TestTheHubKnowsEveryoneItRoutesFor(t *testing.T) {
|
|
// The replies have to get back. A hub that does not hold a peer cannot answer it.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
at("home", "house", "10.42.0.3", "", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hub := peersOf(t, g, "anchor")
|
|
for _, want := range []string{"laptop", "home"} {
|
|
if _, ok := hub[want]; !ok {
|
|
t.Errorf("the hub does not hold %s, so replies to it have nowhere to go", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMeshWithNoHubIsRefused(t *testing.T) {
|
|
// Not an empty graph. A mesh with no hub has no path between sites, and answering "no peers"
|
|
// would look like a working network in which nothing can reach anything — which is how the
|
|
// old arrangement failed, silently, when nobody knew the address convention.
|
|
_, err := Compute([]Node{
|
|
at("a", "", "10.42.0.1", "", false),
|
|
at("b", "", "10.42.0.2", "", false),
|
|
}, cidr)
|
|
if !errors.Is(err, ErrNoHub) {
|
|
t.Fatalf("a mesh with no hub gave %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAnUnreachableHubIsRefused(t *testing.T) {
|
|
// The hub is the one node that must be dialable from wherever the others are. Left
|
|
// unchecked, every node would be given a peer it can never reach and the mesh would look
|
|
// configured and be silent.
|
|
_, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
}, cidr)
|
|
if err == nil {
|
|
t.Fatal("a hub with no endpoint was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "must be reachable") {
|
|
t.Errorf("the refusal does not say why: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestANodeWithNoPlaceYetIsSkippedRatherThanFatal(t *testing.T) {
|
|
// A node that has enrolled and not yet been given an address is an ordinary in-between state.
|
|
// Failing the whole graph over it would mean no node gets a network because one is half done.
|
|
g, err := Compute([]Node{
|
|
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("laptop", "", "10.42.0.2", "", false),
|
|
{Name: "newcomer", Key: "", Address: ""},
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := g["newcomer"]; ok {
|
|
t.Error("a node with no key or address was given a peer list")
|
|
}
|
|
if _, ok := peersOf(t, g, "laptop")["newcomer"]; ok {
|
|
t.Error("a node with no key was put in somebody's peer list")
|
|
}
|
|
}
|
|
|
|
func TestNobodyPeersWithThemselves(t *testing.T) {
|
|
g, err := Compute([]Node{
|
|
at("anchor", "house", "10.42.0.1", "198.51.100.1:51820", true),
|
|
at("desk", "house", "10.42.0.2", "192.0.2.2:51820", false),
|
|
}, cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for node, peers := range g {
|
|
for _, p := range peers {
|
|
if p.Name == node {
|
|
t.Errorf("%s peers with itself", node)
|
|
}
|
|
}
|
|
}
|
|
}
|