原文:知乎文章
我发现 Remote 的网络部分很容易被一句“支持 P2P 和 Relay”糊弄过去。这句话不能说错,但基本等于什么都没说。
这里的 Remote,指的是我在做的 DeepSeek Harness Remote。
简单说,就是让手机或另一台电脑,不用暴露 Host 的公网端口,也能安全连接正在运行 Harness 的机器。
比如 Host 明明在家里的电脑上,手机又在外面的移动网络里,两边都躲在 NAT 后面,也没有公网 IP。它们怎么找到彼此?P2P 打不通怎么办?用了 TURN 或 Relay 之后,Server 会不会看见 Prompt 和源码?Wi-Fi 切到 5G,又为什么不能把刚才那条请求悄悄再发一次?
这篇文档就把这几件事从头讲一遍。
先给最朴素的结论:
Host 不开放公网端口。Host 和 Client 都主动连向 Remote Server,再优先尝试 WebRTC 直连;直连不成就走 TURN,最后回落到 WebSocket Relay。无论走哪条路,Harness 业务消息都要先经过 Noise 端到端加密。
默认的选路顺序可以写成:
LAN -> P2P -> TURN -> Relay
不过要注意,这不是四套 Remote 协议。
LAN、P2P 和 TURN 都承载同一个 WebRTC DataChannel,区别只在数据包最后走了哪一对 ICE candidate;Relay 则把同样的 Noise 密文放进已有的 Control WebSocket。上层的 Harness RPC 不会因为换了一条网络路径,就突然变成另一种业务协议。
本文不打算逐项抄协议,主要负责把这套网络原理讲成人话。
先看整条链路
先说最容易误解的地方:Remote 并不是让 Host 对公网开一个端口,然后等 Client 主动找上门。
Host 只建立出站连接,不监听公网端口。Client 也是主动连接 Server。于是家庭宽带没有公网 IP、路由器没有端口转发、公司电脑藏在防火墙后面,都不妨碍它们先去同一个地方报到。
Remote Server 在这里更像前台:确认账号和设备、维护在线状态、介绍双方认识、转发 WebRTC signaling,必要时再帮忙搬运密文。
它不是 Harness 业务明文的终点。
一条 Remote 连接 -> 三个平面
把所有流量都叫“网络请求”会越讲越乱。
Remote 实际上把它们拆成了三个职责不同的平面。
REST:先解决“你是谁”
Host 和 Client 通过 HTTPS 完成这些事情:
- 账号登录与设备注册;
- access/refresh token 轮换;
- Host 列表、设备详情和在线状态查询;
- 在 membership 保护下查询设备 identity key;
- 按
connectionId获取短期 ICE/TURN 配置。
这一层处理账号、设备和连接准备,不搬运 Harness 会话。
生产环境必须使用 HTTPS,只有 localhost 开发环境允许 HTTP。Token 只能放进认证 header 或请求体,不能塞进 URL、日志或 WebRTC signaling。这个限制看上去有点啰嗦,但 token 一旦进 URL,后面的浏览器历史、代理日志和监控系统都可能顺手留一份,神奇的不安全事故往往就是这么来的。
Control:让两端找到彼此
Host 和 Client 各自建立一条经过 device credential 认证的出站 WebSocket:
wss://<server>/ws/v1/connectControl channel 使用可读 JSON frame,负责:
hello、协议版本、frame limits 和 capability 协商;connect.request/connect.incoming/connect.accepted;- WebRTC offer、answer 和 ICE candidate;
- 最终的
transport.selected; - opaque Noise handshake bytes;
- Relay 模式下的 Noise ciphertext;
- ping/pong 和连接级错误。
Server 需要看懂连接属于谁、信令该转给谁,所以路由信息不可能全部隐藏。不过 Control frame 不能出现 Harness 业务明文。Noise 握手字节和 Relay 密文对 Server 来说都只是需要原样转发的不透明数据。
Data:真正搬运 Remote 消息
数据面只有两个实际出口:
- WebRTC 成功时,Noise ciphertext 作为 ordered DataChannel binary message 直接发送;
- WebRTC 不可用或协商失败时,Noise ciphertext 放进 Control WebSocket 的
relayframe,由 Server 转发。
所以从上层看,始终只是一个 RemoteTransport。
今天走局域网,明天走 TURN,后天在公司网络里只能走 Relay,Harness 业务层不需要跟着重写一遍。
两台设备到底怎样连上?
现在按一次完整的连接顺序往下走。

