gates / gates (push) Successful in 48s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
117 lines
5.0 KiB
Go
117 lines
5.0 KiB
Go
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
|
)
|
|
|
|
type initPIDRunner struct{ pid string }
|
|
|
|
func (r initPIDRunner) Run(_ context.Context, name string, _ ...string) ([]byte, []byte, error) {
|
|
if name == "lxc-info" {
|
|
return []byte(r.pid + "\n"), nil, nil
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (r initPIDRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
|
|
return r.Run(ctx, name, args...)
|
|
}
|
|
|
|
// R-1395: on 2026-11-14 the N100's time service came back and stepped the host's clock by +32.7 s. The kernel
|
|
// computes the boot TIME from the wall clock, so guest_boot_id moved and the controller recreated every drive-backed
|
|
// app. The consequence asserted: the boot KEY does not move when the boot time moves; it does move on a host reboot
|
|
// and on a guest reboot; and it is "" (so the controller falls back to the old token) when the boot_id is unreadable.
|
|
// COMPANION RED-PROOFS (observed, felhom.eu audits/night-build-2026-11-14/C/redproof-agent-boot-key.txt).
|
|
func TestGuestBootKey_AClockStepDoesNotMoveIt(t *testing.T) {
|
|
oldB, oldK, oldS := hostBtimeFn, hostBootIDFn, procStarttimeFn
|
|
t.Cleanup(func() { hostBtimeFn, hostBootIDFn, procStarttimeFn = oldB, oldK, oldS })
|
|
btime, bootID, start := "1794112518", "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11", "4242"
|
|
hostBtimeFn = func() string { return btime }
|
|
hostBootIDFn = func() string { return bootID }
|
|
procStarttimeFn = func(pid string) string {
|
|
if pid != "31337" {
|
|
t.Errorf("start time read for pid %q, want the guest's init", pid)
|
|
}
|
|
return start
|
|
}
|
|
b := NewGuestBinder(initPIDRunner{"31337"}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
ctx := context.Background()
|
|
|
|
id0, key0 := b.GuestBootID(ctx, 9201), b.GuestBootKey(ctx, 9201)
|
|
if id0 != "1794112518-4242" || key0 != "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11-4242" {
|
|
t.Fatalf("tokens = %q / %q", id0, key0)
|
|
}
|
|
btime = "1794112550" // THE STEP: the wall clock jumped +32 s, so the kernel's btime did too
|
|
id1, key1 := b.GuestBootID(ctx, 9201), b.GuestBootKey(ctx, 9201)
|
|
if id1 == id0 {
|
|
t.Fatalf("control: the old token did not move with the boot time (%q) — the test does not stage the defect", id1)
|
|
}
|
|
if key1 != key0 {
|
|
t.Fatalf("the boot key moved with the clock: %q → %q — a clock step reads as a new boot", key0, key1)
|
|
}
|
|
if strings.Contains(key1, "1794") {
|
|
t.Errorf("the boot key carries the boot time: %q", key1)
|
|
}
|
|
bootID = "c0ffee00-1111-2222-3333-444455556666" // a host reboot
|
|
if k := b.GuestBootKey(ctx, 9201); k == key0 {
|
|
t.Errorf("a host reboot (new boot_id) did not change the key: %q", k)
|
|
}
|
|
bootID, start = "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11", "991234" // a guest reboot
|
|
if k := b.GuestBootKey(ctx, 9201); k == key0 || !strings.HasSuffix(k, "-991234") {
|
|
t.Errorf("a guest reboot (new init start) did not change the key: %q", k)
|
|
}
|
|
for _, bad := range []string{"", "not-a-boot-id", "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11\nx"} {
|
|
bootID = bad
|
|
if k := b.GuestBootKey(ctx, 9201); k != "" {
|
|
t.Errorf("boot_id %q gave the key %q, want \"\" (the controller then reads the old token)", bad, k)
|
|
}
|
|
if id := b.GuestBootID(ctx, 9201); id == "" {
|
|
t.Errorf("the old token must still be sent when the boot_id is unreadable")
|
|
}
|
|
}
|
|
// what /disks sends: both tokens from ONE read of the guest's init — the init is asked for once, not twice
|
|
bootID, start = "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11", "4242"
|
|
reads := 0
|
|
procStarttimeFn = func(string) string { reads++; return start }
|
|
if id, key := b.GuestBootTokens(ctx, 9201); id != "1794112550-4242" || key != "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11-4242" || reads != 1 {
|
|
t.Errorf("GuestBootTokens = %q, %q after %d read(s) of the init — want both tokens from ONE read", id, key, reads)
|
|
}
|
|
bootID, start = "5b1f6c1e-8f0a-4c56-9d1c-0a8c3b7d2e11", "" // the guest does not run
|
|
if k := b.GuestBootKey(ctx, 9201); k != "" {
|
|
t.Errorf("a guest that does not run gave the key %q", k)
|
|
}
|
|
}
|
|
|
|
// The key is no use if /disks does not carry it: both tokens are on the wire, the old one unchanged for controllers
|
|
// that do not know the key.
|
|
func TestDisks_CarriesBothBootTokens(t *testing.T) {
|
|
srv := targetRowServer(t, "", []hub.StorageTarget{})
|
|
srv.guestAttach = &fakeGuestAttacher{}
|
|
body := do(t, srv.Handler(), "GET", "/disks", "A", "").Body.Bytes()
|
|
var w struct {
|
|
Data struct {
|
|
ID string `json:"guest_boot_id"`
|
|
Key string `json:"guest_boot_key"`
|
|
} `json:"data"`
|
|
ID string `json:"guest_boot_id"`
|
|
Key string `json:"guest_boot_key"`
|
|
}
|
|
if err := json.Unmarshal(body, &w); err != nil {
|
|
t.Fatalf("decode: %v (%s)", err, body)
|
|
}
|
|
if w.Data.ID+w.ID != "boot-1" || w.Data.Key+w.Key != "key-1" {
|
|
t.Fatalf("/disks boot tokens: id=%q key=%q, want boot-1 and key-1 (%s)", w.Data.ID+w.ID, w.Data.Key+w.Key, body)
|
|
}
|
|
}
|
|
|
|
// The real binder is what main.go hands the server: it must have the method (the interface makes this a build
|
|
// error if not — stated here so the reason is on record).
|
|
var _ GuestAttacher = (*GuestBinder)(nil)
|