wgtunnel: S3 Part 1 — pure-Go keygen + hub wire (WireWireguard, report stanza, RegisterWG)
key.go: create-once 0600/0700, corrupt-refusal (never overwrite — may be escrowed identity), clamp for CANONICAL STORED form (x/crypto X25519 clamps derivation internally — discovered during red-proof (c); the stored-clamped test is the real anchor). Fixed vectors generated with real wg pubkey (provenance in test). hub: WireDesiredState.Wireguard + WireguardStatus report stanza + RegisterWG client (typed errors, token-free). S2 golden copied BYTE-IDENTICAL + field-exact decode test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -144,6 +144,55 @@ func (c *Client) FetchDesiredState(ctx context.Context) (*DesiredStateResponse,
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// WGRegisterResponse is the hub's answer to a WG pubkey registration (S3; hub S2
|
||||
// handleRegisterHostWG). Existed=true = idempotent re-register (nothing moved hub-side).
|
||||
type WGRegisterResponse struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
AssignedIP string `json:"assigned_ip"` // "10.77.0.2/32"
|
||||
Existed bool `json:"existed"`
|
||||
Generation int64 `json:"generation"`
|
||||
Sync string `json:"sync"` // hub→endpoint push status: ok | deferred:… | disabled | unchanged
|
||||
}
|
||||
|
||||
// RegisterWG registers this host's WG public key with the hub (S3 — doc 06 §3.3 step 2; POST
|
||||
// /hosts/{host_id}/wg, per-host key, self-scoped server-side). The hub allocates/keeps the /32,
|
||||
// bumps the desired generation on real change, and pushes the peer to the endpoint. Errors are
|
||||
// typed (transport vs HTTP: 403 auth, 404 unknown host, 409 conflict/endpoint-unset) and never
|
||||
// include the bearer token. Only the PUBLIC key ever travels.
|
||||
func (c *Client) RegisterWG(ctx context.Context, pubkey string) (*WGRegisterResponse, error) {
|
||||
if c.hostID == "" {
|
||||
return nil, fmt.Errorf("hub: RegisterWG requires a configured host_id")
|
||||
}
|
||||
body, err := json.Marshal(map[string]string{"pubkey": pubkey})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hub: marshaling wg registration: %w", err)
|
||||
}
|
||||
url := c.baseURL + "/api/v1/hosts/" + c.hostID + "/wg"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hub: building wg-register request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, &TransportError{Err: err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, &HTTPError{StatusCode: resp.StatusCode, BodyTail: tail(raw, 256)}
|
||||
}
|
||||
var out WGRegisterResponse
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, fmt.Errorf("hub: decoding wg-register response: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// JobWire is one queued signed-op job as served by GET /hosts/{id}/jobs (slice 10A). The blob is
|
||||
// OPAQUE to the hub — for slice 10B it is a base64 `SignedJobEnvelope` (op-blob + armored SSHSIG)
|
||||
// the agent verifies before executing.
|
||||
|
||||
Reference in New Issue
Block a user