package inventory import ( "errors" "strings" "testing" ) // novox/hq ADR 0105: the mesh adopts the predecessor's tunnel in place. The controller reads the // hub's address and range from the adopted tunnel, assigns an enrolling node the address its key // already had, and refuses to hand out an address the tunnel already holds. const ( tunnelKey = "TUNNEL-KEY-the-found-interfaces-public-key=" peerTwo = "PEER-KEY-two=============================" peerThree = "PEER-KEY-three===========================" ) // theFoundTunnel is what a hub presents at enrolment: the predecessor's interface on a // documentation range, with two peers each routed one address. func theFoundTunnel() Tunnel { return 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: tunnelKey, Peers: []TunnelPeer{{PublicKey: peerTwo, Address: "192.0.2.2/32"}, {PublicKey: peerThree, Address: "192.0.2.3"}}, } } // anAdoptedHub is an adopted node that enrolled with the found tunnel's key and presented the // tunnel, then was placed as the hub — the order genesis does it in. func anAdoptedHub(t *testing.T, inv *Inventory) Node { t.Helper() hub, err := inv.AddNodeAs(t.Context(), "anchor", true) if err != nil { t.Fatal(err) } if err := inv.RecordOverlayKey(t.Context(), hub.ID, tunnelKey); err != nil { t.Fatal(err) } if err := inv.RecordTunnel(t.Context(), hub.ID, theFoundTunnel()); err != nil { t.Fatal(err) } if err := inv.SetPlace(t.Context(), "anchor", "anchor.example:51900", "hosting", true, ""); err != nil { t.Fatal(err) } return hub } func TestTheHubsAddressAndRangeComeFromTheAdoptedTunnel(t *testing.T) { inv := fresh(t) hub := anAdoptedHub(t, inv) tunnel, name, adopted, err := inv.AdoptedTunnel(t.Context()) if err != nil { t.Fatal(err) } if !adopted || name != "anchor" || tunnel.Range != "192.0.2.0/24" || tunnel.Port != 51900 { t.Fatalf("the adopted tunnel did not read back: adopted=%t on %s, %+v", adopted, name, tunnel) } if len(tunnel.Peers) != 2 || tunnel.Peers[0].Address != "192.0.2.2" || tunnel.Peers[1].Address != "192.0.2.3" { t.Fatalf("the peers did not read back as one host address each: %+v", tunnel.Peers) } // Whatever range the caller would allocate from, the hub is at the tunnel's own address. address, err := inv.AssignAddress(t.Context(), hub.ID, "10.42.0.0/16") if err != nil { t.Fatal(err) } if address != "192.0.2.1" { t.Fatalf("the hub was given %s, not the address the tunnel it took over had", address) } } func TestAnEnrollingNodeKeepsTheAddressTheTunnelHadForItsKey(t *testing.T) { inv := fresh(t) anAdoptedHub(t, inv) // A predecessor machine enrols: its host took its found interface's key as its overlay key, // which is the key the hub's tunnel already routes to. peer, err := inv.AddNodeAs(t.Context(), "home-server", true) if err != nil { t.Fatal(err) } if err := inv.RecordOverlayKey(t.Context(), peer.ID, peerThree); err != nil { t.Fatal(err) } address, err := inv.AssignAddress(t.Context(), peer.ID, "192.0.2.0/24") if err != nil { t.Fatal(err) } if address != "192.0.2.3" { t.Fatalf("the enrolling peer was given %s, not the 192.0.2.3 the tunnel had for its key", address) } carried, err := inv.CarriedPeers(t.Context()) if err != nil { t.Fatal(err) } byKey := map[string]CarriedPeer{} for _, c := range carried { byKey[c.PublicKey] = c } if byKey[peerThree].EnrolledAs != "home-server" || byKey[peerTwo].EnrolledAs != "" { t.Fatalf("the registry cannot say which peer is a node now: %+v", carried) } } func TestAFreshNodeIsNeverGivenAnAddressTheTunnelHolds(t *testing.T) { inv := fresh(t) anAdoptedHub(t, inv) // .1 is the hub, .2 and .3 are peers of the tunnel that have not enrolled: a new machine with // a key of its own gets the next one, from the same range. fresh, err := inv.AddNode(t.Context(), "laptop") if err != nil { t.Fatal(err) } if err := inv.RecordOverlayKey(t.Context(), fresh.ID, "A-KEY-OF-ITS-OWN========================"); err != nil { t.Fatal(err) } address, err := inv.AssignAddress(t.Context(), fresh.ID, "192.0.2.0/24") if err != nil { t.Fatal(err) } if address != "192.0.2.4" { t.Fatalf("a fresh node was given %s; 192.0.2.2 and .3 are the tunnel's peers and .1 its hub", address) } } func TestATunnelUnderAnotherKeyIsNotAdopted(t *testing.T) { // A hub whose overlay key is not the found tunnel's would drop every peer's packets on the // found port (ADR 0105, option 2). Such a tunnel is recorded and not adopted: the mesh keeps // its own range, and ADR 0100's non-overlap rule stands for it. inv := fresh(t) hub, err := inv.AddNodeAs(t.Context(), "anchor", true) if err != nil { t.Fatal(err) } if err := inv.RecordOverlayKey(t.Context(), hub.ID, "THE-MESHS-OWN-KEY======================="); err != nil { t.Fatal(err) } if err := inv.RecordTunnel(t.Context(), hub.ID, theFoundTunnel()); err != nil { t.Fatal(err) } if err := inv.SetPlace(t.Context(), "anchor", "anchor.example:51820", "hosting", true, ""); err != nil { t.Fatal(err) } if _, _, adopted, err := inv.AdoptedTunnel(t.Context()); err != nil || adopted { t.Fatalf("a tunnel under another key was adopted (err %v)", err) } if carried, err := inv.CarriedPeers(t.Context()); err != nil || len(carried) != 0 { t.Fatalf("peers of a tunnel that was not adopted are carried: %+v (err %v)", carried, err) } if address, err := inv.AssignAddress(t.Context(), hub.ID, "10.42.0.0/16"); err != nil || address != "10.42.0.1" { t.Fatalf("the hub was given %s (err %v); it should allocate from the mesh's own range", address, err) } } func TestAPeerRoutedARangeIsRefused(t *testing.T) { inv := fresh(t) hub, err := inv.AddNodeAs(t.Context(), "anchor", true) if err != nil { t.Fatal(err) } found := theFoundTunnel() found.Peers = append(found.Peers, TunnelPeer{PublicKey: "WIDE", Address: "198.51.100.0/24"}) err = inv.RecordTunnel(t.Context(), hub.ID, found) if err == nil || !strings.Contains(err.Error(), "names a range") { t.Fatalf("a peer routed a whole range was recorded as a machine with an address: %v", err) } if _, err := inv.TunnelOf(t.Context(), "anchor"); !errors.Is(err, ErrNoTunnel) { t.Fatalf("a refused tunnel was recorded anyway: %v", err) } }