feat(hub): operator OOB peer + oob_peer_ip desired-state merge (H1 Part 1)

store.SetOperatorOOBPeer/GetOperatorOOBPeer (empty-host_id wg_peers row, explicit
/32, validated in-subnet/not-reserved/not-taken, last-write-wins rotation).
PUT/GET /admin/wg/operator-peer (global key). mergeWireguard adds oob_peer_ip when
an operator peer exists (absent = byte-identical). BumpAllHostGenerations forces
fleet re-fetch. Non-hollow tests both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-05 22:08:06 +02:00
parent a3ee93e97e
commit 0ec7555126
6 changed files with 342 additions and 1 deletions
+64
View File
@@ -385,6 +385,70 @@ func TestDesiredState_WireguardMergeMatchesGolden(t *testing.T) {
}
}
// H1: with a fleet operator OOB peer registered, a host's served wireguard block carries oob_peer_ip
// (the operator's /32) so the agent renders it into AllowedIPs. Without it, no key (byte-identical —
// covered by TestDesiredState_WireguardMergeMatchesGolden).
func TestDesiredState_IncludesOOBPeerIPWhenOperatorRegistered(t *testing.T) {
h, st, _ := newTestHandler(t)
seedHost(t, st, "h1", "c1", "HKEY1")
putGoldenEndpoint(t, h)
h.SetWGSyncer(&fakeWGSyncer{})
if rr := do(h, http.MethodPost, "/hosts/h1/wg", "HKEY1", `{"pubkey":"`+testPK(1)+`"}`); rr.Code != 200 {
t.Fatalf("register: %d", rr.Code)
}
// no operator peer yet → served block has NO oob_peer_ip
got := servedWG(t, h)
if _, has := got["oob_peer_ip"]; has {
t.Fatalf("oob_peer_ip present with no operator peer: %v", got)
}
// register the operator peer (global key)
if rr := do(h, http.MethodPut, "/admin/wg/operator-peer", globalKey,
`{"pubkey":"`+opTestPubkey+`","assigned_ip":"10.77.0.250"}`); rr.Code != 200 {
t.Fatalf("set operator peer: %d %s", rr.Code, servedBody(t, h))
}
got = servedWG(t, h)
if got["oob_peer_ip"] != "10.77.0.250" {
t.Fatalf("served oob_peer_ip = %v, want 10.77.0.250", got["oob_peer_ip"])
}
// read-back route
if rr := do(h, http.MethodGet, "/admin/wg/operator-peer", globalKey, ""); rr.Code != 200 {
t.Fatalf("operator-peer read-back: %d", rr.Code)
}
// a per-host key cannot set the operator peer
if rr := do(h, http.MethodPut, "/admin/wg/operator-peer", "HKEY1",
`{"pubkey":"`+opTestPubkey+`","assigned_ip":"10.77.0.248"}`); rr.Code != 403 {
t.Fatalf("host key setting operator peer must be 403, got %d", rr.Code)
}
}
const opTestPubkey = "cdmN4U+fjR18zBk+SKoceJQyz9HgA9+hN8/FiKF1u0o="
func servedWG(t *testing.T, h *Handler) map[string]any {
t.Helper()
rr := do(h, http.MethodGet, "/hosts/h1/desired-state", "HKEY1", "")
if rr.Code != 200 {
t.Fatalf("GET desired-state: %d", rr.Code)
}
var got struct {
DesiredState json.RawMessage `json:"desired_state"`
}
json.Unmarshal(rr.Body.Bytes(), &got)
var doc map[string]any
json.Unmarshal(got.DesiredState, &doc)
wg, _ := doc["wireguard"].(map[string]any)
if wg == nil {
t.Fatalf("no wireguard block in served state: %s", got.DesiredState)
}
return wg
}
func servedBody(t *testing.T, h *Handler) string {
return do(h, http.MethodGet, "/hosts/h1/desired-state", "HKEY1", "").Body.String()
}
func TestAdminSetDesiredState_RejectsWireguardKey(t *testing.T) {
h, st, _ := newTestHandler(t)
seedHost(t, st, "h1", "c1", "HKEY1")