1. 两边先上线
Host 与 Client 分别用自己的 device credential 建立 WSS。
它们不共用设备身份,也不靠一个长期万能 token 到处跑。
2. 协商双方都会什么
双方通过 hello / hello.ack 协商协议版本、frame limits 和 transport.* capability。
这一步很现实:
不是代码里写了 WebRTC,当前运行环境就一定能加载 WebRTC runtime;
也不是新版 Client 遇到旧版 Host,双方就能假装功能一致。
两边当前能支持的能力(网络情况、协议版本、兼容版本等等),是靠双方协商出来的。
3. Client 请求连接某一台 Host
Server 先校验同账号 membership、设备状态和 Host presence。通过以后,它创建一个 connectionId,并把连接请求通知 Host。
connectionId 很重要。
后面的 signaling、选路、握手和 Relay frame 都要绑定到它,不能把 A 连接的消息顺手塞进 B 连接。
4. 尝试 WebRTC
如果双方能力和运行环境都支持,Client 发起 offer/answer/ICE 协商。
ICE 会收集并测试多类 candidate:本机地址、STUN 得到的公网映射地址,以及 TURN relay 地址。
简单理解,就是先看看两边能不能直接说话;不能的话,再看看有没有一台 TURN 服务器愿意帮忙转包。
如果 WebRTC runtime 加载失败、Server 禁用 WebRTC、ICE 配置拿不到,或者协商超时,流程不会硬撑,会清理临时 RTC 状态并准备回落 Relay。
5. 明确最终走哪条路
DataChannel 可用以后,Initiator 先发送 transport.selected。
Host 在同一个 connectionId 上确认选择一致,之后才继续 Noise 握手。
这个顺序不是 UI 上显示 LAN 还是 P2P 那么简单,它本身就是安全状态的一部分。
Host 不能看见某个异步 RTC 回调就自行脑补“应该直连成功了”。选路消息乱序时,握手会暂存;Noise channel 建立以后,迟到或冲突的 transport.selected 也不能再把已有连接偷偷切去另一条数据面。
6. 建立 Noise 安全通道
双方通过 Control channel 交换 Noise IK handshake。这里不仅是在生成一把临时密钥,也在把连接绑定到已经固定和信任的设备 identity。
Server membership 与 Host 本地 trusted peer 必须同时成立。只在 Server 账号里“看起来是同一个人”不够,只在 Host 本地有一份旧身份也不够。
Noise 握手完成以后,Harness 业务通信才真正开始。
LAN、P2P、TURN、Relay 到底差在哪?
四个名字经常一起出现,但它们解决的问题不完全相同。
| 模式 | 实际承载 | 常见场景 | 谁转发业务密文 |
|---|---|---|---|
| LAN | WebRTC DataChannel | 两端处于可直达的物理本地网络 | 无中间转发 |
| P2P | WebRTC DataChannel | 公网 NAT 穿透、IPv6 或 overlay 网络直连 | 无中间转发 |
| TURN | WebRTC DataChannel 经 TURN relay | 不能直连,但网络允许访问 TURN | TURN 服务 |
| Relay | WSS 中的 relay frame | WebRTC 不可用、失败或网络严格受限 | Remote Server |

