The pipeline was observable from a merge to an artifact and went dark where it touched a machine: a node's report is control traffic only the control plane reads, so nothing said which version a machine runs, or that it refused to (novox/hq ADR 0134). The control plane now states both under the seat it holds — a role's events belong to the role and keep their address when the holder is replaced — and only when the report is news, because a machine reconciles every minute and a fact per report would be a fact per minute per machine. Whether a report is news is the store's answer: it holds the previous one, so the listener returns it and the server states the fact. That also gives the catch-up replay a subject the controller may publish: it was published as a module's event from a module called "control-plane", which does not exist, so the controller's own account refused it and every catalogue that asked what it missed was answered with nothing.
136 lines
5.0 KiB
Go
136 lines
5.0 KiB
Go
package link_test
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/identity"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// novox/hq ADR 0105: a hub that enrolled before the mesh knew to take a tunnel over rekeys onto the
|
|
// found tunnel's key without re-enrolling — which would rotate every key it holds and remake every
|
|
// credential the mesh sealed to it. The rekey rides in a report and is signed with the node's
|
|
// identity key; the mesh verifies it against the key it recorded, and refuses one signed by
|
|
// another key or one already applied.
|
|
|
|
const (
|
|
ownKey = "THE-MESHS-OWN-KEY======================="
|
|
tunnelKey = "TUNNEL-KEY-the-found-interfaces-public-key="
|
|
)
|
|
|
|
func theTunnel() *link.Tunnel {
|
|
return &link.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: []link.TunnelPeer{{PublicKey: "PEER-A=", Address: "192.0.2.2/32"}}}
|
|
}
|
|
|
|
// anEnrolledHub is a hub the way it stands before the feature: adopted, placed, its overlay key its
|
|
// own, its identity key recorded — and a mesh holding both stores.
|
|
func anEnrolledHub(t *testing.T) (link.Enrolment, inventory.Node, ed25519.PrivateKey) {
|
|
t.Helper()
|
|
inv := inventory.ForTest(t)
|
|
ident := identity.ForTest(t)
|
|
ctx := t.Context()
|
|
hub, err := inv.AddNodeAs(ctx, "anchor", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ident.RecordNodeKey(ctx, hub.ID, public); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordOverlayKey(ctx, hub.ID, ownKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetPlace(ctx, "anchor", "anchor.example:51900", "hosting", true, "10.42.0.1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return link.Enrolment{Inventory: inv, Identity: ident}, hub, private
|
|
}
|
|
|
|
func TestASignedRekeyMovesTheHubOntoItsTunnel(t *testing.T) {
|
|
e, hub, private := anEnrolledHub(t)
|
|
ctx := t.Context()
|
|
rekey := &link.Rekey{Previous: ownKey, OverlayKey: tunnelKey, Tunnel: theTunnel()}
|
|
rekey.Proof = ed25519.Sign(private, link.RekeyProof("anchor", ownKey, tunnelKey, theTunnel()))
|
|
|
|
if _, err := e.Heard(ctx, link.Report{Node: "anchor", Rekey: rekey}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
placed, err := e.Inventory.Overlays(ctx)
|
|
if err != nil || len(placed) != 1 {
|
|
t.Fatal(placed, err)
|
|
}
|
|
if placed[0].Key != tunnelKey || placed[0].Address != "192.0.2.1" {
|
|
t.Fatalf("the hub is not on the tunnel's key and address: %+v", placed[0])
|
|
}
|
|
tunnel, _, adopted, err := e.Inventory.AdoptedTunnel(ctx)
|
|
if err != nil || !adopted || tunnel.Range != "192.0.2.0/24" || len(tunnel.Peers) != 1 {
|
|
t.Fatalf("the tunnel is not adopted after the rekey: %+v %t %v", tunnel, adopted, err)
|
|
}
|
|
_ = hub
|
|
|
|
// Replayed, it is stale: the previous key it names is no longer the node's.
|
|
_, err = e.Heard(ctx, link.Report{Node: "anchor", Rekey: rekey})
|
|
if err == nil || !strings.Contains(err.Error(), "previous overlay key") {
|
|
t.Fatalf("a replayed rekey was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestARekeySignedByAnotherKeyIsRefusedAndChangesNothing(t *testing.T) {
|
|
e, _, _ := anEnrolledHub(t)
|
|
ctx := t.Context()
|
|
_, stranger, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rekey := &link.Rekey{Previous: ownKey, OverlayKey: tunnelKey, Tunnel: theTunnel()}
|
|
rekey.Proof = ed25519.Sign(stranger, link.RekeyProof("anchor", ownKey, tunnelKey, theTunnel()))
|
|
|
|
_, err = e.Heard(ctx, link.Report{Node: "anchor", Rekey: rekey})
|
|
if err == nil || !strings.Contains(err.Error(), "not signed by anchor's identity key") {
|
|
t.Fatalf("a rekey signed by a stranger was accepted: %v", err)
|
|
}
|
|
placed, _ := e.Inventory.Overlays(ctx)
|
|
if placed[0].Key != ownKey || placed[0].Address != "10.42.0.1" {
|
|
t.Fatalf("a refused rekey changed the record: %+v", placed[0])
|
|
}
|
|
if _, _, adopted, _ := e.Inventory.AdoptedTunnel(ctx); adopted {
|
|
t.Fatal("a refused rekey recorded a tunnel")
|
|
}
|
|
|
|
// And a proof moved to another tunnel — the signature was over one tunnel, the message names
|
|
// another — does not verify either.
|
|
moved := &link.Rekey{Previous: ownKey, OverlayKey: tunnelKey, Tunnel: theTunnel()}
|
|
other := theTunnel()
|
|
other.Port = 51820
|
|
moved.Proof = ed25519.Sign(mustPrivate(t, e, "anchor"), link.RekeyProof("anchor", ownKey, tunnelKey, other))
|
|
if _, err := e.Heard(ctx, link.Report{Node: "anchor", Rekey: moved}); err == nil {
|
|
t.Fatal("a proof over another tunnel was accepted")
|
|
}
|
|
}
|
|
|
|
// mustPrivate is a fresh key recorded as the node's live one, for signing in a test that needs
|
|
// the node's own signature after the fixture's key is out of scope.
|
|
func mustPrivate(t *testing.T, e link.Enrolment, node string) ed25519.PrivateKey {
|
|
t.Helper()
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n, err := e.Inventory.NodeByName(t.Context(), node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := e.Identity.RecordNodeKey(t.Context(), n.ID, public); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return private
|
|
}
|