Files
mesh-sdk/go/compose/network_test.go
T
jschoubben c154bdedd3 Keep a machine that does not resolve on the private network at its place, so one machine's modules refuse only its own declaration (issue 1833)
A machine whose own set could not be resolved was left off the private network; when it was the hub, the network
could not be composed and every machine's declaration failed. Its placement (key and address) is its last good place
and does not change when its modules fail, so it stays on the network and only its own declaration is refused. A
refused machine with no place is left off, and a network that cannot be composed from what remains still fails.
2026-10-22 15:03:17 +02:00

307 lines
12 KiB
Go

package compose
import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/overlay"
)
// onTheTunnel makes the hub of an Input what mesh-controller's network_test.go makes of anchor in
// TestTheOverlaysRangeIsTheAdoptedTunnels: adopted, enrolled with the found tunnel's key, and presenting the tunnel
// (Tunnel{wg0, wg-quick@wg0, port 51900, 192.0.2.1/24, range 192.0.2.0/24, one peer PEER-TWO}). Placed on it when
// replaced is true: its address the tunnel's, its endpoint on the tunnel's port, as `overlay place anchor --endpoint
// 198.51.100.10:51900 --site hosting --hub` leaves it.
func onTheTunnel(t *testing.T, in *Input, replaced bool) {
t.Helper()
const key = "THE-TUNNELS-KEY========================="
tunnel := Tunnel{Interface: "wg0", Unit: "wg-quick@wg0", Config: "/etc/wireguard/wg0.conf", Port: 51900,
Address: "192.0.2.1/24", Range: "192.0.2.0/24", PublicKey: key,
Peers: []TunnelPeer{{PublicKey: "PEER-TWO", Address: "192.0.2.2"}}}
hub := nodeOf(t, in, "anchor")
hub.Adopted = true
hub.Place.Key = key
if replaced {
hub.Place.Address, hub.Place.Endpoint, hub.Place.Site = "192.0.2.1", "198.51.100.10:51900", "hosting"
}
in.Tunnels = map[string]FoundTunnel{"anchor": {Tunnel: tunnel, NodeAdopted: true}}
in.Adopted = &AdoptedTunnel{Tunnel: tunnel, Hub: "anchor", Adopted: true}
in.CarriedPeers = []CarriedPeer{{PublicKey: "PEER-TWO", Address: "192.0.2.2"}}
}
// novox/hq ADR 0105 (mesh-controller network_test.go TestTheOverlaysRangeIsTheAdoptedTunnels): the range every address is
// composed from is the adopted tunnel's, read from the tunnel the hub holds, never stored anywhere else. The environment
// value of the controller is Input.OverlayCIDR here.
func TestTheOverlaysRangeIsTheAdoptedTunnels(t *testing.T) {
in := aMesh(t)
in.OverlayCIDR = "10.99.0.0/16"
if got := newComposer(in).overlayRange(); got != "10.99.0.0/16" {
t.Fatalf("without an adopted tunnel the range is not what genesis said: %q", got)
}
in.OverlayCIDR = ""
if got := newComposer(in).overlayRange(); got != DefaultOverlayCIDR {
t.Fatalf("with nothing said the range is %q, not the default", got)
}
in.OverlayCIDR = "10.99.0.0/16"
onTheTunnel(t, &in, true)
if got := newComposer(in).overlayRange(); got != "192.0.2.0/24" {
t.Fatalf("with an adopted tunnel the range is %q, not the tunnel's", got)
}
// A tunnel the hub found and did not take (its key is not the tunnel's) does not give its range.
in.Adopted.Adopted = false
if got := newComposer(in).overlayRange(); got != "10.99.0.0/16" {
t.Fatalf("a tunnel the hub did not take gave its range: %q", got)
}
in.Adopted.Adopted = true
// And the hub's declaration carries the peer and the takeover.
nodes, computed, err := Graph(in)
if err != nil {
t.Fatal(err)
}
var hubNode overlay.Node
for _, n := range nodes {
if n.Name == "anchor" {
hubNode = n
}
}
if hubNode.TakesOver == nil || hubNode.TakesOver.Unit != "wg-quick@wg0" {
t.Errorf("the hub is not told to take over the found tunnel: %+v", hubNode)
}
carried := false
for _, p := range computed["anchor"] {
if p.Key == "PEER-TWO" && p.Allowed == "192.0.2.2/32" {
carried = true
}
}
if !carried {
t.Errorf("the hub's peer list does not carry the tunnel's peer: %+v", computed["anchor"])
}
}
// A takeover is composed only for a hub whose placement agrees with the tunnel (mesh-controller network_test.go
// TestATakeoverIsNotComposedForAHubPlacedOffItsTunnel): an address or an endpoint port that differs would have the host stop
// the found interface and raise the mesh's where no peer is listening.
func TestATakeoverIsNotComposedForAHubPlacedOffItsTunnel(t *testing.T) {
in := aMesh(t)
onTheTunnel(t, &in, false)
// aMesh placed anchor at 10.77.0.1 on :51820, the record of a hub placed before it took the tunnel over.
_, _, err := Graph(in)
if err == nil {
t.Fatal("a takeover was composed for a hub whose address and port are not the tunnel's")
}
for _, want := range []string{"10.77.0.1", "192.0.2.1", "51820", "51900", "overlay place anchor"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not say %q: %v", want, err)
}
}
// Re-placed on the tunnel, it composes.
onTheTunnel(t, &in, true)
if _, _, err := Graph(in); err != nil {
t.Fatalf("re-placed on the tunnel, the graph still refuses: %v", err)
}
}
// 1360/AC2: the generator gives the addresses, peers and names of aMesh: anchor the hub at 10.77.0.1, laptop at 10.77.0.2
// peering through it, both written for their machines.
func TestTheGeneratorGivesTheMeshItsAddressesAndPeers(t *testing.T) {
in := aMesh(t)
gens, err := Generators(in)
if err != nil {
t.Fatal(err)
}
g, ok := gens[overlay.Name].(*overlay.Generator)
if !ok {
t.Fatalf("the generator is %T", gens[overlay.Name])
}
addr := map[string]string{}
for _, n := range g.Nodes() {
addr[n.Name] = n.Address
}
if addr["anchor"] != "10.77.0.1" || addr["laptop"] != "10.77.0.2" || len(addr) != 2 {
t.Fatalf("the addresses are %v", addr)
}
peers := g.Graph()
if len(peers["laptop"]) != 1 || peers["laptop"][0].Name != "anchor" || peers["laptop"][0].Endpoint != "anchor.example:51820" {
t.Errorf("laptop's peers are %+v, want the hub", peers["laptop"])
}
if len(peers["anchor"]) != 1 || peers["anchor"][0].Name != "laptop" || peers["anchor"][0].Allowed != "10.77.0.2/32" {
t.Errorf("anchor's peers are %+v, want laptop", peers["anchor"])
}
for _, node := range []string{"anchor", "laptop"} {
res, on, err := g.Resources(node)
if err != nil || !on || len(res) == 0 {
t.Errorf("%s is given no network: %v %v %d", node, on, err, len(res))
}
}
}
// 1360/AC3: a node not on the private network (it runs nothing that puts it there) gets no address and no network, and the
// one that stayed keeps its own.
func TestANodeNotOnThePrivateNetworkGetsNoAddressAndNoNetwork(t *testing.T) {
in := aMesh(t)
nodeOf(t, &in, "laptop").Assigned = nil // it keeps its place and its address, and stops running the network
gens, err := Generators(in)
if err != nil {
t.Fatal(err)
}
g := gens[overlay.Name].(*overlay.Generator)
if len(g.Nodes()) != 1 || g.Nodes()[0].Name != "anchor" {
t.Fatalf("the network is over %+v, want anchor alone", g.Nodes())
}
if res, on, err := g.Resources("laptop"); on || err != nil || len(res) != 0 {
t.Errorf("a machine that left the network is given one: %v %v %v", res, on, err)
}
if _, in := g.Graph()["laptop"]; in {
t.Error("a machine that left the network has peers")
}
}
// 1360/AC3: a node whose resolution is refused is said with today's words: the network missing its hub names the
// machine that could not be resolved, which is almost always why. The hub here holds no complete place (no key): a refused
// machine with one keeps it on the network (issue 1833), so only one without is missing from it.
func TestARefusedNodeIsSaidWhenTheNetworkCannotBeComposed(t *testing.T) {
in := aMesh(t)
nodeOf(t, &in, "anchor").Place.Key = ""
one := catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
two := catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
in.Catalogue["rival-one"], in.Catalogue["rival-two"] = one, two
hub := nodeOf(t, &in, "anchor")
hub.Assigned = append(hub.Assigned, "rival-one", "rival-two") // its own set is incoherent: the hub does not resolve
_, err := Generators(in)
if err == nil {
t.Fatal("a network without its hub was composed")
}
for _, want := range []string{overlay.ErrNoHub.Error(), "and 1 node(s) could not be resolved at all, which is probably why:", " anchor: "} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not say %q:\n%v", want, err)
}
}
}
// 1360/AC4: Generators changes nothing in its Input.
func TestGeneratorsChangesNothingInItsInput(t *testing.T) {
unchanged(t, func() Input {
in := aMesh(t)
onTheTunnel(t, &in, true)
in.Joining = []string{"laptop"}
return in
}, func(in Input) {
if _, err := Generators(in); err != nil {
t.Fatal(err)
}
})
}
// Each field of Input that the network reads reaches the generator: change it and the generator changes.
func TestEachFieldOfTheInputReachesTheGenerator(t *testing.T) {
addressesOf := func(in Input) map[string]string {
t.Helper()
gens, err := Generators(in)
if err != nil {
t.Fatalf("%v", err)
}
out := map[string]string{}
for _, n := range gens[overlay.Name].(*overlay.Generator).Nodes() {
out[n.Name] = n.Address
}
return out
}
base := addressesOf(aMesh(t))
if len(base) != 2 {
t.Fatalf("the base mesh has %v", base)
}
t.Run("Catalogue: without the module offering it nobody is on the network", func(t *testing.T) {
in := aMesh(t)
// The module stays known, so every machine still resolves: a machine that does not resolve keeps its place (issue 1833).
in.Catalogue = map[string]catalogue.Manifest{overlay.Name: {Module: overlay.Name, Version: "1"}}
if got := addressesOf(in); len(got) != 0 {
t.Errorf("got %v", got)
}
})
t.Run("Nodes.Place: an address", func(t *testing.T) {
in := aMesh(t)
nodeOf(t, &in, "laptop").Place.Address = "10.77.0.9"
if got := addressesOf(in); got["laptop"] != "10.77.0.9" {
t.Errorf("got %v", got)
}
})
t.Run("Nodes.Assigned", func(t *testing.T) {
in := aMesh(t)
nodeOf(t, &in, "laptop").Assigned = nil
if got := addressesOf(in); len(got) != 1 {
t.Errorf("got %v", got)
}
})
t.Run("Nodes.Capabilities: a capability the module needs", func(t *testing.T) {
in := aMesh(t)
m := in.Catalogue[overlay.Name]
m.Capabilities = []string{"wireguard"}
in.Catalogue[overlay.Name] = m
nodeOf(t, &in, "laptop").Capabilities = map[string]bool{}
got, err := Generators(in)
if err != nil {
t.Fatal(err)
}
if n := got[overlay.Name].(*overlay.Generator).Nodes(); len(n) != 1 || n[0].Name != "anchor" {
t.Errorf("a machine without the capability is on the network: %+v", n)
}
})
t.Run("OverlayCIDR", func(t *testing.T) {
in := aMesh(t)
in.OverlayCIDR = "10.99.0.0/16"
_, graph, err := Graph(in)
if err != nil {
t.Fatal(err)
}
found := false
for _, p := range graph["laptop"] {
found = found || p.Allowed == "10.99.0.0/16"
}
if !found {
t.Errorf("the hub's route is not the range given: %+v", graph["laptop"])
}
})
t.Run("Tunnels and Adopted and CarriedPeers", func(t *testing.T) {
in := aMesh(t)
onTheTunnel(t, &in, true)
nodes, graph, err := Graph(in)
if err != nil || len(nodes) != 2 {
t.Fatalf("%v %v", nodes, err)
}
if len(graph["anchor"]) < 2 {
t.Errorf("the hub's peers do not include the carried one: %+v", graph["anchor"])
}
})
t.Run("Joining: a machine with a key and an address is on the network before it is anything else", func(t *testing.T) {
in := aMesh(t)
in.Nodes = append(in.Nodes, Node{Name: "newcomer", Place: Place{Key: keyOf(t, "newcomer"), Address: "10.77.0.3"}})
if got := addressesOf(in); got["newcomer"] != "" {
t.Fatalf("a machine that runs nothing is on the network: %v", got)
}
in.Joining = []string{"newcomer"}
if got := addressesOf(in); got["newcomer"] != "10.77.0.3" {
t.Errorf("a joining machine is not on the network: %v", got)
}
})
t.Run("Holdings: a holder on record settles a seat two assignments could hold", func(t *testing.T) {
withSeat(t)
in := aMesh(t)
for _, name := range []string{"holder-one", "holder-two"} {
in.Catalogue[name] = aBrokerClaiming(name)
}
laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = append(laptop.Assigned, "holder-one", "holder-two")
// Without a key: a machine that does not resolve and holds a complete place keeps it on the network (issue 1833).
laptop.Place.Key = ""
if got := addressesOf(in); got["laptop"] != "" {
t.Fatalf("a machine whose set claims one seat twice is on the network: %v", got)
}
in.Holdings = []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "laptop", Module: "holder-one"}}
if got := addressesOf(in); got["laptop"] != "10.77.0.2" {
t.Errorf("a holder on record did not settle the seat: %v", got)
}
})
}