1. RustDesk 连接模式总览
RustDesk 的核心服务端由两个组件构成:
- hbbs(ID/Rendezvous 服务器):负责 ID 注册、信令交换、NAT 类型探测、协助 UDP 打洞协商。本身不转发业务数据。
- hbbr(Relay 服务器):纯 TCP/UDP 数据转发节点。仅在 P2P 直连失败或被强制时使用。
1.1 P2P 直连 vs 中继转发的本质区别
| 维度 | P2P 直连 | 中继转发 |
|---|---|---|
| 数据路径 | 控制端 ⇄ 被控端(直连 UDP/TC) | 控制端 → hbbr → 被控端 |
| 服务器参与 | 仅 hbbs 协商,不经过 hbbr | hbbr 全程转发所有数据 |
| 带宽成本 | 几乎为零(仅少量信令) | 中继服务器承担双向带宽 |
| 延迟 | 双方直连 RTT | 双方到 hbbr 的 RTT 之和 |
| 安全模型 | 端到端加密(被控端公钥) | 端到端加密(hbbr 无法解密) |
| 失败场景 | 对称型 NAT × 对称型 NAT | 几乎不会失败(除非 hbbr 不可达) |
1.2 三种连接场景
- 同局域网:两端在同一网段,直接走局域网 IP,不经任何公网服务器。
- 公网 IP 或 NAT 穿透成功:通过 hbbs 协商候选地址,UDP 打洞成功后建立 P2P 直连。
- 无法直连:自动回退到中继模式,数据通过 hbbr 转发。
1.3 流量路径对比
P2P 直连路径:
控制端 ──┐
├──(UDP 直连,hbbs 不参与数据)──┐
被控端 ──┘ │
↓
业务数据直达
中继路径:
控制端 ──TCP/UDP──> hbbr(21117) ──TCP/UDP──> 被控端
全量数据在此转发
2. 连接建立的完整流程
被控端 → hbbs(21116) 注册 ID 和网络地址(RegisterPeer)
控制端 → hbbs(21116) 查询被控端地址( punch_hole_request )
双方 → hbbs 协助尝试 P2P 直连(UDP 打洞)
├─ 成功 → P2P 直连,数据不经过服务器
└─ 失败 → 自动回退中继模式(RequestRelay),数据经 hbbr 转发
2.1 每一步的技术细节
Step 1 — 被控端注册
被控端启动后向 hbbs 的 21116 端口发送 RegisterPeer 消息(UDP),携带自己的 ID 和公钥。hbbs 记录 ID → (IP, Port, NAT 类型) 映射,并保活心跳。
Step 2 — 控制端查询
控制端输入被控端 ID,向 hbbs 发送 punch_hole_request,hbbs 返回被控端的公网地址和 NAT 类型。
Step 3 — UDP 打洞协商
两端通过 hbbs 交换候选地址(local + server reflexive + relay candidate),尝试 ICE 候选配对。
Step 4 — DTLS/TLS 握手
候选配对成功后,双方进行 DTLS(UDP)/ TLS(TCP)握手,建立加密通道。后续业务流量走该加密通道。
Step 5 — 失败回退
若 ICE 候选全部失败或握手超时,控制端发送 RequestRelay 给 hbbs,hbbs 返回 hbbr 地址,控制端与 hbbr 建立 TCP 连接,被控端也连接到同一 hbbr 会话,hbbr 开始双向转发。
2.2 STUN/ICE/DTLS 在 RustDesk 中的角色
- STUN:RustDesk 使用 hbbs 兼作 STUN,通过
21116/udp探测 server reflexive 地址(SRFLX)。也支持外部 STUN(默认UDP:8000)。 - ICE:候选收集 = 主机候选(host)+ 服务器反射候选(srflx)+ 中继候选(relay)。按优先级配对尝试。
- DTLS:UDP 直连成功后的加密层,基于被控端的
id_ed25519.pub公钥进行 ECDH 密钥协商,保证端到端加密。
3. 源码级解析:如何判断和切换连接模式
3.1 force_relay 标志的传递链
force_relay 标志从 UI 触发,经过参数序列化,最终影响 Rendezvous Mediator 的连接决策。
// src/ui_session_interface.rs (lines 1276-1296)
pub fn reconnect(&self, force_relay: bool) {
if true == force_relay {
self.lc.write().unwrap().force_relay = true;
}
// ... 后续触发重新连接
}
reconnect 在 UI 点击「重连」或「强制中继连接」时调用,将 force_relay 写入 LocalConfig,下次连接时读取。
// src/ui_interface.rs (lines 1062-1067)
pub fn new_remote(id: String, remote_type: String, force_relay: bool) {
let mut args = vec![];
if force_relay {
args.push("--relay".to_string());
}
// ...
}
CLI 子进程通过 --relay 参数传递强制中继语义。--relay 在解析阶段会剥离 ID 中的 /r 后缀:
// src/ui_interface.rs (lines 1442-1443)
// 从 ID 中剥离 /r 后缀,并设置 force_relay = true
let force_relay = id.ends_with("/r");
let id = id.trim_end_matches("/r").to_string();
传递链总结:
UI 勾选「始终使用中继」 ┐
ID 输入 "xxx/r" ├──> force_relay = true ──> --relay 参数
CLI --relay ┘ │
↓
Rendezvous Mediator 跳过 P2P,直接走 Relay
3.2 Rendezvous Mediator 的 relay 选择逻辑
// src/rendezvous_mediator.rs (lines 14-84)
fn get_relay_server(&self, provided_by_rendezvous_server: String) -> String {
let mut relay_server = Config::get_option("relay-server");
if relay_server.is_empty() {
relay_server = provided_by_rendezvous_server;
}
if relay_server.is_empty() {
relay_server = crate::increase_port(&self.host, 1);
}
relay_server
}
fallback 链(三个来源,优先级从高到低):
- 用户配置(
Config::get_option("relay-server")):客户端设置中显式填写的「中继服务器」地址。 - rendezvous 服务器响应(
provided_by_rendezvous_server):hbbs 在RegisterPeer响应中返回的 hbbr 地址(由 hbbs 启动时的-r参数指定)。 - 默认值(
increase_port(&self.host, 1)):取 hbbs 的 host,端口 +1(即 hbbs 端口 21116 → hbbr 端口 21117)。
RequestRelay 处理流程(src/rendezvous_mediator.rs lines 14-84 核心区段):
// 简化伪代码
match msg::RendezvousMessage::request_relay() {
Some(relay_request) => {
let relay_server = self.get_relay_server(relay_request.relay_server);
self.create_relay_connection(relay_server, uuid, secure).await?;
}
}
3.3 TCP 连接建立
// src/server.rs (lines 23-38)
async fn create_relay_connection_(
relay_server: &str,
uuid: &str,
secure: bool,
ipv4: bool,
) -> ResultType<()> {
let mut stream = socket_client::connect_tcp(
socket_client::ipv4_to_ipv6(
crate::check_port(relay_server, RELAY_PORT),
ipv4,
),
CONNECT_TIMEOUT,
).await?;
// 发送 Relay 建立请求,携带 uuid 标识会话
// hbbr 据此将控制端和被控端的两个连接配对
let mut msg_out = RendezvousMessage::new();
msg_out.set_request_relay(...);
stream.send(&msg_out).await?;
Ok(())
}
关键点:
RELAY_PORT默认值在src/config.rs中定义为21116,但check_port会在地址未带端口时套用该值(实际生产中 hbbr 监听 21117,由get_relay_server的 fallback 链补齐)。ipv4_to_ipv6:当客户端强制 IPv4 但服务器返回 IPv6 地址时做兼容转换。CONNECT_TIMEOUT:TCP 连接超时,避免长时间卡死。uuid:hbbr 用同一个 uuid 把控制端的入站连接和被控端的出站连接桥接起来。
4. 五种判断当前连接模式的方法
4.1 UI 状态栏(最简单)
RustDesk 客户端连接建立后,标题栏/状态栏会显示连接类型:
- 显示 「P2P」/「直接连接」/「Direct」 → 直连
- 显示 「中继」/「Relay」 → 中继
注意:UI 文案随版本变化,以实际显示为准。
4.2 日志分析(最准确)
启动客户端时启用 debug 日志:
# Linux/macOS
RUST_LOG=debug rustdesk
# Windows (PowerShell)
$env:RUST_LOG="debug"; rustdesk.exe
关键日志模式对照表:
| 日志关键词 | 含义 | 连接模式判定 |
|---|---|---|
create_relay requested |
已发起中继请求 | 中继 |
Failed to create relay connection |
中继连接失败 | 失败(需排查 hbbr) |
Latency of ... |
正在测量候选地址延迟 | 协商中(未确定) |
skip duplicate relay request messages |
重复中继请求被节流 | 中继重试 |
punch hole done |
UDP 打洞成功 | P2P 直连 |
relay established |
中继通道已建立 | 中继 |
4.3 网络流量分析
# 检查是否与 hbbr(21117) 建立 TCP 连接 —— 有则中继
ss -tn | grep 21117
# 或使用 netstat
netstat -tn | grep 21117
# tcpdump 抓包确认是否走 21117
tcpdump -i any port 21117 -n
# nethogs 实时监控进程级流量,确认 rustdesk 进程是否在与 hbbr 通信
nethogs
# iftop 查看带宽走向,识别是否经过中继服务器 IP
iftop -i eth0
判断逻辑:若 rustdesk 进程与 21117 端口有持续 TCP 流量,则是中继;若仅有 21116/udp 短暂信令流量,且大量流量直接到对端 IP,则是 P2P。
4.4 延迟判断法
- P2P 直连延迟通常 < 50ms(同城/同运营商)。
- 中继延迟 ≈ 控制端到 hbbr 的 RTT + hbbr 到被控端的 RTT,通常显著高于直连。
- 操作:
ping <对端公网IP>vsping <hbbr域名>,对比延迟差。若实际连接延迟接近 hbbr 的延迟,则大概率是中继。
4.5 强制测试法
利用 /r 后缀强制中继,对比延迟:
被控端 ID: 123456789
直连测试:直接输入 123456789
中继测试:输入 123456789/r
- 若
/r强制中继后延迟显著高于不加/r,说明原本是 P2P 直连。 - 若两者延迟接近,说明原本就是中继(P2P 打洞失败已回退)。
5. 自建服务器部署实战
5.1 Docker Compose 部署
version: '3'
services:
hbbs:
image: rustdesk/rustdesk-server:latest
container_name: rustdesk-hbbs
# -r 指定 hbbr 的对外可访问地址(客户端将连接此地址)
# 替换 <SERVER_IP> 为你的服务器公网 IP 或域名
command: hbbs -r <SERVER_IP>:21117
volumes:
- ./data:/root
ports:
- "21115:21115"
- "21116:21116/tcp"
- "21116:21116/udp"
- "21118:21118"
restart: unless-stopped
# 可选:强制加密
environment:
- ENCRYPTED_ONLY=1
hbbr:
image: rustdesk/rustdesk-server:latest
container_name: rustdesk-hbbr
command: hbbr
volumes:
- ./data:/root
ports:
- "21117:21117"
- "21119:21119"
restart: unless-stopped
部署步骤:
mkdir -p /opt/rustdesk/data
cd /opt/rustdesk
# 将上面的 docker-compose.yml 写入
docker compose up -d
# 查看密钥(客户端需填入此公钥)
cat data/id_ed25519.pub
# 验证端口监听
ss -tlnp | grep -E '2111[5-9]'
5.2 端口规划表
| 端口 | 协议 | 用途 | 必须开放 |
|---|---|---|---|
| 21115 | TCP | NAT 类型测试 | 是 |
| 21116 | TCP/UDP | ID 注册/心跳/打洞协商 | 是 |
| 21117 | TCP | 中继数据转发 | 是 |
| 21118 | TCP | Web 客户端(可选) | 否 |
| 21119 | UDP | 中继 UDP(可选) | 否 |
防火墙配置示例(ufw):
ufw allow 21115/tcp
ufw allow 21116/tcp
ufw allow 21116/udp
ufw allow 21117/tcp
ufw allow 21118/tcp
ufw allow 21119/udp
ufw reload
5.3 客户端配置
打开 RustDesk 客户端 → 设置 → 网络 → ID/中继服务器:
- ID 服务器:
<SERVER_IP>或 hbbs 域名 - 中继服务器:留空则自动使用 hbbs 的
-r值;也可显式填<SERVER_IP>:21117 - API 服务器:可选,留空则用公共 API
- Key:
data/id_ed25519.pub文件内容
强制中继选项:在连接窗口勾选「始终使用中继连接」或在 ID 后加 /r,适用于排查 P2P 故障或验证中继链路。
6. P2P 直连失败的常见原因与排查
6.1 NAT 类型矩阵
| 控制端 NAT | 被控端 NAT | P2P 可行性 | 说明 |
|---|---|---|---|
| 完全锥型 | 完全锥型 | 可,UDP 打洞 | 最易成功 |
| 完全锥型 | 对称型 | 可,端口预测 | 部分成功率 |
| Port Restricted | Port Restricted | 可,UDP 打洞 | 成功率较高 |
| Symmetric | Symmetric | 不可,需中继 | 必须走 hbbr |
| Symmetric | Full Cone | 可,端口预测 | 单向可行 |
自测 NAT 类型:
# 使用 hbbs 兼作 STUN,或外部工具
# Linux 上可用 stun-client
stunclient stun.l.google.com:19302
6.2 防火墙排查
# 检查 UDP 21116 是否被阻止(信令通道)
nc -zuv <SERVER_IP> 21116
# 检查 TCP 21117(中继通道)
nc -zv <SERVER_IP> 21117
# 检查本机出站是否被防火墙限制
sudo ufw status verbose
常见问题:
- 云服务器安全组未放行 UDP 21116 → 打洞协商失败。
- 客户端出口防火墙拦截 UDP → P2P 无法建立。
- 运营商对 UDP 限速/丢弃 → 表现为 P2P 不稳定,建议启用 TCP 直连或中继。
6.3 IPv4/IPv6 不匹配
现象:日志显示 Failed to create relay connection 但 hbbr 正常运行。
原因:服务器仅监听 IPv6,客户端强制 IPv4(或反之)。src/server.rs 中的 ipv4_to_ipv6 转换仅能处理地址格式,无法解决监听缺失。
解决:
# 确认双栈监听
ss -tlnp | grep 21117
# 应同时出现 0.0.0.0:21117 和 [::]:21117
# Docker 默认双栈,若用裸机部署,监听 :: 即可(Linux 默认 v6only=0 双栈)
7. 优化直连成功率的实践
7.1 UPnP/NAT-PMP 配置
在路由器开启 UPnP/NAT-PMP,RustDesk 会自动尝试通过 UPnP 在 NAT 设备上打洞映射端口。
验证:
# Linux 上安装 miniupnpc 验证 UPnP 是否生效
upnpc -l
# 应能看到 rustdesk 进程映射的端口条目
注意事项:
- 部分 ISP 光猫禁用 UPnP,需桥接后自建路由。
- UPnP 存在安全风险,建议仅在可信网络开启。
7.2 IPv6 部署
IPv6 通常无 NAT 限制,可直接端到端通信。
# 服务器配置 IPv6 监听(Docker 默认支持)
# 客户端需确保本机有公网 IPv6 地址
ip -6 addr show | grep "scope global"
# 测试 IPv6 连通性
ping6 -c 3 <服务器IPv6>
hbbs 启动时 -r 同时支持 IPv4 和 IPv6 地址([2001:db8::1]:21117)。
7.3 公网 IP 直连
为被控端配置公网 IP 或在路由器做端口映射:
路由器端口映射:
公网 <SERVER_IP>:21116/udp → 被控端内网 IP:21116/udp
公网 <SERVER_IP>:21116/tcp → 被控端内网 IP:21116/tcp
适用于被控端在固定地点(如家庭服务器、办公电脑)的场景,可大幅提升 P2P 成功率。
7.4 TURN 服务器(类似 WebRTC)
RustDesk 中继(hbbr)本身即承担 TURN 角色。若需要独立的 TURN 兼容层(如与浏览器 WebRTC 互通),可部署 coturn:
# 安装 coturn
apt install coturn
# /etc/turnserver.conf
listening-port=3478
external-ip=<SERVER_IP>
realm=rustdesk.example.com
user=rustdesk:rustdesk_secret
lt-cred-mech
fingerprint
no-tls
no-dtls
no-udp-relay
但需注意:RustDesk 原生协议不直接走标准 TURN,此节仅作为中继备选方案参考,实际生产环境优先用 hbbr。
8. 安全性考量
8.1 中继模式下的端到端加密
RustDesk 在 P2P 和中继模式下都采用端到端加密:加密密钥由控制端和被控端通过被控端的 id_ed25519.pub 公钥协商,hbbr 仅转发密文,无法解密。
8.2 强制加密
# 启动 hbbs/hbbr 时强制只接受加密会话
# Docker 环境变量
environment:
- ENCRYPTED_ONLY=1
# 或命令行
docker run -e ENCRYPTED_ONLY=1 rustdesk/rustdesk-server hbbs -r <IP>:21117
开启后未配置正确密钥的客户端将无法连接。
8.3 密钥管理
data/id_ed25519(私钥)+data/id_ed25519.pub(公钥)- 私钥必须妥善保管,泄露后攻击者可伪造 hbbs。
- 客户端必须配置正确的公钥,否则有中间人攻击风险(攻击者伪造 hbbs 和 hbbr,劫持会话)。
- 轮换密钥:替换
data/id_ed25519*后重启服务并同步更新所有客户端 Key。
8.4 防止中间人攻击
- 客户端首次连接时校验 hbbs 公钥指纹。
- 不要在生产环境关闭密钥校验(
Key留空仅用于测试)。 - 在不可信网络(如公共 WiFi)下,建议勾选「始终使用中继」+强制加密,避免 P2P 暴露真实 IP。
9. 运维监控
9.1 实时监控
# 监控 hbbr 容器资源占用(间接反映中继流量)
docker stats rustdesk-hbbr
# 实时带宽监控
vnstat -l -i eth0
# 查看当前活跃中继连接数
ss -tn state established '( sport = :21117 or dport = :21117 )' | wc -l
9.2 P2P vs 中继比例统计
需开启客户端 debug 日志并收集到中心化日志服务器:
# 统计中继请求次数
grep -c "create_relay" /var/log/rustdesk.log
# 统计 P2P 成功次数(punch hole done)
grep -c "punch hole done" /var/log/rustdesk.log
# 中继失败次数
grep -c "Failed to create relay connection" /var/log/rustdesk.log
9.3 成本控制策略
按量付费场景下(如云服务器按流量计费),中继流量是主要成本来源:
- 优先 P2P:通过 UPnP/IPv6/公网 IP 提升 P2P 成功率,减少中继用量。
- 限制中继带宽:在 hbbr 上用
tc或 nginx stream 限速。 - 分区域部署 hbbr:按地理分布部署多 hbbr,客户端就近接入,降低单点带宽。
- 监控告警:当
ss中 21117 活跃连接数 > 阈值时告警,排查异常流量。 - 审计日志:hbbs 支持
KEY环境变量限制可注册的客户端,防止滥用中继。
# tc 限制 hbbr 出站带宽示例(限制到 50Mbps)
tc qdisc add dev eth0 root tbf rate 50mbit burst 32kbit latency 400ms
附录:关键源码索引
| 文件 | 行号 | 内容 |
|---|---|---|
src/ui_session_interface.rs |
1276-1296 | reconnect 设置 force_relay |
src/ui_interface.rs |
1062-1067 | new_remote 添加 --relay 参数 |
src/ui_interface.rs |
1442-1443 | 从 ID 剥离 /r 后缀 |
src/rendezvous_mediator.rs |
14-84 | RequestRelay 处理 + get_relay_server |
src/server.rs |
23-38 | create_relay_connection_ 建立 TCP |
src/config.rs |
- | RELAY_PORT 默认 21116(实际 hbbr 用 21117) |
附录:常用排错命令速查
# 1. 验证 hbbs/hbbr 端口监听
ss -tlnp | grep -E '2111[5-9]'
# 2. 验证 UDP 端口可达性
nc -zuv <SERVER_IP> 21116
# 3. 抓取协商阶段流量
tcpdump -i any 'port 21116' -nn -X
# 4. 抓取中继流量
tcpdump -i any 'port 21117' -nn
# 5. 客户端 debug 日志
RUST_LOG=debug rustdesk 2>&1 | tee rustdesk.log
# 6. 查看密钥
cat /opt/rustdesk/data/id_ed25519.pub
# 7. 重启服务
docker compose restart hbbs hbbr
浙公网安备 33010602011771号