Ask a node's engine for a fresh setuid search at the terminal (hq issue 361)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

After the operator removes by hand what the last search found, no apply says so and nothing searched again
until the next day. node setuid-search <node> signs the ask as a hand-over is, under its own context, on a
subject only the node's engine hears and only the controller may publish.
This commit is contained in:
jochen
2026-10-10 01:56:56 +02:00
parent 272ca2a578
commit 9cef820117
9 changed files with 237 additions and 15 deletions
+59
View File
@@ -0,0 +1,59 @@
package link
import (
"context"
"crypto/ed25519"
"encoding/json"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/testbus"
)
// A fresh setuid search is asked of the machine on its own subject, signed under its own context — never a
// hand-over's — naming no path, and the engine's answer comes back (novox/hq issue 361). The engine holds the
// same subject and context (mesh-host internal/link, TestTheSetuidSearchAskKeepsItsSubjectAndContext).
func TestASetuidSearchIsAskedOfTheMachineSignedUnderItsOwnContext(t *testing.T) {
if broker.AskSetuidSearchSubject("laptop") != "mesh.node.laptop.ask.setuid-search" ||
SetuidSearchContext != "novox-mesh setuid-search v1\n" {
t.Fatalf("the subject %q, the context %q", broker.AskSetuidSearchSubject("laptop"), SetuidSearchContext)
}
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(conn.Close)
signer, public := testSigner(t)
var heard HandOverAsk
engine, err := conn.Subscribe(broker.AskSetuidSearchSubject("laptop"), func(msg *nats.Msg) {
var signed SignedHandOver
_ = json.Unmarshal(msg.Data, &signed)
if ed25519.Verify(public, append([]byte(HandOverContext), signed.Ask...), signed.Signature) ||
!ed25519.Verify(public, append([]byte(SetuidSearchContext), signed.Ask...), signed.Signature) {
_ = msg.Respond([]byte(`{"refused":"not signed as a setuid search"}`))
return
}
_ = json.Unmarshal(signed.Ask, &heard)
body, _ := json.Marshal(HandOverAnswer{Said: "a new search starts, asked by " + heard.By})
_ = msg.Respond(body)
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = engine.Unsubscribe() })
a, err := AskSetuidSearch(context.Background(), conn, signer, "laptop", "jo", 5*time.Second)
if err != nil || a.Said != "a new search starts, asked by jo" {
t.Fatalf("answered %+v, %v", a, err)
}
if heard.Node != "laptop" || heard.Path != "" || heard.Nonce == "" || heard.Expires.IsZero() {
t.Fatalf("the engine heard %+v", heard)
}
if _, err := AskSetuidSearch(context.Background(), conn, signer, "anchor", "jo", 5*time.Second); err == nil ||
!strings.Contains(err.Error(), "no search was started") {
t.Fatalf("a machine with no engine listening: %v", err)
}
}