mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
feat: 添加 RustDesk 协议支持和项目文档
- 新增 RustDesk 模块,支持与 RustDesk 客户端连接 - 实现会合服务器协议和 P2P 连接 - 支持 NaCl 加密和密钥交换 - 添加视频帧和 HID 事件适配器 - 添加 Protobuf 协议定义 (message.proto, rendezvous.proto) - 新增完整项目文档 - 各功能模块文档 (video, hid, msd, otg, webrtc 等) - hwcodec 和 RustDesk 协议技术报告 - 系统架构和技术栈文档 - 更新 Web 前端 RustDesk 配置界面和 API
This commit is contained in:
69
docs/report/rustdesk/00-overview.md
Normal file
69
docs/report/rustdesk/00-overview.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# RustDesk 通信协议技术报告
|
||||
|
||||
## 概述
|
||||
|
||||
本报告详细分析 RustDesk 远程桌面软件的客户端与服务器之间的通信协议,包括 Rendezvous 服务器(hbbs)、Relay 服务器(hbbr)以及客户端之间的 P2P 连接机制。
|
||||
|
||||
## 文档结构
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [01-architecture.md](01-architecture.md) | 整体架构设计 |
|
||||
| [02-rendezvous-protocol.md](02-rendezvous-protocol.md) | Rendezvous 服务器协议 |
|
||||
| [03-relay-protocol.md](03-relay-protocol.md) | Relay 服务器协议 |
|
||||
| [04-p2p-connection.md](04-p2p-connection.md) | P2P 连接流程 |
|
||||
| [05-message-format.md](05-message-format.md) | 消息格式定义 |
|
||||
| [06-encryption.md](06-encryption.md) | 加密机制 |
|
||||
| [07-nat-traversal.md](07-nat-traversal.md) | NAT 穿透技术 |
|
||||
| [08-onekvm-comparison.md](08-onekvm-comparison.md) | **One-KVM 实现对比分析** |
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. Rendezvous Server (hbbs)
|
||||
- **功能**: ID 注册、Peer 发现、NAT 类型检测、连接协调
|
||||
- **端口**: 21116 (TCP/UDP), 21115 (NAT 测试), 21118 (WebSocket)
|
||||
- **源文件**: `rustdesk-server/src/rendezvous_server.rs`
|
||||
|
||||
### 2. Relay Server (hbbr)
|
||||
- **功能**: 当 P2P 连接失败时提供数据中转
|
||||
- **端口**: 21117 (TCP), 21119 (WebSocket)
|
||||
- **源文件**: `rustdesk-server/src/relay_server.rs`
|
||||
|
||||
### 3. 客户端 (RustDesk)
|
||||
- **功能**: 远程桌面控制、文件传输、屏幕共享
|
||||
- **核心模块**:
|
||||
- `rendezvous_mediator.rs` - 与 Rendezvous 服务器通信
|
||||
- `client.rs` - 客户端连接逻辑
|
||||
- `server/connection.rs` - 被控端连接处理
|
||||
|
||||
## 协议栈
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (Video/Audio/Keyboard/Mouse/File) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Message Layer │
|
||||
│ (Protobuf Messages) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Security Layer │
|
||||
│ (Sodium: X25519 + ChaCha20) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Transport Layer │
|
||||
│ (TCP/UDP/WebSocket/KCP) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 关键技术特点
|
||||
|
||||
1. **混合连接模式**: 优先尝试 P2P 直连,失败后自动切换到 Relay 中转
|
||||
2. **多协议支持**: TCP、UDP、WebSocket、KCP
|
||||
3. **端到端加密**: 使用 libsodium 实现的 X25519 密钥交换和 ChaCha20-Poly1305 对称加密
|
||||
4. **NAT 穿透**: 支持 UDP 打洞和 TCP 打洞技术
|
||||
5. **服务器签名**: 可选的服务器公钥签名验证,防止中间人攻击
|
||||
|
||||
## 版本信息
|
||||
|
||||
- 分析基于 RustDesk 最新版本源码
|
||||
- Protocol Buffer 版本: proto3
|
||||
- 加密库: libsodium (sodiumoxide)
|
||||
218
docs/report/rustdesk/01-architecture.md
Normal file
218
docs/report/rustdesk/01-architecture.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# RustDesk 架构设计
|
||||
|
||||
## 系统架构图
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Rendezvous Server │
|
||||
│ (hbbs) │
|
||||
│ Port: 21116 │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────────────────────┼──────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ Client A │ │ Client B │ │ Client C │
|
||||
│ (控制端) │ │ (被控端) │ │ (被控端) │
|
||||
└───────┬───────┘ └───────┬───────┘ └───────────────┘
|
||||
│ │
|
||||
│ P2P Connection │
|
||||
│◄────────────────────────►│
|
||||
│ │
|
||||
│ (如果 P2P 失败) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
└─►│ Relay Server │◄──────┘
|
||||
│ (hbbr) │
|
||||
│ Port: 21117 │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
## 服务器组件详解
|
||||
|
||||
### Rendezvous Server (hbbs)
|
||||
|
||||
**监听端口:**
|
||||
| 端口 | 协议 | 用途 |
|
||||
|------|------|------|
|
||||
| 21116 | TCP | 主要通信端口,处理 punch hole 请求 |
|
||||
| 21116 | UDP | Peer 注册、NAT 类型检测 |
|
||||
| 21115 | TCP | NAT 测试专用端口 |
|
||||
| 21118 | WebSocket | Web 客户端支持 |
|
||||
|
||||
**核心数据结构:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:64-83
|
||||
pub struct RendezvousServer {
|
||||
tcp_punch: Arc<Mutex<HashMap<SocketAddr, Sink>>>, // TCP punch hole 连接
|
||||
pm: PeerMap, // Peer 映射表
|
||||
tx: Sender, // 消息发送通道
|
||||
relay_servers: Arc<RelayServers>, // 可用 Relay 服务器列表
|
||||
relay_servers0: Arc<RelayServers>, // 原始 Relay 服务器列表
|
||||
rendezvous_servers: Arc<Vec<String>>, // Rendezvous 服务器列表
|
||||
inner: Arc<Inner>, // 内部配置
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
serial: i32, // 配置序列号
|
||||
version: String, // 软件版本
|
||||
software_url: String, // 软件更新 URL
|
||||
mask: Option<Ipv4Network>, // LAN 掩码
|
||||
local_ip: String, // 本地 IP
|
||||
sk: Option<sign::SecretKey>, // 服务器签名密钥
|
||||
}
|
||||
```
|
||||
|
||||
**Peer 数据结构:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/peer.rs:32-42
|
||||
pub struct Peer {
|
||||
pub socket_addr: SocketAddr, // 最后注册的地址
|
||||
pub last_reg_time: Instant, // 最后注册时间
|
||||
pub guid: Vec<u8>, // 数据库 GUID
|
||||
pub uuid: Bytes, // 设备 UUID
|
||||
pub pk: Bytes, // 公钥
|
||||
pub info: PeerInfo, // Peer 信息
|
||||
pub reg_pk: (u32, Instant), // 注册频率限制
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Server (hbbr)
|
||||
|
||||
**监听端口:**
|
||||
| 端口 | 协议 | 用途 |
|
||||
|------|------|------|
|
||||
| 21117 | TCP | 主要中转端口 |
|
||||
| 21119 | WebSocket | Web 客户端支持 |
|
||||
|
||||
**核心特性:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:40-44
|
||||
static DOWNGRADE_THRESHOLD_100: AtomicUsize = AtomicUsize::new(66); // 降级阈值
|
||||
static DOWNGRADE_START_CHECK: AtomicUsize = AtomicUsize::new(1_800_000); // 检测开始时间(ms)
|
||||
static LIMIT_SPEED: AtomicUsize = AtomicUsize::new(32 * 1024 * 1024); // 限速(bit/s)
|
||||
static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024);// 总带宽
|
||||
static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024);// 单连接带宽
|
||||
```
|
||||
|
||||
## 客户端架构
|
||||
|
||||
### 核心模块
|
||||
|
||||
```
|
||||
rustdesk/src/
|
||||
├── rendezvous_mediator.rs # Rendezvous 服务器通信
|
||||
├── client.rs # 控制端核心逻辑
|
||||
├── server/
|
||||
│ ├── mod.rs # 被控端服务
|
||||
│ ├── connection.rs # 连接处理
|
||||
│ ├── video_service.rs # 视频服务
|
||||
│ ├── audio_service.rs # 音频服务
|
||||
│ └── input_service.rs # 输入服务
|
||||
├── common.rs # 通用函数(加密、解密)
|
||||
└── platform/ # 平台特定代码
|
||||
```
|
||||
|
||||
### RendezvousMediator
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:44-50
|
||||
pub struct RendezvousMediator {
|
||||
addr: TargetAddr<'static>, // 服务器地址
|
||||
host: String, // 服务器主机名
|
||||
host_prefix: String, // 主机前缀
|
||||
keep_alive: i32, // 保活间隔
|
||||
}
|
||||
```
|
||||
|
||||
**两种连接模式:**
|
||||
|
||||
1. **UDP 模式** (默认):
|
||||
- 用于 Peer 注册和心跳
|
||||
- 更低延迟
|
||||
- 可能被某些防火墙阻止
|
||||
|
||||
2. **TCP 模式**:
|
||||
- 用于代理环境
|
||||
- WebSocket 模式
|
||||
- 更可靠
|
||||
|
||||
## 连接流程概述
|
||||
|
||||
### 被控端启动流程
|
||||
|
||||
```
|
||||
1. 生成设备 ID 和密钥对
|
||||
2. 连接 Rendezvous Server
|
||||
3. 发送 RegisterPeer 消息
|
||||
4. 如果需要,发送 RegisterPk 注册公钥
|
||||
5. 定期发送心跳保持在线状态
|
||||
6. 等待 PunchHole 或 RequestRelay 请求
|
||||
```
|
||||
|
||||
### 控制端连接流程
|
||||
|
||||
```
|
||||
1. 输入目标设备 ID
|
||||
2. 连接 Rendezvous Server
|
||||
3. 发送 PunchHoleRequest 消息
|
||||
4. 根据响应决定连接方式:
|
||||
a. 直连 (P2P): 使用 PunchHole 信息尝试打洞
|
||||
b. 局域网: 使用 LocalAddr 信息直连
|
||||
c. 中转: 通过 Relay Server 连接
|
||||
5. 建立安全加密通道
|
||||
6. 发送 LoginRequest 进行身份验证
|
||||
7. 开始远程控制会话
|
||||
```
|
||||
|
||||
## 数据流
|
||||
|
||||
### 视频流
|
||||
|
||||
```
|
||||
被控端 控制端
|
||||
│ │
|
||||
│ VideoFrame (H264/VP9/...) │
|
||||
├─────────────────────────────────►│
|
||||
│ │
|
||||
│ 加密 → 传输 → 解密 → 解码 → 显示 │
|
||||
```
|
||||
|
||||
### 输入流
|
||||
|
||||
```
|
||||
控制端 被控端
|
||||
│ │
|
||||
│ MouseEvent/KeyEvent │
|
||||
├─────────────────────────────────►│
|
||||
│ │
|
||||
│ 加密 → 传输 → 解密 → 模拟输入 │
|
||||
```
|
||||
|
||||
## 高可用设计
|
||||
|
||||
### 多服务器支持
|
||||
|
||||
- 客户端可配置多个 Rendezvous Server
|
||||
- 自动选择延迟最低的服务器
|
||||
- 连接失败时自动切换备用服务器
|
||||
|
||||
### Relay Server 选择
|
||||
|
||||
- 支持配置多个 Relay Server
|
||||
- 轮询算法分配负载
|
||||
- 定期检查 Relay Server 可用性
|
||||
|
||||
### 重连机制
|
||||
|
||||
```rust
|
||||
// 连接超时和重试参数
|
||||
const REG_INTERVAL: i64 = 12_000; // 注册间隔 12 秒
|
||||
const REG_TIMEOUT: i32 = 30_000; // 注册超时 30 秒
|
||||
const CONNECT_TIMEOUT: u64 = 18_000; // 连接超时 18 秒
|
||||
```
|
||||
438
docs/report/rustdesk/02-rendezvous-protocol.md
Normal file
438
docs/report/rustdesk/02-rendezvous-protocol.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# Rendezvous 服务器协议
|
||||
|
||||
## 概述
|
||||
|
||||
Rendezvous Server(hbbs)是 RustDesk 的核心协调服务器,负责:
|
||||
- Peer ID 注册和发现
|
||||
- 公钥存储和分发
|
||||
- NAT 类型检测
|
||||
- P2P 连接协调(打洞辅助)
|
||||
- Relay Server 分配
|
||||
|
||||
## 协议消息定义
|
||||
|
||||
所有消息使用 Protocol Buffers 定义在 `protos/rendezvous.proto`:
|
||||
|
||||
```protobuf
|
||||
message RendezvousMessage {
|
||||
oneof union {
|
||||
RegisterPeer register_peer = 6;
|
||||
RegisterPeerResponse register_peer_response = 7;
|
||||
PunchHoleRequest punch_hole_request = 8;
|
||||
PunchHole punch_hole = 9;
|
||||
PunchHoleSent punch_hole_sent = 10;
|
||||
PunchHoleResponse punch_hole_response = 11;
|
||||
FetchLocalAddr fetch_local_addr = 12;
|
||||
LocalAddr local_addr = 13;
|
||||
ConfigUpdate configure_update = 14;
|
||||
RegisterPk register_pk = 15;
|
||||
RegisterPkResponse register_pk_response = 16;
|
||||
SoftwareUpdate software_update = 17;
|
||||
RequestRelay request_relay = 18;
|
||||
RelayResponse relay_response = 19;
|
||||
TestNatRequest test_nat_request = 20;
|
||||
TestNatResponse test_nat_response = 21;
|
||||
PeerDiscovery peer_discovery = 22;
|
||||
OnlineRequest online_request = 23;
|
||||
OnlineResponse online_response = 24;
|
||||
KeyExchange key_exchange = 25;
|
||||
HealthCheck hc = 26;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 核心流程
|
||||
|
||||
### 1. Peer 注册流程
|
||||
|
||||
**客户端 → 服务器:RegisterPeer**
|
||||
|
||||
```protobuf
|
||||
message RegisterPeer {
|
||||
string id = 1; // Peer ID (如 "123456789")
|
||||
int32 serial = 2; // 配置序列号
|
||||
}
|
||||
```
|
||||
|
||||
**服务器处理逻辑:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:318-333
|
||||
Some(rendezvous_message::Union::RegisterPeer(rp)) => {
|
||||
if !rp.id.is_empty() {
|
||||
log::trace!("New peer registered: {:?} {:?}", &rp.id, &addr);
|
||||
self.update_addr(rp.id, addr, socket).await?;
|
||||
// 如果服务器配置更新,发送 ConfigUpdate
|
||||
if self.inner.serial > rp.serial {
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_configure_update(ConfigUpdate {
|
||||
serial: self.inner.serial,
|
||||
rendezvous_servers: (*self.rendezvous_servers).clone(),
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, addr).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:RegisterPeerResponse**
|
||||
|
||||
```protobuf
|
||||
message RegisterPeerResponse {
|
||||
bool request_pk = 2; // 是否需要注册公钥
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 公钥注册流程
|
||||
|
||||
当服务器检测到 Peer 的公钥为空或 IP 变化时,会请求注册公钥。
|
||||
|
||||
**客户端 → 服务器:RegisterPk**
|
||||
|
||||
```protobuf
|
||||
message RegisterPk {
|
||||
string id = 1; // Peer ID
|
||||
bytes uuid = 2; // 设备 UUID
|
||||
bytes pk = 3; // Ed25519 公钥
|
||||
string old_id = 4; // 旧 ID(如果更换)
|
||||
}
|
||||
```
|
||||
|
||||
**服务器处理逻辑:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:334-418
|
||||
Some(rendezvous_message::Union::RegisterPk(rk)) => {
|
||||
// 验证 UUID 和公钥
|
||||
if rk.uuid.is_empty() || rk.pk.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let id = rk.id;
|
||||
let ip = addr.ip().to_string();
|
||||
|
||||
// ID 长度检查
|
||||
if id.len() < 6 {
|
||||
return send_rk_res(socket, addr, UUID_MISMATCH).await;
|
||||
}
|
||||
|
||||
// IP 封锁检查
|
||||
if !self.check_ip_blocker(&ip, &id).await {
|
||||
return send_rk_res(socket, addr, TOO_FREQUENT).await;
|
||||
}
|
||||
|
||||
// UUID 匹配验证
|
||||
let peer = self.pm.get_or(&id).await;
|
||||
// ... UUID 验证逻辑 ...
|
||||
|
||||
// 更新数据库
|
||||
if changed {
|
||||
self.pm.update_pk(id, peer, addr, rk.uuid, rk.pk, ip).await;
|
||||
}
|
||||
|
||||
// 发送成功响应
|
||||
msg_out.set_register_pk_response(RegisterPkResponse {
|
||||
result: register_pk_response::Result::OK.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:RegisterPkResponse**
|
||||
|
||||
```protobuf
|
||||
message RegisterPkResponse {
|
||||
enum Result {
|
||||
OK = 0;
|
||||
UUID_MISMATCH = 2;
|
||||
ID_EXISTS = 3;
|
||||
TOO_FREQUENT = 4;
|
||||
INVALID_ID_FORMAT = 5;
|
||||
NOT_SUPPORT = 6;
|
||||
SERVER_ERROR = 7;
|
||||
}
|
||||
Result result = 1;
|
||||
int32 keep_alive = 2; // 心跳间隔
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Punch Hole 请求流程
|
||||
|
||||
当控制端要连接被控端时,首先发送 PunchHoleRequest。
|
||||
|
||||
**控制端 → 服务器:PunchHoleRequest**
|
||||
|
||||
```protobuf
|
||||
message PunchHoleRequest {
|
||||
string id = 1; // 目标 Peer ID
|
||||
NatType nat_type = 2; // 请求方的 NAT 类型
|
||||
string licence_key = 3; // 许可证密钥
|
||||
ConnType conn_type = 4; // 连接类型
|
||||
string token = 5; // 认证令牌
|
||||
string version = 6; // 客户端版本
|
||||
}
|
||||
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0;
|
||||
ASYMMETRIC = 1;
|
||||
SYMMETRIC = 2;
|
||||
}
|
||||
|
||||
enum ConnType {
|
||||
DEFAULT_CONN = 0;
|
||||
FILE_TRANSFER = 1;
|
||||
PORT_FORWARD = 2;
|
||||
RDP = 3;
|
||||
VIEW_CAMERA = 4;
|
||||
}
|
||||
```
|
||||
|
||||
**服务器处理逻辑:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:674-765
|
||||
async fn handle_punch_hole_request(...) {
|
||||
// 1. 验证许可证密钥
|
||||
if !key.is_empty() && ph.licence_key != key {
|
||||
return Ok((PunchHoleResponse { failure: LICENSE_MISMATCH }, None));
|
||||
}
|
||||
|
||||
// 2. 查找目标 Peer
|
||||
if let Some(peer) = self.pm.get(&id).await {
|
||||
let (elapsed, peer_addr) = peer.read().await;
|
||||
|
||||
// 3. 检查在线状态
|
||||
if elapsed >= REG_TIMEOUT {
|
||||
return Ok((PunchHoleResponse { failure: OFFLINE }, None));
|
||||
}
|
||||
|
||||
// 4. 判断是否同一局域网
|
||||
let same_intranet = (peer_is_lan && is_lan) ||
|
||||
(peer_addr.ip() == addr.ip());
|
||||
|
||||
if same_intranet {
|
||||
// 请求获取本地地址
|
||||
msg_out.set_fetch_local_addr(FetchLocalAddr {
|
||||
socket_addr: AddrMangle::encode(addr).into(),
|
||||
relay_server,
|
||||
});
|
||||
} else {
|
||||
// 发送 Punch Hole 请求给被控端
|
||||
msg_out.set_punch_hole(PunchHole {
|
||||
socket_addr: AddrMangle::encode(addr).into(),
|
||||
nat_type: ph.nat_type,
|
||||
relay_server,
|
||||
});
|
||||
}
|
||||
return Ok((msg_out, Some(peer_addr)));
|
||||
}
|
||||
|
||||
// Peer 不存在
|
||||
Ok((PunchHoleResponse { failure: ID_NOT_EXIST }, None))
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 被控端:PunchHole 或 FetchLocalAddr**
|
||||
|
||||
```protobuf
|
||||
message PunchHole {
|
||||
bytes socket_addr = 1; // 控制端地址(编码)
|
||||
string relay_server = 2; // Relay 服务器地址
|
||||
NatType nat_type = 3; // 控制端 NAT 类型
|
||||
}
|
||||
|
||||
message FetchLocalAddr {
|
||||
bytes socket_addr = 1; // 控制端地址(编码)
|
||||
string relay_server = 2; // Relay 服务器地址
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 被控端响应流程
|
||||
|
||||
**被控端 → 服务器:PunchHoleSent 或 LocalAddr**
|
||||
|
||||
```protobuf
|
||||
message PunchHoleSent {
|
||||
bytes socket_addr = 1; // 控制端地址
|
||||
string id = 2; // 被控端 ID
|
||||
string relay_server = 3; // Relay 服务器
|
||||
NatType nat_type = 4; // 被控端 NAT 类型
|
||||
string version = 5; // 客户端版本
|
||||
}
|
||||
|
||||
message LocalAddr {
|
||||
bytes socket_addr = 1; // 控制端地址
|
||||
bytes local_addr = 2; // 被控端本地地址
|
||||
string relay_server = 3; // Relay 服务器
|
||||
string id = 4; // 被控端 ID
|
||||
string version = 5; // 客户端版本
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 控制端:PunchHoleResponse**
|
||||
|
||||
```protobuf
|
||||
message PunchHoleResponse {
|
||||
bytes socket_addr = 1; // 被控端地址
|
||||
bytes pk = 2; // 被控端公钥(已签名)
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 0;
|
||||
OFFLINE = 2;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
oneof union {
|
||||
NatType nat_type = 5;
|
||||
bool is_local = 6; // 是否为局域网连接
|
||||
}
|
||||
string other_failure = 7;
|
||||
int32 feedback = 8;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Relay 请求流程
|
||||
|
||||
当 P2P 连接失败或 NAT 类型不支持打洞时,使用 Relay。
|
||||
|
||||
**客户端 → 服务器:RequestRelay**
|
||||
|
||||
```protobuf
|
||||
message RequestRelay {
|
||||
string id = 1; // 目标 Peer ID
|
||||
string uuid = 2; // 连接 UUID(用于配对)
|
||||
bytes socket_addr = 3; // 本端地址
|
||||
string relay_server = 4; // 指定的 Relay 服务器
|
||||
bool secure = 5; // 是否使用加密
|
||||
string licence_key = 6; // 许可证密钥
|
||||
ConnType conn_type = 7; // 连接类型
|
||||
string token = 8; // 认证令牌
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:RelayResponse**
|
||||
|
||||
```protobuf
|
||||
message RelayResponse {
|
||||
bytes socket_addr = 1; // 对端地址
|
||||
string uuid = 2; // 连接 UUID
|
||||
string relay_server = 3; // Relay 服务器地址
|
||||
oneof union {
|
||||
string id = 4; // 对端 ID
|
||||
bytes pk = 5; // 对端公钥
|
||||
}
|
||||
string refuse_reason = 6; // 拒绝原因
|
||||
string version = 7; // 版本
|
||||
int32 feedback = 9;
|
||||
}
|
||||
```
|
||||
|
||||
## NAT 类型检测
|
||||
|
||||
**客户端 → 服务器:TestNatRequest**
|
||||
|
||||
```protobuf
|
||||
message TestNatRequest {
|
||||
int32 serial = 1; // 配置序列号
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:TestNatResponse**
|
||||
|
||||
```protobuf
|
||||
message TestNatResponse {
|
||||
int32 port = 1; // 观测到的源端口
|
||||
ConfigUpdate cu = 2; // 配置更新
|
||||
}
|
||||
```
|
||||
|
||||
NAT 检测原理:
|
||||
1. 客户端同时向主端口(21116)和 NAT 测试端口(21115)发送请求
|
||||
2. 比较两次响应中观测到的源端口
|
||||
3. 如果端口一致,则为 ASYMMETRIC NAT(适合打洞)
|
||||
4. 如果端口不一致,则为 SYMMETRIC NAT(需要 Relay)
|
||||
|
||||
## 地址编码
|
||||
|
||||
RustDesk 使用 `AddrMangle` 对 SocketAddr 进行编码:
|
||||
|
||||
```rust
|
||||
// 编码示例
|
||||
// IPv4: 4 bytes IP + 2 bytes port = 6 bytes
|
||||
// IPv6: 16 bytes IP + 2 bytes port = 18 bytes
|
||||
pub fn encode(addr: SocketAddr) -> Vec<u8>;
|
||||
pub fn decode(bytes: &[u8]) -> SocketAddr;
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 服务器签名
|
||||
|
||||
当服务器配置了私钥时,会对 Peer 的公钥进行签名:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:1160-1182
|
||||
async fn get_pk(&mut self, version: &str, id: String) -> Bytes {
|
||||
if version.is_empty() || self.inner.sk.is_none() {
|
||||
Bytes::new()
|
||||
} else {
|
||||
match self.pm.get(&id).await {
|
||||
Some(peer) => {
|
||||
let pk = peer.read().await.pk.clone();
|
||||
// 使用服务器私钥签名 IdPk
|
||||
sign::sign(
|
||||
&IdPk { id, pk, ..Default::default() }
|
||||
.write_to_bytes()
|
||||
.unwrap_or_default(),
|
||||
self.inner.sk.as_ref().unwrap(),
|
||||
).into()
|
||||
}
|
||||
_ => Bytes::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IP 封锁
|
||||
|
||||
服务器实现了 IP 封锁机制防止滥用:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:866-894
|
||||
async fn check_ip_blocker(&self, ip: &str, id: &str) -> bool {
|
||||
let mut lock = IP_BLOCKER.lock().await;
|
||||
if let Some(old) = lock.get_mut(ip) {
|
||||
// 每秒请求超过 30 次则封锁
|
||||
if counter.0 > 30 {
|
||||
return false;
|
||||
}
|
||||
// 每天超过 300 个不同 ID 则封锁
|
||||
if counter.0.len() > 300 {
|
||||
return !is_new;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
```
|
||||
|
||||
## 时序图
|
||||
|
||||
### 完整连接建立流程
|
||||
|
||||
```
|
||||
控制端 Rendezvous Server 被控端
|
||||
│ │ │
|
||||
│ PunchHoleRequest │ │
|
||||
├──────────────────────►│ │
|
||||
│ │ PunchHole │
|
||||
│ ├──────────────────────►│
|
||||
│ │ │
|
||||
│ │ PunchHoleSent │
|
||||
│ │◄──────────────────────┤
|
||||
│ PunchHoleResponse │ │
|
||||
│◄──────────────────────┤ │
|
||||
│ │ │
|
||||
│ ─────────── P2P Connection ──────────────────│
|
||||
│◄─────────────────────────────────────────────►│
|
||||
```
|
||||
318
docs/report/rustdesk/03-relay-protocol.md
Normal file
318
docs/report/rustdesk/03-relay-protocol.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Relay 服务器协议
|
||||
|
||||
## 概述
|
||||
|
||||
Relay Server(hbbr)是 RustDesk 的数据中转服务器,当 P2P 连接无法建立时(如双方都在 Symmetric NAT 后面),所有通信数据通过 Relay Server 转发。
|
||||
|
||||
## 服务器架构
|
||||
|
||||
### 监听端口
|
||||
|
||||
| 端口 | 协议 | 用途 |
|
||||
|------|------|------|
|
||||
| 21117 | TCP | 主要中转端口 |
|
||||
| 21119 | WebSocket | Web 客户端支持 |
|
||||
|
||||
### 核心配置
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:40-46
|
||||
static DOWNGRADE_THRESHOLD_100: AtomicUsize = AtomicUsize::new(66); // 0.66
|
||||
static DOWNGRADE_START_CHECK: AtomicUsize = AtomicUsize::new(1_800_000); // 30分钟 (ms)
|
||||
static LIMIT_SPEED: AtomicUsize = AtomicUsize::new(32 * 1024 * 1024); // 32 Mb/s
|
||||
static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024);// 1024 Mb/s
|
||||
static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024);// 128 Mb/s
|
||||
const BLACKLIST_FILE: &str = "blacklist.txt";
|
||||
const BLOCKLIST_FILE: &str = "blocklist.txt";
|
||||
```
|
||||
|
||||
## 连接配对机制
|
||||
|
||||
### 配对原理
|
||||
|
||||
Relay Server 使用 UUID 来配对两个客户端的连接:
|
||||
|
||||
1. 第一个客户端连接并发送 `RequestRelay` 消息(包含 UUID)
|
||||
2. 服务器将该连接存储在等待队列中
|
||||
3. 第二个客户端使用相同的 UUID 连接
|
||||
4. 服务器将两个连接配对,开始转发数据
|
||||
|
||||
### 配对流程
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:425-462
|
||||
async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limiter: Limiter) {
|
||||
let mut stream = stream;
|
||||
if let Ok(Some(Ok(bytes))) = timeout(30_000, stream.recv()).await {
|
||||
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
|
||||
if let Some(rendezvous_message::Union::RequestRelay(rf)) = msg_in.union {
|
||||
// 验证许可证密钥
|
||||
if !key.is_empty() && rf.licence_key != key {
|
||||
log::warn!("Relay authentication failed from {}", addr);
|
||||
return;
|
||||
}
|
||||
|
||||
if !rf.uuid.is_empty() {
|
||||
// 尝试查找配对
|
||||
let mut peer = PEERS.lock().await.remove(&rf.uuid);
|
||||
if let Some(peer) = peer.as_mut() {
|
||||
// 找到配对,开始中转
|
||||
log::info!("Relay request {} got paired", rf.uuid);
|
||||
relay(addr, &mut stream, peer, limiter).await;
|
||||
} else {
|
||||
// 没找到,存储等待配对
|
||||
log::info!("New relay request {} from {}", rf.uuid, addr);
|
||||
PEERS.lock().await.insert(rf.uuid.clone(), Box::new(stream));
|
||||
sleep(30.).await; // 等待 30 秒
|
||||
PEERS.lock().await.remove(&rf.uuid); // 超时移除
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据转发
|
||||
|
||||
### 转发逻辑
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:464-566
|
||||
async fn relay(
|
||||
addr: SocketAddr,
|
||||
stream: &mut impl StreamTrait,
|
||||
peer: &mut Box<dyn StreamTrait>,
|
||||
total_limiter: Limiter,
|
||||
) -> ResultType<()> {
|
||||
let limiter = <Limiter>::new(SINGLE_BANDWIDTH.load(Ordering::SeqCst) as f64);
|
||||
let blacklist_limiter = <Limiter>::new(LIMIT_SPEED.load(Ordering::SeqCst) as _);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 从 peer 接收数据,发送给 stream
|
||||
res = peer.recv() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
// 带宽限制
|
||||
if blacked || downgrade {
|
||||
blacklist_limiter.consume(bytes.len() * 8).await;
|
||||
} else {
|
||||
limiter.consume(bytes.len() * 8).await;
|
||||
}
|
||||
total_limiter.consume(bytes.len() * 8).await;
|
||||
stream.send_raw(bytes.into()).await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 从 stream 接收数据,发送给 peer
|
||||
res = stream.recv() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
// 带宽限制
|
||||
limiter.consume(bytes.len() * 8).await;
|
||||
total_limiter.consume(bytes.len() * 8).await;
|
||||
peer.send_raw(bytes.into()).await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = timer.tick() => {
|
||||
// 超时检测
|
||||
if last_recv_time.elapsed().as_secs() > 30 {
|
||||
bail!("Timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 降级检测
|
||||
if elapsed > DOWNGRADE_START_CHECK && total > elapsed * downgrade_threshold {
|
||||
downgrade = true;
|
||||
log::info!("Downgrade {}, exceed threshold", id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 原始模式
|
||||
|
||||
当两端都支持原始模式时,跳过 protobuf 解析以提高性能:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:440-444
|
||||
if !stream.is_ws() && !peer.is_ws() {
|
||||
peer.set_raw();
|
||||
stream.set_raw();
|
||||
log::info!("Both are raw");
|
||||
}
|
||||
```
|
||||
|
||||
## 带宽控制
|
||||
|
||||
### 多级限速
|
||||
|
||||
1. **总带宽限制**:整个服务器的总带宽
|
||||
2. **单连接限制**:每个中转连接的带宽
|
||||
3. **黑名单限速**:对黑名单 IP 的特殊限制
|
||||
|
||||
### 降级机制
|
||||
|
||||
当连接持续占用高带宽时,会触发降级:
|
||||
|
||||
```rust
|
||||
// 条件:
|
||||
// 1. 连接时间 > DOWNGRADE_START_CHECK (30分钟)
|
||||
// 2. 平均带宽 > SINGLE_BANDWIDTH * 0.66
|
||||
// 降级后使用 LIMIT_SPEED (32 Mb/s) 限速
|
||||
if elapsed > DOWNGRADE_START_CHECK.load(Ordering::SeqCst)
|
||||
&& !downgrade
|
||||
&& total > elapsed * downgrade_threshold
|
||||
{
|
||||
downgrade = true;
|
||||
}
|
||||
```
|
||||
|
||||
## 安全控制
|
||||
|
||||
### 黑名单
|
||||
|
||||
用于限速特定 IP:
|
||||
|
||||
```
|
||||
# blacklist.txt
|
||||
192.168.1.100
|
||||
10.0.0.50
|
||||
```
|
||||
|
||||
### 封锁名单
|
||||
|
||||
用于完全拒绝特定 IP:
|
||||
|
||||
```
|
||||
# blocklist.txt
|
||||
1.2.3.4
|
||||
5.6.7.8
|
||||
```
|
||||
|
||||
### 运行时管理命令
|
||||
|
||||
通过本地 TCP 连接(仅限 localhost)发送命令:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:152-324
|
||||
match fds.next() {
|
||||
Some("h") => // 帮助
|
||||
Some("blacklist-add" | "ba") => // 添加黑名单
|
||||
Some("blacklist-remove" | "br") => // 移除黑名单
|
||||
Some("blacklist" | "b") => // 查看黑名单
|
||||
Some("blocklist-add" | "Ba") => // 添加封锁名单
|
||||
Some("blocklist-remove" | "Br") => // 移除封锁名单
|
||||
Some("blocklist" | "B") => // 查看封锁名单
|
||||
Some("downgrade-threshold" | "dt") => // 设置降级阈值
|
||||
Some("downgrade-start-check" | "t") => // 设置降级检测时间
|
||||
Some("limit-speed" | "ls") => // 设置限速
|
||||
Some("total-bandwidth" | "tb") => // 设置总带宽
|
||||
Some("single-bandwidth" | "sb") => // 设置单连接带宽
|
||||
Some("usage" | "u") => // 查看使用统计
|
||||
}
|
||||
```
|
||||
|
||||
## 协议消息
|
||||
|
||||
### RequestRelay
|
||||
|
||||
用于建立中转连接的请求消息:
|
||||
|
||||
```protobuf
|
||||
message RequestRelay {
|
||||
string id = 1; // 目标 Peer ID
|
||||
string uuid = 2; // 连接 UUID(配对用)
|
||||
bytes socket_addr = 3; // 本端地址
|
||||
string relay_server = 4; // Relay 服务器
|
||||
bool secure = 5; // 是否加密
|
||||
string licence_key = 6; // 许可证密钥
|
||||
ConnType conn_type = 7; // 连接类型
|
||||
string token = 8; // 认证令牌
|
||||
}
|
||||
```
|
||||
|
||||
## 时序图
|
||||
|
||||
### 中转连接建立
|
||||
|
||||
```
|
||||
客户端 A Relay Server 客户端 B
|
||||
│ │ │
|
||||
│ RequestRelay(uuid) │ │
|
||||
├─────────────────────────►│ │
|
||||
│ │ │
|
||||
│ │ (存储等待配对) │
|
||||
│ │ │
|
||||
│ │ RequestRelay(uuid) │
|
||||
│ │◄───────────────────────────┤
|
||||
│ │ │
|
||||
│ │ (配对成功) │
|
||||
│ │ │
|
||||
│ ◄────────── 数据转发 ─────────────────────────────────►│
|
||||
│ │ │
|
||||
```
|
||||
|
||||
### 数据转发
|
||||
|
||||
```
|
||||
客户端 A Relay Server 客户端 B
|
||||
│ │ │
|
||||
│ ────[数据]───────► │ │
|
||||
│ │ ────[数据]───────► │
|
||||
│ │ │
|
||||
│ │ ◄───[数据]──────── │
|
||||
│ ◄───[数据]──────── │ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 零拷贝
|
||||
|
||||
使用 `Bytes` 类型减少内存拷贝:
|
||||
|
||||
```rust
|
||||
async fn send_raw(&mut self, bytes: Bytes) -> ResultType<()>;
|
||||
```
|
||||
|
||||
### WebSocket 支持
|
||||
|
||||
支持 WebSocket 协议以穿越防火墙:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
impl StreamTrait for tokio_tungstenite::WebSocketStream<TcpStream> {
|
||||
async fn recv(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
if let Some(msg) = self.next().await {
|
||||
match msg {
|
||||
Ok(tungstenite::Message::Binary(bytes)) => {
|
||||
Some(Ok(bytes[..].into()))
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 监控指标
|
||||
|
||||
服务器跟踪以下指标:
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| elapsed | 连接持续时间 (ms) |
|
||||
| total | 总传输数据量 (bit) |
|
||||
| highest | 最高瞬时速率 (kb/s) |
|
||||
| speed | 当前速率 (kb/s) |
|
||||
|
||||
通过 `usage` 命令查看:
|
||||
|
||||
```
|
||||
192.168.1.100:12345: 3600s 1024.00MB 50000kb/s 45000kb/s 42000kb/s
|
||||
```
|
||||
424
docs/report/rustdesk/04-p2p-connection.md
Normal file
424
docs/report/rustdesk/04-p2p-connection.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# P2P 连接流程
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 优先尝试建立 P2P 直连,只有在直连失败时才使用 Relay 中转。P2P 连接支持多种方式:
|
||||
- TCP 打洞
|
||||
- UDP 打洞(KCP)
|
||||
- 局域网直连
|
||||
- IPv6 直连
|
||||
|
||||
## 连接决策流程
|
||||
|
||||
```
|
||||
开始连接
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ 是否强制 Relay?│
|
||||
└──────┬───────┘
|
||||
是 │ 否
|
||||
┌─────────┴─────────┐
|
||||
▼ ▼
|
||||
使用 Relay 检查 NAT 类型
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
双方都是对称 NAT? 有一方是可穿透 NAT
|
||||
│ │
|
||||
是 │ │
|
||||
▼ ▼
|
||||
使用 Relay 尝试 P2P 连接
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
同一局域网? 不同网络
|
||||
│ │
|
||||
是 │ │
|
||||
▼ ▼
|
||||
局域网直连 尝试打洞
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
TCP 打洞成功? UDP 打洞成功?
|
||||
│ │
|
||||
是 │ 否 是 │ 否
|
||||
▼ │ ▼ │
|
||||
TCP P2P 连接 └───────────► KCP P2P 连接 │
|
||||
▼
|
||||
使用 Relay
|
||||
```
|
||||
|
||||
## 客户端连接入口
|
||||
|
||||
```rust
|
||||
// rustdesk/src/client.rs:188-230
|
||||
impl Client {
|
||||
pub async fn start(
|
||||
peer: &str,
|
||||
key: &str,
|
||||
token: &str,
|
||||
conn_type: ConnType,
|
||||
interface: impl Interface,
|
||||
) -> ResultType<...> {
|
||||
// 检查是否为 IP 直连
|
||||
if hbb_common::is_ip_str(peer) {
|
||||
return connect_tcp_local(check_port(peer, RELAY_PORT + 1), None, CONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
// 检查是否为域名:端口格式
|
||||
if hbb_common::is_domain_port_str(peer) {
|
||||
return connect_tcp_local(peer, None, CONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
// 通过 Rendezvous Server 连接
|
||||
let (rendezvous_server, servers, _) = crate::get_rendezvous_server(1_000).await;
|
||||
Self::_start_inner(peer, key, token, conn_type, interface, rendezvous_server, servers).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 被控端处理连接请求
|
||||
|
||||
### 处理 PunchHole 消息
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:554-619
|
||||
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||
let relay_server = self.get_relay_server(ph.relay_server);
|
||||
|
||||
// 判断是否需要 Relay
|
||||
if ph.nat_type.enum_value() == Ok(NatType::SYMMETRIC)
|
||||
|| Config::get_nat_type() == NatType::SYMMETRIC as i32
|
||||
|| relay
|
||||
{
|
||||
// 使用 Relay
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
return self.create_relay(ph.socket_addr, relay_server, uuid, server, true, true).await;
|
||||
}
|
||||
|
||||
// 尝试 UDP 打洞
|
||||
if ph.udp_port > 0 {
|
||||
peer_addr.set_port(ph.udp_port as u16);
|
||||
self.punch_udp_hole(peer_addr, server, msg_punch).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 尝试 TCP 打洞
|
||||
log::debug!("Punch tcp hole to {:?}", peer_addr);
|
||||
let socket = {
|
||||
let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
// 关键步骤:尝试连接对方,让 NAT 建立映射
|
||||
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
|
||||
socket
|
||||
};
|
||||
|
||||
// 发送 PunchHoleSent 告知 Rendezvous Server
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_punch_hole_sent(PunchHoleSent {
|
||||
socket_addr: ph.socket_addr,
|
||||
id: Config::get_id(),
|
||||
relay_server,
|
||||
nat_type: nat_type.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
});
|
||||
socket.send_raw(msg_out.write_to_bytes()?).await?;
|
||||
|
||||
// 接受控制端连接
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 处理 FetchLocalAddr(局域网连接)
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:481-552
|
||||
async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
let relay_server = self.get_relay_server(fla.relay_server.clone());
|
||||
|
||||
// 尝试局域网直连
|
||||
if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() {
|
||||
if let Err(err) = self.handle_intranet_(fla.clone(), server.clone(), relay_server.clone()).await {
|
||||
log::debug!("Failed to handle intranet: {:?}, will try relay", err);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 局域网直连失败,使用 Relay
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
self.create_relay(fla.socket_addr, relay_server, uuid, server, true, true).await
|
||||
}
|
||||
|
||||
async fn handle_intranet_(&self, fla: FetchLocalAddr, server: ServerPtr, relay_server: String) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
|
||||
// 发送本地地址给 Rendezvous Server
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_local_addr(LocalAddr {
|
||||
id: Config::get_id(),
|
||||
socket_addr: AddrMangle::encode(peer_addr).into(),
|
||||
local_addr: AddrMangle::encode(local_addr).into(),
|
||||
relay_server,
|
||||
version: crate::VERSION.to_owned(),
|
||||
});
|
||||
socket.send_raw(msg_out.write_to_bytes()?).await?;
|
||||
|
||||
// 接受连接
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## UDP 打洞 (KCP)
|
||||
|
||||
### 打洞原理
|
||||
|
||||
UDP 打洞利用 NAT 的端口映射特性:
|
||||
|
||||
1. A 向 Rendezvous Server 注册,NAT 创建映射 `A_internal:port1 → A_external:port2`
|
||||
2. B 同样注册,创建映射 `B_internal:port3 → B_external:port4`
|
||||
3. A 向 B 的外部地址发送 UDP 包,A 的 NAT 创建到 B 的映射
|
||||
4. B 向 A 的外部地址发送 UDP 包,B 的 NAT 创建到 A 的映射
|
||||
5. 如果 NAT 不是 Symmetric 类型,双方的包可以到达对方
|
||||
|
||||
```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);
|
||||
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.clone(), peer_addr, peer_addr, server).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### KCP 协议
|
||||
|
||||
RustDesk 在 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(())
|
||||
}
|
||||
```
|
||||
|
||||
## TCP 打洞
|
||||
|
||||
### 原理
|
||||
|
||||
TCP 打洞比 UDP 更难,因为 TCP 需要三次握手。基本思路:
|
||||
|
||||
1. A 和 B 都尝试同时向对方发起连接
|
||||
2. 第一个 SYN 包会被对方的 NAT 丢弃(因为没有映射)
|
||||
3. 但这个 SYN 包会在 A 的 NAT 上创建映射
|
||||
4. 当 B 的 SYN 包到达 A 的 NAT 时,由于已有映射,会被转发给 A
|
||||
5. 连接建立
|
||||
|
||||
### 实现
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:604-617
|
||||
log::debug!("Punch tcp hole to {:?}", peer_addr);
|
||||
let mut socket = {
|
||||
let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
// 关键:使用相同的本地地址尝试连接对方
|
||||
// 这会在 NAT 上创建映射,使对方的连接请求能够到达
|
||||
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
|
||||
socket
|
||||
};
|
||||
```
|
||||
|
||||
## Relay 连接
|
||||
|
||||
当 P2P 失败时,使用 Relay:
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:434-479
|
||||
async fn create_relay(
|
||||
&self,
|
||||
socket_addr: Vec<u8>,
|
||||
relay_server: String,
|
||||
uuid: String,
|
||||
server: ServerPtr,
|
||||
secure: bool,
|
||||
initiate: bool,
|
||||
) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&socket_addr);
|
||||
log::info!(
|
||||
"create_relay requested from {:?}, relay_server: {}, uuid: {}, secure: {}",
|
||||
peer_addr, relay_server, uuid, secure,
|
||||
);
|
||||
|
||||
// 连接 Rendezvous Server 发送 RelayResponse
|
||||
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let mut msg_out = Message::new();
|
||||
let mut rr = RelayResponse {
|
||||
socket_addr: socket_addr.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
if initiate {
|
||||
rr.uuid = uuid.clone();
|
||||
rr.relay_server = relay_server.clone();
|
||||
rr.set_id(Config::get_id());
|
||||
}
|
||||
msg_out.set_relay_response(rr);
|
||||
socket.send(&msg_out).await?;
|
||||
|
||||
// 连接 Relay Server
|
||||
crate::create_relay_connection(
|
||||
server,
|
||||
relay_server,
|
||||
uuid,
|
||||
peer_addr,
|
||||
secure,
|
||||
is_ipv4(&self.addr),
|
||||
).await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## IPv6 支持
|
||||
|
||||
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()
|
||||
}
|
||||
```
|
||||
|
||||
## 连接状态机
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────┐ ┌────┴────┐
|
||||
│ 等待连接 │──────PunchHoleRequest──────►│正在连接 │
|
||||
└───────────┘ └────┬────┘
|
||||
│
|
||||
┌──────────────────────────────┼──────────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ P2P TCP │ │ P2P UDP/KCP │ │ Relay │
|
||||
│ 连接中 │ │ 连接中 │ │ 连接中 │
|
||||
└─────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
成功 │ 失败 成功 │ 失败 成功 │ 失败
|
||||
│ │ │ │ │ │
|
||||
▼ │ ▼ │ ▼ │
|
||||
┌──────────┐│ ┌──────────┐│ ┌──────────┐│
|
||||
│已连接 ││ │已连接 ││ │已连接 ││
|
||||
│(直连) ││ │(UDP) ││ │(中转) ││
|
||||
└──────────┘│ └──────────┘│ └──────────┘│
|
||||
│ │ │
|
||||
└──────────────►尝试 Relay◄───┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 直接连接模式
|
||||
|
||||
用户可以配置允许直接 TCP 连接(不经过 Rendezvous Server):
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:727-792
|
||||
async fn direct_server(server: ServerPtr) {
|
||||
let mut listener = None;
|
||||
let mut port = get_direct_port(); // 默认 21118
|
||||
|
||||
loop {
|
||||
let disabled = !option2bool(OPTION_DIRECT_SERVER, &Config::get_option(OPTION_DIRECT_SERVER));
|
||||
|
||||
if !disabled && listener.is_none() {
|
||||
match hbb_common::tcp::listen_any(port as _).await {
|
||||
Ok(l) => {
|
||||
listener = Some(l);
|
||||
log::info!("Direct server listening on: {:?}", l.local_addr());
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to start direct server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(l) = listener.as_mut() {
|
||||
if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await {
|
||||
stream.set_nodelay(true).ok();
|
||||
log::info!("direct access from {}", addr);
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
crate::server::create_tcp_connection(server, stream, addr, false).await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
574
docs/report/rustdesk/05-message-format.md
Normal file
574
docs/report/rustdesk/05-message-format.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# 消息格式定义
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 使用 Protocol Buffers (protobuf) 定义所有网络消息格式。主要有两个 proto 文件:
|
||||
|
||||
- `rendezvous.proto` - Rendezvous/Relay 服务器通信消息
|
||||
- `message.proto` - 客户端之间通信消息
|
||||
|
||||
## Rendezvous 消息 (rendezvous.proto)
|
||||
|
||||
### 顶层消息
|
||||
|
||||
```protobuf
|
||||
message RendezvousMessage {
|
||||
oneof union {
|
||||
RegisterPeer register_peer = 6;
|
||||
RegisterPeerResponse register_peer_response = 7;
|
||||
PunchHoleRequest punch_hole_request = 8;
|
||||
PunchHole punch_hole = 9;
|
||||
PunchHoleSent punch_hole_sent = 10;
|
||||
PunchHoleResponse punch_hole_response = 11;
|
||||
FetchLocalAddr fetch_local_addr = 12;
|
||||
LocalAddr local_addr = 13;
|
||||
ConfigUpdate configure_update = 14;
|
||||
RegisterPk register_pk = 15;
|
||||
RegisterPkResponse register_pk_response = 16;
|
||||
SoftwareUpdate software_update = 17;
|
||||
RequestRelay request_relay = 18;
|
||||
RelayResponse relay_response = 19;
|
||||
TestNatRequest test_nat_request = 20;
|
||||
TestNatResponse test_nat_response = 21;
|
||||
PeerDiscovery peer_discovery = 22;
|
||||
OnlineRequest online_request = 23;
|
||||
OnlineResponse online_response = 24;
|
||||
KeyExchange key_exchange = 25;
|
||||
HealthCheck hc = 26;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册相关
|
||||
|
||||
```protobuf
|
||||
// Peer 注册
|
||||
message RegisterPeer {
|
||||
string id = 1; // Peer ID
|
||||
int32 serial = 2; // 配置序列号
|
||||
}
|
||||
|
||||
message RegisterPeerResponse {
|
||||
bool request_pk = 2; // 是否需要注册公钥
|
||||
}
|
||||
|
||||
// 公钥注册
|
||||
message RegisterPk {
|
||||
string id = 1; // Peer ID
|
||||
bytes uuid = 2; // 设备 UUID
|
||||
bytes pk = 3; // Ed25519 公钥
|
||||
string old_id = 4; // 旧 ID
|
||||
}
|
||||
|
||||
message RegisterPkResponse {
|
||||
enum Result {
|
||||
OK = 0;
|
||||
UUID_MISMATCH = 2;
|
||||
ID_EXISTS = 3;
|
||||
TOO_FREQUENT = 4;
|
||||
INVALID_ID_FORMAT = 5;
|
||||
NOT_SUPPORT = 6;
|
||||
SERVER_ERROR = 7;
|
||||
}
|
||||
Result result = 1;
|
||||
int32 keep_alive = 2;
|
||||
}
|
||||
```
|
||||
|
||||
### 连接协调相关
|
||||
|
||||
```protobuf
|
||||
// 连接类型
|
||||
enum ConnType {
|
||||
DEFAULT_CONN = 0;
|
||||
FILE_TRANSFER = 1;
|
||||
PORT_FORWARD = 2;
|
||||
RDP = 3;
|
||||
VIEW_CAMERA = 4;
|
||||
}
|
||||
|
||||
// NAT 类型
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0;
|
||||
ASYMMETRIC = 1; // 可打洞
|
||||
SYMMETRIC = 2; // 需要中转
|
||||
}
|
||||
|
||||
// Punch Hole 请求
|
||||
message PunchHoleRequest {
|
||||
string id = 1; // 目标 Peer ID
|
||||
NatType nat_type = 2;
|
||||
string licence_key = 3;
|
||||
ConnType conn_type = 4;
|
||||
string token = 5;
|
||||
string version = 6;
|
||||
}
|
||||
|
||||
// Punch Hole 响应
|
||||
message PunchHoleResponse {
|
||||
bytes socket_addr = 1; // 目标地址
|
||||
bytes pk = 2; // 公钥(已签名)
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 0;
|
||||
OFFLINE = 2;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
oneof union {
|
||||
NatType nat_type = 5;
|
||||
bool is_local = 6;
|
||||
}
|
||||
string other_failure = 7;
|
||||
int32 feedback = 8;
|
||||
}
|
||||
|
||||
// 服务器转发给被控端
|
||||
message PunchHole {
|
||||
bytes socket_addr = 1; // 控制端地址
|
||||
string relay_server = 2;
|
||||
NatType nat_type = 3;
|
||||
}
|
||||
|
||||
// 被控端发送给服务器
|
||||
message PunchHoleSent {
|
||||
bytes socket_addr = 1;
|
||||
string id = 2;
|
||||
string relay_server = 3;
|
||||
NatType nat_type = 4;
|
||||
string version = 5;
|
||||
}
|
||||
```
|
||||
|
||||
### Relay 相关
|
||||
|
||||
```protobuf
|
||||
// Relay 请求
|
||||
message RequestRelay {
|
||||
string id = 1;
|
||||
string uuid = 2; // 配对 UUID
|
||||
bytes socket_addr = 3;
|
||||
string relay_server = 4;
|
||||
bool secure = 5;
|
||||
string licence_key = 6;
|
||||
ConnType conn_type = 7;
|
||||
string token = 8;
|
||||
}
|
||||
|
||||
// Relay 响应
|
||||
message RelayResponse {
|
||||
bytes socket_addr = 1;
|
||||
string uuid = 2;
|
||||
string relay_server = 3;
|
||||
oneof union {
|
||||
string id = 4;
|
||||
bytes pk = 5;
|
||||
}
|
||||
string refuse_reason = 6;
|
||||
string version = 7;
|
||||
int32 feedback = 9;
|
||||
}
|
||||
```
|
||||
|
||||
## 会话消息 (message.proto)
|
||||
|
||||
### 顶层消息
|
||||
|
||||
```protobuf
|
||||
message Message {
|
||||
oneof union {
|
||||
SignedId signed_id = 3;
|
||||
PublicKey public_key = 4;
|
||||
TestDelay test_delay = 5;
|
||||
VideoFrame video_frame = 6;
|
||||
LoginRequest login_request = 7;
|
||||
LoginResponse login_response = 8;
|
||||
Hash hash = 9;
|
||||
MouseEvent mouse_event = 10;
|
||||
AudioFrame audio_frame = 11;
|
||||
CursorData cursor_data = 12;
|
||||
CursorPosition cursor_position = 13;
|
||||
uint64 cursor_id = 14;
|
||||
KeyEvent key_event = 15;
|
||||
Clipboard clipboard = 16;
|
||||
FileAction file_action = 17;
|
||||
FileResponse file_response = 18;
|
||||
Misc misc = 19;
|
||||
Cliprdr cliprdr = 20;
|
||||
MessageBox message_box = 21;
|
||||
SwitchSidesResponse switch_sides_response = 22;
|
||||
VoiceCallRequest voice_call_request = 23;
|
||||
VoiceCallResponse voice_call_response = 24;
|
||||
PeerInfo peer_info = 25;
|
||||
PointerDeviceEvent pointer_device_event = 26;
|
||||
Auth2FA auth_2fa = 27;
|
||||
MultiClipboards multi_clipboards = 28;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 认证相关
|
||||
|
||||
```protobuf
|
||||
// ID 和公钥
|
||||
message IdPk {
|
||||
string id = 1;
|
||||
bytes pk = 2;
|
||||
}
|
||||
|
||||
// 密钥交换
|
||||
message PublicKey {
|
||||
bytes asymmetric_value = 1; // X25519 公钥
|
||||
bytes symmetric_value = 2; // 加密的对称密钥
|
||||
}
|
||||
|
||||
// 签名的 ID
|
||||
message SignedId {
|
||||
bytes id = 1; // 签名的 IdPk
|
||||
}
|
||||
|
||||
// 密码哈希挑战
|
||||
message Hash {
|
||||
string salt = 1;
|
||||
string challenge = 2;
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
bytes password = 2; // 加密的密码
|
||||
string my_id = 4;
|
||||
string my_name = 5;
|
||||
OptionMessage option = 6;
|
||||
oneof union {
|
||||
FileTransfer file_transfer = 7;
|
||||
PortForward port_forward = 8;
|
||||
ViewCamera view_camera = 15;
|
||||
}
|
||||
bool video_ack_required = 9;
|
||||
uint64 session_id = 10;
|
||||
string version = 11;
|
||||
OSLogin os_login = 12;
|
||||
string my_platform = 13;
|
||||
bytes hwid = 14;
|
||||
}
|
||||
|
||||
// 登录响应
|
||||
message LoginResponse {
|
||||
oneof union {
|
||||
string error = 1;
|
||||
PeerInfo peer_info = 2;
|
||||
}
|
||||
bool enable_trusted_devices = 3;
|
||||
}
|
||||
|
||||
// 2FA 认证
|
||||
message Auth2FA {
|
||||
string code = 1;
|
||||
bytes hwid = 2;
|
||||
}
|
||||
```
|
||||
|
||||
### 视频相关
|
||||
|
||||
```protobuf
|
||||
// 编码后的视频帧
|
||||
message EncodedVideoFrame {
|
||||
bytes data = 1;
|
||||
bool key = 2; // 是否关键帧
|
||||
int64 pts = 3; // 时间戳
|
||||
}
|
||||
|
||||
message EncodedVideoFrames {
|
||||
repeated EncodedVideoFrame frames = 1;
|
||||
}
|
||||
|
||||
// 视频帧
|
||||
message VideoFrame {
|
||||
oneof union {
|
||||
EncodedVideoFrames vp9s = 6;
|
||||
RGB rgb = 7;
|
||||
YUV yuv = 8;
|
||||
EncodedVideoFrames h264s = 10;
|
||||
EncodedVideoFrames h265s = 11;
|
||||
EncodedVideoFrames vp8s = 12;
|
||||
EncodedVideoFrames av1s = 13;
|
||||
}
|
||||
int32 display = 14; // 显示器索引
|
||||
}
|
||||
|
||||
// 显示信息
|
||||
message DisplayInfo {
|
||||
sint32 x = 1;
|
||||
sint32 y = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
string name = 5;
|
||||
bool online = 6;
|
||||
bool cursor_embedded = 7;
|
||||
Resolution original_resolution = 8;
|
||||
double scale = 9;
|
||||
}
|
||||
```
|
||||
|
||||
### 输入相关
|
||||
|
||||
```protobuf
|
||||
// 鼠标事件
|
||||
message MouseEvent {
|
||||
int32 mask = 1; // 按钮掩码
|
||||
sint32 x = 2;
|
||||
sint32 y = 3;
|
||||
repeated ControlKey modifiers = 4;
|
||||
}
|
||||
|
||||
// 键盘事件
|
||||
message KeyEvent {
|
||||
bool down = 1; // 按下/释放
|
||||
bool press = 2; // 单击
|
||||
oneof union {
|
||||
ControlKey control_key = 3;
|
||||
uint32 chr = 4; // 字符码
|
||||
uint32 unicode = 5; // Unicode
|
||||
string seq = 6; // 字符序列
|
||||
uint32 win2win_hotkey = 7;
|
||||
}
|
||||
repeated ControlKey modifiers = 8;
|
||||
KeyboardMode mode = 9;
|
||||
}
|
||||
|
||||
// 键盘模式
|
||||
enum KeyboardMode {
|
||||
Legacy = 0;
|
||||
Map = 1;
|
||||
Translate = 2;
|
||||
Auto = 3;
|
||||
}
|
||||
|
||||
// 控制键枚举(部分)
|
||||
enum ControlKey {
|
||||
Unknown = 0;
|
||||
Alt = 1;
|
||||
Backspace = 2;
|
||||
CapsLock = 3;
|
||||
Control = 4;
|
||||
Delete = 5;
|
||||
// ... 更多按键
|
||||
CtrlAltDel = 100;
|
||||
LockScreen = 101;
|
||||
}
|
||||
```
|
||||
|
||||
### 音频相关
|
||||
|
||||
```protobuf
|
||||
// 音频格式
|
||||
message AudioFormat {
|
||||
uint32 sample_rate = 1;
|
||||
uint32 channels = 2;
|
||||
}
|
||||
|
||||
// 音频帧
|
||||
message AudioFrame {
|
||||
bytes data = 1; // Opus 编码数据
|
||||
}
|
||||
```
|
||||
|
||||
### 剪贴板相关
|
||||
|
||||
```protobuf
|
||||
// 剪贴板格式
|
||||
enum ClipboardFormat {
|
||||
Text = 0;
|
||||
Rtf = 1;
|
||||
Html = 2;
|
||||
ImageRgba = 21;
|
||||
ImagePng = 22;
|
||||
ImageSvg = 23;
|
||||
Special = 31;
|
||||
}
|
||||
|
||||
// 剪贴板内容
|
||||
message Clipboard {
|
||||
bool compress = 1;
|
||||
bytes content = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
ClipboardFormat format = 5;
|
||||
string special_name = 6;
|
||||
}
|
||||
|
||||
message MultiClipboards {
|
||||
repeated Clipboard clipboards = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 文件传输相关
|
||||
|
||||
```protobuf
|
||||
// 文件操作
|
||||
message FileAction {
|
||||
oneof union {
|
||||
ReadDir read_dir = 1;
|
||||
FileTransferSendRequest send = 2;
|
||||
FileTransferReceiveRequest receive = 3;
|
||||
FileDirCreate create = 4;
|
||||
FileRemoveDir remove_dir = 5;
|
||||
FileRemoveFile remove_file = 6;
|
||||
ReadAllFiles all_files = 7;
|
||||
FileTransferCancel cancel = 8;
|
||||
FileTransferSendConfirmRequest send_confirm = 9;
|
||||
FileRename rename = 10;
|
||||
ReadEmptyDirs read_empty_dirs = 11;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件响应
|
||||
message FileResponse {
|
||||
oneof union {
|
||||
FileDirectory dir = 1;
|
||||
FileTransferBlock block = 2;
|
||||
FileTransferError error = 3;
|
||||
FileTransferDone done = 4;
|
||||
FileTransferDigest digest = 5;
|
||||
ReadEmptyDirsResponse empty_dirs = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件传输块
|
||||
message FileTransferBlock {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
bytes data = 3;
|
||||
bool compressed = 4;
|
||||
uint32 blk_id = 5;
|
||||
}
|
||||
|
||||
// 文件条目
|
||||
message FileEntry {
|
||||
FileType entry_type = 1;
|
||||
string name = 2;
|
||||
bool is_hidden = 3;
|
||||
uint64 size = 4;
|
||||
uint64 modified_time = 5;
|
||||
}
|
||||
```
|
||||
|
||||
### 杂项消息
|
||||
|
||||
```protobuf
|
||||
message Misc {
|
||||
oneof union {
|
||||
ChatMessage chat_message = 4;
|
||||
SwitchDisplay switch_display = 5;
|
||||
PermissionInfo permission_info = 6;
|
||||
OptionMessage option = 7;
|
||||
AudioFormat audio_format = 8;
|
||||
string close_reason = 9;
|
||||
bool refresh_video = 10;
|
||||
bool video_received = 12;
|
||||
BackNotification back_notification = 13;
|
||||
bool restart_remote_device = 14;
|
||||
// ... 更多选项
|
||||
}
|
||||
}
|
||||
|
||||
// Peer 信息
|
||||
message PeerInfo {
|
||||
string username = 1;
|
||||
string hostname = 2;
|
||||
string platform = 3;
|
||||
repeated DisplayInfo displays = 4;
|
||||
int32 current_display = 5;
|
||||
bool sas_enabled = 6;
|
||||
string version = 7;
|
||||
Features features = 9;
|
||||
SupportedEncoding encoding = 10;
|
||||
SupportedResolutions resolutions = 11;
|
||||
string platform_additions = 12;
|
||||
WindowsSessions windows_sessions = 13;
|
||||
}
|
||||
|
||||
// 选项消息
|
||||
message OptionMessage {
|
||||
enum BoolOption {
|
||||
NotSet = 0;
|
||||
No = 1;
|
||||
Yes = 2;
|
||||
}
|
||||
ImageQuality image_quality = 1;
|
||||
BoolOption lock_after_session_end = 2;
|
||||
BoolOption show_remote_cursor = 3;
|
||||
BoolOption privacy_mode = 4;
|
||||
BoolOption block_input = 5;
|
||||
int32 custom_image_quality = 6;
|
||||
BoolOption disable_audio = 7;
|
||||
BoolOption disable_clipboard = 8;
|
||||
BoolOption enable_file_transfer = 9;
|
||||
SupportedDecoding supported_decoding = 10;
|
||||
int32 custom_fps = 11;
|
||||
// ... 更多选项
|
||||
}
|
||||
```
|
||||
|
||||
## 消息编码
|
||||
|
||||
### 长度前缀
|
||||
|
||||
TCP 传输时使用长度前缀编码:
|
||||
|
||||
```rust
|
||||
// hbb_common/src/bytes_codec.rs
|
||||
pub struct BytesCodec {
|
||||
state: DecodeState,
|
||||
raw: bool,
|
||||
}
|
||||
|
||||
impl Decoder for BytesCodec {
|
||||
type Item = BytesMut;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<BytesMut>, Self::Error> {
|
||||
if self.raw {
|
||||
// 原始模式:直接返回数据
|
||||
if buf.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(buf.split()))
|
||||
}
|
||||
} else {
|
||||
// 标准模式:4 字节长度前缀 + 数据
|
||||
match self.state {
|
||||
DecodeState::Head => {
|
||||
if buf.len() < 4 {
|
||||
return Ok(None);
|
||||
}
|
||||
let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||
buf.advance(4);
|
||||
self.state = DecodeState::Data(len);
|
||||
self.decode(buf)
|
||||
}
|
||||
DecodeState::Data(len) => {
|
||||
if buf.len() < len {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = buf.split_to(len);
|
||||
self.state = DecodeState::Head;
|
||||
Ok(Some(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 加密模式
|
||||
|
||||
当启用加密时,消息结构为:
|
||||
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────────────────┐
|
||||
│ Length(4) │ Nonce(8) │ Encrypted Data(N) │
|
||||
└─────────────┴─────────────┴─────────────────────────┘
|
||||
```
|
||||
342
docs/report/rustdesk/06-encryption.md
Normal file
342
docs/report/rustdesk/06-encryption.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# 加密机制
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 使用 libsodium (sodiumoxide) 库实现端到端加密,主要包含:
|
||||
|
||||
- **Ed25519**: 用于身份签名和验证
|
||||
- **X25519**: 用于密钥交换
|
||||
- **ChaCha20-Poly1305**: 用于对称加密
|
||||
|
||||
## 密钥类型
|
||||
|
||||
### 1. 身份密钥对 (Ed25519)
|
||||
|
||||
用于 Peer 身份认证和签名:
|
||||
|
||||
```rust
|
||||
// 生成密钥对
|
||||
use sodiumoxide::crypto::sign;
|
||||
let (pk, sk) = sign::gen_keypair();
|
||||
// pk: sign::PublicKey (32 bytes)
|
||||
// sk: sign::SecretKey (64 bytes)
|
||||
```
|
||||
|
||||
### 2. 服务器签名密钥
|
||||
|
||||
Rendezvous Server 可以配置签名密钥,用于签名 Peer 公钥:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:1185-1210
|
||||
fn get_server_sk(key: &str) -> (String, Option<sign::SecretKey>) {
|
||||
let mut out_sk = None;
|
||||
let mut key = key.to_owned();
|
||||
|
||||
// 如果是 base64 编码的私钥
|
||||
if let Ok(sk) = base64::decode(&key) {
|
||||
if sk.len() == sign::SECRETKEYBYTES {
|
||||
log::info!("The key is a crypto private key");
|
||||
key = base64::encode(&sk[(sign::SECRETKEYBYTES / 2)..]); // 公钥部分
|
||||
let mut tmp = [0u8; sign::SECRETKEYBYTES];
|
||||
tmp[..].copy_from_slice(&sk);
|
||||
out_sk = Some(sign::SecretKey(tmp));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是占位符,生成新密钥对
|
||||
if key.is_empty() || key == "-" || key == "_" {
|
||||
let (pk, sk) = crate::common::gen_sk(0);
|
||||
out_sk = sk;
|
||||
if !key.is_empty() {
|
||||
key = pk;
|
||||
}
|
||||
}
|
||||
|
||||
if !key.is_empty() {
|
||||
log::info!("Key: {}", key);
|
||||
}
|
||||
(key, out_sk)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 会话密钥 (X25519 + ChaCha20)
|
||||
|
||||
用于客户端之间的加密通信:
|
||||
|
||||
```rust
|
||||
// hbb_common/src/tcp.rs:27-28
|
||||
#[derive(Clone)]
|
||||
pub struct Encrypt(pub Key, pub u64, pub u64);
|
||||
// Key: secretbox::Key (32 bytes)
|
||||
// u64: 发送计数器
|
||||
// u64: 接收计数器
|
||||
```
|
||||
|
||||
## 密钥交换流程
|
||||
|
||||
### 1. 身份验证
|
||||
|
||||
客户端首先交换签名的身份:
|
||||
|
||||
```protobuf
|
||||
message IdPk {
|
||||
string id = 1; // Peer ID
|
||||
bytes pk = 2; // Ed25519 公钥
|
||||
}
|
||||
|
||||
message SignedId {
|
||||
bytes id = 1; // 签名的 IdPk (by server or self)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. X25519 密钥交换
|
||||
|
||||
使用 X25519 ECDH 生成共享密钥:
|
||||
|
||||
```rust
|
||||
// 生成临时密钥对
|
||||
use sodiumoxide::crypto::box_;
|
||||
let (our_pk, our_sk) = box_::gen_keypair();
|
||||
|
||||
// 计算共享密钥
|
||||
let shared_secret = box_::curve25519xsalsa20poly1305::scalarmult(&our_sk, &their_pk);
|
||||
|
||||
// 派生对称密钥
|
||||
let symmetric_key = secretbox::Key::from_slice(&shared_secret[..32]).unwrap();
|
||||
```
|
||||
|
||||
### 3. 对称密钥消息
|
||||
|
||||
```protobuf
|
||||
message PublicKey {
|
||||
bytes asymmetric_value = 1; // X25519 公钥
|
||||
bytes symmetric_value = 2; // 加密的对称密钥(用于额外安全)
|
||||
}
|
||||
```
|
||||
|
||||
## 会话加密
|
||||
|
||||
### 加密实现
|
||||
|
||||
```rust
|
||||
// hbb_common/src/tcp.rs
|
||||
impl Encrypt {
|
||||
pub fn new(key: Key) -> Self {
|
||||
Self(key, 0, 0) // 初始化计数器为 0
|
||||
}
|
||||
|
||||
// 加密
|
||||
pub fn enc(&mut self, data: &[u8]) -> Vec<u8> {
|
||||
self.1 += 1; // 递增发送计数器
|
||||
let nonce = self.get_nonce(self.1);
|
||||
let encrypted = secretbox::seal(data, &nonce, &self.0);
|
||||
|
||||
// 格式: nonce (8 bytes) + encrypted data
|
||||
let mut result = Vec::with_capacity(8 + encrypted.len());
|
||||
result.extend_from_slice(&self.1.to_le_bytes());
|
||||
result.extend_from_slice(&encrypted);
|
||||
result
|
||||
}
|
||||
|
||||
// 解密
|
||||
pub fn dec(&mut self, data: &mut BytesMut) -> io::Result<()> {
|
||||
if data.len() < 8 + secretbox::MACBYTES {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "too short"));
|
||||
}
|
||||
|
||||
// 提取 nonce
|
||||
let counter = u64::from_le_bytes(data[..8].try_into().unwrap());
|
||||
|
||||
// 防重放攻击检查
|
||||
if counter <= self.2 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "replay attack"));
|
||||
}
|
||||
self.2 = counter;
|
||||
|
||||
let nonce = self.get_nonce(counter);
|
||||
let plaintext = secretbox::open(&data[8..], &nonce, &self.0)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decrypt failed"))?;
|
||||
|
||||
data.clear();
|
||||
data.extend_from_slice(&plaintext);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_nonce(&self, counter: u64) -> Nonce {
|
||||
let mut nonce = [0u8; 24];
|
||||
nonce[..8].copy_from_slice(&counter.to_le_bytes());
|
||||
Nonce(nonce)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消息格式
|
||||
|
||||
加密后的消息结构:
|
||||
|
||||
```
|
||||
┌──────────────────┬─────────────────────────────────────────┐
|
||||
│ Counter (8B) │ Encrypted Data + MAC (N+16 bytes) │
|
||||
└──────────────────┴─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 密码验证
|
||||
|
||||
### 挑战-响应机制
|
||||
|
||||
被控端生成随机盐和挑战,控制端计算哈希响应:
|
||||
|
||||
```protobuf
|
||||
message Hash {
|
||||
string salt = 1; // 随机盐
|
||||
string challenge = 2; // 随机挑战
|
||||
}
|
||||
```
|
||||
|
||||
### 密码处理
|
||||
|
||||
```rust
|
||||
// 客户端计算密码哈希
|
||||
fn get_password_hash(password: &str, salt: &str) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(salt.as_bytes());
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
// 发送加密的密码(使用对称密钥加密)
|
||||
fn encrypt_password(password_hash: &[u8], symmetric_key: &Key) -> Vec<u8> {
|
||||
secretbox::seal(password_hash, &nonce, symmetric_key)
|
||||
}
|
||||
```
|
||||
|
||||
## 服务器公钥验证
|
||||
|
||||
### 签名验证
|
||||
|
||||
如果 Rendezvous Server 配置了密钥,会签名 Peer 公钥:
|
||||
|
||||
```rust
|
||||
// 服务器签名 IdPk
|
||||
let signed_id_pk = sign::sign(
|
||||
&IdPk { id, pk, ..Default::default() }
|
||||
.write_to_bytes()?,
|
||||
&server_sk,
|
||||
);
|
||||
|
||||
// 客户端验证
|
||||
fn verify_server_signature(signed_pk: &[u8], server_pk: &sign::PublicKey) -> Option<IdPk> {
|
||||
if let Ok(verified) = sign::verify(signed_pk, server_pk) {
|
||||
return IdPk::parse_from_bytes(&verified).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
### 客户端获取服务器公钥
|
||||
|
||||
```rust
|
||||
pub async fn get_rs_pk(id: &str) -> ResultType<(String, sign::PublicKey)> {
|
||||
// 从配置或 Rendezvous Server 获取公钥
|
||||
let key = Config::get_option("key");
|
||||
if !key.is_empty() {
|
||||
if let Ok(pk) = base64::decode(&key) {
|
||||
if pk.len() == sign::PUBLICKEYBYTES {
|
||||
return Ok((key, sign::PublicKey::from_slice(&pk).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... 从服务器获取
|
||||
}
|
||||
```
|
||||
|
||||
## TCP 连接加密
|
||||
|
||||
### 安全 TCP 握手
|
||||
|
||||
```rust
|
||||
// rustdesk/src/common.rs
|
||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
// 1. 生成临时 X25519 密钥对
|
||||
let (our_pk, our_sk) = box_::gen_keypair();
|
||||
|
||||
// 2. 发送我们的公钥
|
||||
let mut msg = Message::new();
|
||||
msg.set_public_key(PublicKey {
|
||||
asymmetric_value: our_pk.0.to_vec().into(),
|
||||
..Default::default()
|
||||
});
|
||||
conn.send(&msg).await?;
|
||||
|
||||
// 3. 接收对方公钥
|
||||
let msg = conn.next_timeout(CONNECT_TIMEOUT).await?
|
||||
.ok_or_else(|| anyhow!("timeout"))?;
|
||||
let their_pk = msg.get_public_key();
|
||||
|
||||
// 4. 计算共享密钥
|
||||
let shared = box_::curve25519xsalsa20poly1305::scalarmult(
|
||||
&our_sk,
|
||||
&box_::PublicKey::from_slice(&their_pk.asymmetric_value)?,
|
||||
);
|
||||
|
||||
// 5. 设置加密
|
||||
conn.set_key(secretbox::Key::from_slice(&shared[..32]).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 安全特性
|
||||
|
||||
### 1. 前向保密
|
||||
|
||||
每个会话使用临时密钥对,即使长期密钥泄露,历史会话仍然安全。
|
||||
|
||||
### 2. 重放攻击防护
|
||||
|
||||
使用递增计数器作为 nonce 的一部分,拒绝旧的或重复的消息。
|
||||
|
||||
### 3. 中间人攻击防护
|
||||
|
||||
- 服务器签名 Peer 公钥
|
||||
- 可配置服务器公钥验证
|
||||
|
||||
### 4. 密码暴力破解防护
|
||||
|
||||
- 使用盐和多次哈希
|
||||
- 服务器端限流
|
||||
|
||||
## 加密算法参数
|
||||
|
||||
| 算法 | 密钥大小 | Nonce 大小 | MAC 大小 |
|
||||
|------|----------|------------|----------|
|
||||
| Ed25519 | 64 bytes (private), 32 bytes (public) | N/A | 64 bytes |
|
||||
| X25519 | 32 bytes | N/A | N/A |
|
||||
| ChaCha20-Poly1305 | 32 bytes | 24 bytes | 16 bytes |
|
||||
|
||||
## 密钥生命周期
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 长期密钥 (Ed25519) │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ 设备首次启动时生成 │ │
|
||||
│ │ 存储在配置文件中 │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 会话密钥 (X25519) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 每次连接时生成 │───►│ 用于密钥协商 │ │
|
||||
│ │ 临时密钥对 │ │ 派生对称密钥 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 对称密钥 (ChaCha20-Poly1305) │ │
|
||||
│ │ 用于会话中的所有消息加密 │ │
|
||||
│ │ 会话结束时销毁 │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
410
docs/report/rustdesk/07-nat-traversal.md
Normal file
410
docs/report/rustdesk/07-nat-traversal.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# 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 |
|
||||
401
docs/report/rustdesk/08-onekvm-comparison.md
Normal file
401
docs/report/rustdesk/08-onekvm-comparison.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# RustDesk 协议 vs One-KVM 实现对比分析
|
||||
|
||||
本文档对比分析 RustDesk 原始协议与 One-KVM 的实现差异。
|
||||
|
||||
## 1. 概述
|
||||
|
||||
One-KVM 作为 IP-KVM 解决方案,只实现了 RustDesk 协议的**被控端(Controlled)** 功能,不实现控制端(Controller)功能。这是设计决策,因为 KVM 设备只需要接收远程控制,不需要控制其他设备。
|
||||
|
||||
### 架构差异
|
||||
|
||||
| 方面 | RustDesk 原版 | One-KVM |
|
||||
|------|---------------|---------|
|
||||
| 角色 | 双向(控制端+被控端) | 单向(仅被控端) |
|
||||
| 连接方式 | P2P + Relay | 仅 Relay (TCP) |
|
||||
| NAT 穿透 | UDP/TCP 打洞 + TURN | 不支持 |
|
||||
| 传输协议 | UDP/TCP | 仅 TCP |
|
||||
|
||||
## 2. 已实现功能
|
||||
|
||||
### 2.1 Rendezvous 协议 (hbbs 通信)
|
||||
|
||||
| 消息类型 | 实现状态 | 备注 |
|
||||
|----------|----------|------|
|
||||
| RegisterPeer | ✅ 已实现 | 注册设备到服务器 |
|
||||
| RegisterPeerResponse | ✅ 已实现 | 处理注册响应 |
|
||||
| RegisterPk | ✅ 已实现 | 注册公钥 |
|
||||
| RegisterPkResponse | ✅ 已实现 | 处理公钥注册响应 |
|
||||
| PunchHoleSent | ✅ 已实现 | 响应打洞请求 |
|
||||
| FetchLocalAddr | ✅ 已实现 | 获取本地地址 |
|
||||
| LocalAddr | ✅ 已实现 | 返回本地地址 |
|
||||
| RequestRelay | ✅ 已实现 | 请求中继连接 |
|
||||
| RelayResponse | ✅ 已实现 | 处理中继响应 |
|
||||
| ConfigUpdate | ✅ 已实现 | 接收配置更新 |
|
||||
|
||||
**实现文件**: `src/rustdesk/rendezvous.rs` (~829 行)
|
||||
|
||||
```rust
|
||||
// 核心结构
|
||||
pub struct RendezvousMediator {
|
||||
config: RustDeskConfig,
|
||||
key_pair: KeyPair,
|
||||
signing_key: SigningKeyPair,
|
||||
socket: UdpSocket,
|
||||
status: Arc<RwLock<RendezvousStatus>>,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 连接协议 (客户端连接)
|
||||
|
||||
| 消息类型 | 实现状态 | 备注 |
|
||||
|----------|----------|------|
|
||||
| SignedId | ✅ 已实现 | 签名身份验证 |
|
||||
| PublicKey | ✅ 已实现 | 公钥交换 |
|
||||
| Hash | ✅ 已实现 | 哈希挑战响应 |
|
||||
| LoginRequest | ✅ 已实现 | 登录认证 |
|
||||
| LoginResponse | ✅ 已实现 | 登录响应 |
|
||||
| TestDelay | ✅ 已实现 | 延迟测试 |
|
||||
| VideoFrame | ✅ 已实现 | 视频帧发送 |
|
||||
| AudioFrame | ✅ 已实现 | 音频帧发送 |
|
||||
| CursorData | ✅ 已实现 | 光标图像 |
|
||||
| CursorPosition | ✅ 已实现 | 光标位置 |
|
||||
| MouseEvent | ✅ 已实现 | 鼠标事件接收 |
|
||||
| KeyEvent | ✅ 已实现 | 键盘事件接收 |
|
||||
|
||||
**实现文件**: `src/rustdesk/connection.rs` (~1349 行)
|
||||
|
||||
```rust
|
||||
// 连接状态机
|
||||
pub enum ConnectionState {
|
||||
WaitingForSignedId,
|
||||
WaitingForPublicKey,
|
||||
WaitingForHash,
|
||||
WaitingForLogin,
|
||||
Authenticated,
|
||||
Streaming,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 加密模块
|
||||
|
||||
| 功能 | 实现状态 | 备注 |
|
||||
|------|----------|------|
|
||||
| Curve25519 密钥对 | ✅ 已实现 | 用于加密 |
|
||||
| Ed25519 签名密钥对 | ✅ 已实现 | 用于签名 |
|
||||
| Ed25519 → Curve25519 转换 | ✅ 已实现 | 密钥派生 |
|
||||
| XSalsa20-Poly1305 | ✅ 已实现 | 会话加密 (secretbox) |
|
||||
| 密码哈希 | ✅ 已实现 | 单重/双重 SHA256 |
|
||||
| 会话密钥协商 | ✅ 已实现 | 对称密钥派生 |
|
||||
|
||||
**实现文件**: `src/rustdesk/crypto.rs` (~468 行)
|
||||
|
||||
```rust
|
||||
// 密钥对结构
|
||||
pub struct KeyPair {
|
||||
secret_key: [u8; 32], // Curve25519 私钥
|
||||
public_key: [u8; 32], // Curve25519 公钥
|
||||
}
|
||||
|
||||
pub struct SigningKeyPair {
|
||||
secret_key: [u8; 64], // Ed25519 私钥
|
||||
public_key: [u8; 32], // Ed25519 公钥
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 视频/音频流
|
||||
|
||||
| 编码格式 | 实现状态 | 备注 |
|
||||
|----------|----------|------|
|
||||
| H.264 | ✅ 已实现 | 主要格式 |
|
||||
| H.265/HEVC | ✅ 已实现 | 高效编码 |
|
||||
| VP8 | ✅ 已实现 | WebRTC 兼容 |
|
||||
| VP9 | ✅ 已实现 | 高质量 |
|
||||
| AV1 | ✅ 已实现 | 新一代编码 |
|
||||
| Opus 音频 | ✅ 已实现 | 低延迟音频 |
|
||||
|
||||
**实现文件**: `src/rustdesk/frame_adapters.rs` (~316 行)
|
||||
|
||||
### 2.5 HID 事件
|
||||
|
||||
| 功能 | 实现状态 | 备注 |
|
||||
|------|----------|------|
|
||||
| 鼠标移动 | ✅ 已实现 | 绝对/相对坐标 |
|
||||
| 鼠标按键 | ✅ 已实现 | 左/中/右键 |
|
||||
| 鼠标滚轮 | ✅ 已实现 | 垂直滚动 |
|
||||
| 键盘按键 | ✅ 已实现 | 按下/释放 |
|
||||
| 控制键映射 | ✅ 已实现 | ControlKey → USB HID |
|
||||
| X11 键码映射 | ✅ 已实现 | X11 → USB HID |
|
||||
|
||||
**实现文件**: `src/rustdesk/hid_adapter.rs` (~386 行)
|
||||
|
||||
### 2.6 协议帧编码
|
||||
|
||||
| 功能 | 实现状态 | 备注 |
|
||||
|------|----------|------|
|
||||
| BytesCodec | ✅ 已实现 | 变长帧编码 |
|
||||
| 1-4 字节头 | ✅ 已实现 | 根据长度自动选择 |
|
||||
| 最大 1GB 消息 | ✅ 已实现 | 与原版一致 |
|
||||
|
||||
**实现文件**: `src/rustdesk/bytes_codec.rs` (~253 行)
|
||||
|
||||
## 3. 未实现功能
|
||||
|
||||
### 3.1 NAT 穿透相关
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| UDP 打洞 | One-KVM 仅使用 TCP 中继 |
|
||||
| TCP 打洞 | 同上 |
|
||||
| STUN/TURN | 不需要 NAT 类型检测 |
|
||||
| TestNat | 同上 |
|
||||
| P2P 直连 | 设计简化,仅支持中继 |
|
||||
|
||||
### 3.2 客户端发起功能
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| PunchHole (发起) | KVM 只接收连接 |
|
||||
| RelayRequest | 同上 |
|
||||
| ConnectPeer | 同上 |
|
||||
| OnlineRequest | 不需要查询其他设备 |
|
||||
|
||||
### 3.3 文件传输
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| FileTransfer | 超出 KVM 功能范围 |
|
||||
| FileAction | 同上 |
|
||||
| FileResponse | 同上 |
|
||||
| FileTransferBlock | 同上 |
|
||||
|
||||
### 3.4 高级功能
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| 剪贴板同步 | 超出 KVM 功能范围 |
|
||||
| 多显示器切换 | One-KVM 使用单一视频源 |
|
||||
| 虚拟显示器 | 不适用 |
|
||||
| 端口转发 | 超出 KVM 功能范围 |
|
||||
| 语音通话 | 不需要 |
|
||||
| RDP 输入 | 不需要 |
|
||||
| 插件系统 | 不支持 |
|
||||
| 软件更新 | One-KVM 有自己的更新机制 |
|
||||
|
||||
### 3.5 权限协商
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| Option 消息 | One-KVM 假设完全控制权限 |
|
||||
| 权限请求 | 同上 |
|
||||
| PermissionInfo | 同上 |
|
||||
|
||||
## 4. 实现差异
|
||||
|
||||
### 4.1 连接模式
|
||||
|
||||
**RustDesk 原版:**
|
||||
```
|
||||
客户端 ──UDP打洞──> 被控端 (P2P 优先)
|
||||
└──Relay──> 被控端 (回退)
|
||||
```
|
||||
|
||||
**One-KVM:**
|
||||
```
|
||||
RustDesk客户端 ──TCP中继──> hbbr服务器 ──> One-KVM设备
|
||||
```
|
||||
|
||||
One-KVM 只支持 TCP 中继连接,不支持 P2P 直连。这简化了实现,但可能增加延迟。
|
||||
|
||||
### 4.2 会话加密
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 支持 ChaCha20-Poly1305 (流式)
|
||||
- 支持 XSalsa20-Poly1305 (secretbox)
|
||||
- 动态协商加密方式
|
||||
|
||||
**One-KVM:**
|
||||
- 仅支持 XSalsa20-Poly1305 (secretbox)
|
||||
- 使用序列号作为 nonce
|
||||
|
||||
```rust
|
||||
// One-KVM 的加密实现
|
||||
fn encrypt_message(&mut self, plaintext: &[u8]) -> Vec<u8> {
|
||||
let nonce = make_nonce(&self.send_nonce);
|
||||
self.send_nonce = self.send_nonce.wrapping_add(1);
|
||||
secretbox::seal(plaintext, &nonce, &self.session_key)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 视频流方向
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 双向视频流(可控制和被控制)
|
||||
- 远程桌面捕获
|
||||
|
||||
**One-KVM:**
|
||||
- 单向视频流(仅发送)
|
||||
- 从 V4L2 设备捕获
|
||||
- 集成到 One-KVM 的 VideoStreamManager
|
||||
|
||||
```rust
|
||||
// One-KVM 视频流集成
|
||||
pub async fn start_video_stream(&self, state: &AppState) {
|
||||
let stream_manager = &state.video_stream_manager;
|
||||
// 从 One-KVM 的视频管理器获取帧
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 HID 事件处理
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 转发到远程系统的输入子系统
|
||||
- 使用 enigo 或 uinput
|
||||
|
||||
**One-KVM:**
|
||||
- 转发到 USB OTG/HID 设备
|
||||
- 控制物理 KVM 目标机器
|
||||
|
||||
```rust
|
||||
// One-KVM HID 适配
|
||||
pub fn convert_mouse_event(event: &RustDeskMouseEvent) -> Option<OneKvmMouseEvent> {
|
||||
// 转换 RustDesk 鼠标事件到 One-KVM HID 事件
|
||||
}
|
||||
|
||||
pub fn convert_key_event(event: &RustDeskKeyEvent) -> Option<OneKvmKeyEvent> {
|
||||
// 转换 RustDesk 键盘事件到 One-KVM HID 事件
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 配置管理
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 使用 TOML/JSON 配置文件
|
||||
- 硬编码默认值
|
||||
|
||||
**One-KVM:**
|
||||
- 集成到 SQLite 配置系统
|
||||
- Web UI 管理
|
||||
- 使用 typeshare 生成 TypeScript 类型
|
||||
|
||||
```rust
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RustDeskConfig {
|
||||
pub enabled: bool,
|
||||
pub rendezvous_server: String,
|
||||
pub device_id: String,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 设备 ID 生成
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 基于 MAC 地址和硬件信息
|
||||
- 固定便携式 ID
|
||||
|
||||
**One-KVM:**
|
||||
- 随机生成 9 位数字
|
||||
- 存储在配置中
|
||||
|
||||
```rust
|
||||
pub fn generate_device_id() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let id: u32 = rng.gen_range(100_000_000..999_999_999);
|
||||
id.to_string()
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 协议兼容性
|
||||
|
||||
### 5.1 完全兼容
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| Rendezvous 注册 | 可与官方 hbbs 服务器通信 |
|
||||
| 中继连接 | 可通过官方 hbbr 服务器中继 |
|
||||
| 加密握手 | 与 RustDesk 客户端兼容 |
|
||||
| 视频编码 | 支持所有主流编码格式 |
|
||||
| HID 事件 | 接收标准 RustDesk 输入事件 |
|
||||
|
||||
### 5.2 部分兼容
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 密码认证 | 仅支持设备密码,不支持一次性密码 |
|
||||
| 会话加密 | 仅 XSalsa20-Poly1305 |
|
||||
|
||||
### 5.3 不兼容
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| P2P 连接 | 客户端必须通过中继连接 |
|
||||
| 文件传输 | 不支持 |
|
||||
| 剪贴板 | 不支持 |
|
||||
|
||||
## 6. 代码结构对比
|
||||
|
||||
### RustDesk 原版结构
|
||||
|
||||
```
|
||||
rustdesk/
|
||||
├── libs/hbb_common/ # 公共库
|
||||
│ ├── protos/ # Protobuf 定义
|
||||
│ └── src/
|
||||
├── src/
|
||||
│ ├── server/ # 被控端服务
|
||||
│ ├── client/ # 控制端
|
||||
│ ├── ui/ # 用户界面
|
||||
│ └── rendezvous_mediator.rs
|
||||
```
|
||||
|
||||
### One-KVM 结构
|
||||
|
||||
```
|
||||
src/rustdesk/
|
||||
├── mod.rs # 模块导出
|
||||
├── config.rs # 配置类型 (~164 行)
|
||||
├── crypto.rs # 加密模块 (~468 行)
|
||||
├── bytes_codec.rs # 帧编码 (~253 行)
|
||||
├── protocol.rs # 消息辅助 (~170 行)
|
||||
├── rendezvous.rs # Rendezvous 中介 (~829 行)
|
||||
├── connection.rs # 连接处理 (~1349 行)
|
||||
├── hid_adapter.rs # HID 转换 (~386 行)
|
||||
└── frame_adapters.rs # 视频/音频适配 (~316 行)
|
||||
```
|
||||
|
||||
**总计**: ~3935 行代码
|
||||
|
||||
## 7. 总结
|
||||
|
||||
### 实现率统计
|
||||
|
||||
| 类别 | RustDesk 功能数 | One-KVM 实现数 | 实现率 |
|
||||
|------|-----------------|----------------|--------|
|
||||
| Rendezvous 协议 | 15+ | 10 | ~67% |
|
||||
| 连接协议 | 30+ | 12 | ~40% |
|
||||
| 加密功能 | 8 | 6 | 75% |
|
||||
| 视频/音频 | 6 | 6 | 100% |
|
||||
| HID 功能 | 6 | 6 | 100% |
|
||||
|
||||
### 设计理念
|
||||
|
||||
One-KVM 的 RustDesk 实现专注于 **IP-KVM 核心功能**:
|
||||
|
||||
1. **精简**: 只实现必要的被控端功能
|
||||
2. **可靠**: 使用 TCP 中继保证连接稳定性
|
||||
3. **集成**: 与 One-KVM 现有视频/HID 系统无缝集成
|
||||
4. **安全**: 完整实现加密和认证机制
|
||||
|
||||
### 客户端兼容性
|
||||
|
||||
One-KVM 可与标准 RustDesk 客户端配合使用:
|
||||
- RustDesk 桌面客户端 (Windows/macOS/Linux)
|
||||
- RustDesk 移动客户端 (Android/iOS)
|
||||
- RustDesk Web 客户端
|
||||
|
||||
只需确保:
|
||||
1. 配置相同的 Rendezvous 服务器
|
||||
2. 使用设备 ID 和密码连接
|
||||
3. 客户端支持中继连接
|
||||
Reference in New Issue
Block a user