6.2 KiB
Daemon 与 Cloud Relay 稳定端点契约
状态:控制面实现完成;真实 WSS Relay 部署仍是独立门禁。
不变服务地址
Daemon 只保存用户注册时填写的 server_url。自部署时它指向 Standalone Server;官方服务时它指向 Cloud Connect 稳定 origin。Cloud 不返回租户后端 URL,不要求 Daemon 感知区域、节点或控制面轮询。
注册成功响应:
{
"device_id": "host_...",
"token": "...",
"name": "Home PC",
"transport_mode": "sealed",
"connection_state": "provisioning",
"retry_after_seconds": 5
}
connection_state 只有 ready 与 provisioning。provisioning 表示凭证已经安全落盘,Daemon 应在同一服务地址的 /ws/daemon 退避重试;不得轮询另一个控制面 API,也不得接受 HTTP redirect 携带 bearer。
旧的 activation_poll_path、relay_url、relay_ready 和每租户 Server handoff 已删除。旧未发布配置必须由 Daemon 明确要求重新注册,不能静默解释为新契约。
错误格式
所有新 API 错误返回:
{
"error_code": "service_provisioning",
"error": "service_provisioning",
"message": "租户 Relay 正在准备",
"retryable": true,
"retry_after_seconds": 5
}
error 暂时作为兼容别名。Daemon 只按 error_code、retryable 和重试提示工作,不按 Cloud 品牌分支。
Relay 内部授权
Cloud Relay 节点通过独立 mTLS 证书身份访问:
POST /api/internal/relay/authorize-devicePOST /api/internal/relay/authorization-deltaPOST /api/internal/relay/authorization-snapshotPOST /api/internal/relay/register-devicePOST /api/internal/relay/heartbeatPOST /api/internal/relay/authorize-phonePOST /api/internal/relay/revoke-phonePOST /api/internal/relay/resolve-device-routePOST /api/internal/relay/resolve-phone-routePOST /api/internal/relay/resolve-tenant-routePOST /api/internal/relay/resolve-handoff-routePOST /api/internal/relay/migrations/advancePOST /api/internal/relay/purges/advance
Relay 节点只使用独立 mTLS 证书身份,不共享全局 bearer。Worker 本身不能读取客户端证书,因此受信终止层必须剥离外部 x-neko-mtls-* 头、验证证书,再用 ingress secret 注入绑定 method/path 的 30 秒 HMAC assertion。缺少该配置或 assertion 时 fail closed。D1 身份行绑定 node ID、SPIFFE ID、证书 SHA-256 指纹、签发/到期和撤销状态;控制面不读取 Authorization: Bearer。
设备授权请求发送 device_id 与设备 token 的 SHA-256 摘要。客户端不能提交 tenant_id。控制面从凭证反查租户与 placement,只允许当前写入节点取得签名快照。
快照使用域分离的规范 JSON 与 Ed25519,包含 tenant 状态、home region、节点、placement generation、authorization revision、有效设备身份、设备显示名、OS、Ed25519/X25519 公钥及凭证摘要。这些公开身份材料让重启后的空 Engine 重建真实 DeviceStore 和 E2E 配对目录,不得合成设备名或丢失公钥。TTL 最大 5 分钟;完整快照每 60 秒刷新,revision delta 默认每 5 秒检查,请求超时为 3 秒,以覆盖 15 秒撤销目标。新连接必须实时联系控制面;旧连接只能持续到现有快照过期。
60 秒完整刷新必须用内部 tenant ID 与当前 placement generation 调用 authorization-snapshot,不得复用此前客户端设备 token 摘要冒充刷新。错误节点、旧 generation、暂停租户或非 active placement 均 fail closed。节点签名 key 必须覆盖整份新快照有效期,旧 public key 的验证保留窗口必须超过最大 TTL。
稳定 Connect Relay 通过 node-mTLS 的 register-device 代理公开 /api/devices/register。Relay 只传自身生成的 opaque source_hash,不得转交或信任外部 forwarding IP 头;daemon 提供单次随机 registration_retry_key。控制面只保存用该 key 加密的同一 credential 响应,最多十分钟,因此响应丢失重试返回同一 token、同一 host 且不再占席位,D1 不保存明文 token。
签名公钥与私钥引用可进入 D1,私钥只能来自 Secret/密钥管理系统,不能写入 D1。
稳定入口不在 home 节点时,控制面只返回 opaque internal endpoint ref。入口从部署 allowlist 选出目标 HTTPS/WSS origin,经 TLS 1.3 mTLS 和绑定 method/path/源节点/目标节点/30 秒时间窗的附加断言转发;客户端看不到节点 URL,也不会携带 credential 跟随 redirect。部署和迁移细节见 docs/relay-operations.md。
手机 PWA handoff
Dashboard 调用 POST /api/pwa/handoff,取得 60 秒单次 ticket 与包含 fragment 的 pwa_url。PWA 必须在发起网络请求前从地址栏清除 fragment,再向稳定 Connect origin 的 POST /api/pwa/handoff/exchange 提交:
{
"ticket": "...",
"pwa_origin": "https://pwa.example",
"name": "My phone",
"phone_ed25519_public": "...",
"phone_x25519_public": "...",
"identity_fingerprint": "..."
}
该 public exchange 必须由 Go Cloud Relay 承载,而不是 Cloud 控制面 Worker。Relay 用已验证 mTLS 节点身份调用内部 consume-phone-handoff,以用途隔离的 HMAC 密钥从 handoff ID 和手机身份确定性派生 phone principal/token/route handle,再把摘要通过幂等 complete-phone-handoff 注册到控制面。完成动作只创建 pending principal/route,不进入授权快照;PWA 第一次携带完整 token 与 route handle 访问时,控制面用一次性 activation nonce 原子激活两者并推进授权 revision。pending 证明窗口为 5 分钟,过期后不能路由,并由保留任务在删除 ticket 前清理。因此即使所有 exchange 响应都在提交后丢失,客户端未取得凭证时也不会留下可用手机身份。相同 ticket、节点和 E2E 身份的重试必须得到完全相同的凭据;不同身份或完成参数一律冲突。PWA 清除 fragment 后只在内存中对网络错误或可重试服务错误作有界重试。只有 Relay 对 PWA 返回 {phone_id, phone_token, route_handle};控制面永远不生成或接收明文 phone token。该流程不会创建任何 phone → device grant;用户仍需逐台配对设备。