Files
DevFlow/docs/02-架构设计/专项设计/u-relay-wire-protocol-2026-10-04.md
T
lxy b994cf2c54 文档: 设计定稿与评估纪律登记
- 无人值守长任务设计定稿,含效率与可观测补遗
- u-relay wire 协议规格与五项评估结论
- 新能力引入评估纪律五项清单
- todo 销账与狗粮短板分析登记
2026-10-04 16:27:32 +08:00

8.4 KiB
Raw Blame History

u-relay Wire 协议规格(改造蓝本)

版本:现行(第三代 agent/commander 命名,兼容层全拆) 来源:E:/wk-lab/u-relay/src/{relay.rs, broadcast.rs, conn.rs} + README.md 适用:df-tunnel / df-miniapp 升级对接


1. 拓扑与端点

Commander(控制端) ──wss──> nginx(:443 TLS 终结) ──ws──> u-relay(:9180)
                                                              ↑
   Agent(执行端)   ──wss 主动出站────────────────────────────┘
                 (穿 NAT,无监听端口)
角色 端点 典型实现
Agent(执行端,事件真相源) GET /ws/agent df-tunnel(桌面端)、u-claw runner
Commander(控制端,指令发起方) GET /ws/commander df-miniapp(小程序)、u-claw console、u-ppt

端点即身份:端点决定期望的 ClientKind,与 Hello.kind 不一致直接拒。


2. 握手协议

2.1 客户端首帧(10s 内必发,否则断开)

{
  "kind": "agent" | "commander",
  "channel_id": "<非空, ≤128 字节, 无控制字符>",
  "token": "<U_RELAY_TOKEN 值>"
}
字段 约束
kind 必须与端点匹配:/ws/agent 端点仅收 "agent";/ws/commander 端点仅收 "commander"
channel_id 配对绑定键;非空 + ≤128B + 无控制字符;同 channel_id 的 agent 与 commander 互为对端
token 服务端常量时间比较(防时序侧信道)

2.2 握手成功响应(relay 立即下发)

{ "kind": "control", "payload": { "control_kind": "hello_ack" } }

客户端判定:收到首条非 error 消息即握手通过(不要求对端在线)。

2.3 握手失败(relay 发错误帧后立即 Close)

{ "kind": "control", "error": "<reason>" }
error 值 触发条件
handshake_failed 10s 未发 Hello / Hello JSON 解析失败 / 首帧非文本 / Hello 超 1 MiB
kind_mismatch Hello.kind 与端点不匹配(如 agent 端点收 commander)
invalid_channel_id channel_id 为空 / >128B / 含控制字符
server_misconfigured 服务端 U_RELAY_TOKEN 未设置
auth_failed token 不匹配

2.4 已下线的旧值(一律拒绝)

维度 现行 已下线(收到即拒)
执行端端点 /ws/agent /ws/runner /ws/device /ws/executor
控制端端点 /ws/commander /ws/console /ws/miniapp /ws/controller
Hello.kind 执行端 "agent" "runner" "device" "executor"
Hello.kind 控制端 "commander" "console" "miniapp" "controller"
配对键字段 channel_id device_id(不再 alias)
错误码 invalid_channel_id invalid_device_id

3. 数据帧(握手通过后)

3.1 客户端 → relay(入站)

入站文本帧即业务 payload,relay 不解析直接透传:

{ "任意 JSON" }

非 JSON 文本会被包成 {"payload": "<原字符串>"}。

特殊语义:若 payload 顶层有 control_kind: "ping",触发 relay 本地心跳响应(见 §4),不透传对端。

3.2 relay → 客户端(出站 BroadcastMessage)

{
  "channel_id": "chan-xxx",
  "kind": "event" | "command" | "control",
  "source": 12345,
  "from": "agent" | "commander",
  "payload": { "原始 JSON" },
  "ts": 1725000000000
}
字段 说明
channel_id 配对通道
kind event=agent→commander;command=commander→agent;control=控制面
source 来源 conn_id(u64,0=NIL 伪值,relay 自构造消息用)
from 来源客户端类型
payload 原始 JSON 透传
ts relay 收到入站帧的毫秒时间戳

3.3 路由规则(relay 内部,客户端无需关心但需理解)

