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
411 lines
14 KiB
Markdown
411 lines
14 KiB
Markdown
# NAT 穿透技术
|
||
|
||
## 概述
|
||
|
||
RustDesk 实现了多种 NAT 穿透技术,以在不同网络环境下建立 P2P 连接:
|
||
|
||
- NAT 类型检测
|
||
- UDP 打洞
|
||
- TCP 打洞
|
||
- Relay 中转(作为后备)
|
||
|
||
## NAT 类型
|
||
|
||
### 分类
|
||
|
||
```protobuf
|
||
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 使用双端口检测法:
|
||
|
||
1. 客户端向 Rendezvous Server 的主端口 (21116) 发送 TestNatRequest
|
||
2. 同时向 NAT 测试端口 (21115) 发送 TestNatRequest
|
||
3. 比较两次响应中观测到的源端口
|
||
|
||
```
|
||
客户端 Rendezvous Server
|
||
│ │
|
||
│ TestNatRequest ────────►│ Port 21116
|
||
│ │
|
||
│ TestNatRequest ────────►│ Port 21115
|
||
│ │
|
||
│◄──────── TestNatResponse │ (包含观测到的源端口)
|
||
│ │
|
||
│ │
|
||
│ 比较两次源端口 │
|
||
│ 相同 → ASYMMETRIC │
|
||
│ 不同 → SYMMETRIC │
|
||
```
|
||
|
||
### 实现代码
|
||
|
||
**客户端发送检测请求:**
|
||
|
||
```rust
|
||
// 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);
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
**服务器响应:**
|
||
|
||
```rust
|
||
// 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 的映射存在,包被转发) │
|
||
│ │
|
||
│ ◄───────── 双向通信建立 ──────────► │
|
||
```
|
||
|
||
### 实现
|
||
|
||
**被控端打洞:**
|
||
|
||
```rust
|
||
// 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 建立:**
|
||
|
||
```rust
|
||
// 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 ───────────► │
|
||
│ │ │ │
|
||
│ ◄─────────── 连接建立 ─────────────────────────────────────►│
|
||
```
|
||
|
||
### 实现
|
||
|
||
```rust
|
||
// 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;
|
||
```
|
||
|
||
## 局域网直连
|
||
|
||
### 检测同一局域网
|
||
|
||
```rust
|
||
// 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 打洞以处理有状态防火墙:
|
||
|
||
```rust
|
||
// 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 同时尝试多种连接方式,选择最快成功的:
|
||
|
||
```rust
|
||
// 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),
|
||
}
|
||
```
|
||
|
||
### 超时控制
|
||
|
||
```rust
|
||
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 |
|