282 lines
11 KiB
Go
282 lines
11 KiB
Go
//go:build integration
|
|
|
|
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 TestAPeerRoutedARangeIsNotCarried(t *testing.T) {
|
|
// A peer routed a whole range is a spoke's view of its hub, never a machine with an address
|
|
// the mesh could carry: skipped, and the single-address peers beside it kept.
|
|
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: "192.0.2.0/24"})
|
|
if err := inv.RecordTunnel(t.Context(), hub.ID, found); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := inv.TunnelOf(t.Context(), "anchor")
|
|
if err != nil || len(got.Peers) != 2 {
|
|
t.Fatalf("the range-routed peer was carried, or the others dropped: %+v %v", got.Peers, err)
|
|
}
|
|
// A single address outside the tunnel's range is still refused: it is not a peer this tunnel
|
|
// routes to.
|
|
found.Peers = []TunnelPeer{{PublicKey: "ELSEWHERE", Address: "198.51.100.7/32"}}
|
|
if err := inv.RecordTunnel(t.Context(), hub.ID, found); err == nil || !strings.Contains(err.Error(), "outside") {
|
|
t.Fatalf("a peer outside the range was recorded: %v", err)
|
|
}
|
|
}
|
|
|
|
// A predecessor spoke's tunnel has one peer — the hub — routed the whole range. Its enrolment must
|
|
// not fail on it: only the hub's peers are ever carried, so a range-routed peer is skipped.
|
|
func TestASpokesTunnelEnrolsWithItsHubPeerSkipped(t *testing.T) {
|
|
inv := fresh(t)
|
|
anAdoptedHub(t, inv)
|
|
spoke, err := inv.AddNodeAs(t.Context(), "home-server", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordOverlayKey(t.Context(), spoke.ID, peerThree); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordTunnel(t.Context(), spoke.ID, Tunnel{
|
|
Interface: "wg0", Unit: "wg-quick@wg0", Config: "/etc/wireguard/wg0.conf", Port: 51900,
|
|
Address: "192.0.2.3/24", Range: "192.0.2.0/24", PublicKey: peerThree,
|
|
Peers: []TunnelPeer{{PublicKey: tunnelKey, Address: "192.0.2.0/24"}},
|
|
}); err != nil {
|
|
t.Fatalf("a spoke-shaped tunnel was refused: %v", err)
|
|
}
|
|
got, err := inv.TunnelOf(t.Context(), "home-server")
|
|
if err != nil || len(got.Peers) != 0 {
|
|
t.Fatalf("the spoke's hub was recorded as a peer to carry: %+v %v", got.Peers, err)
|
|
}
|
|
// Nothing about the hub's carried peers changed: still two, one now enrolled.
|
|
carried, err := inv.CarriedPeers(t.Context())
|
|
if err != nil || len(carried) != 2 {
|
|
t.Fatalf("carried peers: %+v %v", carried, err)
|
|
}
|
|
if address, err := inv.AssignAddress(t.Context(), spoke.ID, "192.0.2.0/24"); err != nil || address != "192.0.2.3" {
|
|
t.Fatalf("the spoke did not keep its address: %s %v", address, err)
|
|
}
|
|
}
|
|
|
|
// Converging the hub flips its mode and nothing else: the range stays the tunnel's and the peers
|
|
// stay carried, or the mesh would renumber itself and drop the peers still reaching it.
|
|
func TestConvergingTheHubKeepsTheRangeAndTheCarriedPeers(t *testing.T) {
|
|
inv := fresh(t)
|
|
hub := anAdoptedHub(t, inv)
|
|
if _, err := inv.AssignAddress(t.Context(), hub.ID, "192.0.2.0/24"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.Converge(t.Context(), "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tunnel, _, adopted, err := inv.AdoptedTunnel(t.Context())
|
|
if err != nil || !adopted || tunnel.Range != "192.0.2.0/24" {
|
|
t.Fatalf("converging renumbered the mesh: adopted=%t %+v %v", adopted, tunnel, err)
|
|
}
|
|
if carried, err := inv.CarriedPeers(t.Context()); err != nil || len(carried) != 2 {
|
|
t.Fatalf("converging dropped the carried peers: %+v %v", carried, err)
|
|
}
|
|
found, err := inv.Tunnels(t.Context())
|
|
if err != nil || found["anchor"].NodeAdopted {
|
|
t.Fatalf("a converged hub still reads as adopted for the takeover: %+v %v", found, err)
|
|
}
|
|
}
|
|
|
|
// A hub that enrolled with a key of its own takes the tunnel over afterwards by rekeying: the key
|
|
// and the tunnel are recorded, the hub moves to the tunnel's address, and the same rekey applied
|
|
// again is stale.
|
|
func TestARekeyTakesTheTunnelOverAfterEnrolment(t *testing.T) {
|
|
inv := fresh(t)
|
|
hub, err := inv.AddNodeAs(t.Context(), "anchor", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const own = "THE-MESHS-OWN-KEY======================="
|
|
if err := inv.RecordOverlayKey(t.Context(), hub.ID, own); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetPlace(t.Context(), "anchor", "anchor.example:51900", "hosting", true, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if address, err := inv.AssignAddress(t.Context(), hub.ID, "10.42.0.0/16"); err != nil || address != "10.42.0.1" {
|
|
t.Fatalf("before the rekey the hub is on the mesh's own range: %s %v", address, err)
|
|
}
|
|
|
|
if err := inv.Rekey(t.Context(), hub.ID, own, tunnelKey, theFoundTunnel()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, adopted, err := inv.AdoptedTunnel(t.Context())
|
|
if err != nil || !adopted {
|
|
t.Fatalf("the tunnel is not adopted after the rekey (%v)", err)
|
|
}
|
|
placed, err := inv.Overlays(t.Context())
|
|
if err != nil || len(placed) != 1 || placed[0].Address != "192.0.2.1" || placed[0].Key != tunnelKey {
|
|
t.Fatalf("the hub did not move to the tunnel's address under the tunnel's key: %+v %v", placed, err)
|
|
}
|
|
if err := inv.Rekey(t.Context(), hub.ID, own, tunnelKey, theFoundTunnel()); !errors.Is(err, ErrStaleRekey) {
|
|
t.Fatalf("the same rekey applied again was not refused as stale: %v", err)
|
|
}
|
|
if err := inv.Rekey(t.Context(), hub.ID, tunnelKey, "ANOTHER-KEY=============================", theFoundTunnel()); err == nil {
|
|
t.Fatal("a rekey to a key that is not the tunnel's was accepted")
|
|
}
|
|
}
|