mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-28 08:31:52 +08:00
- 新增 RustDesk 模块,支持与 RustDesk 客户端连接 - 实现会合服务器协议和 P2P 连接 - 支持 NaCl 加密和密钥交换 - 添加视频帧和 HID 事件适配器 - 添加 Protobuf 协议定义 (message.proto, rendezvous.proto) - 新增完整项目文档 - 各功能模块文档 (video, hid, msd, otg, webrtc 等) - hwcodec 和 RustDesk 协议技术报告 - 系统架构和技术栈文档 - 更新 Web 前端 RustDesk 配置界面和 API
14 KiB
14 KiB
NAT 穿透技术
概述
RustDesk 实现了多种 NAT 穿透技术,以在不同网络环境下建立 P2P 连接:
- NAT 类型检测
- UDP 打洞
- TCP 打洞
- Relay 中转(作为后备)
NAT 类型
分类
enum NatType {
UNKNOWN_NAT = 0; // 未知
ASYMMETRIC = 1; // 非对称 NAT (Cone NAT) - 可打洞
SYMMETRIC = 2; // 对称 NAT - 通常需要 Relay
}
NAT 类型说明
| 类型 | 描述 | 可打洞 |
|---|---|---|
| Full Cone | 外部端口固定,任何外部主机可访问 | ✅ 最容易 |
| Restricted Cone | 外部端口固定,仅允许曾发送过数据的 IP | ✅ 容易 |
| Port Restricted Cone | 外部端口固定,仅允许曾发送过数据的 IP:Port | ✅ 可能 |
| Symmetric | 每个目标地址使用不同外部端口 | ❌ 困难 |
NAT 类型检测
检测原理
RustDesk 使用双端口检测法:
- 客户端向 Rendezvous Server 的主端口 (21116) 发送 TestNatRequest
- 同时向 NAT 测试端口 (21115) 发送 TestNatRequest
- 比较两次响应中观测到的源端口
客户端 Rendezvous Server
│ │
│ TestNatRequest ────────►│ Port 21116
│ │
│ TestNatRequest ────────►│ Port 21115
│ │
│◄──────── TestNatResponse │ (包含观测到的源端口)
│ │
│ │
│ 比较两次源端口 │
│ 相同 → ASYMMETRIC │
│ 不同 → SYMMETRIC │
实现代码
客户端发送检测请求:
// rustdesk/src/lib.rs
pub fn test_nat_type() {
tokio::spawn(async move {
let rendezvous_server = Config::get_rendezvous_servers().first().cloned();
if let Some(host) = rendezvous_server {
// 连接主端口
let host = check_port(&host, RENDEZVOUS_PORT);
// 连接 NAT 测试端口
let host2 = crate::increase_port(&host, -1);
// 发送测试请求
let mut msg = RendezvousMessage::new();
msg.set_test_nat_request(TestNatRequest {
serial: Config::get_serial(),
});
// 收集两次响应的端口
let port1 = send_and_get_port(&host, &msg).await;
let port2 = send_and_get_port(&host2, &msg).await;
// 判断 NAT 类型
let nat_type = if port1 == port2 {
NatType::ASYMMETRIC // 可打洞
} else {
NatType::SYMMETRIC // 需要 Relay
};
Config::set_nat_type(nat_type as i32);
}
});
}
服务器响应:
// rustdesk-server/src/rendezvous_server.rs:1080-1087
Some(rendezvous_message::Union::TestNatRequest(_)) => {
let mut msg_out = RendezvousMessage::new();
msg_out.set_test_nat_response(TestNatResponse {
port: addr.port() as _, // 返回观测到的源端口
..Default::default()
});
stream.send(&msg_out).await.ok();
}
UDP 打洞
原理
UDP 打洞利用 NAT 的端口映射机制:
A (内网) B (内网)
│ │
│ ──► NAT_A ──► Internet ──► NAT_B ──► (丢弃) │
│ │
│ │
│ (NAT_A 创建了映射 A:port → A_ext:port_a) │
│ │
│ │
│ (丢弃) ◄── NAT_A ◄── Internet ◄── NAT_B ◄── │
│ │
│ │
│ (NAT_B 创建了映射 B:port → B_ext:port_b) │
│ │
│ ──► NAT_A ──► Internet ──► NAT_B ──► │
│ (NAT_A 的映射存在,包被转发) │
│ │
│ ◄── NAT_A ◄── Internet ◄── NAT_B ◄── │
│ (NAT_B 的映射存在,包被转发) │
│ │
│ ◄───────── 双向通信建立 ──────────► │
实现
被控端打洞:
// rustdesk/src/rendezvous_mediator.rs:621-642
async fn punch_udp_hole(
&self,
peer_addr: SocketAddr,
server: ServerPtr,
msg_punch: PunchHoleSent,
) -> ResultType<()> {
let mut msg_out = Message::new();
msg_out.set_punch_hole_sent(msg_punch);
// 创建 UDP socket
let (socket, addr) = new_direct_udp_for(&self.host).await?;
let data = msg_out.write_to_bytes()?;
// 发送到 Rendezvous Server(会转发给控制端)
socket.send_to(&data, addr).await?;
// 多次发送以增加成功率
let socket_cloned = socket.clone();
tokio::spawn(async move {
for _ in 0..2 {
let tm = (hbb_common::time_based_rand() % 20 + 10) as f32 / 1000.;
hbb_common::sleep(tm).await;
socket.send_to(&data, addr).await.ok();
}
});
// 等待对方连接
udp_nat_listen(socket_cloned, peer_addr, peer_addr, server).await?;
Ok(())
}
UDP 监听和 KCP 建立:
// rustdesk/src/rendezvous_mediator.rs:824-851
async fn udp_nat_listen(
socket: Arc<tokio::net::UdpSocket>,
peer_addr: SocketAddr,
peer_addr_v4: SocketAddr,
server: ServerPtr,
) -> ResultType<()> {
// 连接到对方地址
socket.connect(peer_addr).await?;
// 执行 UDP 打洞
let res = crate::punch_udp(socket.clone(), true).await?;
// 建立 KCP 可靠传输层
let stream = crate::kcp_stream::KcpStream::accept(
socket,
Duration::from_millis(CONNECT_TIMEOUT as _),
res,
).await?;
// 创建连接
crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true).await?;
Ok(())
}
KCP 协议
RustDesk 在 UDP 上使用 KCP 提供可靠传输,KCP 特点:
- 更激进的重传策略
- 更低的延迟
- 可配置的可靠性级别
TCP 打洞
原理
TCP 打洞比 UDP 困难,因为 TCP 需要三次握手。技巧是让双方同时发起连接:
A NAT_A NAT_B B
│ │ │ │
│ ─── SYN ───────────────►│─────────│────► (丢弃,无映射) │
│ │ │ │
│ (NAT_A 创建到 B 的映射) │ │ │
│ │ │ │
│ (丢弃,无映射) ◄─────────│─────────│◄─── SYN ───────────── │
│ │ │ │
│ │ │ (NAT_B 创建到 A 的映射) │
│ │ │ │
│ ─── SYN ───────────────►│─────────│────► SYN ───────────► │
│ │ │ (映射存在,转发成功) │
│ │ │ │
│ ◄─── SYN+ACK ──────────│─────────│◄─── SYN+ACK ───────── │
│ │ │ │
│ ─── ACK ───────────────►│─────────│────► ACK ───────────► │
│ │ │ │
│ ◄─────────── 连接建立 ─────────────────────────────────────►│
实现
// rustdesk/src/rendezvous_mediator.rs:604-617
log::debug!("Punch tcp hole to {:?}", peer_addr);
let mut socket = {
// 1. 先连接 Rendezvous Server 获取本地地址
let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
let local_addr = socket.local_addr();
// 2. 用相同的本地地址尝试连接对方
// 这会在 NAT 上创建映射
// 虽然连接会失败,但映射已建立
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
socket
};
// 3. 发送 PunchHoleSent 通知服务器
// 服务器会转发给控制端
let mut msg_out = Message::new();
msg_out.set_punch_hole_sent(msg_punch);
socket.send_raw(msg_out.write_to_bytes()?).await?;
// 4. 等待控制端连接
// 由于已有映射,控制端的连接可以成功
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
局域网直连
检测同一局域网
// rustdesk-server/src/rendezvous_server.rs:721-728
let same_intranet: bool = !ws
&& (peer_is_lan && is_lan || {
match (peer_addr, addr) {
(SocketAddr::V4(a), SocketAddr::V4(b)) => a.ip() == b.ip(),
(SocketAddr::V6(a), SocketAddr::V6(b)) => a.ip() == b.ip(),
_ => false,
}
});
局域网连接流程
控制端 Rendezvous Server 被控端
│ │ │
│ PunchHoleRequest ────►│ │
│ │ │
│ │ (检测到同一局域网) │
│ │ │
│ │ FetchLocalAddr ──────►│
│ │ │
│ │◄────── LocalAddr ────────│
│ │ (包含被控端内网地址) │
│ │ │
│◄─ PunchHoleResponse ──│ │
│ (is_local=true) │ │
│ (socket_addr=内网地址)│ │
│ │ │
│ ─────────── 直接连接内网地址 ────────────────────►│
IPv6 支持
IPv6 通常不需要 NAT 穿透,但 RustDesk 仍支持 IPv6 打洞以处理有状态防火墙:
// rustdesk/src/rendezvous_mediator.rs:808-822
async fn start_ipv6(
peer_addr_v6: SocketAddr,
peer_addr_v4: SocketAddr,
server: ServerPtr,
) -> bytes::Bytes {
crate::test_ipv6().await;
if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await {
let server = server.clone();
tokio::spawn(async move {
allow_err!(udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server).await);
});
return local_addr_v6;
}
Default::default()
}
连接策略决策树
开始连接
│
▼
┌───────────────┐
│ NAT 类型检测 │
└───────┬───────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
ASYMMETRIC UNKNOWN SYMMETRIC
│ │ │
▼ ▼ │
┌──────────┐ ┌──────────┐ │
│ 尝试 UDP │ │ 尝试 TCP │ │
│ 打洞 │ │ 打洞 │ │
└────┬─────┘ └────┬─────┘ │
│ │ │
成功 │ 失败 成功 │ 失败 │
▼ │ ▼ │ │
┌────────┐│ ┌────────┐│ │
│UDP P2P ││ │TCP P2P ││ │
└────────┘│ └────────┘│ │
│ │ │
└───────┬───────┘ │
│ │
▼ │
┌───────────────┐ │
│ 使用 Relay │◄─────────┘
└───────────────┘
性能优化
多路径尝试
RustDesk 同时尝试多种连接方式,选择最快成功的:
// rustdesk/src/client.rs:342-364
let mut connect_futures = Vec::new();
// 同时尝试 UDP 和 TCP
if udp.0.is_some() {
connect_futures.push(Self::_start_inner(..., udp).boxed());
}
connect_futures.push(Self::_start_inner(..., (None, None)).boxed());
// 使用 select_ok 选择第一个成功的
match select_ok(connect_futures).await {
Ok(conn) => Ok(conn),
Err(e) => Err(e),
}
超时控制
const CONNECT_TIMEOUT: u64 = 18_000; // 18 秒
const REG_TIMEOUT: i32 = 30_000; // 30 秒
// 连接超时处理
if let Ok(Ok((stream, addr))) = timeout(CONNECT_TIMEOUT, socket.accept()).await {
// 连接成功
} else {
// 超时,尝试其他方式
}
常见问题和解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 双 Symmetric NAT | 两端都是对称 NAT | 使用 Relay |
| 防火墙阻止 UDP | 企业防火墙 | 使用 TCP 或 WebSocket |
| 端口预测失败 | NAT 端口分配不规律 | 多次尝试或使用 Relay |
| IPv6 不通 | ISP 或防火墙问题 | 回退到 IPv4 |