Copy mesh-controller's overlay generator into mesh-sdk, from mesh-control 6c2a8d62 (issue 1360)
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user