消息 投递目标
Event(agent 发) 同 channel_id 的所有 commander(除来源自身)
Command(commander 发) 同 channel_id 的所有 agent(除来源自身)
Control 同 channel_id 的所有连接(除来源自身)

方向硬约束(relay 路由层强制):

  • Event 永不投递给 agent
  • Command 永不投递给 commander

多端登录:同 channel_id 可并存多个 agent 和多个 commander,全投递(Vec 索引)。

3.4 帧限制

限制 值
入站文本帧 ≤ 1 MiB(含 Hello),超限直接断开
二进制帧 忽略(MVP 仅文本)
WS Ping/Pong 协议层自动处理(tungstenite),不进业务流

4. 心跳协议

4.1 客户端发起

{ "control_kind": "ping" }

关键:relay 收到后本地直接回 pong,不透传对端,也不依赖对端在线。

4.2 relay 响应

{
  "kind": "control",
  "from": "agent",
  "source": 0,
  "payload": { "control_kind": "pong" },
  "ts": 1725000000000
}

字段细节:

  • source: 0 是 ConnId::NIL 伪值 —— 若填接收方自己的 conn_id,路由防回环会跳过,pong 永远回不来
  • from: "agent" 是 relay 代理执行端身份 —— 让 commander 端识别为合法控制响应

4.3 客户端实现要点

端 用途
Commander 用 pong 更新 lastPongTime,看门狗超时则重连
Agent(跑桌面/移动网络) 用 pong 重置读死线,死线超时走既有重连

5. 部署配置

5.1 环境变量

变量 默认 说明
U_RELAY_ADDR 0.0.0.0:8080 监听地址
U_RELAY_TOKEN 无(必填) 配对 token,未设置启动即 exit 1
RUST_LOG info tracing env-filter

5.2 nginx 反代参考

location /ws/ {
    proxy_pass http://127.0.0.1:9180;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
}

5.3 部署形态

  • musl 静态编译:x86_64-unknown-linux-musl → 单二进制 ~3.6MB
  • 无 openssl/native-tls:TLS 完全由 nginx 终结
  • systemd 单元:/usr/local/bin/u-relay + /etc/u-relay.env(权限 600)

6. 客户端实现 Checklist

Agent(执行端,如 df-tunnel)

  • 连 wss://host/ws/agent
  • Hello {kind:"agent", channel_id, token}
  • 等首条非 error 帧判定握手通过
  • 心跳:周期发 {"control_kind":"ping"},等 pong 重置读死线
  • 入站 BroadcastMessage:仅处理 kind=="command"(来自 commander 的指令)和 kind=="control"
  • 出站:业务事件 JSON 直接作为文本帧发送(relay 包成 Event 投递给 commander)

Commander(控制端,如 df-miniapp)

  • 连 wss://host/ws/commander
  • Hello {kind:"commander", channel_id, token}
  • 等首条非 error 帧判定握手通过
  • 心跳:周期发 {"control_kind":"ping"},等 pong 更新 lastPongTime 看门狗
  • 入站 BroadcastMessage:仅处理 kind=="event"(来自 agent 的事件)和 kind=="control"
  • 出站:业务指令 JSON 直接作为文本帧发送(relay 包成 Command 投递给 agent)

7. 离线语义

场景 行为
对端不在线时发消息 relay 丢弃(不缓存),debug 日志记录"广播无对端在线"
客户端判断对端在线 通过 PresenceState(relay 不主动推送,由客户端心跳或业务层判活)
重连 客户端指数退避自行实现(df-miniapp 已有 reconnectBaseDelay/reconnectMaxDelay)

8. 安全说明

  • token 常量时间比较:relay 端实现,防时序侧信道
  • channel_id 校验:格式防御,无 per-channel token 绑定(Phase3 留)
  • 风险:单共享 token 下,持 token 者可声明任意 channel_id 冒充该通道 —— 生产部署需确保 token 不泄露

9. 与旧版 df-relay 协议差异速查

维度 df-relay(已下线) u-relay(现行)
端点 /ws/device /ws/miniapp /ws/agent /ws/commander
Hello.kind "device" "miniapp" "agent" "commander"
配对键 device_id channel_id
心跳 pong 由对端回(路径长) relay 本地回(路径短)
旧名兼容 多代别名 一律拒绝
错误码 invalid_device_id invalid_channel_id
帧上限 无 1 MiB
token env DF_RELAY_TOKEN U_RELAY_TOKEN