Hotdry.

Article

RustDesk P2P NAT穿透协议详解:ED25519 密钥交换与 UDP 打洞时序

深入解析 RustDesk 开源远程桌面如何实现 P2P 直连,涵盖 ED25519 身份验证、UDP 打洞消息时序、中继 fallback 策略及工程参数配置。

2026-04-18systems

在远程桌面场景中,延迟是用户体验的核心指标。当被控端位于家庭或企业内网时,如何绕过 NAT 设备建立低延迟的 P2P 直连,成为 RustDesk 这类自托管方案的关键技术挑战。本文聚焦 RustDesk 在 GitHub 上的最新实现,从协议层面拆解其 NAT 穿透方案,包括 ED25519 密钥交换、UDP 打洞消息格式、超时参数配置以及 TCP 中继 fallback 机制,为开发者提供可落地的工程参数。

1. 整体架构:Rendezvous Server 充当打洞协调人

RustDesk 的 NAT 穿透架构遵循经典的 Rendezvous 模式,由部署在公网的 hbbs(RustDesk Server)承担三种角色:客户端发现、连接握手协调、以及 UDP/TCP 打洞信令转发。客户端首次启动时会向 hbbs 注册自身的公钥 ID 和 NAT 类型信息,服务器将这些元数据存储在内存映射中供其他客户端查询。

整个连接建立过程可以概括为三个阶段:首先是发现阶段,客户端从服务器获取对端的公钥 ID 和公网映射地址;其次是打洞阶段,双方在服务器协助下完成 UDP hole punching,尝试建立直接 UDP 通道;最后是数据传输阶段,如果 UDP 打洞成功,视频、音频、鼠标键盘事件等业务数据将直接通过 P2P 通道传输,绕过中继服务器以获得最低延迟。

值得注意的是,RustDesk 在设计上将信令传输与数据传输分离:信令消息(打洞请求、密钥交换确认等)始终经过 Rendezvous Server 转发,而实际业务数据则尝试走 P2P 通道,只有在打洞失败时才 fallback 到 TCP 中继模式。

2. ED25519 密钥交换:身份认证与密钥派生

RustDesk 使用 ED25519 椭圆曲线数字签名算法作为对等端身份验证的核心机制。每个客户端在首次启动时生成一对 ED25519 密钥,私钥保存在本地配置文件(config.json)中,公钥则作为客户端唯一标识符上报给 Rendezvous Server。这一设计确保了即使攻击者能够截获打洞信令,也无法伪装成合法客户端发起连接。

密钥交换的具体流程如下:发起端(Client A)向服务器请求连接目标客户端(Client B)时,服务器返回 B 的公钥 ID;A 使用自身私钥对打洞请求消息进行签名,并将签名附加在 PunchHoleRequest 中发送给服务器;服务器验证 A 的签名有效后,将请求转发给 B;B 收到请求后,同样验证 A 的签名,确认对端身份合法后才开始 UDP 打洞操作。

从工程实现角度看,ED25519 密钥的存在使得 RustDesk 不需要依赖额外的 PKI 体系,完全去中心化。公钥本身就是客户端身份标识,这简化了密钥管理流程。但这也意味着首次连接时需要人工确认对端公钥指纹,否则存在中间人攻击风险 —— 这也是 RustDesk 在首次连接时会弹出 “确认远程电脑身份” 提示的原因。

3. UDP 打洞协议:消息格式与时序控制

UDP hole punching 的核心在于利用 NAT 设备的会话表老化机制。当两个客户端分别向对端的公网地址发送 UDP 报文时,NAT 设备会为这两个方向的 UDP 会话创建映射表项,从而允许后续的 UDP 报文直接穿透。RustDesk 将这一过程封装为一系列结构化的信令消息。

3.1 消息类型定义

根据源码分析,RustDesk 的打洞协议包含以下核心消息类型,每个消息都通过 Protobuf 进行序列化和传输:

PunchHoleRequest:由发起端发送给 Rendezvous Server,携带目标客户端的 ID、自身公网地址(socket_addr)、UDP 端口号(udp_port)以及协议版本号。服务器收到该请求后,根据目标 ID 查找对应客户端的注册信息,并构造打洞响应。

PunchHole:服务器转发给目标客户端的消息,包含发起端的公网地址(socket_addr)、发起端的 ID、 relay_server(中继服务器地址,供 UDP 打洞失败时使用)、NAT 类型(nat_type,标识发起端的 NAT 穿越难度)以及 IPv6 地址(socket_addr_v6,如果可用)。NAT 类型字段至关重要,它帮助对端判断打洞策略 —— 对于全锥型 NAT 打洞成功率较高,而对于对称型 NAT 则建议直接走 TCP fallback。

PunchHoleSent:目标客户端收到 PunchHole 后,向服务器确认打洞消息已收到,同时告知服务器自身的公网映射地址。服务器收到此确认后,会向双方发送就绪信号,触发实际的 UDP 打洞尝试。

PunchHoleResponse / PunchHoleReady:双方完成打洞后的确认消息,表示 UDP 通道已成功建立,后续可以切换到数据传输模式。

3.2 打洞时序与重试机制

RustDesk 的 UDP 打洞采用同步时序控制,而非完全异步的批量发包方式。具体流程为:A 发送 PunchHoleRequest → 服务器转发 PunchHole 给 B → B 发送 PunchHoleSent 给服务器 → 服务器向 A 发送就绪信号 → A 和 B 双方同时向对端公网地址发送 UDP 探测报文 → NAT 设备创建映射表项 → 双方收到对端报文后确认打洞成功。

