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 三种连接场景

  1. 同局域网:两端在同一网段,直接走局域网 IP,不经任何公网服务器。
  2. 公网 IP 或 NAT 穿透成功:通过 hbbs 协商候选地址,UDP 打洞成功后建立 P2P 直连。
  3. 无法直连:自动回退到中继模式,数据通过 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 链(三个来源,优先级从高到低)

  1. 用户配置Config::get_option("relay-server")):客户端设置中显式填写的「中继服务器」地址。
  2. rendezvous 服务器响应provided_by_rendezvous_server):hbbs 在 RegisterPeer 响应中返回的 hbbr 地址(由 hbbs 启动时的 -r 参数指定)。
  3. 默认值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> vs ping <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
  • Keydata/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 成本控制策略

按量付费场景下(如云服务器按流量计费),中继流量是主要成本来源:

  1. 优先 P2P:通过 UPnP/IPv6/公网 IP 提升 P2P 成功率,减少中继用量。
  2. 限制中继带宽:在 hbbr 上用 tc 或 nginx stream 限速。
  3. 分区域部署 hbbr:按地理分布部署多 hbbr,客户端就近接入,降低单点带宽。
  4. 监控告警:当 ss 中 21117 活跃连接数 > 阈值时告警,排查异常流量。
  5. 审计日志: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