Three machines across two sites, two of them behind no reachable address, all nine paths open. The mesh computes the graph, delivers it as a declaration, and the nodes bring it up. Every fault below looked like success from inside the mesh: the graph was right, the files were right, the services were up, every node reported it had applied. None was reachable by reasoning. A running interface does not re-read its configuration. A node joins, every existing node's peer list changes, the file is replaced -- and the service is already running, so nothing reloads it. Fixed as declared state rather than a command: the service must reflect the file. A command to restart would be an action, and the link may not carry one. The host refused exactly that, which is how this shape was arrived at. A hub sharing a site with a spoke appeared twice in that spoke's peer list -- once as a direct peer, once as the route of last resort. WireGuard takes one entry per key and refuses the file. The ordinary shape of a small mesh, and in none of the tests written before it ran. Two nodes at one site that neither can be dialled were peered directly. Nobody opens the path, and the direct route is more specific than the hub's, so it wins and blackholes -- this design's own warning arriving in its implementation. They now route through the hub unless one end can be dialled. And Docker sets the FORWARD policy to DROP, so a hub with ip_forward enabled carried nothing between its spokes. The substrate at tier 1 silently breaks the network at tier 2, and nothing in either tier's state says so. The hub inserts its own rule above those chains and removes it on the way down. Two weak tests found by injection along the way: one asserted the keepalive rule only against the hub, whose peer entries happen not to set that field at all, so it tested an absence; the other checked the firewall rules by looking for FORWARD anywhere, which the PostDown line satisfies on its own.
307 lines
11 KiB
Go
307 lines
11 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")
|
|
}
|
|
}
|