LAN:本地网络不是另一套协议
LAN 只是 WebRTC 最终选中的 candidate pair 被识别为物理本地路径。
RFC 1918 私网地址、链路本地、回环地址,以及 mDNS/prflx 的组合会参与判断。不过 100.64.0.0/10 这类 CGNAT 地址和 Tailscale overlay 地址不能看见 host candidate 就算作 LAN,否则一条绕过虚拟广域网络的连接,会被 UI 写成“局域网直连”。看起来很快,概念却错了。
这些路径统一记为 P2P。
P2P:能直连当然最好,但不能迷信
P2P 通过 ICE/STUN 尝试让 Client 与 Host 直接交换 DataChannel 流量。通常延迟更低,也不消耗 Server 的业务转发带宽。
不过它能否成功,取决于 NAT 类型、防火墙、IPv4/IPv6、企业网络策略,以及当前平台的 WebRTC 实现。(技术上能支持的,现在基本也完成了。)
代码写得再厉害,也说服不了一台明确禁止 UDP 的公司网关。
TURN:还是 WebRTC,只是有人帮忙转包
直接 candidate pair 连不通时,WebRTC 可以使用 Server 下发的短期 TURN credential。
TURN 转发的是 WebRTC 数据包。外层仍有 DataChannel 的 DTLS,里面还有 DSH Remote 的 Noise E2EE。因此 TURN 服务虽然参与转发,却不能读取 Harness 业务明文。
Relay:最后那条更朴素的路
Relay 不再要求 UDP、NAT 打洞或端到端可达。它直接复用已经建立的 WSS,把 Base64URL 编码的 Noise ciphertext 放进 relay frame。
这条路通常更容易穿过企业代理和严格 NAT,代价是延迟与 Server 带宽开销可能高于直连。
Remote Server 可以看到路由所需的连接元数据,但禁止解密、解析、持久化或记录 Relay ciphertext。
换句话说,它负责搬箱子,不负责拆箱。( 它也没有拆箱的能力,算是囚笼里面的猛兽。
自动选路不是“P2P 成功学”
默认 Client 会按下面的层次尝试:
- LAN/P2P direct path;
- TURN path;
- WebSocket Relay。
如果用户选择 Relay-only,或者 Host 配置了 forceRelay,双方就不会尝试 WebRTC。
这套设计的重点不是把 P2P 成功率写进宣传页,而是让连接在真实网络里有退路。WebRTC runtime 不存在、ICE/TURN 配置失败、DataChannel 超时、公司网络禁 UDP,都不应该直接把 Remote 变成“不可用”。
不过,当前自动降级主要覆盖连接建立阶段。
已经建立连接以后,Wi-Fi 切到移动网络、设备休眠再唤醒,或者 DataChannel 突然关闭,无缝迁移还在完善。当前更安全的做法是关闭受影响的 Remote connection,重新建立 transport 和 Noise channel。
为什么不直接换条路继续发?
因为一个会改变远端状态的 mutation 发出去以后,断线只说明“我没收到结果”,不等于“对方没执行”。在结果未知时静默重发,比让用户重新连接更危险。网络体验可以继续优化,业务语义不能靠猜。
换了路,为什么安全边界没有跟着变?
WebRTC 有 DTLS,WSS 有 TLS,TURN 自己也有传输保护。那为什么还要套一层 Noise?

因为这些加密分别终止在不同的中间节点。
WSS 的 TLS 会在 Remote Server 终止;TURN 参与 WebRTC 包转发;网络路径也可能从直连切到中继。如果只相信底层传输加密,换一条路就可能顺手换了谁能看到明文。
Noise 把规则固定下来:Harness 业务消息只在通过身份校验的 Client 和 Host 两端加解密。
LAN、P2P、TURN 和 Relay 搬的都是同一种端到端密文。
因此下面这些情况都必须 fail closed:
- 未知
connectionId; - 错误 target 或 identity mismatch;
- 非法的选路、握手或业务消息顺序;
- 重放、重复分片或超限消息;
- 未完成 membership 与 trusted peer 双重校验。
身份、密钥和重放保护本身还可以单独再讲一篇。
这里只要记住一件事:
网络路径可以降级,可信身份不能跟着降级。
大消息不是一股脑塞进 DataChannel
Prompt 可能带图片,工具输出可能很大,
File Viewer 还要读取文件分块。只说“DataChannel 能发二进制”显然不够。
当前链路有几层明确限制:
- WebRTC DataChannel 名称是
dsh,并使用ordered: true; - Noise transport 单消息最大 65,535 bytes;
- 编码后的业务消息超过 48 KiB 时,先在 authenticated secure channel 内分片;
- 完整 secure message 的重组上限为 4 MiB;
- WebRTC 还会对较大的 Noise ciphertext 做透明 chunking,以适配不同 DataChannel 实现;
- 图片 Prompt、attachment response 和 File Viewer range read 另有各自的业务分块与并发上限。
Relay frame 会携带 connectionId、目标设备、递增诊断 counter 和 Noise ciphertext。
WebRTC chunking 则发生在 Noise 之下:
接收端先还原完整密文,再交给 Noise 解密,不改变上层的加密语义。
这里宁可限制多一点,也不能收到几块数据以后“差不多拼一下”。
重复、乱序、越界、超限都应该直接失败。
断线之后,哪些东西会消失?
Host 的 Control WebSocket 支持带 jitter 的指数退避重连。
多 Client 连接彼此隔离,一条逻辑 connection 断开时,
只清理它自己的 Noise/RTC、pending RPC 和 stream,不影响其他 Client。
业务连接断开后,安全的恢复顺序是:
- 未完成的 tunnel RPC 失败;
- 原生 mux/host 或 Typert Remote stream 关闭;
- Desktop Remote 退出活动数据面并回落 Local;
- 再次连接时重新做 membership/trust 校验、选路和 Noise 握手;
- 由官方 Harness UI 重开 stream,并重新读取历史记录作为新的 baseline。
项目不会私自维护另一套 Harness event replay buffer,也不会自动重放结果未知的 mutation。
这多少显得不够“丝滑”,但它至少不会为了动画连续,把一次可能已经执行的操作再执行一遍。
对防火墙和路由器有什么要求?
最低要求很简单:设备能够通过 HTTPS/WSS 访问 Remote Server,Relay 就有机会工作。
Host 不需要接受公网入站连接,也不需要在路由器上配置端口转发。
如果希望提高 LAN、P2P 或 TURN 的成功率,就需要允许 WebRTC/ICE 使用 UDP,并允许访问 Server 返回的 STUN/TURN 地址。具体端口由实际部署的 ICE 配置决定,不应该在客户端里写死某个公网 TURN 地址或端口。
只允许 HTTP 代理、启用 TLS inspection 或严格限制 UDP 的企业网络,
可能让 WebRTC 直接失败。此时应该回落到 WSS Relay。
现在看起来Relay 应该是足够可靠的一个兜底路子。
能观测什么,又不能记录什么?
Client 可以把当前模式显示为 LAN、P2P、TURN、Relay 或 Disconnected,也可以统计连接期间发送和接收的密文字节。
WebRTC 诊断还可以包含 ICE 状态、candidate pair 和 fallback 原因。这些信息对判断“为什么在家能直连,进公司就只能 Relay”很有用。
但诊断不能包含:
- token 与私钥;
- Noise secret;
- Prompt、源码和工具输出;
- 解密后的业务 payload。
IP、candidate、连接时间和字节数虽然不是业务正文,依然属于敏感网络元数据。日志导出或上传之前仍要脱敏。E2EE 不等于匿名网络,这两个概念不能混在一起。
这套方案现在做到哪一步了?
目前已经具备这些基础链路:
- Protocol v1 Control/Relay 与 capability 协商;
- WebRTC offer/answer/ICE、STUN/TURN 短期 credential 接入;
- LAN/P2P/TURN 路径识别、DataChannel chunking 和 Relay fallback;
- Desktop Plugin、Android 与 VS Code Client 的 Adaptive transport 接入;
- 所有路径上的 Noise E2EE 和按
connectionId隔离。
Remote 网络这件事,最后并没有什么神奇魔法。
两边先出站连接同一个 Server,Server 介绍它们认识;
能直连就直连,不能直连就找人搬密文;
无论谁搬,箱子都只在可信的 Client 和 Host 两端打开。
大抵如此。