集群管理与 vphone-agent

用 vphone-agent 把任意 Mac 变成集群节点,经 vphone-manager Cluster 面板远程管理 VM

集群管理与 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:

  1. Add Node…:填入节点 IP/主机名、端口(默认 8740)、token。
  2. Test Connection 验证连通与认证通过后保存。
  3. 节点卡片显示在线状态、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 节点(POST agent/update,可选 X-Expected-SHA256 哈希门禁)→ agent 原子替换自身后 execv 自重启 → 轮询确认版本匹配。节点 agent 重启不影响托管 VM。
  • Sync Apps…(ClusterAppUpdateSheet,embedded 节点专用):把整只 VPhone.app 推送到 embedded 节点(POST app/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