Files
nekonest-cloud/docs/daemon-relay-handoff.md

90 lines
6.2 KiB
Markdown

# Daemon 与 Cloud Relay 稳定端点契约
状态:控制面实现完成;真实 WSS Relay 部署仍是独立门禁。
## 不变服务地址
Daemon 只保存用户注册时填写的 `server_url`。自部署时它指向 Standalone Server;官方服务时它指向 Cloud Connect 稳定 origin。Cloud 不返回租户后端 URL,不要求 Daemon 感知区域、节点或控制面轮询。
注册成功响应:
```json
{
"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 错误返回:
```json
{
"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-device`
- `POST /api/internal/relay/authorization-delta`
- `POST /api/internal/relay/authorization-snapshot`
- `POST /api/internal/relay/register-device`
- `POST /api/internal/relay/heartbeat`
- `POST /api/internal/relay/authorize-phone`
- `POST /api/internal/relay/revoke-phone`
- `POST /api/internal/relay/resolve-device-route`
- `POST /api/internal/relay/resolve-phone-route`
- `POST /api/internal/relay/resolve-tenant-route`
- `POST /api/internal/relay/resolve-handoff-route`
- `POST /api/internal/relay/migrations/advance`
- `POST /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` 提交:
```json
{
"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;用户仍需逐台配对设备。