Files
mesh-host/internal/overlay/graph_test.go
T
jschoubben acb720d2bf Merge five moved overlay tests into two, offsetting the tests this release adds
The test budget: mesh-host is over 500 unit tests, so the three tests
added for ADR 0593 §6 and §7 are paid for by folding the private
network's hub refusals, its MTU cases and its no-self-peer check into
table-driven tests, each case kept.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:39:51 +01:00

334 lines
12 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 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 TestAHubAtYourOwnSiteAppearsOnceNotTwice(t *testing.T) {
// WireGuard takes one entry per public key. A hub that shares a site with a spoke was being
// emitted twice — once as a direct peer and once as the route of last resort — producing a
// configuration the interface refuses, from a mesh that thought it had succeeded.
//
// Found in the lab on the first two machines that shared a site with their hub, which is the
// ordinary case for a small mesh and was in none of the tests above.
g, err := Compute([]Node{
at("anchor", "lab", "10.42.0.1", "192.0.2.10:51820", true),
at("laptop", "lab", "10.42.0.2", "", false),
}, cidr)
if err != nil {
t.Fatal(err)
}
seen := map[string]int{}
for _, p := range g["laptop"] {
seen[p.Key]++
}
for key, count := range seen {
if count > 1 {
t.Errorf("the key %s appears %d times; WireGuard takes one entry per key", key, count)
}
}
if len(g["laptop"]) != 1 {
t.Fatalf("laptop has %d peers; the hub at its own site is one peer, not two", len(g["laptop"]))
}
// And that single entry has to carry everything, or the node keeps a direct path to the hub
// and loses its route to everywhere else.
if got := g["laptop"][0].Allowed; got != cidr {
t.Errorf("the single entry allows %s; it is both the direct path and the route of last "+
"resort, so it carries the whole overlay", got)
}
// And nobody peers with themselves, here or in a site of two dialable nodes.
other, 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 _, graph := range []Graph{g, other} {
for node, peers := range graph {
for _, p := range peers {
if p.Name == node {
t.Errorf("%s peers with itself", node)
}
}
}
}
}
func TestTwoUnreachableNodesAtOneSiteDoNotPeerDirectly(t *testing.T) {
// Nobody would open the path, and the direct route is more specific than the hub's — so it
// wins and blackholes. Both nodes would reach the hub perfectly and be unable to reach each
// other, which is the worst arrangement available: it looks configured and is not.
//
// This is the design's own warning arriving in its implementation, and the lab found it with
// two machines at one site behind no reachable address — the ordinary shape of a house.
g, err := Compute([]Node{
at("anchor", "datacentre", "10.42.0.1", "198.51.100.1:51820", true),
at("desk", "house", "10.42.0.2", "", false),
at("server", "house", "10.42.0.3", "", false),
}, cidr)
if err != nil {
t.Fatal(err)
}
if _, ok := peersOf(t, g, "desk")["server"]; ok {
t.Error("two nodes that neither can be dialled were peered directly; neither could open " +
"the path and the route is more specific than the hub's, so it blackholes")
}
// And they must still be able to reach each other — through the hub.
if hub := peersOf(t, g, "desk")["anchor"]; hub.Allowed != cidr {
t.Errorf("desk's route to everything else allows %s", hub.Allowed)
}
}
func TestOneReachableNodeIsEnoughToPeerDirectly(t *testing.T) {
// The other side of it: if either end can be dialled, the path can be opened, and using it
// keeps their traffic off a hub 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", "", false),
}, cidr)
if err != nil {
t.Fatal(err)
}
if _, ok := peersOf(t, g, "server")["desk"]; !ok {
t.Error("a node did not peer with a reachable neighbour at its own site")
}
if _, ok := peersOf(t, g, "desk")["server"]; !ok {
t.Error("the reachable node did not hold its unreachable neighbour, so replies have " +
"nowhere to go")
}
}
// novox/hq ADR 0105: a hub that took over the predecessor's tunnel carries every peer that tunnel
// had, under the key and at the address the peer knows, until a node enrols with that key.
func TestTheHubCarriesTheTunnelsPeersUntilTheyEnrol(t *testing.T) {
hub := at("anchor", "hosting", "192.0.2.1", "198.51.100.1:51900", true)
hub.Carried = []Carried{
{Key: "key-home", Address: "192.0.2.2"},
{Key: "key-workstation", Address: "192.0.2.3"},
}
// The machine behind key-home enrolled: it is a node now, at the address it kept.
home := at("home", "house", "192.0.2.2", "", false)
home.Key = "key-home"
g, err := Compute([]Node{hub, home}, "192.0.2.0/24")
if err != nil {
t.Fatal(err)
}
peers := peersOf(t, g, "anchor")
carried, ok := peers[CarriedName("key-workstation")]
if !ok {
t.Fatalf("the hub does not carry the peer that has not enrolled: %+v", g["anchor"])
}
if carried.Allowed != "192.0.2.3/32" || carried.Endpoint != "" || carried.Key != "key-workstation" {
t.Errorf("a carried peer is not the tunnel's own entry — same key, its one address, no endpoint: %+v", carried)
}
if _, twice := peers[CarriedName("key-home")]; twice {
t.Error("a peer that enrolled is carried as well as listed as a node: WireGuard takes one entry per key")
}
if node, ok := peers["home"]; !ok || node.Key != "key-home" || node.Allowed != "192.0.2.2/32" {
t.Errorf("the enrolled peer is not the node it became: %+v", node)
}
// Carried peers are the hub's business only: a spoke routes everything through the hub.
if _, leaked := peersOf(t, g, "home")[CarriedName("key-workstation")]; leaked {
t.Error("a carried peer appeared in a spoke's peer list")
}
}
// A mesh whose hub cannot carry the paths between sites is refused, never answered as an empty graph: with no hub there
// is no path between sites, and a hub with no endpoint is a peer every node is given and none can reach. Either would
// look configured and be silent, which is how the old arrangement failed when nobody knew the address convention.
func TestAHubThatCannotCarryThePathsIsRefused(t *testing.T) {
for _, c := range []struct {
name string
nodes []Node
says func(error) bool
}{
{"no hub", []Node{at("a", "", "10.42.0.1", "", false), at("b", "", "10.42.0.2", "", false)},
func(err error) bool { return errors.Is(err, ErrNoHub) }},
{"a hub with no endpoint", []Node{at("anchor", "datacentre", "10.42.0.1", "", true), at("laptop", "", "10.42.0.2", "", false)},
func(err error) bool { return err != nil && strings.Contains(err.Error(), "must be reachable") }},
} {
if _, err := Compute(c.nodes, cidr); !c.says(err) {
t.Errorf("%s: %v", c.name, err)
}
}
}