Files
mesh-controller/internal/overlay/graph_test.go
jschoubben 3c836f0abb Adopt the predecessor's tunnel in place: its range, its address, its peers
On an adopted hub the private network takes over the tunnel it finds rather
than running beside it (hq ADR 0105): two tunnels leave the mesh's unreachable
through the provider's filter, so no machine can ever join.

The node presents the found tunnel when it enrols, under the key it took as
its own; the inventory records it (node.tunnel, tunnel_peer — migration 0031)
and the mesh composes from it: the overlay's range is the adopted tunnel's,
the hub is placed at the tunnel's address on the tunnel's port, and every
peer the tunnel had is carried in the hub's peer list as a peer of the
tunnel, not a node of the mesh, until a node enrols with that key — which
then keeps the address the tunnel had for it. A fresh node never gets an
address the tunnel holds. The hub's declaration tells the host which unit to
take over; the host's account of carrying it is recorded and shown.

Every reader of the range follows the setting; nothing stores it. A found
tunnel under another key is recorded and not adopted, so ADR 0100's
non-overlap rule keeps applying where a tunnel is left running beside the
mesh's. A lab bed and test skeleton for "How it is checked" are under lab/.
2026-09-23 23:26:34 +02:00

343 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 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)
}
}
}
}
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)
}
}
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")
}
}