为了提高打洞成功率,RustDesk 实现了多端口重试机制:客户端会尝试使用多个不同的 UDP 端口进行打洞,每个端口的探测超时时间通常设置在 2-3 秒。如果在该时间窗口内未收到对端响应,则切换到下一个端口重试。整个打洞过程的全局超时通常不超过 10 秒,超过后自动触发 TCP fallback。

此外,RustDesk 还实现了打洞消息去重逻辑:服务器和客户端会检测短时间内重复的 PunchHole 消息,跳过重复请求以避免资源浪费。这一机制在网络抖动或消息重传场景下尤为重要。

4. NAT 类型检测与打洞策略适配

不同类型的 NAT 设备对 UDP hole punching 的友好程度差异显著。RustDesk 通过检测本地 NAT 类型来动态调整打洞策略,虽然具体的检测逻辑在开源代码中相对简化,但仍然遵循以下原则:

全锥型 NAT(Full Cone NAT):对任何主机发往自身公网地址的 UDP 报文都会转发到内网客户端,打洞成功率最高。RustDesk 对此类 NAT 会优先尝试 UDP 直连。

受限锥型 NAT(Restricted Cone NAT):只接受曾经向其发送过 UDP 报文的主机发来的响应。RustDesk 需要先由内网客户端主动向外发送探测报文建立映射,才能进行后续打洞。

端口受限锥型 NAT(Port Restricted Cone NAT):除了受限锥型的限制外,还要求外部主机的端口号与之前发送过的目的端口一致。RustDesk 在此类 NAT 环境下的打洞成功率会有所下降。

对称型 NAT(Symmetric NAT):每个外部目的地址对应不同的内部映射端口,传统的 UDP hole punching 几乎无法穿透。RustDesk 对此类 NAT 直接跳过 UDP 打洞,快速切换到 TCP 中继模式。

从工程参数角度看,RustDesk 在代码中将 NAT 类型标记为 nat_type 字段,如果该字段为 UNKNOWN_NAT 或检测到对称型 NAT 特征,系统会默认走 TCP fallback 路径,避免无谓的打洞重试消耗用户等待时间。

5. TCP Fallback 与中继策略

尽管 UDP 打洞在多数场景下有效,但面对对称型 NAT、企业防火墙 UDP 阻断或极端网络环境时,必须有可靠的 fallback 机制。RustDesk 的解决方案是 TCP 打洞 + 中继转发 的双层保障。

5.1 TCP 打洞(TCP Hole Punching)

当 UDP 打洞失败后,RustDesk 会尝试 TCP 打洞。其原理与 UDP 类似:双方分别向服务器的 TCP 端口发起连接,服务器在内部将两个 TCP 连接 “桥接” 在一起,使两端认为建立了直连。TCP 打洞的消息格式与 UDP 类似,只是传输层协议从 UDP 更换为 TCP。

TCP 打洞的优势在于它可以穿透大多数仅允许 HTTP(80/443 端口)出站的防火墙。但其代价是延迟高于 UDP 直连,且服务器需要维护更多的长连接资源。

5.2 中继服务器配置

RustDesk 允许用户自定义中继服务器地址。在 PunchHole 消息中包含 relay_server 字段,当 P2P 直连(包括 UDP 和 TCP 打洞)均失败时,客户端会使用该服务器作为数据中继。所有业务数据将通过中继服务器转发,这种模式牺牲了延迟但保证了可用性。

从部署角度,企业自托管 RustDesk 时需要同时部署 hbbs(注册服务器)和 hbbr(中继服务器)。生产环境中建议将中继服务器部署在低延迟数据中心,并配置足够的带宽以支撑并发中继连接。典型的中继服务器配置参数包括:最大并发连接数(默认数千连接)、单连接带宽上限、以及连接超时时间(通常为 30-60 秒)。

6. 工程实践:关键配置参数与监控要点

基于上述协议分析,以下是 RustDesk NAT 穿透部署的关键工程参数,供运维人员参考:

UDP 打洞超时:单次打洞尝试的默认超时为 2-3 秒,总超时不超过 10 秒。如果网络环境复杂(如跨境连接),可适当延长至 15 秒。

打洞重试次数:客户端默认会尝试 3-5 个不同端口。建议在内网规模较大的企业环境中,将此参数调低以加快失败检测速度。

中继服务器端口:hbbs 监听的 UDP 端口通常为 21116,TCP 中继端口为 21117。确保防火墙放行这些端口的入站和出站流量。

NAT 类型日志:RustDesk 会在客户端日志中输出 nat_type 字段,运维人员可通过该字段判断打洞难度。如果大量客户端显示 UNKNOWN_NAT 或对称型 NAT,说明网络环境不支持 P2P 直连,应重点优化中继服务器带宽。

连接质量监控:建议监控 P2P 直连成功率(UDP 打洞成功比例)作为核心指标。成功率低于 60% 时,应考虑优化网络拓扑或增强中继服务器容量。

7. 小结

RustDesk 的 P2P NAT 穿透方案是开源远程桌面领域的经典工程实践。它通过 Rendezvous Server 协调打洞信令、使用 ED25519 实现去中心化身份认证、设计多消息类型描述打洞状态,并配套 TCP fallback 机制确保可用性。从协议层面看,其核心创新在于将 NAT 类型检测结果编码到打洞消息中,使对端可以动态调整策略。工程落地时,关键在于合理配置打洞超时参数、确保中继服务器带宽充足,并通过日志监控持续优化 P2P 连接成功率。


参考资料

systems