集群管理与 vphone-agent
集群管理与 vphone-agent
vphone 集群让你从一台 Mac 的 vphone-manager 界面管理多台 Mac 上的虚拟 iPhone。每个节点运行一个 headless 守护进程 vphone-agent,以 HTTP API(Bearer token 认证)暴露本机的 VM 库与生命周期操作;控制端(vphone-manager)通过 Cluster 面板连接并管理这些节点。
架构速览
agent 本身不触碰 Virtualization.framework、不需要私有 entitlements——VM 由它拉起的签名 vphone-cli 子进程承担,因此 agent 可以正常签名分发到任意 Mac 上运行。agent 重启也不影响其托管的 VM 子进程(重新通过 lsof 发现)。
构建与启动
# 构建(release,无签名步骤——无 entitlements)
make agent_build
# 产出 .build/release/vphone-agent
# 节点侧启动(默认 127.0.0.1:8740,token 自动生成 ~/.config/vphone-agent/token)
.build/release/vphone-agent
# 跨机管理:必须显式绑定全部接口
.build/release/vphone-agent --bind 0.0.0.0 --port 8740 --token <your-token>
# TLS:--tls-cert/--tls-key(PEM,成对)
⚠️ --bind 默认值 127.0.0.1 是真 loopback——其他机器完全无法连上。跨机管理必须显式加 --bind 0.0.0.0,该参数同时作用于主 HTTP 端口与屏幕流/SSH/事件推送等临时 TCP 端口。
认证 token
- 所有 API 请求要求
Authorization: Bearer <token>,缺失或错误一律返回 401。 - 三种提供方式:
--token <token>显式指定;--token-file <path>;或缺省时自动生成并落盘~/.config/vphone-agent/token(32 字节随机数 hex,权限 0600)。
冒烟验证
TOKEN=$(cat ~/.config/vphone-agent/token)
curl -H "Authorization: Bearer $TOKEN" http://<node-ip>:8740/api/v1/status
# → {"ok":true,"apiVersion":1,"protocolVersion":"2","hostname":"...","vmCount":...}
curl -H "Authorization: Bearer $TOKEN" http://<node-ip>:8740/api/v1/vms
# → 节点上的 VM 列表
status 响应里的 protocolVersion 是协议契约版本(当前 "2"),控制端与节点不匹配时会拒绝通信并提示升级。
控制端:Cluster 面板
在 vphone-manager 中打开工具栏的 Cluster:
- Add Node…:填入节点 IP/主机名、端口(默认 8740)、token。
- Test Connection 验证连通与认证通过后保存。
- 节点卡片显示在线状态、VM 列表、链路质量(RTT/带宽/LINK 行)与 agent 版本徽标(有更新时提示)。
除手动添加外,同一局域网内运行 agent 的节点会通过 mDNS(_vphone-node._tcp)自动出现在发现列表中。mDNS TXT 记录携带 role(agent/embedded/controller)和协议版本 pv,可识别版本不匹配的节点。
配对协议
配对支持三种方式:
- 主控发起(Pair):调节点
pairing/start→ 显示 6 位码 → 节点弹窗审批 → 自动完成配对。 - 输码配对(Enter Code):节点侧生成码,主控输入码直接完成。
- 节点→主控(Request Pairing):节点在管理端条目上请求配对,生成预批准码,主控输码完成。
码有效期 300 秒,一次性消费。嵌入式 agent(Manager 内嵌)有审批门禁——远程请求需节点操作员弹窗确认后才可通过;standalone agent(headless)无门禁,码本身即授权。
路由表
共享路由表 53 条(agentRoutes(provider:),standalone 与 embedded 两个前端同表,保证 wire 行为一致):
| 族 | 路由 |
|---|---|
| 节点 | status / health / stats / ping / logs;audit/logs |
| VM 列表与生命周期 | vms;vms/{name}/start / stop / force-stop / reset / delete / rename / host-control |
| Per-VM 查询 | vms/{name}/status / metrics / config / proxy-config / boot-log / auto-recover / snapshots |
| 配置 | vms/{name}/config(CPU/内存/屏幕/SSH 端口);vms/{name}/proxy-config(网络代理) |
| vphoned 热升级 | PUT vms/{name}/vphoned(暂存新签名二进制到 .vphoned.signed) |
| 端口代理 | vms/{name}/screen-stream-port / ssh-proxy-port / event-stream-port |
| 远程端口转发 | vms/{name}/forward / forward/remove;forwards |
| 创建任务 | vms/create(异步);vms/create/{taskId}/events(事件流) |
| IPA 暂存上传 | PUT staging/ipa/{sha256}(远程创建前置上传) |
| 镜像导出/导入/克隆 | vms/{name}/export / vms/import / vms/{name}/clone |
| 快照 | vms/{name}/snapshots(保存/恢复/删除) |
| 固件 | firmware/entries / download / probe / refresh |
| 自愈 | vms/{name}/auto-recover |
standalone 专属 5 条:agent/version、agent/update、token/rotate、pairing/start、pairing/confirm。embedded 自有 pairing/start + pairing/confirm + app/update。
临时 TCP 端口与 proxy 鉴权(v2)
agent 提供四个按需分配的临时 TCP 端口服务,均需 per-port token 鉴权:
| 服务 | 端点 | 行为 |
|---|---|---|
| 屏幕流 | POST screen-stream-port |
客户端发送参数行,proxy 原样转发 VM 的 vphone.sock,随后双向帧中继(H.264/mjpeg) |
| SSH | POST ssh-proxy-port |
透明双向字节转发到节点本机 SSH 端口 |
| 事件推送 | POST event-stream-port |
固定 2s 节奏推 JSON 快照帧(status+vms+stats) |
| 端口转发 | POST forward |
透明双向转发到 VM 进程的转发 listener(→ vsock 1339 → guest 目标) |
鉴权协议:TCP 连接后先发一行 auth <token>\n(token = 响应体 token 字段,per-port 随机 32B hex),服务端验证后回 {"ok":true}\n,失败断开。listener 绑定地址跟随 --bind。
Manager 侧 SSH 中继(RemoteSSHRelay)在本地注入 auth 行并消费 ack,ssh 二进制只见到 127.0.0.1:<localPort>,无需改造。常驻 SSH 转发(RemoteSSHForwardManager)把 SSH 桥固定映射到本地端口,不随 tab 关闭而断,agent 重启后自动重申请重绑。
链路质量监视(NodeLinkMonitor)
Cluster 面板打开期间,对每个节点做链路质量探测:
- RTT:每 5s
GET /api/v1/ping,滚动 12 样本窗口,显示最新/最小/平均/抖动。 - 带宽:自适应 bulk 探测
GET /api/v1/ping/{size},1 MiB 起步——快则 ×4(上限 8 MiB),慢则 ÷4(下限 128 KiB);≥ 5 Mbps 每 60s 复测,否则 300s。 - 降级:连续 3 次失败 → 判定失联;agent 返回 404(无该端点)→ 终止探测。
- UI:节点卡片 LINK 行(延迟 <20ms 绿 / 20–80ms amber / >80ms 红 + 带宽 + 手动复测)。
持久远程 SSH 转发
RemoteSSHForwardManager 提供 per-node 常驻 SSH 转发工厂:
- 远程 VM 详情 SSH tab 的固定端口映射控件,活跃时显示
127.0.0.1:<port>。 - 不随 tab 关闭断开,agent 重启后 5s debounce 自愈。
- MCP
vm_ssh_exec的远程路径复用已有转发,否则建 ephemeral 的。
集群 App 同步
Cluster 工具栏提供两个同步操作:
- Sync Agents…(
ClusterAgentUpdateSheet):把内置 agent 二进制推送到选中 standalone 节点(POSTagent/update,可选X-Expected-SHA256哈希门禁)→ agent 原子替换自身后execv自重启 → 轮询确认版本匹配。节点 agent 重启不影响托管 VM。 - Sync Apps…(
ClusterAppUpdateSheet,embedded 节点专用):把整只 VPhone.app 推送到 embedded 节点(POSTapp/update,body 为 zip 后 app ≤ 400 MB,哈希门禁)→ 节点解压验证签名 → spawn detached relauncher 替换 app → 通过 mDNS 发现新端口。推送源优先旁挂VPhone.app.zip,否则现场 zip 运行中 app。
版本一致性与远程更新
三层身份各司其职:
| 身份 | 载体 | 语义 |
|---|---|---|
protocolVersion |
status + mDNS TXT pv |
协议契约版本(当前 "2"),不匹配即拒绝通信 |
agentVersion |
status | 人类可读版本 $(BUILD_VERSION)+$(GIT_HASH);embedded 前缀 embedded- |
buildHash |
status | standalone = agent 二进制 SHA-256;embedded = git commit |
节点卡片徽标:buildHash ≠ 内置 agent 哈希时显示 amber 更新提示,相等时绿色 Up to date。版本漂移告警:在线节点按 buildHash 分组,组数 ≥ 2 时对少数派发版本不匹配告警。
节点掉线 stale 与缓存清理
NodeStalenessTracker 解决节点彻底离线后侧栏/图库残留幽灵 VM 的问题:
- 标记 stale:连续 3 次轮询失败(15s tick 下 ≈45s)→ 节点进入 stale 状态,其 VM 显示「离线缓存」amber 徽标(数据仍是缓存值,节点恢复即消失)。
- 清空缓存:连续 6 次失败(≈90s)→ 该节点缓存列表整体清空,幽灵 VM 从侧栏与图库消失。
- 一次成功即恢复:轮询成功或事件流任意一帧到达都会清零计数并回填缓存。
- 事件订阅豁免:有活跃事件流订阅的节点跳过错轮询,不受计数影响。
控制端链路质量与刷新机制
Cluster 面板刷新以事件推送为主数据源(agent 侧 2s 快照推送),轮询兜底:
- onAppear 立即并发刷新所有节点 + 并发尝试事件流订阅。
- 5s 轮询仅对无活跃事件订阅的节点兜底。
- 30s 周期重试订阅。
- 瞬时网络错误重试:幂等 GET 遇超时/断连/DNS 失败等瞬时错误自动重试(最多 2 次、250/500ms 退避);POST/PUT/DELETE 有副作用,绝不重试。
安全模型与注意事项
- 授权粒度:一个 token = 节点全权。没有 per-VM / per-command 的细分授权。
- 传输加密:裸 HTTP 只适用于可信内网。跨站点部署请使用
--tls-cert/--tls-key(PEM,成对提供)启用 TLS,或走 Tailscale/WireGuard 隧道。 - 临时端口:屏幕流、SSH 代理、事件推送与端口转发 proxy 使用按需分配的临时 TCP 端口,各自有独立的 per-port token 握手鉴权(与 Bearer token 同信任级),绑定地址跟随
--bind。 - fail-fast 版本纪律:协议版本不匹配的节点在配对/连接阶段被明确拒绝,而不是静默降级。
- 传输层加固:并发连接上限 256;分相位空闲超时——请求读取连续 30s 无新字节(慢速半包攻击防护)或 keep-alive 空闲 120s 即断开;token 轮换运行期即时生效,无需重启 agent。
深入阅读
- 完整路由表、文件索引与 API 端点语义:仓库
docs/cluster.md - 控制端 GUI(vphone-manager)整体说明:仓库
docs/manager.md