hub: S1 store — wg_endpoints/wg_peers tables + /32 allocator (doc 06 §3.2)

Additive migration; AddWGPeer = one tx, idempotent on pubkey, lowest-free-host
allocation skipping network/pbs_tunnel_ip/broadcast, UNIQUE(assigned_ip) race
backstop with one internal retry; typed ErrWGEndpointUnset/ErrWGSubnetExhausted.
Group-A tests + red-proof (allocator-ignores-rows mutation fails 3 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-03 23:32:39 +02:00
parent 7fb20d5fb0
commit b18f6aee1b
3 changed files with 402 additions and 0 deletions
+29
View File
@@ -367,6 +367,35 @@ func (s *Store) migrate() error {
return err
}
// S1 offsite connectivity (doc 06 §3.2): the WG endpoint record + peer registry. The hub is the
// source of truth; the endpoint converges on it (SSH push, internal/wgsync). No `status` column
// on wg_peers — presence in the table IS the desired state; the doc-06 `status` field belongs to
// the S2 host-join. assigned_ip stores the BARE IP (no /32 — that's presentation, appended by
// the API layer and the sync payload). UNIQUE(assigned_ip) is the allocator's race backstop.
_, err = s.db.Exec(`
CREATE TABLE IF NOT EXISTS wg_endpoints (
endpoint_id TEXT PRIMARY KEY,
dns_name TEXT NOT NULL,
wg_port INTEGER NOT NULL,
server_pubkey TEXT NOT NULL,
tunnel_subnet TEXT NOT NULL,
pbs_tunnel_ip TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS wg_peers (
pubkey TEXT PRIMARY KEY,
assigned_ip TEXT NOT NULL UNIQUE,
host_id TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
);
`)
if err != nil {
return err
}
return nil
}