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. func TestARefusedNodeIsSaidWhenTheNetworkCannotBeComposed(t *testing.T) { in := aMesh(t) 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 nobody is on the network", func(t *testing.T) { in := aMesh(t) in.Catalogue = map[string]catalogue.Manifest{} 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") 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) } }) }