mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-28 16:41:52 +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:
@@ -113,6 +113,11 @@ gpio-cdev = "0.6"
|
|||||||
# H264 hardware/software encoding (hwcodec from rustdesk)
|
# H264 hardware/software encoding (hwcodec from rustdesk)
|
||||||
hwcodec = { path = "libs/hwcodec" }
|
hwcodec = { path = "libs/hwcodec" }
|
||||||
|
|
||||||
|
# RustDesk protocol support
|
||||||
|
prost = "0.13"
|
||||||
|
sodiumoxide = "0.2"
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
# High-performance pixel format conversion (libyuv)
|
# High-performance pixel format conversion (libyuv)
|
||||||
libyuv = { path = "res/vcpkg/libyuv" }
|
libyuv = { path = "res/vcpkg/libyuv" }
|
||||||
|
|
||||||
@@ -123,6 +128,9 @@ typeshare = "1.0"
|
|||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.13"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
18
build.rs
18
build.rs
@@ -14,8 +14,26 @@ fn main() {
|
|||||||
|
|
||||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
||||||
|
|
||||||
|
// Compile protobuf files for RustDesk protocol
|
||||||
|
compile_protos();
|
||||||
|
|
||||||
// Rerun if the script itself changes
|
// Rerun if the script itself changes
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
println!("cargo:rerun-if-changed=protos/rendezvous.proto");
|
||||||
|
println!("cargo:rerun-if-changed=protos/message.proto");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile protobuf files using prost-build
|
||||||
|
fn compile_protos() {
|
||||||
|
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||||
|
|
||||||
|
prost_build::Config::new()
|
||||||
|
.out_dir(&out_dir)
|
||||||
|
.compile_protos(
|
||||||
|
&["protos/rendezvous.proto", "protos/message.proto"],
|
||||||
|
&["protos/"],
|
||||||
|
)
|
||||||
|
.expect("Failed to compile protobuf files");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert days since Unix epoch to year-month-day
|
/// Convert days since Unix epoch to year-month-day
|
||||||
|
|||||||
130
docs/README.md
Normal file
130
docs/README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# One-KVM 技术文档
|
||||||
|
|
||||||
|
本目录包含 One-KVM 项目的完整技术文档。
|
||||||
|
|
||||||
|
## 文档结构
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md # 本文件 - 文档索引
|
||||||
|
├── system-architecture.md # 系统架构文档
|
||||||
|
├── tech-stack.md # 技术栈文档
|
||||||
|
└── modules/ # 模块文档
|
||||||
|
├── video.md # 视频模块
|
||||||
|
├── hid.md # HID 模块
|
||||||
|
├── otg.md # OTG 模块
|
||||||
|
├── msd.md # MSD 模块
|
||||||
|
├── atx.md # ATX 模块
|
||||||
|
├── audio.md # 音频模块
|
||||||
|
├── webrtc.md # WebRTC 模块
|
||||||
|
├── rustdesk.md # RustDesk 模块
|
||||||
|
├── auth.md # 认证模块
|
||||||
|
├── config.md # 配置模块
|
||||||
|
├── events.md # 事件模块
|
||||||
|
└── web.md # Web 模块
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速导航
|
||||||
|
|
||||||
|
### 核心文档
|
||||||
|
|
||||||
|
| 文档 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| [系统架构](./system-architecture.md) | 整体架构设计、数据流、模块依赖 |
|
||||||
|
| [技术栈](./tech-stack.md) | 使用的技术、库和开发规范 |
|
||||||
|
|
||||||
|
### 功能模块
|
||||||
|
|
||||||
|
| 模块 | 描述 | 关键文件 |
|
||||||
|
|------|------|---------|
|
||||||
|
| [Video](./modules/video.md) | 视频采集和编码 | `src/video/` |
|
||||||
|
| [HID](./modules/hid.md) | 键盘鼠标控制 | `src/hid/` |
|
||||||
|
| [OTG](./modules/otg.md) | USB Gadget 管理 | `src/otg/` |
|
||||||
|
| [MSD](./modules/msd.md) | 虚拟存储设备 | `src/msd/` |
|
||||||
|
| [ATX](./modules/atx.md) | 电源控制 | `src/atx/` |
|
||||||
|
| [Audio](./modules/audio.md) | 音频采集编码 | `src/audio/` |
|
||||||
|
| [WebRTC](./modules/webrtc.md) | WebRTC 流媒体 | `src/webrtc/` |
|
||||||
|
| [RustDesk](./modules/rustdesk.md) | RustDesk 协议集成 | `src/rustdesk/` |
|
||||||
|
|
||||||
|
### 基础设施
|
||||||
|
|
||||||
|
| 模块 | 描述 | 关键文件 |
|
||||||
|
|------|------|---------|
|
||||||
|
| [Auth](./modules/auth.md) | 认证和会话 | `src/auth/` |
|
||||||
|
| [Config](./modules/config.md) | 配置管理 | `src/config/` |
|
||||||
|
| [Events](./modules/events.md) | 事件系统 | `src/events/` |
|
||||||
|
| [Web](./modules/web.md) | HTTP API | `src/web/` |
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ One-KVM System │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Web Frontend (Vue3) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Axum Web Server │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ AppState │ │
|
||||||
|
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||||
|
│ │ │ Video │ │ HID │ │ MSD │ │ ATX │ │ │
|
||||||
|
│ │ │ Module │ │ Module │ │ Module │ │ Module │ │ │
|
||||||
|
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||||
|
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||||
|
│ │ │ Audio │ │ WebRTC │ │RustDesk│ │ Events │ │ │
|
||||||
|
│ │ │ Module │ │ Module │ │ Module │ │ Bus │ │ │
|
||||||
|
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Hardware Layer │ │
|
||||||
|
│ │ V4L2 │ USB OTG │ GPIO │ ALSA │ Network │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键特性
|
||||||
|
|
||||||
|
- **单一二进制**: Web UI + 后端一体化部署
|
||||||
|
- **双流模式**: WebRTC (H264/H265/VP8/VP9) + MJPEG
|
||||||
|
- **USB OTG**: 虚拟键鼠、虚拟存储
|
||||||
|
- **硬件加速**: VAAPI/RKMPP/V4L2 M2M
|
||||||
|
- **RustDesk**: 跨平台远程访问
|
||||||
|
- **无配置文件**: SQLite 配置存储
|
||||||
|
|
||||||
|
## 目标平台
|
||||||
|
|
||||||
|
| 平台 | 架构 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| aarch64-unknown-linux-gnu | ARM64 | 主要目标 |
|
||||||
|
| armv7-unknown-linux-gnueabihf | ARMv7 | 备选 |
|
||||||
|
| x86_64-unknown-linux-gnu | x86-64 | 开发/测试 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建前端
|
||||||
|
cd web && npm install && npm run build && cd ..
|
||||||
|
|
||||||
|
# 构建后端
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
./target/release/one-kvm --enable-https
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关链接
|
||||||
|
|
||||||
|
- [项目仓库](https://github.com/mofeng-git/One-KVM)
|
||||||
|
- [开发计划](./DEVELOPMENT_PLAN.md)
|
||||||
|
- [项目目标](./PROJECT_GOALS.md)
|
||||||
484
docs/modules/atx.md
Normal file
484
docs/modules/atx.md
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
# ATX 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
ATX 模块提供电源控制功能,通过 GPIO 或 USB 继电器控制目标计算机的电源和重置按钮。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- 电源按钮控制
|
||||||
|
- 重置按钮控制
|
||||||
|
- 电源 LED 状态监视
|
||||||
|
- Wake-on-LAN 支持
|
||||||
|
- 多后端支持 (GPIO/USB 继电器)
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/atx/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── controller.rs # AtxController (11KB)
|
||||||
|
├── executor.rs # 动作执行器 (10KB)
|
||||||
|
├── types.rs # 类型定义 (7KB)
|
||||||
|
├── led.rs # LED 监视 (5KB)
|
||||||
|
└── wol.rs # Wake-on-LAN (5KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ATX Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Web API
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ AtxController │
|
||||||
|
│ (controller.rs) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┼─────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌────────┐ ┌────────┐
|
||||||
|
│ Power │ │ Reset │ │ LED │
|
||||||
|
│Executor│ │Executor│ │Monitor │
|
||||||
|
└───┬────┘ └───┬────┘ └───┬────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌────────┐ ┌────────┐
|
||||||
|
│ GPIO │ │ GPIO │ │ GPIO │
|
||||||
|
│ or USB │ │ or USB │ │ Input │
|
||||||
|
│ Relay │ │ Relay │ │ │
|
||||||
|
└───┬────┘ └───┬────┘ └───┬────┘
|
||||||
|
│ │ │
|
||||||
|
└───────────┼─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────┐
|
||||||
|
│ Target PC │
|
||||||
|
│ (ATX Header) │
|
||||||
|
└───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 AtxController (controller.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AtxController {
|
||||||
|
/// 电源按钮配置
|
||||||
|
power: Arc<AtxButton>,
|
||||||
|
|
||||||
|
/// 重置按钮配置
|
||||||
|
reset: Arc<AtxButton>,
|
||||||
|
|
||||||
|
/// LED 监视器
|
||||||
|
led_monitor: Arc<RwLock<Option<LedMonitor>>>,
|
||||||
|
|
||||||
|
/// WoL 控制器
|
||||||
|
wol: Arc<RwLock<Option<WolController>>>,
|
||||||
|
|
||||||
|
/// 当前状态
|
||||||
|
state: Arc<RwLock<AtxState>>,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtxController {
|
||||||
|
/// 创建控制器
|
||||||
|
pub fn new(config: &AtxConfig, events: Arc<EventBus>) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 短按电源按钮 (开机/正常关机)
|
||||||
|
pub async fn power_short_press(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 长按电源按钮 (强制关机)
|
||||||
|
pub async fn power_long_press(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 按重置按钮
|
||||||
|
pub async fn reset_press(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取电源状态
|
||||||
|
pub fn power_state(&self) -> PowerState;
|
||||||
|
|
||||||
|
/// 发送 WoL 魔术包
|
||||||
|
pub async fn wake_on_lan(&self, mac: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn state(&self) -> AtxState;
|
||||||
|
|
||||||
|
/// 重新加载配置
|
||||||
|
pub async fn reload(&self, config: &AtxConfig) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AtxState {
|
||||||
|
/// 是否可用
|
||||||
|
pub available: bool,
|
||||||
|
|
||||||
|
/// 电源是否开启
|
||||||
|
pub power_on: bool,
|
||||||
|
|
||||||
|
/// 最后操作时间
|
||||||
|
pub last_action: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
|
/// 错误信息
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PowerState {
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 AtxButton (executor.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AtxButton {
|
||||||
|
/// 按钮名称
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// 驱动类型
|
||||||
|
driver: AtxDriverType,
|
||||||
|
|
||||||
|
/// GPIO 句柄
|
||||||
|
gpio: Option<LineHandle>,
|
||||||
|
|
||||||
|
/// USB 继电器句柄
|
||||||
|
relay: Option<UsbRelay>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: AtxKeyConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtxButton {
|
||||||
|
/// 创建按钮
|
||||||
|
pub fn new(name: &str, config: &AtxKeyConfig) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 短按 (100ms)
|
||||||
|
pub async fn short_press(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 长按 (3000ms)
|
||||||
|
pub async fn long_press(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 自定义按压时间
|
||||||
|
pub async fn press(&self, duration: Duration) -> Result<()>;
|
||||||
|
|
||||||
|
/// 设置输出状态
|
||||||
|
fn set_output(&self, high: bool) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AtxDriverType {
|
||||||
|
/// GPIO 直连
|
||||||
|
Gpio,
|
||||||
|
|
||||||
|
/// USB 继电器
|
||||||
|
UsbRelay,
|
||||||
|
|
||||||
|
/// 禁用
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 LedMonitor (led.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct LedMonitor {
|
||||||
|
/// GPIO 引脚
|
||||||
|
pin: u32,
|
||||||
|
|
||||||
|
/// GPIO 句柄
|
||||||
|
line: LineHandle,
|
||||||
|
|
||||||
|
/// 当前状态
|
||||||
|
state: Arc<AtomicBool>,
|
||||||
|
|
||||||
|
/// 监视任务
|
||||||
|
monitor_task: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedMonitor {
|
||||||
|
/// 创建监视器
|
||||||
|
pub fn new(config: &AtxLedConfig) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 启动监视
|
||||||
|
pub fn start(&mut self, events: Arc<EventBus>) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止监视
|
||||||
|
pub fn stop(&mut self);
|
||||||
|
|
||||||
|
/// 获取当前状态
|
||||||
|
pub fn state(&self) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 WolController (wol.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct WolController {
|
||||||
|
/// 网络接口
|
||||||
|
interface: String,
|
||||||
|
|
||||||
|
/// 广播地址
|
||||||
|
broadcast_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WolController {
|
||||||
|
/// 创建控制器
|
||||||
|
pub fn new(interface: Option<&str>) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 发送 WoL 魔术包
|
||||||
|
pub async fn wake(&self, mac: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 构建魔术包
|
||||||
|
fn build_magic_packet(mac: &[u8; 6]) -> [u8; 102];
|
||||||
|
|
||||||
|
/// 解析 MAC 地址
|
||||||
|
fn parse_mac(mac: &str) -> Result<[u8; 6]>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct AtxConfig {
|
||||||
|
/// 是否启用
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// 电源按钮配置
|
||||||
|
pub power: AtxKeyConfig,
|
||||||
|
|
||||||
|
/// 重置按钮配置
|
||||||
|
pub reset: AtxKeyConfig,
|
||||||
|
|
||||||
|
/// LED 监视配置
|
||||||
|
pub led: AtxLedConfig,
|
||||||
|
|
||||||
|
/// WoL 配置
|
||||||
|
pub wol: WolConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct AtxKeyConfig {
|
||||||
|
/// 驱动类型
|
||||||
|
pub driver: AtxDriverType,
|
||||||
|
|
||||||
|
/// GPIO 芯片 (如 /dev/gpiochip0)
|
||||||
|
pub gpio_chip: Option<String>,
|
||||||
|
|
||||||
|
/// GPIO 引脚号
|
||||||
|
pub gpio_pin: Option<u32>,
|
||||||
|
|
||||||
|
/// USB 继电器设备
|
||||||
|
pub relay_device: Option<String>,
|
||||||
|
|
||||||
|
/// 继电器通道
|
||||||
|
pub relay_channel: Option<u8>,
|
||||||
|
|
||||||
|
/// 激活电平
|
||||||
|
pub active_level: ActiveLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct AtxLedConfig {
|
||||||
|
/// 是否启用
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// GPIO 芯片
|
||||||
|
pub gpio_chip: Option<String>,
|
||||||
|
|
||||||
|
/// GPIO 引脚号
|
||||||
|
pub gpio_pin: Option<u32>,
|
||||||
|
|
||||||
|
/// 激活电平
|
||||||
|
pub active_level: ActiveLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ActiveLevel {
|
||||||
|
High,
|
||||||
|
Low,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AtxConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
power: AtxKeyConfig::default(),
|
||||||
|
reset: AtxKeyConfig::default(),
|
||||||
|
led: AtxLedConfig::default(),
|
||||||
|
wol: WolConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/atx/status` | GET | 获取 ATX 状态 |
|
||||||
|
| `/api/atx/power/short` | POST | 短按电源 |
|
||||||
|
| `/api/atx/power/long` | POST | 长按电源 |
|
||||||
|
| `/api/atx/reset` | POST | 按重置 |
|
||||||
|
| `/api/atx/wol` | POST | 发送 WoL |
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/atx/status
|
||||||
|
{
|
||||||
|
"available": true,
|
||||||
|
"power_on": true,
|
||||||
|
"last_action": "2024-01-15T10:30:00Z",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/atx/wol
|
||||||
|
// Request: { "mac": "00:11:22:33:44:55" }
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 硬件连接
|
||||||
|
|
||||||
|
### 6.1 GPIO 直连
|
||||||
|
|
||||||
|
```
|
||||||
|
One-KVM Device Target PC
|
||||||
|
┌─────────────┐ ┌─────────────┐
|
||||||
|
│ GPIO Pin │───────────────│ Power SW │
|
||||||
|
│ (Output) │ │ │
|
||||||
|
└─────────────┘ └─────────────┘
|
||||||
|
|
||||||
|
接线说明:
|
||||||
|
- GPIO 引脚连接到 ATX 电源按钮
|
||||||
|
- 使用光耦或继电器隔离 (推荐)
|
||||||
|
- 注意电平匹配
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 USB 继电器
|
||||||
|
|
||||||
|
```
|
||||||
|
One-KVM Device USB Relay Target PC
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ USB │───────────────│ Relay │──────────│ Power SW │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
|
||||||
|
优点:
|
||||||
|
- 完全隔离
|
||||||
|
- 无需担心电平问题
|
||||||
|
- 更安全
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SystemEvent {
|
||||||
|
AtxStateChanged {
|
||||||
|
power_on: bool,
|
||||||
|
last_action: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
AtxActionPerformed {
|
||||||
|
action: String, // "power_short" | "power_long" | "reset" | "wol"
|
||||||
|
success: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AtxError {
|
||||||
|
#[error("ATX not available")]
|
||||||
|
NotAvailable,
|
||||||
|
|
||||||
|
#[error("GPIO error: {0}")]
|
||||||
|
GpioError(String),
|
||||||
|
|
||||||
|
#[error("Relay error: {0}")]
|
||||||
|
RelayError(String),
|
||||||
|
|
||||||
|
#[error("WoL error: {0}")]
|
||||||
|
WolError(String),
|
||||||
|
|
||||||
|
#[error("Invalid MAC address: {0}")]
|
||||||
|
InvalidMac(String),
|
||||||
|
|
||||||
|
#[error("Operation in progress")]
|
||||||
|
Busy,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 使用示例
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let atx = AtxController::new(&config, events)?;
|
||||||
|
|
||||||
|
// 开机
|
||||||
|
atx.power_short_press().await?;
|
||||||
|
|
||||||
|
// 检查状态
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
|
if atx.power_state() == PowerState::On {
|
||||||
|
println!("PC is now on");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制关机
|
||||||
|
atx.power_long_press().await?;
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
atx.reset_press().await?;
|
||||||
|
|
||||||
|
// Wake-on-LAN
|
||||||
|
atx.wake_on_lan("00:11:22:33:44:55").await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 常见问题
|
||||||
|
|
||||||
|
### Q: GPIO 无法控制?
|
||||||
|
|
||||||
|
1. 检查引脚配置
|
||||||
|
2. 检查权限 (`/dev/gpiochip*`)
|
||||||
|
3. 检查接线
|
||||||
|
|
||||||
|
### Q: LED 状态不正确?
|
||||||
|
|
||||||
|
1. 检查 active_level 配置
|
||||||
|
2. 检查 GPIO 输入模式
|
||||||
|
|
||||||
|
### Q: WoL 不工作?
|
||||||
|
|
||||||
|
1. 检查目标 PC BIOS 设置
|
||||||
|
2. 检查网卡支持
|
||||||
|
3. 检查网络广播
|
||||||
463
docs/modules/audio.md
Normal file
463
docs/modules/audio.md
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
# Audio 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
Audio 模块负责音频采集和编码,支持 ALSA 采集和 Opus 编码。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- ALSA 音频采集
|
||||||
|
- Opus 编码
|
||||||
|
- 多质量配置
|
||||||
|
- WebSocket/WebRTC 传输
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/audio/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── controller.rs # AudioController (15KB)
|
||||||
|
├── capture.rs # ALSA 采集 (12KB)
|
||||||
|
├── encoder.rs # Opus 编码 (8KB)
|
||||||
|
├── shared_pipeline.rs # 共享管道 (15KB)
|
||||||
|
├── monitor.rs # 健康监视 (11KB)
|
||||||
|
└── device.rs # 设备枚举 (8KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Audio Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
ALSA Device (hw:0,0)
|
||||||
|
│
|
||||||
|
│ PCM 48kHz/16bit/Stereo
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ AudioCapturer │
|
||||||
|
│ (capture.rs) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ SharedAudioPipeline │
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ Opus Encoder │ │
|
||||||
|
│ │ 48kHz → 24-96 kbps │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
└────────────────┬────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────┴─────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────┐ ┌─────────────┐
|
||||||
|
│ WebSocket │ │ WebRTC │
|
||||||
|
│ Stream │ │ Audio Track │
|
||||||
|
└─────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 AudioController (controller.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AudioController {
|
||||||
|
/// 采集器
|
||||||
|
capturer: Arc<RwLock<Option<AudioCapturer>>>,
|
||||||
|
|
||||||
|
/// 共享管道
|
||||||
|
pipeline: Arc<SharedAudioPipeline>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: Arc<RwLock<AudioConfig>>,
|
||||||
|
|
||||||
|
/// 状态
|
||||||
|
state: Arc<RwLock<AudioState>>,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioController {
|
||||||
|
/// 创建控制器
|
||||||
|
pub fn new(config: &AudioConfig, events: Arc<EventBus>) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 启动音频
|
||||||
|
pub async fn start(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止音频
|
||||||
|
pub async fn stop(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 订阅音频帧
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame>;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn status(&self) -> AudioStatus;
|
||||||
|
|
||||||
|
/// 设置质量
|
||||||
|
pub fn set_quality(&self, quality: AudioQuality) -> Result<()>;
|
||||||
|
|
||||||
|
/// 列出设备
|
||||||
|
pub fn list_devices(&self) -> Vec<AudioDeviceInfo>;
|
||||||
|
|
||||||
|
/// 重新加载配置
|
||||||
|
pub async fn reload(&self, config: &AudioConfig) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AudioStatus {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub streaming: bool,
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub channels: u16,
|
||||||
|
pub bitrate: u32,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 AudioCapturer (capture.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AudioCapturer {
|
||||||
|
/// PCM 句柄
|
||||||
|
pcm: PCM,
|
||||||
|
|
||||||
|
/// 设备名
|
||||||
|
device: String,
|
||||||
|
|
||||||
|
/// 采样率
|
||||||
|
sample_rate: u32,
|
||||||
|
|
||||||
|
/// 通道数
|
||||||
|
channels: u16,
|
||||||
|
|
||||||
|
/// 帧大小
|
||||||
|
frame_size: usize,
|
||||||
|
|
||||||
|
/// 运行状态
|
||||||
|
running: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioCapturer {
|
||||||
|
/// 打开设备
|
||||||
|
pub fn open(device: &str, config: &CaptureConfig) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 读取音频帧
|
||||||
|
pub fn read_frame(&self) -> Result<Vec<i16>>;
|
||||||
|
|
||||||
|
/// 启动采集
|
||||||
|
pub fn start(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止采集
|
||||||
|
pub fn stop(&self);
|
||||||
|
|
||||||
|
/// 是否运行中
|
||||||
|
pub fn is_running(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CaptureConfig {
|
||||||
|
pub sample_rate: u32, // 48000
|
||||||
|
pub channels: u16, // 2
|
||||||
|
pub frame_size: usize, // 960 (20ms)
|
||||||
|
pub buffer_size: usize, // 4800
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 OpusEncoder (encoder.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct OpusEncoder {
|
||||||
|
/// Opus 编码器
|
||||||
|
encoder: audiopus::Encoder,
|
||||||
|
|
||||||
|
/// 采样率
|
||||||
|
sample_rate: u32,
|
||||||
|
|
||||||
|
/// 通道数
|
||||||
|
channels: u16,
|
||||||
|
|
||||||
|
/// 帧大小
|
||||||
|
frame_size: usize,
|
||||||
|
|
||||||
|
/// 码率
|
||||||
|
bitrate: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpusEncoder {
|
||||||
|
/// 创建编码器
|
||||||
|
pub fn new(quality: AudioQuality) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 编码 PCM 数据
|
||||||
|
pub fn encode(&mut self, pcm: &[i16]) -> Result<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 设置码率
|
||||||
|
pub fn set_bitrate(&mut self, bitrate: u32) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取码率
|
||||||
|
pub fn bitrate(&self) -> u32;
|
||||||
|
|
||||||
|
/// 重置编码器
|
||||||
|
pub fn reset(&mut self) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 SharedAudioPipeline (shared_pipeline.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SharedAudioPipeline {
|
||||||
|
/// 采集器
|
||||||
|
capturer: Arc<RwLock<Option<AudioCapturer>>>,
|
||||||
|
|
||||||
|
/// 编码器
|
||||||
|
encoder: Arc<Mutex<OpusEncoder>>,
|
||||||
|
|
||||||
|
/// 广播通道
|
||||||
|
tx: broadcast::Sender<AudioFrame>,
|
||||||
|
|
||||||
|
/// 采集任务
|
||||||
|
capture_task: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: Arc<RwLock<AudioConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedAudioPipeline {
|
||||||
|
/// 创建管道
|
||||||
|
pub fn new(config: &AudioConfig) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 启动管道
|
||||||
|
pub async fn start(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止管道
|
||||||
|
pub async fn stop(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 订阅音频帧
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame>;
|
||||||
|
|
||||||
|
/// 获取统计
|
||||||
|
pub fn stats(&self) -> PipelineStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AudioFrame {
|
||||||
|
/// Opus 数据
|
||||||
|
pub data: Bytes,
|
||||||
|
|
||||||
|
/// 时间戳
|
||||||
|
pub timestamp: u64,
|
||||||
|
|
||||||
|
/// 帧序号
|
||||||
|
pub sequence: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 音频质量
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum AudioQuality {
|
||||||
|
/// 24 kbps - 最低带宽
|
||||||
|
VeryLow,
|
||||||
|
|
||||||
|
/// 48 kbps - 低带宽
|
||||||
|
Low,
|
||||||
|
|
||||||
|
/// 64 kbps - 中等
|
||||||
|
Medium,
|
||||||
|
|
||||||
|
/// 96 kbps - 高质量
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioQuality {
|
||||||
|
pub fn bitrate(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::VeryLow => 24000,
|
||||||
|
Self::Low => 48000,
|
||||||
|
Self::Medium => 64000,
|
||||||
|
Self::High => 96000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct AudioConfig {
|
||||||
|
/// 是否启用
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// 设备名
|
||||||
|
pub device: Option<String>,
|
||||||
|
|
||||||
|
/// 音频质量
|
||||||
|
pub quality: AudioQuality,
|
||||||
|
|
||||||
|
/// 自动启动
|
||||||
|
pub auto_start: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AudioConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
device: None, // 使用默认设备
|
||||||
|
quality: AudioQuality::Medium,
|
||||||
|
auto_start: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/audio/status` | GET | 获取音频状态 |
|
||||||
|
| `/api/audio/start` | POST | 启动音频 |
|
||||||
|
| `/api/audio/stop` | POST | 停止音频 |
|
||||||
|
| `/api/audio/devices` | GET | 列出设备 |
|
||||||
|
| `/api/audio/quality` | GET | 获取质量 |
|
||||||
|
| `/api/audio/quality` | POST | 设置质量 |
|
||||||
|
| `/api/ws/audio` | WS | 音频流 |
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/audio/status
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"streaming": true,
|
||||||
|
"device": "hw:0,0",
|
||||||
|
"sample_rate": 48000,
|
||||||
|
"channels": 2,
|
||||||
|
"bitrate": 64000,
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/audio/devices
|
||||||
|
{
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"name": "hw:0,0",
|
||||||
|
"description": "USB Audio Device",
|
||||||
|
"is_default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. WebSocket 音频流
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 连接 WebSocket
|
||||||
|
const ws = new WebSocket('/api/ws/audio');
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
// 初始化 Opus 解码器
|
||||||
|
const decoder = new OpusDecoder();
|
||||||
|
|
||||||
|
// 接收音频帧
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const frame = new Uint8Array(event.data);
|
||||||
|
const pcm = decoder.decode(frame);
|
||||||
|
audioContext.play(pcm);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SystemEvent {
|
||||||
|
AudioStateChanged {
|
||||||
|
enabled: bool,
|
||||||
|
streaming: bool,
|
||||||
|
device: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AudioError {
|
||||||
|
#[error("Device not found: {0}")]
|
||||||
|
DeviceNotFound(String),
|
||||||
|
|
||||||
|
#[error("Device busy: {0}")]
|
||||||
|
DeviceBusy(String),
|
||||||
|
|
||||||
|
#[error("ALSA error: {0}")]
|
||||||
|
AlsaError(String),
|
||||||
|
|
||||||
|
#[error("Encoder error: {0}")]
|
||||||
|
EncoderError(String),
|
||||||
|
|
||||||
|
#[error("Not streaming")]
|
||||||
|
NotStreaming,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 使用示例
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let controller = AudioController::new(&config, events)?;
|
||||||
|
|
||||||
|
// 启动音频
|
||||||
|
controller.start().await?;
|
||||||
|
|
||||||
|
// 订阅音频帧
|
||||||
|
let mut rx = controller.subscribe();
|
||||||
|
while let Ok(frame) = rx.recv().await {
|
||||||
|
// 处理 Opus 数据
|
||||||
|
send_to_client(frame.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止
|
||||||
|
controller.stop().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 常见问题
|
||||||
|
|
||||||
|
### Q: 找不到音频设备?
|
||||||
|
|
||||||
|
1. 检查 ALSA 配置
|
||||||
|
2. 运行 `arecord -l`
|
||||||
|
3. 检查权限
|
||||||
|
|
||||||
|
### Q: 音频延迟高?
|
||||||
|
|
||||||
|
1. 减小帧大小
|
||||||
|
2. 降低质量
|
||||||
|
3. 检查网络
|
||||||
|
|
||||||
|
### Q: 音频断断续续?
|
||||||
|
|
||||||
|
1. 增大缓冲区
|
||||||
|
2. 检查 CPU 负载
|
||||||
|
3. 使用更低质量
|
||||||
340
docs/modules/auth.md
Normal file
340
docs/modules/auth.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# Auth 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
Auth 模块提供用户认证和会话管理功能。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- 用户管理
|
||||||
|
- 密码哈希 (Argon2)
|
||||||
|
- 会话管理
|
||||||
|
- 认证中间件
|
||||||
|
- 权限控制
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/auth/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── user.rs # 用户管理 (5KB)
|
||||||
|
├── session.rs # 会话管理 (4KB)
|
||||||
|
├── password.rs # 密码哈希 (1KB)
|
||||||
|
└── middleware.rs # 中间件 (4KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心组件
|
||||||
|
|
||||||
|
### 2.1 UserStore (user.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct UserStore {
|
||||||
|
db: Pool<Sqlite>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserStore {
|
||||||
|
/// 创建存储
|
||||||
|
pub async fn new(db: Pool<Sqlite>) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 创建用户
|
||||||
|
pub async fn create_user(&self, user: &CreateUser) -> Result<User>;
|
||||||
|
|
||||||
|
/// 获取用户
|
||||||
|
pub async fn get_user(&self, id: &str) -> Result<Option<User>>;
|
||||||
|
|
||||||
|
/// 按用户名获取
|
||||||
|
pub async fn get_by_username(&self, username: &str) -> Result<Option<User>>;
|
||||||
|
|
||||||
|
/// 更新用户
|
||||||
|
pub async fn update_user(&self, id: &str, update: &UpdateUser) -> Result<()>;
|
||||||
|
|
||||||
|
/// 删除用户
|
||||||
|
pub async fn delete_user(&self, id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 列出用户
|
||||||
|
pub async fn list_users(&self) -> Result<Vec<User>>;
|
||||||
|
|
||||||
|
/// 验证密码
|
||||||
|
pub async fn verify_password(&self, username: &str, password: &str) -> Result<Option<User>>;
|
||||||
|
|
||||||
|
/// 更新密码
|
||||||
|
pub async fn update_password(&self, id: &str, new_password: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 检查是否需要初始化
|
||||||
|
pub async fn needs_setup(&self) -> Result<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct User {
|
||||||
|
pub id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub role: UserRole,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UserRole {
|
||||||
|
Admin,
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateUser {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub role: UserRole,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 SessionStore (session.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SessionStore {
|
||||||
|
/// 会话映射
|
||||||
|
sessions: RwLock<HashMap<String, Session>>,
|
||||||
|
|
||||||
|
/// 会话超时
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionStore {
|
||||||
|
/// 创建存储
|
||||||
|
pub fn new(timeout: Duration) -> Self;
|
||||||
|
|
||||||
|
/// 创建会话
|
||||||
|
pub fn create_session(&self, user: &User) -> String;
|
||||||
|
|
||||||
|
/// 获取会话
|
||||||
|
pub fn get_session(&self, token: &str) -> Option<Session>;
|
||||||
|
|
||||||
|
/// 删除会话
|
||||||
|
pub fn delete_session(&self, token: &str);
|
||||||
|
|
||||||
|
/// 清理过期会话
|
||||||
|
pub fn cleanup_expired(&self);
|
||||||
|
|
||||||
|
/// 刷新会话
|
||||||
|
pub fn refresh_session(&self, token: &str) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Session {
|
||||||
|
pub token: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub role: UserRole,
|
||||||
|
pub created_at: Instant,
|
||||||
|
pub last_active: Instant,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 密码哈希 (password.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 哈希密码
|
||||||
|
pub fn hash_password(password: &str) -> Result<String> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let hash = argon2
|
||||||
|
.hash_password(password.as_bytes(), &salt)?
|
||||||
|
.to_string();
|
||||||
|
Ok(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证密码
|
||||||
|
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
||||||
|
let parsed_hash = PasswordHash::new(hash)?;
|
||||||
|
Ok(Argon2::default()
|
||||||
|
.verify_password(password.as_bytes(), &parsed_hash)
|
||||||
|
.is_ok())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 认证中间件 (middleware.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn auth_middleware(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
cookies: Cookies,
|
||||||
|
request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
// 获取 session token
|
||||||
|
let token = cookies
|
||||||
|
.get("session_id")
|
||||||
|
.map(|c| c.value().to_string());
|
||||||
|
|
||||||
|
// 验证会话
|
||||||
|
let session = token
|
||||||
|
.and_then(|t| state.sessions.get_session(&t));
|
||||||
|
|
||||||
|
if let Some(session) = session {
|
||||||
|
// 将用户信息注入请求
|
||||||
|
let mut request = request;
|
||||||
|
request.extensions_mut().insert(session);
|
||||||
|
next.run(request).await
|
||||||
|
} else {
|
||||||
|
StatusCode::UNAUTHORIZED.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn admin_middleware(
|
||||||
|
session: Extension<Session>,
|
||||||
|
request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
if session.role == UserRole::Admin {
|
||||||
|
next.run(request).await
|
||||||
|
} else {
|
||||||
|
StatusCode::FORBIDDEN.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 权限 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `/api/auth/login` | POST | Public | 登录 |
|
||||||
|
| `/api/auth/logout` | POST | User | 登出 |
|
||||||
|
| `/api/auth/check` | GET | User | 检查认证 |
|
||||||
|
| `/api/auth/password` | POST | User | 修改密码 |
|
||||||
|
| `/api/users` | GET | Admin | 列出用户 |
|
||||||
|
| `/api/users` | POST | Admin | 创建用户 |
|
||||||
|
| `/api/users/:id` | DELETE | Admin | 删除用户 |
|
||||||
|
| `/api/setup/init` | POST | Public | 初始化设置 |
|
||||||
|
|
||||||
|
### 请求/响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// POST /api/auth/login
|
||||||
|
// Request:
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response:
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "admin",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/auth/check
|
||||||
|
{
|
||||||
|
"authenticated": true,
|
||||||
|
"user": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "admin",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct AuthConfig {
|
||||||
|
/// 会话超时 (秒)
|
||||||
|
pub session_timeout_secs: u64,
|
||||||
|
|
||||||
|
/// 是否启用认证
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AuthConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
session_timeout_secs: 86400, // 24 小时
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 安全特性
|
||||||
|
|
||||||
|
### 5.1 密码存储
|
||||||
|
|
||||||
|
- Argon2id 哈希
|
||||||
|
- 随机盐值
|
||||||
|
- 不可逆
|
||||||
|
|
||||||
|
### 5.2 会话安全
|
||||||
|
|
||||||
|
- 随机 token (UUID v4)
|
||||||
|
- HTTPOnly Cookie
|
||||||
|
- 会话超时
|
||||||
|
- 自动清理
|
||||||
|
|
||||||
|
### 5.3 权限控制
|
||||||
|
|
||||||
|
- 两级权限: Admin / User
|
||||||
|
- 中间件检查
|
||||||
|
- 敏感操作需 Admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 使用示例
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 创建用户
|
||||||
|
let user = users.create_user(&CreateUser {
|
||||||
|
username: "admin".to_string(),
|
||||||
|
password: "password123".to_string(),
|
||||||
|
role: UserRole::Admin,
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if let Some(user) = users.verify_password("admin", "password123").await? {
|
||||||
|
// 创建会话
|
||||||
|
let token = sessions.create_session(&user);
|
||||||
|
|
||||||
|
// 设置 Cookie
|
||||||
|
cookies.add(Cookie::build("session_id", token)
|
||||||
|
.http_only(true)
|
||||||
|
.path("/")
|
||||||
|
.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话
|
||||||
|
if let Some(session) = sessions.get_session(&token) {
|
||||||
|
println!("User: {}", session.username);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("Invalid credentials")]
|
||||||
|
InvalidCredentials,
|
||||||
|
|
||||||
|
#[error("User not found")]
|
||||||
|
UserNotFound,
|
||||||
|
|
||||||
|
#[error("User already exists")]
|
||||||
|
UserExists,
|
||||||
|
|
||||||
|
#[error("Session expired")]
|
||||||
|
SessionExpired,
|
||||||
|
|
||||||
|
#[error("Permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("Setup required")]
|
||||||
|
SetupRequired,
|
||||||
|
}
|
||||||
|
```
|
||||||
297
docs/modules/config.md
Normal file
297
docs/modules/config.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Config 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
Config 模块提供配置管理功能,所有配置存储在 SQLite 数据库中。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- SQLite 配置存储
|
||||||
|
- 类型安全的配置结构
|
||||||
|
- 热重载支持
|
||||||
|
- TypeScript 类型生成
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/config/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── schema.rs # 配置结构定义 (12KB)
|
||||||
|
└── store.rs # SQLite 存储 (8KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心组件
|
||||||
|
|
||||||
|
### 2.1 ConfigStore (store.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ConfigStore {
|
||||||
|
db: Pool<Sqlite>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigStore {
|
||||||
|
/// 创建存储
|
||||||
|
pub async fn new(db_path: &Path) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 获取完整配置
|
||||||
|
pub async fn get_config(&self) -> Result<AppConfig>;
|
||||||
|
|
||||||
|
/// 更新配置
|
||||||
|
pub async fn update_config(&self, config: &AppConfig) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取单个配置项
|
||||||
|
pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>>;
|
||||||
|
|
||||||
|
/// 设置单个配置项
|
||||||
|
pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<()>;
|
||||||
|
|
||||||
|
/// 删除配置项
|
||||||
|
pub async fn delete(&self, key: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 重置为默认
|
||||||
|
pub async fn reset_to_default(&self) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 AppConfig (schema.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct AppConfig {
|
||||||
|
/// 视频配置
|
||||||
|
pub video: VideoConfig,
|
||||||
|
|
||||||
|
/// 流配置
|
||||||
|
pub stream: StreamConfig,
|
||||||
|
|
||||||
|
/// HID 配置
|
||||||
|
pub hid: HidConfig,
|
||||||
|
|
||||||
|
/// MSD 配置
|
||||||
|
pub msd: MsdConfig,
|
||||||
|
|
||||||
|
/// ATX 配置
|
||||||
|
pub atx: AtxConfig,
|
||||||
|
|
||||||
|
/// 音频配置
|
||||||
|
pub audio: AudioConfig,
|
||||||
|
|
||||||
|
/// 认证配置
|
||||||
|
pub auth: AuthConfig,
|
||||||
|
|
||||||
|
/// Web 配置
|
||||||
|
pub web: WebConfig,
|
||||||
|
|
||||||
|
/// RustDesk 配置
|
||||||
|
pub rustdesk: RustDeskConfig,
|
||||||
|
|
||||||
|
/// 扩展配置
|
||||||
|
pub extensions: ExtensionsConfig,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 各模块配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct VideoConfig {
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub format: Option<String>,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub fps: u32,
|
||||||
|
pub quality: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct StreamConfig {
|
||||||
|
pub mode: StreamMode,
|
||||||
|
pub bitrate_kbps: u32,
|
||||||
|
pub gop_size: u32,
|
||||||
|
pub encoder: EncoderType,
|
||||||
|
pub stun_server: Option<String>,
|
||||||
|
pub turn_server: Option<String>,
|
||||||
|
pub turn_username: Option<String>,
|
||||||
|
pub turn_password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct HidConfig {
|
||||||
|
pub backend: HidBackendType,
|
||||||
|
pub ch9329_device: Option<String>,
|
||||||
|
pub ch9329_baud_rate: Option<u32>,
|
||||||
|
pub default_mouse_mode: MouseMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他配置结构
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. TypeScript 类型生成
|
||||||
|
|
||||||
|
使用 `#[typeshare]` 属性自动生成 TypeScript 类型:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct VideoConfig {
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的 TypeScript:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface VideoConfig {
|
||||||
|
device?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
生成命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/generate-types.sh
|
||||||
|
# 或
|
||||||
|
typeshare src --lang=typescript --output-file=web/src/types/generated.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 权限 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `/api/config` | GET | Admin | 获取完整配置 |
|
||||||
|
| `/api/config` | PATCH | Admin | 更新配置 |
|
||||||
|
| `/api/config/video` | GET | Admin | 获取视频配置 |
|
||||||
|
| `/api/config/video` | PATCH | Admin | 更新视频配置 |
|
||||||
|
| `/api/config/stream` | GET | Admin | 获取流配置 |
|
||||||
|
| `/api/config/stream` | PATCH | Admin | 更新流配置 |
|
||||||
|
| `/api/config/hid` | GET | Admin | 获取 HID 配置 |
|
||||||
|
| `/api/config/hid` | PATCH | Admin | 更新 HID 配置 |
|
||||||
|
| `/api/config/reset` | POST | Admin | 重置为默认 |
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/config/video
|
||||||
|
{
|
||||||
|
"device": "/dev/video0",
|
||||||
|
"format": "MJPEG",
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"fps": 30,
|
||||||
|
"quality": 80
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/config/video
|
||||||
|
// Request:
|
||||||
|
{
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response: 更新后的完整配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置热重载
|
||||||
|
|
||||||
|
配置更改后自动重载相关组件:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 更新配置
|
||||||
|
config_store.update_config(&new_config).await?;
|
||||||
|
|
||||||
|
// 发布配置变更事件
|
||||||
|
events.publish(SystemEvent::ConfigChanged {
|
||||||
|
section: "video".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 各组件监听事件并重载
|
||||||
|
// VideoStreamManager::on_config_changed()
|
||||||
|
// HidController::reload()
|
||||||
|
// etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 数据库结构
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
配置以 JSON 格式存储:
|
||||||
|
|
||||||
|
```
|
||||||
|
key: "app_config"
|
||||||
|
value: { "video": {...}, "hid": {...}, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 使用示例
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 获取配置
|
||||||
|
let config = config_store.get_config().await?;
|
||||||
|
println!("Video device: {:?}", config.video.device);
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
let mut config = config_store.get_config().await?;
|
||||||
|
config.video.width = 1280;
|
||||||
|
config.video.height = 720;
|
||||||
|
config_store.update_config(&config).await?;
|
||||||
|
|
||||||
|
// 获取单个配置项
|
||||||
|
let video: Option<VideoConfig> = config_store.get("video").await?;
|
||||||
|
|
||||||
|
// 设置单个配置项
|
||||||
|
config_store.set("video", &video_config).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 默认配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
video: VideoConfig {
|
||||||
|
device: None,
|
||||||
|
format: None,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
fps: 30,
|
||||||
|
quality: 80,
|
||||||
|
},
|
||||||
|
stream: StreamConfig {
|
||||||
|
mode: StreamMode::Mjpeg,
|
||||||
|
bitrate_kbps: 2000,
|
||||||
|
gop_size: 60,
|
||||||
|
encoder: EncoderType::H264,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
353
docs/modules/events.md
Normal file
353
docs/modules/events.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# Events 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
Events 模块提供事件总线功能,用于模块间通信和状态广播。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- 事件发布/订阅
|
||||||
|
- 多订阅者广播
|
||||||
|
- WebSocket 事件推送
|
||||||
|
- 状态变更通知
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/events/
|
||||||
|
└── mod.rs # EventBus 实现
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Event System │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||||
|
│ Video │ │ HID │ │ Audio │
|
||||||
|
│ Module │ │ Module │ │ Module │
|
||||||
|
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
|
||||||
|
│ │ │
|
||||||
|
│ publish() │ publish() │ publish()
|
||||||
|
└──────────────────┼──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ EventBus │
|
||||||
|
│ (broadcast channel) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┐
|
||||||
|
│ │ │
|
||||||
|
│ subscribe() │ subscribe() │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||||
|
│ WebSocket │ │ DeviceInfo │ │ Internal │
|
||||||
|
│ Handler │ │ Broadcaster │ │ Tasks │
|
||||||
|
└────────────────┘ └────────────────┘ └────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 EventBus
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct EventBus {
|
||||||
|
/// 广播发送器
|
||||||
|
tx: broadcast::Sender<SystemEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventBus {
|
||||||
|
/// 创建事件总线
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, _) = broadcast::channel(1024);
|
||||||
|
Self { tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发布事件
|
||||||
|
pub fn publish(&self, event: SystemEvent) {
|
||||||
|
let _ = self.tx.send(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 订阅事件
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<SystemEvent> {
|
||||||
|
self.tx.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 SystemEvent
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub enum SystemEvent {
|
||||||
|
// 视频事件
|
||||||
|
StreamStateChanged {
|
||||||
|
state: String,
|
||||||
|
device: Option<String>,
|
||||||
|
resolution: Option<Resolution>,
|
||||||
|
fps: Option<f32>,
|
||||||
|
},
|
||||||
|
|
||||||
|
VideoDeviceChanged {
|
||||||
|
added: Vec<String>,
|
||||||
|
removed: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
// HID 事件
|
||||||
|
HidStateChanged {
|
||||||
|
backend: String,
|
||||||
|
initialized: bool,
|
||||||
|
keyboard_connected: bool,
|
||||||
|
mouse_connected: bool,
|
||||||
|
mouse_mode: String,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
// MSD 事件
|
||||||
|
MsdStateChanged {
|
||||||
|
mode: String,
|
||||||
|
connected: bool,
|
||||||
|
image: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
MsdDownloadProgress {
|
||||||
|
download_id: String,
|
||||||
|
downloaded: u64,
|
||||||
|
total: u64,
|
||||||
|
speed: u64,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ATX 事件
|
||||||
|
AtxStateChanged {
|
||||||
|
power_on: bool,
|
||||||
|
last_action: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 音频事件
|
||||||
|
AudioStateChanged {
|
||||||
|
enabled: bool,
|
||||||
|
streaming: bool,
|
||||||
|
device: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 配置事件
|
||||||
|
ConfigChanged {
|
||||||
|
section: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设备信息汇总
|
||||||
|
DeviceInfo {
|
||||||
|
video: VideoInfo,
|
||||||
|
hid: HidInfo,
|
||||||
|
msd: MsdInfo,
|
||||||
|
atx: AtxInfo,
|
||||||
|
audio: AudioInfo,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 系统错误
|
||||||
|
SystemError {
|
||||||
|
module: String,
|
||||||
|
severity: String,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
// RustDesk 事件
|
||||||
|
RustDeskStatusChanged {
|
||||||
|
status: String,
|
||||||
|
device_id: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
RustDeskConnectionOpened {
|
||||||
|
connection_id: String,
|
||||||
|
peer_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
RustDeskConnectionClosed {
|
||||||
|
connection_id: String,
|
||||||
|
peer_id: String,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 设备信息广播器
|
||||||
|
|
||||||
|
在 `main.rs` 中启动的后台任务:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn spawn_device_info_broadcaster(
|
||||||
|
state: Arc<AppState>,
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut rx = events.subscribe();
|
||||||
|
let mut debounce = tokio::time::interval(Duration::from_millis(100));
|
||||||
|
let mut pending = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// 收到事件
|
||||||
|
result = rx.recv() => {
|
||||||
|
if result.is_ok() {
|
||||||
|
pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防抖定时器
|
||||||
|
_ = debounce.tick() => {
|
||||||
|
if pending {
|
||||||
|
pending = false;
|
||||||
|
// 收集设备信息
|
||||||
|
let device_info = state.get_device_info().await;
|
||||||
|
// 广播
|
||||||
|
events.publish(SystemEvent::DeviceInfo(device_info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. WebSocket 事件推送
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn ws_handler(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
ws.on_upgrade(|socket| handle_ws(socket, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_ws(mut socket: WebSocket, state: Arc<AppState>) {
|
||||||
|
let mut rx = state.events.subscribe();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// 发送事件给客户端
|
||||||
|
result = rx.recv() => {
|
||||||
|
if let Ok(event) = result {
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
if socket.send(Message::Text(json)).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收客户端消息
|
||||||
|
msg = socket.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(Message::Close(_))) | None => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 使用示例
|
||||||
|
|
||||||
|
### 6.1 发布事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 视频模块发布状态变更
|
||||||
|
events.publish(SystemEvent::StreamStateChanged {
|
||||||
|
state: "streaming".to_string(),
|
||||||
|
device: Some("/dev/video0".to_string()),
|
||||||
|
resolution: Some(Resolution { width: 1920, height: 1080 }),
|
||||||
|
fps: Some(30.0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// HID 模块发布状态变更
|
||||||
|
events.publish(SystemEvent::HidStateChanged {
|
||||||
|
backend: "otg".to_string(),
|
||||||
|
initialized: true,
|
||||||
|
keyboard_connected: true,
|
||||||
|
mouse_connected: true,
|
||||||
|
mouse_mode: "absolute".to_string(),
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 订阅事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut rx = events.subscribe();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(SystemEvent::StreamStateChanged { state, .. }) => {
|
||||||
|
println!("Stream state: {}", state);
|
||||||
|
}
|
||||||
|
Ok(SystemEvent::HidStateChanged { backend, .. }) => {
|
||||||
|
println!("HID backend: {}", backend);
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 前端事件处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 连接 WebSocket
|
||||||
|
const ws = new WebSocket('/api/ws');
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'StreamStateChanged':
|
||||||
|
updateStreamStatus(data);
|
||||||
|
break;
|
||||||
|
case 'HidStateChanged':
|
||||||
|
updateHidStatus(data);
|
||||||
|
break;
|
||||||
|
case 'MsdStateChanged':
|
||||||
|
updateMsdStatus(data);
|
||||||
|
break;
|
||||||
|
case 'DeviceInfo':
|
||||||
|
updateAllDevices(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 最佳实践
|
||||||
|
|
||||||
|
### 8.1 事件粒度
|
||||||
|
|
||||||
|
- 使用细粒度事件便于精确更新
|
||||||
|
- DeviceInfo 用于初始化和定期同步
|
||||||
|
|
||||||
|
### 8.2 防抖
|
||||||
|
|
||||||
|
- 使用 100ms 防抖避免事件风暴
|
||||||
|
- 合并多个快速变更
|
||||||
|
|
||||||
|
### 8.3 错误处理
|
||||||
|
|
||||||
|
- 发布失败静默忽略 (fire-and-forget)
|
||||||
|
- 订阅者断开自动清理
|
||||||
850
docs/modules/hid.md
Normal file
850
docs/modules/hid.md
Normal file
@@ -0,0 +1,850 @@
|
|||||||
|
# HID 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
HID (Human Interface Device) 模块负责将键盘和鼠标事件转发到目标计算机,是 One-KVM 实现远程控制的核心模块。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- 键盘事件处理 (按键、修饰键)
|
||||||
|
- 鼠标事件处理 (移动、点击、滚轮)
|
||||||
|
- 支持绝对和相对鼠标模式
|
||||||
|
- 多后端支持 (OTG、CH9329)
|
||||||
|
- WebSocket 和 DataChannel 输入
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/hid/
|
||||||
|
├── mod.rs # HidController (16KB)
|
||||||
|
├── backend.rs # 后端抽象
|
||||||
|
├── otg.rs # OTG 后端 (33KB)
|
||||||
|
├── ch9329.rs # CH9329 串口后端 (46KB)
|
||||||
|
├── keymap.rs # 按键映射 (14KB)
|
||||||
|
├── types.rs # 类型定义
|
||||||
|
├── monitor.rs # 健康监视 (14KB)
|
||||||
|
├── datachannel.rs # DataChannel 适配 (8KB)
|
||||||
|
└── websocket.rs # WebSocket 适配 (6KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ HID Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Browser Input Events
|
||||||
|
│
|
||||||
|
┌─────────┴─────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ WebSocket │ │ DataChannel │
|
||||||
|
│ Handler │ │ Handler │
|
||||||
|
│ (websocket.rs) │ │(datachannel.rs) │
|
||||||
|
└────────┬────────┘ └────────┬────────┘
|
||||||
|
│ │
|
||||||
|
└──────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ HidController │
|
||||||
|
│ (mod.rs) │
|
||||||
|
│ - send_keyboard() │
|
||||||
|
│ - send_mouse() │
|
||||||
|
│ - select_backend() │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│
|
||||||
|
┌──────────┼──────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│ OTG Backend│ │ CH9329 │ │ None │
|
||||||
|
│ (otg.rs) │ │ Backend │ │ (dummy) │
|
||||||
|
└──────┬──────┘ └────┬─────┘ └──────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────┐ ┌──────────┐
|
||||||
|
│ /dev/hidg* │ │ Serial │
|
||||||
|
│ USB Gadget │ │ Port │
|
||||||
|
└─────────────┘ └──────────┘
|
||||||
|
│ │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ Target PC │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 后端选择
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Backend Selection │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
HidBackendType::Otg
|
||||||
|
│
|
||||||
|
├── 检查 OtgService 是否可用
|
||||||
|
│
|
||||||
|
├── 请求 HID 函数 (3个设备)
|
||||||
|
│ ├── /dev/hidg0 (键盘)
|
||||||
|
│ ├── /dev/hidg1 (相对鼠标)
|
||||||
|
│ └── /dev/hidg2 (绝对鼠标)
|
||||||
|
│
|
||||||
|
└── 创建 OtgHidBackend
|
||||||
|
|
||||||
|
HidBackendType::Ch9329 { port, baud_rate }
|
||||||
|
│
|
||||||
|
├── 打开串口设备
|
||||||
|
│
|
||||||
|
├── 初始化 CH9329 芯片
|
||||||
|
│
|
||||||
|
└── 创建 Ch9329HidBackend
|
||||||
|
|
||||||
|
HidBackendType::None
|
||||||
|
│
|
||||||
|
└── 创建空后端 (丢弃所有事件)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 HidController (mod.rs)
|
||||||
|
|
||||||
|
HID 控制器主类,统一管理所有 HID 操作。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct HidController {
|
||||||
|
/// 当前后端
|
||||||
|
backend: Arc<RwLock<Box<dyn HidBackend>>>,
|
||||||
|
|
||||||
|
/// 后端类型
|
||||||
|
backend_type: Arc<RwLock<HidBackendType>>,
|
||||||
|
|
||||||
|
/// OTG 服务引用
|
||||||
|
otg_service: Arc<OtgService>,
|
||||||
|
|
||||||
|
/// 健康监视器
|
||||||
|
monitor: Arc<HidHealthMonitor>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: Arc<RwLock<HidConfig>>,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
|
||||||
|
/// 鼠标模式
|
||||||
|
mouse_mode: Arc<RwLock<MouseMode>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HidController {
|
||||||
|
/// 初始化控制器
|
||||||
|
pub async fn init(
|
||||||
|
otg_service: Arc<OtgService>,
|
||||||
|
config: &HidConfig,
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
) -> Result<Arc<Self>>;
|
||||||
|
|
||||||
|
/// 发送键盘事件
|
||||||
|
pub async fn send_keyboard(&self, event: &KeyboardEvent) -> Result<()>;
|
||||||
|
|
||||||
|
/// 发送鼠标事件
|
||||||
|
pub async fn send_mouse(&self, event: &MouseEvent) -> Result<()>;
|
||||||
|
|
||||||
|
/// 设置鼠标模式
|
||||||
|
pub fn set_mouse_mode(&self, mode: MouseMode);
|
||||||
|
|
||||||
|
/// 获取鼠标模式
|
||||||
|
pub fn get_mouse_mode(&self) -> MouseMode;
|
||||||
|
|
||||||
|
/// 重新加载配置
|
||||||
|
pub async fn reload(&self, config: &HidConfig) -> Result<()>;
|
||||||
|
|
||||||
|
/// 重置 HID 状态
|
||||||
|
pub async fn reset(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取状态信息
|
||||||
|
pub fn info(&self) -> HidInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HidInfo {
|
||||||
|
pub backend: String,
|
||||||
|
pub initialized: bool,
|
||||||
|
pub keyboard_connected: bool,
|
||||||
|
pub mouse_connected: bool,
|
||||||
|
pub mouse_mode: MouseMode,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 HidBackend Trait (backend.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[async_trait]
|
||||||
|
pub trait HidBackend: Send + Sync {
|
||||||
|
/// 发送键盘事件
|
||||||
|
async fn send_keyboard(&self, event: &KeyboardEvent) -> Result<()>;
|
||||||
|
|
||||||
|
/// 发送鼠标事件
|
||||||
|
async fn send_mouse(&self, event: &MouseEvent, mode: MouseMode) -> Result<()>;
|
||||||
|
|
||||||
|
/// 重置状态
|
||||||
|
async fn reset(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取后端信息
|
||||||
|
fn info(&self) -> HidBackendInfo;
|
||||||
|
|
||||||
|
/// 检查连接状态
|
||||||
|
fn is_connected(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HidBackendInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub backend_type: HidBackendType,
|
||||||
|
pub keyboard_connected: bool,
|
||||||
|
pub mouse_connected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum HidBackendType {
|
||||||
|
/// USB OTG gadget 模式
|
||||||
|
Otg,
|
||||||
|
|
||||||
|
/// CH9329 串口 HID 控制器
|
||||||
|
Ch9329 {
|
||||||
|
port: String,
|
||||||
|
baud_rate: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 禁用 HID
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 OTG 后端 (otg.rs)
|
||||||
|
|
||||||
|
通过 Linux USB OTG gadget 模拟 HID 设备。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct OtgHidBackend {
|
||||||
|
/// HID 设备路径
|
||||||
|
paths: HidDevicePaths,
|
||||||
|
|
||||||
|
/// 键盘设备文件
|
||||||
|
keyboard_fd: RwLock<Option<File>>,
|
||||||
|
|
||||||
|
/// 相对鼠标设备文件
|
||||||
|
mouse_rel_fd: RwLock<Option<File>>,
|
||||||
|
|
||||||
|
/// 绝对鼠标设备文件
|
||||||
|
mouse_abs_fd: RwLock<Option<File>>,
|
||||||
|
|
||||||
|
/// 当前键盘状态
|
||||||
|
keyboard_state: Mutex<KeyboardState>,
|
||||||
|
|
||||||
|
/// OTG 服务引用
|
||||||
|
otg_service: Arc<OtgService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OtgHidBackend {
|
||||||
|
/// 创建 OTG 后端
|
||||||
|
pub async fn new(otg_service: Arc<OtgService>) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 打开 HID 设备
|
||||||
|
async fn open_devices(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 关闭 HID 设备
|
||||||
|
async fn close_devices(&self);
|
||||||
|
|
||||||
|
/// 写入键盘报告
|
||||||
|
fn write_keyboard_report(&self, report: &KeyboardReport) -> Result<()>;
|
||||||
|
|
||||||
|
/// 写入鼠标报告
|
||||||
|
fn write_mouse_report(&self, report: &[u8], absolute: bool) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HidDevicePaths {
|
||||||
|
pub keyboard: PathBuf, // /dev/hidg0
|
||||||
|
pub mouse_relative: PathBuf, // /dev/hidg1
|
||||||
|
pub mouse_absolute: PathBuf, // /dev/hidg2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HID 报告格式
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 键盘报告 (8 字节)
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct KeyboardReport {
|
||||||
|
pub modifiers: u8, // Ctrl, Shift, Alt, GUI
|
||||||
|
pub reserved: u8, // 保留
|
||||||
|
pub keys: [u8; 6], // 最多 6 个按键 scancode
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 相对鼠标报告 (4 字节)
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct MouseRelativeReport {
|
||||||
|
pub buttons: u8, // 按钮状态
|
||||||
|
pub x: i8, // X 移动 (-127 ~ 127)
|
||||||
|
pub y: i8, // Y 移动 (-127 ~ 127)
|
||||||
|
pub wheel: i8, // 滚轮 (-127 ~ 127)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绝对鼠标报告 (6 字节)
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct MouseAbsoluteReport {
|
||||||
|
pub buttons: u8, // 按钮状态
|
||||||
|
pub x: u16, // X 坐标 (0 ~ 32767)
|
||||||
|
pub y: u16, // Y 坐标 (0 ~ 32767)
|
||||||
|
pub wheel: i8, // 滚轮 (-127 ~ 127)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 CH9329 后端 (ch9329.rs)
|
||||||
|
|
||||||
|
通过 CH9329 芯片(串口转 HID)实现 HID 功能。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Ch9329HidBackend {
|
||||||
|
/// 串口设备
|
||||||
|
port: Mutex<Box<dyn SerialPort>>,
|
||||||
|
|
||||||
|
/// 设备路径
|
||||||
|
device_path: String,
|
||||||
|
|
||||||
|
/// 波特率
|
||||||
|
baud_rate: u32,
|
||||||
|
|
||||||
|
/// 当前键盘状态
|
||||||
|
keyboard_state: Mutex<KeyboardState>,
|
||||||
|
|
||||||
|
/// 连接状态
|
||||||
|
connected: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ch9329HidBackend {
|
||||||
|
/// 创建 CH9329 后端
|
||||||
|
pub fn new(device: &str, baud_rate: u32) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 发送命令
|
||||||
|
fn send_command(&self, cmd: &[u8]) -> Result<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 发送键盘数据包
|
||||||
|
fn send_keyboard_packet(&self, report: &KeyboardReport) -> Result<()>;
|
||||||
|
|
||||||
|
/// 发送鼠标数据包
|
||||||
|
fn send_mouse_packet(&self, report: &[u8], absolute: bool) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CH9329 协议
|
||||||
|
|
||||||
|
```
|
||||||
|
帧格式:
|
||||||
|
┌──────┬──────┬──────┬──────────┬──────────┬──────┐
|
||||||
|
│ HEAD │ ADDR │ CMD │ LEN │ DATA │ SUM │
|
||||||
|
│ 0x57 │ 0xAB │ 0xXX │ data_len │ payload │ csum │
|
||||||
|
└──────┴──────┴──────┴──────────┴──────────┴──────┘
|
||||||
|
|
||||||
|
命令码:
|
||||||
|
0x02 - 发送键盘数据
|
||||||
|
0x04 - 发送绝对鼠标数据
|
||||||
|
0x05 - 发送相对鼠标数据
|
||||||
|
0x0E - 获取芯片信息
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 事件类型
|
||||||
|
|
||||||
|
### 4.1 键盘事件 (types.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct KeyboardEvent {
|
||||||
|
/// 按下的键列表
|
||||||
|
pub keys: Vec<KeyCode>,
|
||||||
|
|
||||||
|
/// 修饰键状态
|
||||||
|
pub modifiers: KeyboardModifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct KeyboardModifiers {
|
||||||
|
pub left_ctrl: bool,
|
||||||
|
pub left_shift: bool,
|
||||||
|
pub left_alt: bool,
|
||||||
|
pub left_gui: bool,
|
||||||
|
pub right_ctrl: bool,
|
||||||
|
pub right_shift: bool,
|
||||||
|
pub right_alt: bool,
|
||||||
|
pub right_gui: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardModifiers {
|
||||||
|
/// 转换为 USB HID 修饰符字节
|
||||||
|
pub fn to_byte(&self) -> u8 {
|
||||||
|
let mut byte = 0u8;
|
||||||
|
if self.left_ctrl { byte |= 0x01; }
|
||||||
|
if self.left_shift { byte |= 0x02; }
|
||||||
|
if self.left_alt { byte |= 0x04; }
|
||||||
|
if self.left_gui { byte |= 0x08; }
|
||||||
|
if self.right_ctrl { byte |= 0x10; }
|
||||||
|
if self.right_shift { byte |= 0x20; }
|
||||||
|
if self.right_alt { byte |= 0x40; }
|
||||||
|
if self.right_gui { byte |= 0x80; }
|
||||||
|
byte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 鼠标事件 (types.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MouseEvent {
|
||||||
|
/// 按钮
|
||||||
|
pub button: Option<MouseButton>,
|
||||||
|
|
||||||
|
/// 事件类型
|
||||||
|
pub event_type: MouseEventType,
|
||||||
|
|
||||||
|
/// 相对移动 X
|
||||||
|
pub dx: i16,
|
||||||
|
|
||||||
|
/// 相对移动 Y
|
||||||
|
pub dy: i16,
|
||||||
|
|
||||||
|
/// 绝对位置 X (0-32767)
|
||||||
|
pub x: u32,
|
||||||
|
|
||||||
|
/// 绝对位置 Y (0-32767)
|
||||||
|
pub y: u32,
|
||||||
|
|
||||||
|
/// 滚轮移动
|
||||||
|
pub wheel: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MouseButton {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Middle,
|
||||||
|
Button4,
|
||||||
|
Button5,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MouseEventType {
|
||||||
|
Press,
|
||||||
|
Release,
|
||||||
|
Move,
|
||||||
|
Wheel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MouseMode {
|
||||||
|
/// 相对模式 (用于普通操作)
|
||||||
|
Relative,
|
||||||
|
|
||||||
|
/// 绝对模式 (用于 BIOS/精确定位)
|
||||||
|
Absolute,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 按键映射
|
||||||
|
|
||||||
|
### 5.1 KeyCode 枚举 (keymap.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum KeyCode {
|
||||||
|
// 字母键
|
||||||
|
KeyA, KeyB, KeyC, /* ... */ KeyZ,
|
||||||
|
|
||||||
|
// 数字键
|
||||||
|
Digit1, Digit2, /* ... */ Digit0,
|
||||||
|
|
||||||
|
// 功能键
|
||||||
|
F1, F2, /* ... */ F12,
|
||||||
|
|
||||||
|
// 控制键
|
||||||
|
Escape, Tab, CapsLock, Space, Enter, Backspace,
|
||||||
|
Insert, Delete, Home, End, PageUp, PageDown,
|
||||||
|
|
||||||
|
// 方向键
|
||||||
|
ArrowUp, ArrowDown, ArrowLeft, ArrowRight,
|
||||||
|
|
||||||
|
// 修饰键
|
||||||
|
ShiftLeft, ShiftRight,
|
||||||
|
ControlLeft, ControlRight,
|
||||||
|
AltLeft, AltRight,
|
||||||
|
MetaLeft, MetaRight,
|
||||||
|
|
||||||
|
// 小键盘
|
||||||
|
Numpad0, Numpad1, /* ... */ Numpad9,
|
||||||
|
NumpadAdd, NumpadSubtract, NumpadMultiply, NumpadDivide,
|
||||||
|
NumpadEnter, NumpadDecimal, NumLock,
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
PrintScreen, ScrollLock, Pause,
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyCode {
|
||||||
|
/// 转换为 USB HID scancode
|
||||||
|
pub fn to_scancode(&self) -> u8;
|
||||||
|
|
||||||
|
/// 从 JavaScript keyCode 转换
|
||||||
|
pub fn from_js_code(code: &str) -> Option<Self>;
|
||||||
|
|
||||||
|
/// 是否为修饰键
|
||||||
|
pub fn is_modifier(&self) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 JavaScript 键码映射
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 前端发送的格式
|
||||||
|
{
|
||||||
|
"type": "keyboard",
|
||||||
|
"keys": ["KeyA", "KeyB"],
|
||||||
|
"modifiers": {
|
||||||
|
"ctrl": false,
|
||||||
|
"shift": true,
|
||||||
|
"alt": false,
|
||||||
|
"meta": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 输入处理器
|
||||||
|
|
||||||
|
### 6.1 WebSocket Handler (websocket.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct WsHidHandler {
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsHidHandler {
|
||||||
|
pub fn new(hid: Arc<HidController>) -> Self;
|
||||||
|
|
||||||
|
/// 处理 WebSocket 消息
|
||||||
|
pub async fn handle_message(&self, msg: &str) -> Result<()> {
|
||||||
|
let event: HidMessage = serde_json::from_str(msg)?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
HidMessage::Keyboard(kb) => {
|
||||||
|
self.hid.send_keyboard(&kb).await?;
|
||||||
|
}
|
||||||
|
HidMessage::Mouse(mouse) => {
|
||||||
|
self.hid.send_mouse(&mouse).await?;
|
||||||
|
}
|
||||||
|
HidMessage::SetMouseMode(mode) => {
|
||||||
|
self.hid.set_mouse_mode(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum HidMessage {
|
||||||
|
#[serde(rename = "keyboard")]
|
||||||
|
Keyboard(KeyboardEvent),
|
||||||
|
|
||||||
|
#[serde(rename = "mouse")]
|
||||||
|
Mouse(MouseEvent),
|
||||||
|
|
||||||
|
#[serde(rename = "mouse_mode")]
|
||||||
|
SetMouseMode(MouseMode),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 DataChannel Handler (datachannel.rs)
|
||||||
|
|
||||||
|
用于 WebRTC 模式下的 HID 事件处理。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct HidDataChannelHandler {
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HidDataChannelHandler {
|
||||||
|
pub fn new(hid: Arc<HidController>) -> Self;
|
||||||
|
|
||||||
|
/// 处理 DataChannel 消息
|
||||||
|
pub async fn handle_message(&self, data: &[u8]) -> Result<()>;
|
||||||
|
|
||||||
|
/// 创建 DataChannel 配置
|
||||||
|
pub fn datachannel_config() -> RTCDataChannelInit;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 健康监视
|
||||||
|
|
||||||
|
### 7.1 HidHealthMonitor (monitor.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct HidHealthMonitor {
|
||||||
|
/// 错误计数
|
||||||
|
error_count: AtomicU32,
|
||||||
|
|
||||||
|
/// 连续错误计数
|
||||||
|
consecutive_errors: AtomicU32,
|
||||||
|
|
||||||
|
/// 最后错误时间
|
||||||
|
last_error: RwLock<Option<Instant>>,
|
||||||
|
|
||||||
|
/// 最后错误消息
|
||||||
|
last_error_msg: RwLock<Option<String>>,
|
||||||
|
|
||||||
|
/// 重试配置
|
||||||
|
config: MonitorConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HidHealthMonitor {
|
||||||
|
/// 记录错误
|
||||||
|
pub fn record_error(&self, error: &str);
|
||||||
|
|
||||||
|
/// 记录成功
|
||||||
|
pub fn record_success(&self);
|
||||||
|
|
||||||
|
/// 是否应该重试
|
||||||
|
pub fn should_retry(&self) -> bool;
|
||||||
|
|
||||||
|
/// 是否需要重新初始化
|
||||||
|
pub fn needs_reinit(&self) -> bool;
|
||||||
|
|
||||||
|
/// 获取健康状态
|
||||||
|
pub fn health_status(&self) -> HealthStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum HealthStatus {
|
||||||
|
Healthy,
|
||||||
|
Degraded { error_rate: f32 },
|
||||||
|
Unhealthy { consecutive_errors: u32 },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 配置
|
||||||
|
|
||||||
|
### 8.1 HID 配置结构
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct HidConfig {
|
||||||
|
/// 后端类型
|
||||||
|
pub backend: HidBackendType,
|
||||||
|
|
||||||
|
/// CH9329 设备路径 (如果使用 CH9329)
|
||||||
|
pub ch9329_device: Option<String>,
|
||||||
|
|
||||||
|
/// CH9329 波特率
|
||||||
|
pub ch9329_baud_rate: Option<u32>,
|
||||||
|
|
||||||
|
/// 默认鼠标模式
|
||||||
|
pub default_mouse_mode: MouseMode,
|
||||||
|
|
||||||
|
/// 鼠标灵敏度 (1-10)
|
||||||
|
pub mouse_sensitivity: u8,
|
||||||
|
|
||||||
|
/// 启用滚轮
|
||||||
|
pub enable_wheel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HidConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
backend: HidBackendType::Otg,
|
||||||
|
ch9329_device: None,
|
||||||
|
ch9329_baud_rate: Some(9600),
|
||||||
|
default_mouse_mode: MouseMode::Absolute,
|
||||||
|
mouse_sensitivity: 5,
|
||||||
|
enable_wheel: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/hid/status` | GET | 获取 HID 状态 |
|
||||||
|
| `/api/hid/reset` | POST | 重置 HID 状态 |
|
||||||
|
| `/api/hid/keyboard` | POST | 发送键盘事件 |
|
||||||
|
| `/api/hid/mouse` | POST | 发送鼠标事件 |
|
||||||
|
| `/api/hid/mouse/mode` | GET | 获取鼠标模式 |
|
||||||
|
| `/api/hid/mouse/mode` | POST | 设置鼠标模式 |
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/hid/status
|
||||||
|
{
|
||||||
|
"backend": "otg",
|
||||||
|
"initialized": true,
|
||||||
|
"keyboard_connected": true,
|
||||||
|
"mouse_connected": true,
|
||||||
|
"mouse_mode": "absolute",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SystemEvent {
|
||||||
|
HidStateChanged {
|
||||||
|
backend: String,
|
||||||
|
initialized: bool,
|
||||||
|
keyboard_connected: bool,
|
||||||
|
mouse_connected: bool,
|
||||||
|
mouse_mode: String,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum HidError {
|
||||||
|
#[error("Backend not initialized")]
|
||||||
|
NotInitialized,
|
||||||
|
|
||||||
|
#[error("Device not found: {0}")]
|
||||||
|
DeviceNotFound(String),
|
||||||
|
|
||||||
|
#[error("Device busy: {0}")]
|
||||||
|
DeviceBusy(String),
|
||||||
|
|
||||||
|
#[error("Write error: {0}")]
|
||||||
|
WriteError(String),
|
||||||
|
|
||||||
|
#[error("Serial port error: {0}")]
|
||||||
|
SerialError(String),
|
||||||
|
|
||||||
|
#[error("Invalid key code: {0}")]
|
||||||
|
InvalidKeyCode(String),
|
||||||
|
|
||||||
|
#[error("OTG service error: {0}")]
|
||||||
|
OtgError(String),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 使用示例
|
||||||
|
|
||||||
|
### 12.1 初始化 HID 控制器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let otg_service = Arc::new(OtgService::new()?);
|
||||||
|
let events = Arc::new(EventBus::new());
|
||||||
|
|
||||||
|
let hid = HidController::init(
|
||||||
|
otg_service,
|
||||||
|
&HidConfig::default(),
|
||||||
|
events,
|
||||||
|
).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 发送键盘事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 按下 Ctrl+C
|
||||||
|
hid.send_keyboard(&KeyboardEvent {
|
||||||
|
keys: vec![KeyCode::KeyC],
|
||||||
|
modifiers: KeyboardModifiers {
|
||||||
|
left_ctrl: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
// 释放所有键
|
||||||
|
hid.send_keyboard(&KeyboardEvent {
|
||||||
|
keys: vec![],
|
||||||
|
modifiers: KeyboardModifiers::default(),
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.3 发送鼠标事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 移动鼠标到绝对位置
|
||||||
|
hid.send_mouse(&MouseEvent {
|
||||||
|
button: None,
|
||||||
|
event_type: MouseEventType::Move,
|
||||||
|
dx: 0,
|
||||||
|
dy: 0,
|
||||||
|
x: 16384, // 屏幕中心
|
||||||
|
y: 16384,
|
||||||
|
wheel: 0,
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
// 点击左键
|
||||||
|
hid.send_mouse(&MouseEvent {
|
||||||
|
button: Some(MouseButton::Left),
|
||||||
|
event_type: MouseEventType::Press,
|
||||||
|
..Default::default()
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
hid.send_mouse(&MouseEvent {
|
||||||
|
button: Some(MouseButton::Left),
|
||||||
|
event_type: MouseEventType::Release,
|
||||||
|
..Default::default()
|
||||||
|
}).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 常见问题
|
||||||
|
|
||||||
|
### Q: OTG 模式下键盘/鼠标不工作?
|
||||||
|
|
||||||
|
1. 检查 `/dev/hidg*` 设备是否存在
|
||||||
|
2. 检查 USB gadget 是否正确配置
|
||||||
|
3. 检查目标 PC 是否识别 USB 设备
|
||||||
|
4. 查看 `dmesg` 日志
|
||||||
|
|
||||||
|
### Q: CH9329 无法初始化?
|
||||||
|
|
||||||
|
1. 检查串口设备路径
|
||||||
|
2. 检查波特率设置
|
||||||
|
3. 使用 `minicom` 测试串口连接
|
||||||
|
|
||||||
|
### Q: 鼠标定位不准确?
|
||||||
|
|
||||||
|
1. 使用绝对鼠标模式
|
||||||
|
2. 校准屏幕分辨率
|
||||||
|
3. 检查缩放设置
|
||||||
|
|
||||||
|
### Q: 按键有延迟?
|
||||||
|
|
||||||
|
1. 检查网络延迟
|
||||||
|
2. 使用 WebRTC 模式
|
||||||
|
3. 减少中间代理
|
||||||
617
docs/modules/msd.md
Normal file
617
docs/modules/msd.md
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
# MSD 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
MSD (Mass Storage Device) 模块提供虚拟存储设备功能,允许将 ISO/IMG 镜像作为 USB 存储设备挂载到目标计算机。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- ISO/IMG 镜像挂载
|
||||||
|
- 镜像下载管理
|
||||||
|
- Ventoy 多 ISO 启动盘
|
||||||
|
- 热插拔支持
|
||||||
|
- 下载进度追踪
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/msd/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── controller.rs # MsdController (20KB)
|
||||||
|
├── image.rs # 镜像管理 (21KB)
|
||||||
|
├── ventoy_drive.rs # Ventoy 驱动 (24KB)
|
||||||
|
├── monitor.rs # 健康监视 (9KB)
|
||||||
|
└── types.rs # 类型定义 (6KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MSD Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Web API
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ MsdController │
|
||||||
|
│ (controller.rs) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┼─────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌───────────┐ ┌───────────┐
|
||||||
|
│ Image │ │ Ventoy │ │ OTG │
|
||||||
|
│ Manager │ │ Drive │ │ Service │
|
||||||
|
│ (image.rs) │ │(ventoy.rs)│ │ │
|
||||||
|
└──────┬──────┘ └─────┬─────┘ └─────┬─────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌───────────┐ ┌───────────┐
|
||||||
|
│ /data/ │ │ exFAT │ │ MSD │
|
||||||
|
│ images/ │ │ Drive │ │ Function │
|
||||||
|
└─────────────┘ └───────────┘ └───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────┐
|
||||||
|
│ Target PC │
|
||||||
|
│ (USB Drive) │
|
||||||
|
└───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 MSD 模式
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MSD Modes │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Image Mode │
|
||||||
|
│ ┌───────────┐ │
|
||||||
|
│ │ ISO/IMG │ ──► MSD LUN ──► Target PC sees single drive │
|
||||||
|
│ │ File │ │
|
||||||
|
│ └───────────┘ │
|
||||||
|
│ 特点: │
|
||||||
|
│ - 单个镜像文件 │
|
||||||
|
│ - 直接挂载 │
|
||||||
|
│ - 适合系统安装 │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Ventoy Mode │
|
||||||
|
│ ┌───────────┐ │
|
||||||
|
│ │ ISO 1 │ │
|
||||||
|
│ ├───────────┤ ┌───────────┐ │
|
||||||
|
│ │ ISO 2 │ ──► │ Ventoy │ ──► Target PC sees bootable drive │
|
||||||
|
│ ├───────────┤ │ Drive │ with ISO selection menu │
|
||||||
|
│ │ ISO 3 │ └───────────┘ │
|
||||||
|
│ └───────────┘ │
|
||||||
|
│ 特点: │
|
||||||
|
│ - 多个 ISO 文件 │
|
||||||
|
│ - exFAT 文件系统 │
|
||||||
|
│ - 启动菜单选择 │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 MsdController (controller.rs)
|
||||||
|
|
||||||
|
MSD 控制器主类。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MsdController {
|
||||||
|
/// 当前状态
|
||||||
|
state: Arc<RwLock<MsdState>>,
|
||||||
|
|
||||||
|
/// 镜像管理器
|
||||||
|
image_manager: Arc<ImageManager>,
|
||||||
|
|
||||||
|
/// Ventoy 驱动器
|
||||||
|
ventoy_drive: Arc<RwLock<Option<VentoyDrive>>>,
|
||||||
|
|
||||||
|
/// OTG 服务
|
||||||
|
otg_service: Arc<OtgService>,
|
||||||
|
|
||||||
|
/// MSD 函数句柄
|
||||||
|
msd_function: Arc<RwLock<Option<MsdFunction>>>,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
|
||||||
|
/// 数据目录
|
||||||
|
data_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MsdController {
|
||||||
|
/// 创建控制器
|
||||||
|
pub async fn new(
|
||||||
|
otg_service: Arc<OtgService>,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
) -> Result<Arc<Self>>;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn state(&self) -> MsdState;
|
||||||
|
|
||||||
|
/// 连接 MSD
|
||||||
|
pub async fn connect(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 断开 MSD
|
||||||
|
pub async fn disconnect(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 切换到镜像模式
|
||||||
|
pub async fn set_image(&self, image_id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 切换到 Ventoy 模式
|
||||||
|
pub async fn set_ventoy(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 清除当前挂载
|
||||||
|
pub async fn clear(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 列出镜像
|
||||||
|
pub fn list_images(&self) -> Vec<ImageInfo>;
|
||||||
|
|
||||||
|
/// 上传镜像
|
||||||
|
pub async fn upload_image(&self, name: &str, data: Bytes) -> Result<ImageInfo>;
|
||||||
|
|
||||||
|
/// 从 URL 下载镜像
|
||||||
|
pub async fn download_image(&self, url: &str) -> Result<String>;
|
||||||
|
|
||||||
|
/// 删除镜像
|
||||||
|
pub async fn delete_image(&self, image_id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取下载进度
|
||||||
|
pub fn get_download_progress(&self, download_id: &str) -> Option<DownloadProgress>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MsdState {
|
||||||
|
/// 是否可用
|
||||||
|
pub available: bool,
|
||||||
|
|
||||||
|
/// 当前模式
|
||||||
|
pub mode: MsdMode,
|
||||||
|
|
||||||
|
/// 是否已连接
|
||||||
|
pub connected: bool,
|
||||||
|
|
||||||
|
/// 当前镜像信息
|
||||||
|
pub current_image: Option<ImageInfo>,
|
||||||
|
|
||||||
|
/// 驱动器信息
|
||||||
|
pub drive_info: Option<DriveInfo>,
|
||||||
|
|
||||||
|
/// 错误信息
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MsdMode {
|
||||||
|
/// 未激活
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// 单镜像模式
|
||||||
|
Image,
|
||||||
|
|
||||||
|
/// Ventoy 模式
|
||||||
|
Drive,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 ImageManager (image.rs)
|
||||||
|
|
||||||
|
镜像文件管理器。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ImageManager {
|
||||||
|
/// 镜像目录
|
||||||
|
images_dir: PathBuf,
|
||||||
|
|
||||||
|
/// 镜像列表缓存
|
||||||
|
images: RwLock<HashMap<String, ImageInfo>>,
|
||||||
|
|
||||||
|
/// 下载任务
|
||||||
|
downloads: RwLock<HashMap<String, DownloadTask>>,
|
||||||
|
|
||||||
|
/// HTTP 客户端
|
||||||
|
http_client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageManager {
|
||||||
|
/// 创建管理器
|
||||||
|
pub fn new(images_dir: PathBuf) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 扫描镜像目录
|
||||||
|
pub fn scan_images(&self) -> Result<Vec<ImageInfo>>;
|
||||||
|
|
||||||
|
/// 获取镜像信息
|
||||||
|
pub fn get_image(&self, id: &str) -> Option<ImageInfo>;
|
||||||
|
|
||||||
|
/// 添加镜像
|
||||||
|
pub async fn add_image(&self, name: &str, data: Bytes) -> Result<ImageInfo>;
|
||||||
|
|
||||||
|
/// 删除镜像
|
||||||
|
pub fn delete_image(&self, id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 开始下载
|
||||||
|
pub async fn start_download(&self, url: &str) -> Result<String>;
|
||||||
|
|
||||||
|
/// 取消下载
|
||||||
|
pub fn cancel_download(&self, download_id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取下载进度
|
||||||
|
pub fn get_download_progress(&self, download_id: &str) -> Option<DownloadProgress>;
|
||||||
|
|
||||||
|
/// 验证镜像文件
|
||||||
|
fn validate_image(path: &Path) -> Result<ImageFormat>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImageInfo {
|
||||||
|
/// 唯一 ID
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
/// 文件名
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// 文件大小
|
||||||
|
pub size: u64,
|
||||||
|
|
||||||
|
/// 格式
|
||||||
|
pub format: ImageFormat,
|
||||||
|
|
||||||
|
/// 创建时间
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// 下载状态
|
||||||
|
pub download_status: Option<DownloadStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ImageFormat {
|
||||||
|
/// ISO 光盘镜像
|
||||||
|
Iso,
|
||||||
|
|
||||||
|
/// 原始磁盘镜像
|
||||||
|
Img,
|
||||||
|
|
||||||
|
/// 未知格式
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DownloadProgress {
|
||||||
|
/// 已下载字节
|
||||||
|
pub downloaded: u64,
|
||||||
|
|
||||||
|
/// 总字节数
|
||||||
|
pub total: u64,
|
||||||
|
|
||||||
|
/// 下载速度 (bytes/sec)
|
||||||
|
pub speed: u64,
|
||||||
|
|
||||||
|
/// 预计剩余时间
|
||||||
|
pub eta_secs: u64,
|
||||||
|
|
||||||
|
/// 状态
|
||||||
|
pub status: DownloadStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DownloadStatus {
|
||||||
|
Pending,
|
||||||
|
Downloading,
|
||||||
|
Completed,
|
||||||
|
Failed(String),
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 VentoyDrive (ventoy_drive.rs)
|
||||||
|
|
||||||
|
Ventoy 可启动驱动器管理。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct VentoyDrive {
|
||||||
|
/// 驱动器路径
|
||||||
|
drive_path: PathBuf,
|
||||||
|
|
||||||
|
/// 镜像路径
|
||||||
|
images: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// 容量
|
||||||
|
capacity: u64,
|
||||||
|
|
||||||
|
/// 已用空间
|
||||||
|
used: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VentoyDrive {
|
||||||
|
/// 创建 Ventoy 驱动器
|
||||||
|
pub fn create(drive_path: PathBuf, capacity: u64) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 添加 ISO
|
||||||
|
pub fn add_iso(&mut self, iso_path: &Path) -> Result<()>;
|
||||||
|
|
||||||
|
/// 移除 ISO
|
||||||
|
pub fn remove_iso(&mut self, name: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 列出 ISO
|
||||||
|
pub fn list_isos(&self) -> Vec<String>;
|
||||||
|
|
||||||
|
/// 获取驱动器信息
|
||||||
|
pub fn info(&self) -> DriveInfo;
|
||||||
|
|
||||||
|
/// 获取驱动器路径
|
||||||
|
pub fn path(&self) -> &Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DriveInfo {
|
||||||
|
/// 容量
|
||||||
|
pub capacity: u64,
|
||||||
|
|
||||||
|
/// 已用空间
|
||||||
|
pub used: u64,
|
||||||
|
|
||||||
|
/// 可用空间
|
||||||
|
pub available: u64,
|
||||||
|
|
||||||
|
/// ISO 列表
|
||||||
|
pub isos: Vec<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 类型定义
|
||||||
|
|
||||||
|
### 4.1 MSD 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct MsdConfig {
|
||||||
|
/// 是否启用 MSD
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// 镜像目录
|
||||||
|
pub images_dir: Option<String>,
|
||||||
|
|
||||||
|
/// 默认模式
|
||||||
|
pub default_mode: MsdMode,
|
||||||
|
|
||||||
|
/// Ventoy 容量 (MB)
|
||||||
|
pub ventoy_capacity_mb: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MsdConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
images_dir: None,
|
||||||
|
default_mode: MsdMode::None,
|
||||||
|
ventoy_capacity_mb: 4096, // 4GB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/msd/status` | GET | 获取 MSD 状态 |
|
||||||
|
| `/api/msd/connect` | POST | 连接 MSD |
|
||||||
|
| `/api/msd/disconnect` | POST | 断开 MSD |
|
||||||
|
| `/api/msd/images` | GET | 列出镜像 |
|
||||||
|
| `/api/msd/images` | POST | 上传镜像 |
|
||||||
|
| `/api/msd/images/:id` | DELETE | 删除镜像 |
|
||||||
|
| `/api/msd/images/download` | POST | 从 URL 下载 |
|
||||||
|
| `/api/msd/images/download/:id` | GET | 获取下载进度 |
|
||||||
|
| `/api/msd/images/download/:id` | DELETE | 取消下载 |
|
||||||
|
| `/api/msd/set-image` | POST | 设置当前镜像 |
|
||||||
|
| `/api/msd/set-ventoy` | POST | 设置 Ventoy 模式 |
|
||||||
|
| `/api/msd/clear` | POST | 清除挂载 |
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/msd/status
|
||||||
|
{
|
||||||
|
"available": true,
|
||||||
|
"mode": "image",
|
||||||
|
"connected": true,
|
||||||
|
"current_image": {
|
||||||
|
"id": "abc123",
|
||||||
|
"name": "ubuntu-22.04.iso",
|
||||||
|
"size": 4700000000,
|
||||||
|
"format": "iso"
|
||||||
|
},
|
||||||
|
"drive_info": null,
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/msd/images
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": "abc123",
|
||||||
|
"name": "ubuntu-22.04.iso",
|
||||||
|
"size": 4700000000,
|
||||||
|
"format": "iso",
|
||||||
|
"created_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/msd/images/download
|
||||||
|
// Request: { "url": "https://example.com/image.iso" }
|
||||||
|
// Response: { "download_id": "xyz789" }
|
||||||
|
|
||||||
|
// GET /api/msd/images/download/xyz789
|
||||||
|
{
|
||||||
|
"downloaded": 1234567890,
|
||||||
|
"total": 4700000000,
|
||||||
|
"speed": 12345678,
|
||||||
|
"eta_secs": 280,
|
||||||
|
"status": "downloading"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SystemEvent {
|
||||||
|
MsdStateChanged {
|
||||||
|
mode: MsdMode,
|
||||||
|
connected: bool,
|
||||||
|
image: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
MsdDownloadProgress {
|
||||||
|
download_id: String,
|
||||||
|
progress: DownloadProgress,
|
||||||
|
},
|
||||||
|
|
||||||
|
MsdDownloadComplete {
|
||||||
|
download_id: String,
|
||||||
|
image_id: String,
|
||||||
|
success: bool,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum MsdError {
|
||||||
|
#[error("MSD not available")]
|
||||||
|
NotAvailable,
|
||||||
|
|
||||||
|
#[error("Already connected")]
|
||||||
|
AlreadyConnected,
|
||||||
|
|
||||||
|
#[error("Not connected")]
|
||||||
|
NotConnected,
|
||||||
|
|
||||||
|
#[error("Image not found: {0}")]
|
||||||
|
ImageNotFound(String),
|
||||||
|
|
||||||
|
#[error("Invalid image format: {0}")]
|
||||||
|
InvalidFormat(String),
|
||||||
|
|
||||||
|
#[error("Download failed: {0}")]
|
||||||
|
DownloadFailed(String),
|
||||||
|
|
||||||
|
#[error("Storage full")]
|
||||||
|
StorageFull,
|
||||||
|
|
||||||
|
#[error("OTG error: {0}")]
|
||||||
|
OtgError(String),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 使用示例
|
||||||
|
|
||||||
|
### 8.1 挂载 ISO 镜像
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let msd = MsdController::new(otg_service, data_dir, events).await?;
|
||||||
|
|
||||||
|
// 列出镜像
|
||||||
|
let images = msd.list_images();
|
||||||
|
println!("Available images: {:?}", images);
|
||||||
|
|
||||||
|
// 设置镜像
|
||||||
|
msd.set_image("abc123").await?;
|
||||||
|
|
||||||
|
// 连接到目标 PC
|
||||||
|
msd.connect().await?;
|
||||||
|
|
||||||
|
// 目标 PC 现在可以看到 USB 驱动器...
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
msd.disconnect().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 从 URL 下载
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 开始下载
|
||||||
|
let download_id = msd.download_image("https://example.com/ubuntu.iso").await?;
|
||||||
|
|
||||||
|
// 监控进度
|
||||||
|
loop {
|
||||||
|
if let Some(progress) = msd.get_download_progress(&download_id) {
|
||||||
|
println!("Progress: {}%", progress.downloaded * 100 / progress.total);
|
||||||
|
|
||||||
|
if matches!(progress.status, DownloadStatus::Completed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 使用 Ventoy 模式
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 切换到 Ventoy 模式
|
||||||
|
msd.set_ventoy().await?;
|
||||||
|
|
||||||
|
// 获取驱动器信息
|
||||||
|
let state = msd.state();
|
||||||
|
if let Some(drive_info) = state.drive_info {
|
||||||
|
println!("Capacity: {} MB", drive_info.capacity / 1024 / 1024);
|
||||||
|
println!("ISOs: {:?}", drive_info.isos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接
|
||||||
|
msd.connect().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 常见问题
|
||||||
|
|
||||||
|
### Q: 镜像无法挂载?
|
||||||
|
|
||||||
|
1. 检查镜像文件完整性
|
||||||
|
2. 确认文件格式正确
|
||||||
|
3. 检查存储空间
|
||||||
|
|
||||||
|
### Q: 目标 PC 不识别?
|
||||||
|
|
||||||
|
1. 检查 USB 连接
|
||||||
|
2. 尝试重新连接
|
||||||
|
3. 查看目标 PC 的设备管理器
|
||||||
|
|
||||||
|
### Q: 下载速度慢?
|
||||||
|
|
||||||
|
1. 检查网络连接
|
||||||
|
2. 使用更近的镜像源
|
||||||
|
3. 检查磁盘 I/O
|
||||||
|
|
||||||
|
### Q: Ventoy 启动失败?
|
||||||
|
|
||||||
|
1. 检查目标 PC BIOS 设置
|
||||||
|
2. 尝试不同的启动模式
|
||||||
|
3. 确认 ISO 文件支持 Ventoy
|
||||||
667
docs/modules/otg.md
Normal file
667
docs/modules/otg.md
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
# OTG 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
OTG (On-The-Go) 模块负责管理 Linux USB Gadget,为 HID 和 MSD 功能提供统一的 USB 设备管理。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- USB Gadget 生命周期管理
|
||||||
|
- HID 函数配置 (键盘、鼠标)
|
||||||
|
- MSD 函数配置 (虚拟存储)
|
||||||
|
- ConfigFS 操作
|
||||||
|
- UDC 绑定/解绑
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/otg/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── service.rs # OtgService (17KB)
|
||||||
|
├── manager.rs # OtgGadgetManager (12KB)
|
||||||
|
├── hid.rs # HID Function (7KB)
|
||||||
|
├── msd.rs # MSD Function (14KB)
|
||||||
|
├── configfs.rs # ConfigFS 操作 (4KB)
|
||||||
|
├── endpoint.rs # 端点分配 (2KB)
|
||||||
|
└── report_desc.rs # HID 报告描述符 (6KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 设计目标
|
||||||
|
|
||||||
|
解决 HID 和 MSD 共享同一个 USB Gadget 的所有权问题:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ OTG Ownership Model │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────┐
|
||||||
|
│ OtgService │ ◄── 唯一所有者
|
||||||
|
│ (service.rs) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┼─────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
enable_hid() enable_msd() 状态查询
|
||||||
|
│ │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│OtgGadgetManager │
|
||||||
|
│ (manager.rs) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┼─────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────┐ ┌───────┐ ┌───────┐
|
||||||
|
│ HID │ │ MSD │ │ UDC │
|
||||||
|
│ Func │ │ Func │ │ Bind │
|
||||||
|
└───────┘ └───────┘ └───────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 ConfigFS 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/sys/kernel/config/usb_gadget/one-kvm/
|
||||||
|
├── idVendor # 0x05ac (Apple)
|
||||||
|
├── idProduct # 0x0001
|
||||||
|
├── bcdDevice # 0x0100
|
||||||
|
├── bcdUSB # 0x0200
|
||||||
|
├── bMaxPacketSize0 # 64
|
||||||
|
│
|
||||||
|
├── strings/
|
||||||
|
│ └── 0x409/ # English
|
||||||
|
│ ├── manufacturer # "One-KVM"
|
||||||
|
│ ├── product # "KVM Device"
|
||||||
|
│ └── serialnumber # UUID
|
||||||
|
│
|
||||||
|
├── configs/
|
||||||
|
│ └── c.1/
|
||||||
|
│ ├── MaxPower # 500
|
||||||
|
│ ├── strings/
|
||||||
|
│ │ └── 0x409/
|
||||||
|
│ │ └── configuration # "Config 1"
|
||||||
|
│ └── (function symlinks)
|
||||||
|
│
|
||||||
|
├── functions/
|
||||||
|
│ ├── hid.usb0/ # 键盘
|
||||||
|
│ │ ├── protocol # 1 (keyboard)
|
||||||
|
│ │ ├── subclass # 1 (boot)
|
||||||
|
│ │ ├── report_length # 8
|
||||||
|
│ │ └── report_desc # (binary)
|
||||||
|
│ │
|
||||||
|
│ ├── hid.usb1/ # 相对鼠标
|
||||||
|
│ │ ├── protocol # 2 (mouse)
|
||||||
|
│ │ ├── subclass # 1 (boot)
|
||||||
|
│ │ ├── report_length # 4
|
||||||
|
│ │ └── report_desc # (binary)
|
||||||
|
│ │
|
||||||
|
│ ├── hid.usb2/ # 绝对鼠标
|
||||||
|
│ │ ├── protocol # 2 (mouse)
|
||||||
|
│ │ ├── subclass # 0 (none)
|
||||||
|
│ │ ├── report_length # 6
|
||||||
|
│ │ └── report_desc # (binary)
|
||||||
|
│ │
|
||||||
|
│ └── mass_storage.usb0/ # 虚拟存储
|
||||||
|
│ ├── stall # 1
|
||||||
|
│ └── lun.0/
|
||||||
|
│ ├── cdrom # 1 (ISO mode)
|
||||||
|
│ ├── ro # 1 (read-only)
|
||||||
|
│ ├── removable # 1
|
||||||
|
│ ├── nofua # 1
|
||||||
|
│ └── file # /path/to/image.iso
|
||||||
|
│
|
||||||
|
└── UDC # UDC 设备名
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 OtgService (service.rs)
|
||||||
|
|
||||||
|
OTG 服务主类,提供统一的 USB Gadget 管理接口。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct OtgService {
|
||||||
|
/// Gadget 管理器
|
||||||
|
manager: Arc<Mutex<OtgGadgetManager>>,
|
||||||
|
|
||||||
|
/// 当前状态
|
||||||
|
state: Arc<RwLock<OtgServiceState>>,
|
||||||
|
|
||||||
|
/// HID 函数句柄
|
||||||
|
hid_function: Arc<RwLock<Option<HidFunction>>>,
|
||||||
|
|
||||||
|
/// MSD 函数句柄
|
||||||
|
msd_function: Arc<RwLock<Option<MsdFunction>>>,
|
||||||
|
|
||||||
|
/// 请求计数器 (lock-free)
|
||||||
|
pending_requests: AtomicU8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OtgService {
|
||||||
|
/// 创建服务
|
||||||
|
pub fn new() -> Result<Self>;
|
||||||
|
|
||||||
|
/// 启用 HID 功能
|
||||||
|
pub async fn enable_hid(&self) -> Result<HidDevicePaths>;
|
||||||
|
|
||||||
|
/// 禁用 HID 功能
|
||||||
|
pub async fn disable_hid(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 启用 MSD 功能
|
||||||
|
pub async fn enable_msd(&self) -> Result<MsdFunction>;
|
||||||
|
|
||||||
|
/// 禁用 MSD 功能
|
||||||
|
pub async fn disable_msd(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn state(&self) -> OtgServiceState;
|
||||||
|
|
||||||
|
/// 检查 HID 是否启用
|
||||||
|
pub fn is_hid_enabled(&self) -> bool;
|
||||||
|
|
||||||
|
/// 检查 MSD 是否启用
|
||||||
|
pub fn is_msd_enabled(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OtgServiceState {
|
||||||
|
/// Gadget 是否激活
|
||||||
|
pub gadget_active: bool,
|
||||||
|
|
||||||
|
/// HID 是否启用
|
||||||
|
pub hid_enabled: bool,
|
||||||
|
|
||||||
|
/// MSD 是否启用
|
||||||
|
pub msd_enabled: bool,
|
||||||
|
|
||||||
|
/// HID 设备路径
|
||||||
|
pub hid_paths: Option<HidDevicePaths>,
|
||||||
|
|
||||||
|
/// 错误信息
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HidDevicePaths {
|
||||||
|
pub keyboard: PathBuf, // /dev/hidg0
|
||||||
|
pub mouse_relative: PathBuf, // /dev/hidg1
|
||||||
|
pub mouse_absolute: PathBuf, // /dev/hidg2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 OtgGadgetManager (manager.rs)
|
||||||
|
|
||||||
|
Gadget 生命周期管理器。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct OtgGadgetManager {
|
||||||
|
/// Gadget 路径
|
||||||
|
gadget_path: PathBuf,
|
||||||
|
|
||||||
|
/// UDC 设备名
|
||||||
|
udc_name: Option<String>,
|
||||||
|
|
||||||
|
/// 是否已创建
|
||||||
|
created: bool,
|
||||||
|
|
||||||
|
/// 是否已绑定
|
||||||
|
bound: bool,
|
||||||
|
|
||||||
|
/// 端点分配器
|
||||||
|
endpoint_allocator: EndpointAllocator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OtgGadgetManager {
|
||||||
|
/// 创建管理器
|
||||||
|
pub fn new() -> Result<Self>;
|
||||||
|
|
||||||
|
/// 创建 Gadget
|
||||||
|
pub fn create_gadget(&mut self, config: &GadgetConfig) -> Result<()>;
|
||||||
|
|
||||||
|
/// 销毁 Gadget
|
||||||
|
pub fn destroy_gadget(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 绑定 UDC
|
||||||
|
pub fn bind_udc(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 解绑 UDC
|
||||||
|
pub fn unbind_udc(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 添加函数
|
||||||
|
pub fn add_function(&mut self, func: &dyn GadgetFunction) -> Result<()>;
|
||||||
|
|
||||||
|
/// 移除函数
|
||||||
|
pub fn remove_function(&mut self, func: &dyn GadgetFunction) -> Result<()>;
|
||||||
|
|
||||||
|
/// 链接函数到配置
|
||||||
|
pub fn link_function(&self, func: &dyn GadgetFunction) -> Result<()>;
|
||||||
|
|
||||||
|
/// 取消链接函数
|
||||||
|
pub fn unlink_function(&self, func: &dyn GadgetFunction) -> Result<()>;
|
||||||
|
|
||||||
|
/// 检测可用 UDC
|
||||||
|
fn detect_udc() -> Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GadgetConfig {
|
||||||
|
pub name: String, // "one-kvm"
|
||||||
|
pub vendor_id: u16, // 0x05ac
|
||||||
|
pub product_id: u16, // 0x0001
|
||||||
|
pub manufacturer: String, // "One-KVM"
|
||||||
|
pub product: String, // "KVM Device"
|
||||||
|
pub serial: String, // UUID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 HID Function (hid.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct HidFunction {
|
||||||
|
/// 键盘函数
|
||||||
|
keyboard: HidFunctionConfig,
|
||||||
|
|
||||||
|
/// 相对鼠标函数
|
||||||
|
mouse_relative: HidFunctionConfig,
|
||||||
|
|
||||||
|
/// 绝对鼠标函数
|
||||||
|
mouse_absolute: HidFunctionConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HidFunctionConfig {
|
||||||
|
/// 函数名
|
||||||
|
pub name: String, // "hid.usb0"
|
||||||
|
|
||||||
|
/// 协议
|
||||||
|
pub protocol: u8, // 1=keyboard, 2=mouse
|
||||||
|
|
||||||
|
/// 子类
|
||||||
|
pub subclass: u8, // 1=boot, 0=none
|
||||||
|
|
||||||
|
/// 报告长度
|
||||||
|
pub report_length: u8,
|
||||||
|
|
||||||
|
/// 报告描述符
|
||||||
|
pub report_desc: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HidFunction {
|
||||||
|
/// 创建 HID 函数
|
||||||
|
pub fn new() -> Self;
|
||||||
|
|
||||||
|
/// 获取键盘报告描述符
|
||||||
|
pub fn keyboard_report_desc() -> Vec<u8>;
|
||||||
|
|
||||||
|
/// 获取相对鼠标报告描述符
|
||||||
|
pub fn mouse_relative_report_desc() -> Vec<u8>;
|
||||||
|
|
||||||
|
/// 获取绝对鼠标报告描述符
|
||||||
|
pub fn mouse_absolute_report_desc() -> Vec<u8>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GadgetFunction for HidFunction {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
fn function_type(&self) -> &str; // "hid"
|
||||||
|
fn configure(&self, path: &Path) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 MSD Function (msd.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MsdFunction {
|
||||||
|
/// 函数名
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// LUN 配置
|
||||||
|
luns: Vec<MsdLun>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MsdLun {
|
||||||
|
/// LUN 编号
|
||||||
|
pub lun_id: u8,
|
||||||
|
|
||||||
|
/// 镜像文件路径
|
||||||
|
pub file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// 是否 CD-ROM 模式
|
||||||
|
pub cdrom: bool,
|
||||||
|
|
||||||
|
/// 是否只读
|
||||||
|
pub readonly: bool,
|
||||||
|
|
||||||
|
/// 是否可移除
|
||||||
|
pub removable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MsdFunction {
|
||||||
|
/// 创建 MSD 函数
|
||||||
|
pub fn new() -> Self;
|
||||||
|
|
||||||
|
/// 设置镜像文件
|
||||||
|
pub fn set_image(&mut self, path: &Path, cdrom: bool) -> Result<()>;
|
||||||
|
|
||||||
|
/// 清除镜像
|
||||||
|
pub fn clear_image(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 弹出介质
|
||||||
|
pub fn eject(&mut self) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GadgetFunction for MsdFunction {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
fn function_type(&self) -> &str; // "mass_storage"
|
||||||
|
fn configure(&self, path: &Path) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 ConfigFS 操作 (configfs.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ConfigFs;
|
||||||
|
|
||||||
|
impl ConfigFs {
|
||||||
|
/// ConfigFS 根路径
|
||||||
|
const ROOT: &'static str = "/sys/kernel/config/usb_gadget";
|
||||||
|
|
||||||
|
/// 创建目录
|
||||||
|
pub fn mkdir(path: &Path) -> Result<()>;
|
||||||
|
|
||||||
|
/// 删除目录
|
||||||
|
pub fn rmdir(path: &Path) -> Result<()>;
|
||||||
|
|
||||||
|
/// 写入文件
|
||||||
|
pub fn write_file(path: &Path, content: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 写入二进制文件
|
||||||
|
pub fn write_binary(path: &Path, data: &[u8]) -> Result<()>;
|
||||||
|
|
||||||
|
/// 读取文件
|
||||||
|
pub fn read_file(path: &Path) -> Result<String>;
|
||||||
|
|
||||||
|
/// 创建符号链接
|
||||||
|
pub fn symlink(target: &Path, link: &Path) -> Result<()>;
|
||||||
|
|
||||||
|
/// 删除符号链接
|
||||||
|
pub fn unlink(path: &Path) -> Result<()>;
|
||||||
|
|
||||||
|
/// 列出目录
|
||||||
|
pub fn list_dir(path: &Path) -> Result<Vec<String>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 端点分配 (endpoint.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct EndpointAllocator {
|
||||||
|
/// 已使用的端点
|
||||||
|
used_endpoints: HashSet<u8>,
|
||||||
|
|
||||||
|
/// 最大端点数
|
||||||
|
max_endpoints: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndpointAllocator {
|
||||||
|
/// 创建分配器
|
||||||
|
pub fn new(max_endpoints: u8) -> Self;
|
||||||
|
|
||||||
|
/// 分配端点
|
||||||
|
pub fn allocate(&mut self, count: u8) -> Result<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 释放端点
|
||||||
|
pub fn release(&mut self, endpoints: &[u8]);
|
||||||
|
|
||||||
|
/// 检查可用端点数
|
||||||
|
pub fn available(&self) -> u8;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 报告描述符 (report_desc.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ReportDescriptor;
|
||||||
|
|
||||||
|
impl ReportDescriptor {
|
||||||
|
/// 标准键盘报告描述符
|
||||||
|
pub fn keyboard() -> Vec<u8> {
|
||||||
|
vec![
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||||
|
0x09, 0x06, // Usage (Keyboard)
|
||||||
|
0xA1, 0x01, // Collection (Application)
|
||||||
|
0x05, 0x07, // Usage Page (Key Codes)
|
||||||
|
0x19, 0xE0, // Usage Minimum (224)
|
||||||
|
0x29, 0xE7, // Usage Maximum (231)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x01, // Logical Maximum (1)
|
||||||
|
0x75, 0x01, // Report Size (1)
|
||||||
|
0x95, 0x08, // Report Count (8)
|
||||||
|
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x75, 0x08, // Report Size (8)
|
||||||
|
0x81, 0x01, // Input (Constant)
|
||||||
|
0x95, 0x06, // Report Count (6)
|
||||||
|
0x75, 0x08, // Report Size (8)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x65, // Logical Maximum (101)
|
||||||
|
0x05, 0x07, // Usage Page (Key Codes)
|
||||||
|
0x19, 0x00, // Usage Minimum (0)
|
||||||
|
0x29, 0x65, // Usage Maximum (101)
|
||||||
|
0x81, 0x00, // Input (Data, Array)
|
||||||
|
0xC0, // End Collection
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 相对鼠标报告描述符
|
||||||
|
pub fn mouse_relative() -> Vec<u8>;
|
||||||
|
|
||||||
|
/// 绝对鼠标报告描述符
|
||||||
|
pub fn mouse_absolute() -> Vec<u8>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 生命周期管理
|
||||||
|
|
||||||
|
### 4.1 初始化流程
|
||||||
|
|
||||||
|
```
|
||||||
|
OtgService::new()
|
||||||
|
│
|
||||||
|
├── 检测 UDC 设备
|
||||||
|
│ └── 读取 /sys/class/udc/
|
||||||
|
│
|
||||||
|
├── 创建 OtgGadgetManager
|
||||||
|
│
|
||||||
|
└── 初始化状态
|
||||||
|
|
||||||
|
enable_hid()
|
||||||
|
│
|
||||||
|
├── 检查 Gadget 是否存在
|
||||||
|
│ └── 如不存在,创建 Gadget
|
||||||
|
│
|
||||||
|
├── 创建 HID 函数
|
||||||
|
│ ├── hid.usb0 (键盘)
|
||||||
|
│ ├── hid.usb1 (相对鼠标)
|
||||||
|
│ └── hid.usb2 (绝对鼠标)
|
||||||
|
│
|
||||||
|
├── 配置函数
|
||||||
|
│ └── 写入报告描述符
|
||||||
|
│
|
||||||
|
├── 链接函数到配置
|
||||||
|
│
|
||||||
|
├── 绑定 UDC (如未绑定)
|
||||||
|
│
|
||||||
|
└── 等待设备节点出现
|
||||||
|
└── /dev/hidg0, hidg1, hidg2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 清理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
disable_hid()
|
||||||
|
│
|
||||||
|
├── 检查是否有其他函数使用
|
||||||
|
│
|
||||||
|
├── 如果只有 HID,解绑 UDC
|
||||||
|
│
|
||||||
|
├── 取消链接 HID 函数
|
||||||
|
│
|
||||||
|
└── 删除 HID 函数目录
|
||||||
|
|
||||||
|
disable_msd()
|
||||||
|
│
|
||||||
|
├── 同上...
|
||||||
|
│
|
||||||
|
└── 如果没有任何函数,销毁 Gadget
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置
|
||||||
|
|
||||||
|
### 5.1 OTG 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct OtgConfig {
|
||||||
|
/// 是否启用 OTG
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// 厂商 ID
|
||||||
|
pub vendor_id: u16,
|
||||||
|
|
||||||
|
/// 产品 ID
|
||||||
|
pub product_id: u16,
|
||||||
|
|
||||||
|
/// 厂商名称
|
||||||
|
pub manufacturer: String,
|
||||||
|
|
||||||
|
/// 产品名称
|
||||||
|
pub product: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OtgConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
vendor_id: 0x05ac, // Apple
|
||||||
|
product_id: 0x0001,
|
||||||
|
manufacturer: "One-KVM".to_string(),
|
||||||
|
product: "KVM Device".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum OtgError {
|
||||||
|
#[error("No UDC device found")]
|
||||||
|
NoUdcDevice,
|
||||||
|
|
||||||
|
#[error("Gadget already exists")]
|
||||||
|
GadgetExists,
|
||||||
|
|
||||||
|
#[error("Gadget not found")]
|
||||||
|
GadgetNotFound,
|
||||||
|
|
||||||
|
#[error("Function already exists: {0}")]
|
||||||
|
FunctionExists(String),
|
||||||
|
|
||||||
|
#[error("UDC busy")]
|
||||||
|
UdcBusy,
|
||||||
|
|
||||||
|
#[error("ConfigFS error: {0}")]
|
||||||
|
ConfigFsError(String),
|
||||||
|
|
||||||
|
#[error("Permission denied: {0}")]
|
||||||
|
PermissionDenied(String),
|
||||||
|
|
||||||
|
#[error("Device node not found: {0}")]
|
||||||
|
DeviceNodeNotFound(String),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 使用示例
|
||||||
|
|
||||||
|
### 7.1 启用 HID
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let otg = OtgService::new()?;
|
||||||
|
|
||||||
|
// 启用 HID
|
||||||
|
let paths = otg.enable_hid().await?;
|
||||||
|
println!("Keyboard: {:?}", paths.keyboard);
|
||||||
|
println!("Mouse relative: {:?}", paths.mouse_relative);
|
||||||
|
println!("Mouse absolute: {:?}", paths.mouse_absolute);
|
||||||
|
|
||||||
|
// 使用设备...
|
||||||
|
|
||||||
|
// 禁用 HID
|
||||||
|
otg.disable_hid().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 启用 MSD
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let otg = OtgService::new()?;
|
||||||
|
|
||||||
|
// 启用 MSD
|
||||||
|
let mut msd = otg.enable_msd().await?;
|
||||||
|
|
||||||
|
// 挂载 ISO
|
||||||
|
msd.set_image(Path::new("/data/ubuntu.iso"), true)?;
|
||||||
|
|
||||||
|
// 弹出
|
||||||
|
msd.eject()?;
|
||||||
|
|
||||||
|
// 禁用 MSD
|
||||||
|
otg.disable_msd().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见问题
|
||||||
|
|
||||||
|
### Q: 找不到 UDC 设备?
|
||||||
|
|
||||||
|
1. 检查内核是否支持 USB Gadget
|
||||||
|
2. 加载必要的内核模块:
|
||||||
|
```bash
|
||||||
|
modprobe libcomposite
|
||||||
|
modprobe usb_f_hid
|
||||||
|
modprobe usb_f_mass_storage
|
||||||
|
```
|
||||||
|
3. 检查 `/sys/class/udc/` 目录
|
||||||
|
|
||||||
|
### Q: 权限错误?
|
||||||
|
|
||||||
|
1. 以 root 运行
|
||||||
|
2. 或配置 udev 规则
|
||||||
|
|
||||||
|
### Q: 设备节点不出现?
|
||||||
|
|
||||||
|
1. 检查 UDC 是否正确绑定
|
||||||
|
2. 查看 `dmesg` 日志
|
||||||
|
3. 检查 ConfigFS 配置
|
||||||
|
|
||||||
|
### Q: 目标 PC 不识别?
|
||||||
|
|
||||||
|
1. 检查 USB 线缆
|
||||||
|
2. 检查报告描述符
|
||||||
|
3. 使用 `lsusb` 确认设备
|
||||||
776
docs/modules/rustdesk.md
Normal file
776
docs/modules/rustdesk.md
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
# RustDesk 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
RustDesk 模块实现 RustDesk 协议集成,允许使用标准 RustDesk 客户端访问 One-KVM 设备。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- RustDesk 协议实现
|
||||||
|
- 渲染服务器 (hbbs) 通信
|
||||||
|
- 中继服务器 (hbbr) 通信
|
||||||
|
- 视频/音频/HID 转换
|
||||||
|
- 端到端加密
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/rustdesk/
|
||||||
|
├── mod.rs # RustDeskService (21KB)
|
||||||
|
├── connection.rs # 连接管理 (49KB)
|
||||||
|
├── rendezvous.rs # 渲染服务器 (32KB)
|
||||||
|
├── crypto.rs # NaCl 加密 (16KB)
|
||||||
|
├── config.rs # 配置 (7KB)
|
||||||
|
├── hid_adapter.rs # HID 适配 (14KB)
|
||||||
|
├── frame_adapters.rs # 帧转换 (9KB)
|
||||||
|
├── protocol.rs # 协议包装 (6KB)
|
||||||
|
└── bytes_codec.rs # 帧编码 (8KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 RustDesk 网络架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ RustDesk Network Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────┐ ┌─────────────┐
|
||||||
|
│ RustDesk │ │ One-KVM │
|
||||||
|
│ Client │ │ Device │
|
||||||
|
└──────┬──────┘ └──────┬──────┘
|
||||||
|
│ │
|
||||||
|
│ 1. 查询设备地址 │
|
||||||
|
│─────────────────────►┌─────────────┐◄──────────────│
|
||||||
|
│ │ hbbs │ │
|
||||||
|
│ │ (Rendezvous)│ │
|
||||||
|
│◄─────────────────────└─────────────┘ │
|
||||||
|
│ 2. 返回地址 │
|
||||||
|
│ │
|
||||||
|
│ 3a. 直接连接 (如果可达) │
|
||||||
|
│────────────────────────────────────────────────────│
|
||||||
|
│ │
|
||||||
|
│ 3b. 中继连接 (如果 NAT) │
|
||||||
|
│─────────────────────►┌─────────────┐◄──────────────│
|
||||||
|
│ │ hbbr │ │
|
||||||
|
│ │ (Relay) │ │
|
||||||
|
│◄─────────────────────└─────────────┘───────────────│
|
||||||
|
│ │
|
||||||
|
│ 4. 建立加密通道 │
|
||||||
|
│◄───────────────────────────────────────────────────│
|
||||||
|
│ │
|
||||||
|
│ 5. 传输视频/音频/HID │
|
||||||
|
│◄───────────────────────────────────────────────────│
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 模块内部架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ RustDesk Module Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────┐
|
||||||
|
│ RustDeskService │
|
||||||
|
│ (mod.rs) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────┼───────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Rendezvous │ │ Connection │ │ Crypto │
|
||||||
|
│ (rendezvous) │ │ (connection) │ │ (crypto) │
|
||||||
|
└────────┬────────┘ └────────┬────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────────────────────────┐
|
||||||
|
│ hbbs Server │ │ Adapters │
|
||||||
|
│ Connection │ │ ┌──────────┐ ┌──────────────────┐ │
|
||||||
|
└─────────────────┘ │ │ HID │ │ Frame │ │
|
||||||
|
│ │ Adapter │ │ Adapters │ │
|
||||||
|
│ └──────────┘ └──────────────────┘ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||||
|
│ HID │ │ Video │ │ Audio │
|
||||||
|
│ Controller│ │ Pipeline │ │ Pipeline │
|
||||||
|
└───────────┘ └───────────┘ └───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 RustDeskService (mod.rs)
|
||||||
|
|
||||||
|
RustDesk 服务主类。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RustDeskService {
|
||||||
|
/// 服务配置
|
||||||
|
config: Arc<RwLock<RustDeskConfig>>,
|
||||||
|
|
||||||
|
/// 渲染连接
|
||||||
|
rendezvous: Arc<RwLock<Option<RendezvousConnection>>>,
|
||||||
|
|
||||||
|
/// 客户端连接
|
||||||
|
connections: Arc<RwLock<HashMap<String, Arc<ClientConnection>>>>,
|
||||||
|
|
||||||
|
/// 加密密钥
|
||||||
|
keys: Arc<RustDeskKeys>,
|
||||||
|
|
||||||
|
/// 视频管道
|
||||||
|
video_pipeline: Arc<SharedVideoPipeline>,
|
||||||
|
|
||||||
|
/// 音频管道
|
||||||
|
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||||
|
|
||||||
|
/// HID 控制器
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
|
||||||
|
/// 服务状态
|
||||||
|
status: Arc<RwLock<ServiceStatus>>,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustDeskService {
|
||||||
|
/// 创建服务
|
||||||
|
pub async fn new(
|
||||||
|
config: RustDeskConfig,
|
||||||
|
video_pipeline: Arc<SharedVideoPipeline>,
|
||||||
|
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
) -> Result<Arc<Self>>;
|
||||||
|
|
||||||
|
/// 启动服务
|
||||||
|
pub async fn start(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止服务
|
||||||
|
pub async fn stop(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取设备 ID
|
||||||
|
pub fn device_id(&self) -> String;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn status(&self) -> ServiceStatus;
|
||||||
|
|
||||||
|
/// 更新配置
|
||||||
|
pub async fn update_config(&self, config: RustDeskConfig) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取连接列表
|
||||||
|
pub fn connections(&self) -> Vec<ConnectionInfo>;
|
||||||
|
|
||||||
|
/// 断开连接
|
||||||
|
pub async fn disconnect(&self, connection_id: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ServiceStatus {
|
||||||
|
Stopped,
|
||||||
|
Starting,
|
||||||
|
Running,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConnectionInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub peer_id: String,
|
||||||
|
pub connected_at: DateTime<Utc>,
|
||||||
|
pub ip: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 RendezvousConnection (rendezvous.rs)
|
||||||
|
|
||||||
|
渲染服务器连接管理。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RendezvousConnection {
|
||||||
|
/// 服务器地址
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
|
||||||
|
/// TCP 连接
|
||||||
|
stream: TcpStream,
|
||||||
|
|
||||||
|
/// 设备 ID
|
||||||
|
device_id: String,
|
||||||
|
|
||||||
|
/// 公钥
|
||||||
|
public_key: [u8; 32],
|
||||||
|
|
||||||
|
/// 注册状态
|
||||||
|
registered: AtomicBool,
|
||||||
|
|
||||||
|
/// 心跳任务
|
||||||
|
heartbeat_task: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RendezvousConnection {
|
||||||
|
/// 连接到渲染服务器
|
||||||
|
pub async fn connect(
|
||||||
|
server: &str,
|
||||||
|
device_id: &str,
|
||||||
|
keys: &RustDeskKeys,
|
||||||
|
) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 注册设备
|
||||||
|
pub async fn register(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 发送心跳
|
||||||
|
async fn heartbeat(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 接收消息
|
||||||
|
pub async fn recv_message(&mut self) -> Result<RendezvousMessage>;
|
||||||
|
|
||||||
|
/// 处理穿孔请求
|
||||||
|
pub async fn handle_punch_request(&self, peer_id: &str) -> Result<SocketAddr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RendezvousMessage {
|
||||||
|
RegisterOk,
|
||||||
|
PunchRequest { peer_id: String, socket_addr: SocketAddr },
|
||||||
|
Heartbeat,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 ClientConnection (connection.rs)
|
||||||
|
|
||||||
|
客户端连接处理。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ClientConnection {
|
||||||
|
/// 连接 ID
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
/// 对端 ID
|
||||||
|
peer_id: String,
|
||||||
|
|
||||||
|
/// 加密通道
|
||||||
|
channel: EncryptedChannel,
|
||||||
|
|
||||||
|
/// 帧适配器
|
||||||
|
frame_adapter: FrameAdapter,
|
||||||
|
|
||||||
|
/// HID 适配器
|
||||||
|
hid_adapter: HidAdapter,
|
||||||
|
|
||||||
|
/// 状态
|
||||||
|
state: Arc<RwLock<ConnectionState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientConnection {
|
||||||
|
/// 创建连接
|
||||||
|
pub async fn new(
|
||||||
|
stream: TcpStream,
|
||||||
|
keys: &RustDeskKeys,
|
||||||
|
peer_public_key: &[u8],
|
||||||
|
) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 处理连接
|
||||||
|
pub async fn handle(
|
||||||
|
&self,
|
||||||
|
video_rx: broadcast::Receiver<EncodedFrame>,
|
||||||
|
audio_rx: broadcast::Receiver<AudioFrame>,
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// 发送视频帧
|
||||||
|
async fn send_video_frame(&self, frame: &EncodedFrame) -> Result<()>;
|
||||||
|
|
||||||
|
/// 发送音频帧
|
||||||
|
async fn send_audio_frame(&self, frame: &AudioFrame) -> Result<()>;
|
||||||
|
|
||||||
|
/// 处理输入事件
|
||||||
|
async fn handle_input(&self, msg: &InputMessage) -> Result<()>;
|
||||||
|
|
||||||
|
/// 关闭连接
|
||||||
|
pub async fn close(&self) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ConnectionState {
|
||||||
|
Handshaking,
|
||||||
|
Authenticating,
|
||||||
|
Connected,
|
||||||
|
Closing,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 RustDeskKeys (crypto.rs)
|
||||||
|
|
||||||
|
加密密钥管理。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RustDeskKeys {
|
||||||
|
/// 设备 ID
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// Curve25519 公钥
|
||||||
|
pub public_key: [u8; 32],
|
||||||
|
|
||||||
|
/// Curve25519 私钥
|
||||||
|
secret_key: [u8; 32],
|
||||||
|
|
||||||
|
/// Ed25519 签名公钥
|
||||||
|
pub sign_public_key: [u8; 32],
|
||||||
|
|
||||||
|
/// Ed25519 签名私钥
|
||||||
|
sign_secret_key: [u8; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustDeskKeys {
|
||||||
|
/// 生成新密钥
|
||||||
|
pub fn generate() -> Self;
|
||||||
|
|
||||||
|
/// 从配置加载
|
||||||
|
pub fn from_config(config: &KeyConfig) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 保存到配置
|
||||||
|
pub fn to_config(&self) -> KeyConfig;
|
||||||
|
|
||||||
|
/// 计算共享密钥
|
||||||
|
pub fn shared_secret(&self, peer_public_key: &[u8; 32]) -> [u8; 32];
|
||||||
|
|
||||||
|
/// 签名消息
|
||||||
|
pub fn sign(&self, message: &[u8]) -> [u8; 64];
|
||||||
|
|
||||||
|
/// 验证签名
|
||||||
|
pub fn verify(public_key: &[u8; 32], message: &[u8], signature: &[u8; 64]) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EncryptedChannel {
|
||||||
|
/// 发送密钥
|
||||||
|
send_key: [u8; 32],
|
||||||
|
|
||||||
|
/// 接收密钥
|
||||||
|
recv_key: [u8; 32],
|
||||||
|
|
||||||
|
/// 发送 nonce
|
||||||
|
send_nonce: AtomicU64,
|
||||||
|
|
||||||
|
/// 接收 nonce
|
||||||
|
recv_nonce: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncryptedChannel {
|
||||||
|
/// 加密消息
|
||||||
|
pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8>;
|
||||||
|
|
||||||
|
/// 解密消息
|
||||||
|
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 HidAdapter (hid_adapter.rs)
|
||||||
|
|
||||||
|
RustDesk HID 事件转换。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct HidAdapter {
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HidAdapter {
|
||||||
|
/// 创建适配器
|
||||||
|
pub fn new(hid: Arc<HidController>) -> Self;
|
||||||
|
|
||||||
|
/// 处理键盘事件
|
||||||
|
pub async fn handle_keyboard(&self, event: &RdKeyboardEvent) -> Result<()>;
|
||||||
|
|
||||||
|
/// 处理鼠标事件
|
||||||
|
pub async fn handle_mouse(&self, event: &RdMouseEvent) -> Result<()>;
|
||||||
|
|
||||||
|
/// 转换键码
|
||||||
|
fn convert_keycode(rd_key: u32) -> Option<KeyCode>;
|
||||||
|
|
||||||
|
/// 转换鼠标按钮
|
||||||
|
fn convert_button(rd_button: u32) -> Option<MouseButton>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk 键盘事件
|
||||||
|
pub struct RdKeyboardEvent {
|
||||||
|
pub keycode: u32,
|
||||||
|
pub down: bool,
|
||||||
|
pub modifiers: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk 鼠标事件
|
||||||
|
pub struct RdMouseEvent {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub mask: u32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 FrameAdapter (frame_adapters.rs)
|
||||||
|
|
||||||
|
帧格式转换。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct FrameAdapter;
|
||||||
|
|
||||||
|
impl FrameAdapter {
|
||||||
|
/// 转换视频帧到 RustDesk 格式
|
||||||
|
pub fn to_rd_video_frame(frame: &EncodedFrame) -> RdVideoFrame;
|
||||||
|
|
||||||
|
/// 转换音频帧到 RustDesk 格式
|
||||||
|
pub fn to_rd_audio_frame(frame: &AudioFrame) -> RdAudioFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk 视频帧
|
||||||
|
pub struct RdVideoFrame {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub key_frame: bool,
|
||||||
|
pub pts: i64,
|
||||||
|
pub format: RdVideoFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RdVideoFormat {
|
||||||
|
H264,
|
||||||
|
H265,
|
||||||
|
VP8,
|
||||||
|
VP9,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk 音频帧
|
||||||
|
pub struct RdAudioFrame {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 协议消息 (protocol.rs)
|
||||||
|
|
||||||
|
Protobuf 消息包装。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// 使用 prost 生成的 protobuf 消息
|
||||||
|
pub mod proto {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/rendezvous.rs"));
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/message.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageCodec;
|
||||||
|
|
||||||
|
impl MessageCodec {
|
||||||
|
/// 编码消息
|
||||||
|
pub fn encode<M: prost::Message>(msg: &M) -> Vec<u8>;
|
||||||
|
|
||||||
|
/// 解码消息
|
||||||
|
pub fn decode<M: prost::Message + Default>(data: &[u8]) -> Result<M>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.8 帧编码 (bytes_codec.rs)
|
||||||
|
|
||||||
|
变长帧协议。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BytesCodec {
|
||||||
|
state: DecodeState,
|
||||||
|
buffer: BytesMut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BytesCodec {
|
||||||
|
/// 编码帧
|
||||||
|
pub fn encode_frame(data: &[u8]) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::with_capacity(4 + data.len());
|
||||||
|
buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||||
|
buf.extend_from_slice(data);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解码帧
|
||||||
|
pub fn decode_frame(&mut self, src: &mut BytesMut) -> Result<Option<Bytes>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DecodeState {
|
||||||
|
Length,
|
||||||
|
Data(usize),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 协议详解
|
||||||
|
|
||||||
|
### 4.1 Protobuf 定义
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
// protos/rendezvous.proto
|
||||||
|
message RegisterPeer {
|
||||||
|
string id = 1;
|
||||||
|
bytes public_key = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterPeerResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PunchHoleRequest {
|
||||||
|
string id = 1;
|
||||||
|
string nat_type = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// protos/message.proto
|
||||||
|
message VideoFrame {
|
||||||
|
bytes data = 1;
|
||||||
|
bool key = 2;
|
||||||
|
int64 pts = 3;
|
||||||
|
VideoCodec codec = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AudioFrame {
|
||||||
|
bytes data = 1;
|
||||||
|
int64 timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyboardEvent {
|
||||||
|
uint32 keycode = 1;
|
||||||
|
bool down = 2;
|
||||||
|
uint32 modifiers = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MouseEvent {
|
||||||
|
int32 x = 1;
|
||||||
|
int32 y = 2;
|
||||||
|
uint32 mask = 3;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 连接握手
|
||||||
|
|
||||||
|
```
|
||||||
|
1. TCP 连接
|
||||||
|
Client ────► Device
|
||||||
|
|
||||||
|
2. 公钥交换
|
||||||
|
Client ◄───► Device
|
||||||
|
|
||||||
|
3. DH 密钥协商
|
||||||
|
shared_secret = X25519(my_private, peer_public)
|
||||||
|
|
||||||
|
4. 密钥派生
|
||||||
|
send_key = HKDF(shared_secret, "send")
|
||||||
|
recv_key = HKDF(shared_secret, "recv")
|
||||||
|
|
||||||
|
5. 认证 (可选)
|
||||||
|
Client ────► Device: encrypted(password)
|
||||||
|
Client ◄──── Device: encrypted(ok/fail)
|
||||||
|
|
||||||
|
6. 开始传输
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct RustDeskConfig {
|
||||||
|
/// 是否启用
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// 渲染服务器地址
|
||||||
|
pub rendezvous_server: String,
|
||||||
|
|
||||||
|
/// 中继服务器地址
|
||||||
|
pub relay_server: Option<String>,
|
||||||
|
|
||||||
|
/// 设备 ID (自动生成)
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
|
||||||
|
/// 访问密码
|
||||||
|
pub password: Option<String>,
|
||||||
|
|
||||||
|
/// 允许的客户端 ID
|
||||||
|
pub allowed_clients: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RustDeskConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
rendezvous_server: "rs-ny.rustdesk.com:21116".to_string(),
|
||||||
|
relay_server: None,
|
||||||
|
device_id: None,
|
||||||
|
password: None,
|
||||||
|
allowed_clients: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/rustdesk/status` | GET | 获取服务状态 |
|
||||||
|
| `/api/rustdesk/start` | POST | 启动服务 |
|
||||||
|
| `/api/rustdesk/stop` | POST | 停止服务 |
|
||||||
|
| `/api/rustdesk/config` | GET | 获取配置 |
|
||||||
|
| `/api/rustdesk/config` | PATCH | 更新配置 |
|
||||||
|
| `/api/rustdesk/device-id` | GET | 获取设备 ID |
|
||||||
|
| `/api/rustdesk/connections` | GET | 获取连接列表 |
|
||||||
|
| `/api/rustdesk/connections/:id` | DELETE | 断开连接 |
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/rustdesk/status
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"device_id": "123456789",
|
||||||
|
"rendezvous_connected": true,
|
||||||
|
"active_connections": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/rustdesk/connections
|
||||||
|
{
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "conn-abc",
|
||||||
|
"peer_id": "987654321",
|
||||||
|
"connected_at": "2024-01-15T10:30:00Z",
|
||||||
|
"ip": "192.168.1.100"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 事件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SystemEvent {
|
||||||
|
RustDeskStatusChanged {
|
||||||
|
status: String,
|
||||||
|
device_id: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
RustDeskConnectionOpened {
|
||||||
|
connection_id: String,
|
||||||
|
peer_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
RustDeskConnectionClosed {
|
||||||
|
connection_id: String,
|
||||||
|
peer_id: String,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RustDeskError {
|
||||||
|
#[error("Service not running")]
|
||||||
|
NotRunning,
|
||||||
|
|
||||||
|
#[error("Already running")]
|
||||||
|
AlreadyRunning,
|
||||||
|
|
||||||
|
#[error("Rendezvous connection failed: {0}")]
|
||||||
|
RendezvousFailed(String),
|
||||||
|
|
||||||
|
#[error("Authentication failed")]
|
||||||
|
AuthFailed,
|
||||||
|
|
||||||
|
#[error("Connection refused")]
|
||||||
|
ConnectionRefused,
|
||||||
|
|
||||||
|
#[error("Encryption error: {0}")]
|
||||||
|
EncryptionError(String),
|
||||||
|
|
||||||
|
#[error("Protocol error: {0}")]
|
||||||
|
ProtocolError(String),
|
||||||
|
|
||||||
|
#[error("Timeout")]
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 使用示例
|
||||||
|
|
||||||
|
### 9.1 启动服务
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let config = RustDeskConfig {
|
||||||
|
enabled: true,
|
||||||
|
rendezvous_server: "rs-ny.rustdesk.com:21116".to_string(),
|
||||||
|
password: Some("mypassword".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let service = RustDeskService::new(
|
||||||
|
config,
|
||||||
|
video_pipeline,
|
||||||
|
audio_pipeline,
|
||||||
|
hid,
|
||||||
|
events,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
service.start().await?;
|
||||||
|
|
||||||
|
println!("Device ID: {}", service.device_id());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 客户端连接
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 打开 RustDesk 客户端
|
||||||
|
2. 输入设备 ID
|
||||||
|
3. 输入密码 (如果设置)
|
||||||
|
4. 连接成功后即可控制
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 常见问题
|
||||||
|
|
||||||
|
### Q: 无法连接到渲染服务器?
|
||||||
|
|
||||||
|
1. 检查网络连接
|
||||||
|
2. 检查服务器地址
|
||||||
|
3. 检查防火墙
|
||||||
|
|
||||||
|
### Q: 客户端连接失败?
|
||||||
|
|
||||||
|
1. 检查设备 ID
|
||||||
|
2. 检查密码
|
||||||
|
3. 检查 NAT 穿透
|
||||||
|
|
||||||
|
### Q: 视频延迟高?
|
||||||
|
|
||||||
|
1. 使用更近的中继服务器
|
||||||
|
2. 检查网络带宽
|
||||||
|
3. 降低视频质量
|
||||||
|
|
||||||
|
### Q: 如何自建服务器?
|
||||||
|
|
||||||
|
参考 RustDesk Server 部署文档:
|
||||||
|
- hbbs: 渲染服务器
|
||||||
|
- hbbr: 中继服务器
|
||||||
895
docs/modules/video.md
Normal file
895
docs/modules/video.md
Normal file
@@ -0,0 +1,895 @@
|
|||||||
|
# Video 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
Video 模块负责视频采集、编码和流传输,是 One-KVM 的核心功能模块。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- V4L2 视频设备采集
|
||||||
|
- 多格式像素转换
|
||||||
|
- 硬件/软件视频编码
|
||||||
|
- MJPEG 和 WebRTC 流传输
|
||||||
|
- 帧去重和质量控制
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/video/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── capture.rs # V4L2 视频采集 (22KB)
|
||||||
|
├── streamer.rs # 视频流服务 (34KB)
|
||||||
|
├── stream_manager.rs # 流管理器 (24KB)
|
||||||
|
├── shared_video_pipeline.rs # 共享视频管道 (35KB)
|
||||||
|
├── h264_pipeline.rs # H264 编码管道 (22KB)
|
||||||
|
├── format.rs # 像素格式定义 (9KB)
|
||||||
|
├── frame.rs # 视频帧结构 (6KB)
|
||||||
|
├── convert.rs # 格式转换 (21KB)
|
||||||
|
└── encoder/ # 编码器
|
||||||
|
├── mod.rs
|
||||||
|
├── traits.rs # Encoder trait
|
||||||
|
├── h264.rs # H264 编码
|
||||||
|
├── h265.rs # H265 编码
|
||||||
|
├── vp8.rs # VP8 编码
|
||||||
|
├── vp9.rs # VP9 编码
|
||||||
|
├── jpeg.rs # JPEG 编码
|
||||||
|
└── registry.rs # 编码器注册表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Video Data Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
V4L2 Device (/dev/video0)
|
||||||
|
│
|
||||||
|
│ Raw frames (MJPEG/YUYV/NV12)
|
||||||
|
▼
|
||||||
|
┌───────────────────┐
|
||||||
|
│ VideoCapturer │ ◄─── capture.rs
|
||||||
|
│ - open_device() │
|
||||||
|
│ - read_frame() │
|
||||||
|
│ - set_format() │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│ VideoFrame
|
||||||
|
▼
|
||||||
|
┌───────────────────┐
|
||||||
|
│ Streamer │ ◄─── streamer.rs
|
||||||
|
│ - start() │
|
||||||
|
│ - stop() │
|
||||||
|
│ - get_info() │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌─────┴─────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌────────┐ ┌────────────────────────────┐
|
||||||
|
│ MJPEG │ │ SharedVideoPipeline │
|
||||||
|
│ Mode │ │ - Decode (MJPEG→YUV) │
|
||||||
|
│ │ │ - Convert (YUV→target) │
|
||||||
|
│ │ │ - Encode (H264/H265/VP8) │
|
||||||
|
└────────┘ └─────────────┬──────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌────────┐ ┌────────┐
|
||||||
|
│ HTTP │ │ WebRTC │
|
||||||
|
│ Stream │ │ RTP │
|
||||||
|
└────────┘ └────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 组件关系
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Component Relationships │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
VideoStreamManager (stream_manager.rs)
|
||||||
|
│
|
||||||
|
├──► Streamer (MJPEG mode)
|
||||||
|
│ └──► VideoCapturer
|
||||||
|
│
|
||||||
|
└──► WebRtcStreamer (WebRTC mode)
|
||||||
|
└──► SharedVideoPipeline
|
||||||
|
├──► VideoCapturer
|
||||||
|
├──► MjpegDecoder
|
||||||
|
├──► YuvConverter
|
||||||
|
└──► Encoders[]
|
||||||
|
├── H264Encoder
|
||||||
|
├── H265Encoder
|
||||||
|
├── VP8Encoder
|
||||||
|
└── VP9Encoder
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 VideoCapturer (capture.rs)
|
||||||
|
|
||||||
|
V4L2 视频采集器,负责从摄像头/采集卡读取视频帧。
|
||||||
|
|
||||||
|
#### 主要接口
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct VideoCapturer {
|
||||||
|
device: Device,
|
||||||
|
stream: Option<MmapStream<'static>>,
|
||||||
|
config: CaptureConfig,
|
||||||
|
format: PixelFormat,
|
||||||
|
resolution: Resolution,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoCapturer {
|
||||||
|
/// 打开视频设备
|
||||||
|
pub fn open(device_path: &str) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 设置视频格式
|
||||||
|
pub fn set_format(&mut self, config: &CaptureConfig) -> Result<()>;
|
||||||
|
|
||||||
|
/// 开始采集
|
||||||
|
pub fn start(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止采集
|
||||||
|
pub fn stop(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 读取一帧
|
||||||
|
pub fn read_frame(&mut self) -> Result<VideoFrame>;
|
||||||
|
|
||||||
|
/// 列出设备支持的格式
|
||||||
|
pub fn list_formats(&self) -> Vec<FormatInfo>;
|
||||||
|
|
||||||
|
/// 列出支持的分辨率
|
||||||
|
pub fn list_resolutions(&self, format: PixelFormat) -> Vec<Resolution>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 采集配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CaptureConfig {
|
||||||
|
pub device: String, // /dev/video0
|
||||||
|
pub width: u32, // 1920
|
||||||
|
pub height: u32, // 1080
|
||||||
|
pub fps: u32, // 30
|
||||||
|
pub format: Option<PixelFormat>, // 优先格式
|
||||||
|
pub buffer_count: u32, // 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 打开设备
|
||||||
|
let mut capturer = VideoCapturer::open("/dev/video0")?;
|
||||||
|
|
||||||
|
// 设置格式
|
||||||
|
capturer.set_format(&CaptureConfig {
|
||||||
|
device: "/dev/video0".to_string(),
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
fps: 30,
|
||||||
|
format: Some(PixelFormat::Mjpeg),
|
||||||
|
buffer_count: 4,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 开始采集
|
||||||
|
capturer.start()?;
|
||||||
|
|
||||||
|
// 读取帧
|
||||||
|
loop {
|
||||||
|
let frame = capturer.read_frame()?;
|
||||||
|
process_frame(frame);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 VideoFrame (frame.rs)
|
||||||
|
|
||||||
|
视频帧数据结构,支持零拷贝和帧去重。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct VideoFrame {
|
||||||
|
/// 帧数据 (引用计数)
|
||||||
|
data: Arc<Bytes>,
|
||||||
|
|
||||||
|
/// xxHash64 缓存 (用于去重)
|
||||||
|
hash: Arc<OnceLock<u64>>,
|
||||||
|
|
||||||
|
/// 分辨率
|
||||||
|
resolution: Resolution,
|
||||||
|
|
||||||
|
/// 像素格式
|
||||||
|
format: PixelFormat,
|
||||||
|
|
||||||
|
/// 行步长
|
||||||
|
stride: u32,
|
||||||
|
|
||||||
|
/// 是否关键帧
|
||||||
|
key_frame: bool,
|
||||||
|
|
||||||
|
/// 帧序号
|
||||||
|
sequence: u64,
|
||||||
|
|
||||||
|
/// 采集时间戳
|
||||||
|
capture_ts: Instant,
|
||||||
|
|
||||||
|
/// 是否有信号
|
||||||
|
online: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoFrame {
|
||||||
|
/// 创建新帧
|
||||||
|
pub fn new(data: Bytes, resolution: Resolution, format: PixelFormat) -> Self;
|
||||||
|
|
||||||
|
/// 获取帧数据
|
||||||
|
pub fn data(&self) -> &[u8];
|
||||||
|
|
||||||
|
/// 计算帧哈希 (懒加载)
|
||||||
|
pub fn hash(&self) -> u64;
|
||||||
|
|
||||||
|
/// 检查帧是否相同 (用于去重)
|
||||||
|
pub fn is_same_as(&self, other: &Self) -> bool;
|
||||||
|
|
||||||
|
/// 克隆帧 (零拷贝)
|
||||||
|
pub fn clone_ref(&self) -> Self;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 PixelFormat (format.rs)
|
||||||
|
|
||||||
|
支持的像素格式定义。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PixelFormat {
|
||||||
|
// 压缩格式
|
||||||
|
Mjpeg, // Motion JPEG (优先级: 100)
|
||||||
|
Jpeg, // Static JPEG (优先级: 99)
|
||||||
|
|
||||||
|
// YUV 4:2:2 打包格式
|
||||||
|
Yuyv, // YUYV/YUY2 (优先级: 80)
|
||||||
|
Yvyu, // YVYU (优先级: 64)
|
||||||
|
Uyvy, // UYVY (优先级: 65)
|
||||||
|
|
||||||
|
// YUV 半平面格式
|
||||||
|
Nv12, // NV12 (优先级: 75)
|
||||||
|
Nv16, // NV16 (优先级: 60)
|
||||||
|
Nv24, // NV24 (优先级: 55)
|
||||||
|
|
||||||
|
// YUV 平面格式
|
||||||
|
Yuv420, // I420/YU12 (优先级: 70)
|
||||||
|
Yvu420, // YV12 (优先级: 63)
|
||||||
|
|
||||||
|
// RGB 格式
|
||||||
|
Rgb565, // RGB565 (优先级: 40)
|
||||||
|
Rgb24, // RGB24 (优先级: 50)
|
||||||
|
Bgr24, // BGR24 (优先级: 49)
|
||||||
|
|
||||||
|
// 灰度
|
||||||
|
Grey, // 8-bit grayscale (优先级: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PixelFormat {
|
||||||
|
/// 获取格式优先级 (越高越好)
|
||||||
|
pub fn priority(&self) -> u32;
|
||||||
|
|
||||||
|
/// 计算帧大小
|
||||||
|
pub fn frame_size(&self, width: u32, height: u32) -> usize;
|
||||||
|
|
||||||
|
/// 转换为 V4L2 FourCC
|
||||||
|
pub fn to_fourcc(&self) -> u32;
|
||||||
|
|
||||||
|
/// 从 V4L2 FourCC 转换
|
||||||
|
pub fn from_fourcc(fourcc: u32) -> Option<Self>;
|
||||||
|
|
||||||
|
/// 是否压缩格式
|
||||||
|
pub fn is_compressed(&self) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 SharedVideoPipeline (shared_video_pipeline.rs)
|
||||||
|
|
||||||
|
多会话共享的视频编码管道。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SharedVideoPipeline {
|
||||||
|
/// 视频采集器
|
||||||
|
capturer: Arc<Mutex<VideoCapturer>>,
|
||||||
|
|
||||||
|
/// MJPEG 解码器
|
||||||
|
decoder: MjpegDecoder,
|
||||||
|
|
||||||
|
/// YUV 转换器
|
||||||
|
converter: YuvConverter,
|
||||||
|
|
||||||
|
/// 编码器实例
|
||||||
|
encoders: HashMap<VideoCodec, Box<dyn Encoder>>,
|
||||||
|
|
||||||
|
/// 活跃会话
|
||||||
|
sessions: Arc<RwLock<Vec<SessionSender>>>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: PipelineConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedVideoPipeline {
|
||||||
|
/// 创建管道
|
||||||
|
pub async fn new(config: PipelineConfig) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 启动管道
|
||||||
|
pub async fn start(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止管道
|
||||||
|
pub async fn stop(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 添加会话订阅
|
||||||
|
pub fn subscribe(&self, codec: VideoCodec) -> Receiver<EncodedFrame>;
|
||||||
|
|
||||||
|
/// 移除会话订阅
|
||||||
|
pub fn unsubscribe(&self, session_id: &str);
|
||||||
|
|
||||||
|
/// 编码单帧 (多编码器)
|
||||||
|
async fn encode_frame(&self, frame: VideoFrame) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 编码流程
|
||||||
|
|
||||||
|
```
|
||||||
|
Input: VideoFrame (MJPEG)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────────┐
|
||||||
|
│ MJPEG Decode │ turbojpeg / VAAPI
|
||||||
|
│ MJPEG → YUV420 │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────────┐
|
||||||
|
│ YUV Convert │ libyuv (SIMD)
|
||||||
|
│ YUV420 → target │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌─────┴─────┬─────────┬─────────┐
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
|
||||||
|
│ H264 │ │ H265 │ │ VP8 │ │ VP9 │
|
||||||
|
│Encoder│ │Encoder│ │Encoder│ │Encoder│
|
||||||
|
└───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
|
||||||
|
│ │ │ │
|
||||||
|
└──────────┴──────────┴──────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
EncodedFrame[]
|
||||||
|
(distribute to sessions)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Streamer (streamer.rs)
|
||||||
|
|
||||||
|
高层视频流服务,管理采集和分发。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Streamer {
|
||||||
|
/// 采集器
|
||||||
|
capturer: Option<Arc<Mutex<VideoCapturer>>>,
|
||||||
|
|
||||||
|
/// 采集任务句柄
|
||||||
|
capture_task: Option<JoinHandle<()>>,
|
||||||
|
|
||||||
|
/// 帧广播通道
|
||||||
|
frame_tx: broadcast::Sender<VideoFrame>,
|
||||||
|
|
||||||
|
/// 状态
|
||||||
|
state: Arc<RwLock<StreamerState>>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: StreamerConfig,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Streamer {
|
||||||
|
/// 创建流服务
|
||||||
|
pub fn new(events: Arc<EventBus>) -> Self;
|
||||||
|
|
||||||
|
/// 启动流
|
||||||
|
pub async fn start(&self, config: StreamerConfig) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止流
|
||||||
|
pub async fn stop(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 订阅帧
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<VideoFrame>;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn state(&self) -> StreamerState;
|
||||||
|
|
||||||
|
/// 获取信息
|
||||||
|
pub fn get_info(&self) -> StreamerInfo;
|
||||||
|
|
||||||
|
/// 应用配置
|
||||||
|
pub async fn apply_config(&self, config: StreamerConfig) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StreamerState {
|
||||||
|
pub status: StreamStatus,
|
||||||
|
pub device: Option<String>,
|
||||||
|
pub resolution: Option<Resolution>,
|
||||||
|
pub format: Option<PixelFormat>,
|
||||||
|
pub fps: f32,
|
||||||
|
pub frame_count: u64,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum StreamStatus {
|
||||||
|
Idle,
|
||||||
|
Starting,
|
||||||
|
Streaming,
|
||||||
|
Stopping,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 VideoStreamManager (stream_manager.rs)
|
||||||
|
|
||||||
|
统一管理 MJPEG 和 WebRTC 流模式。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct VideoStreamManager {
|
||||||
|
/// MJPEG 流服务
|
||||||
|
mjpeg_streamer: Arc<Streamer>,
|
||||||
|
|
||||||
|
/// WebRTC 流服务
|
||||||
|
webrtc_streamer: Arc<RwLock<Option<WebRtcStreamer>>>,
|
||||||
|
|
||||||
|
/// 当前模式
|
||||||
|
mode: Arc<RwLock<StreamMode>>,
|
||||||
|
|
||||||
|
/// 配置存储
|
||||||
|
config_store: ConfigStore,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoStreamManager {
|
||||||
|
/// 创建管理器
|
||||||
|
pub fn new(config_store: ConfigStore, events: Arc<EventBus>) -> Self;
|
||||||
|
|
||||||
|
/// 启动流
|
||||||
|
pub async fn start(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 停止流
|
||||||
|
pub async fn stop(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 切换模式
|
||||||
|
pub async fn set_mode(&self, mode: StreamMode) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取当前模式
|
||||||
|
pub fn get_mode(&self) -> StreamMode;
|
||||||
|
|
||||||
|
/// 获取设备列表
|
||||||
|
pub fn list_devices(&self) -> Vec<DeviceInfo>;
|
||||||
|
|
||||||
|
/// 获取统计信息
|
||||||
|
pub fn get_stats(&self) -> StreamStats;
|
||||||
|
|
||||||
|
/// 获取 MJPEG 订阅
|
||||||
|
pub fn subscribe_mjpeg(&self) -> broadcast::Receiver<VideoFrame>;
|
||||||
|
|
||||||
|
/// 创建 WebRTC 会话
|
||||||
|
pub async fn create_webrtc_session(&self, params: SessionParams) -> Result<Session>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum StreamMode {
|
||||||
|
Mjpeg,
|
||||||
|
Webrtc,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 编码器系统
|
||||||
|
|
||||||
|
### 4.1 Encoder Trait (encoder/traits.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait Encoder: Send + Sync {
|
||||||
|
/// 编码一帧
|
||||||
|
fn encode(&mut self, frame: &VideoFrame) -> Result<EncodedFrame>;
|
||||||
|
|
||||||
|
/// 获取编码器类型
|
||||||
|
fn codec(&self) -> VideoCodec;
|
||||||
|
|
||||||
|
/// 获取当前码率
|
||||||
|
fn bitrate(&self) -> u32;
|
||||||
|
|
||||||
|
/// 设置码率
|
||||||
|
fn set_bitrate(&mut self, bitrate: u32) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取 GOP 大小
|
||||||
|
fn gop_size(&self) -> u32;
|
||||||
|
|
||||||
|
/// 强制关键帧
|
||||||
|
fn force_keyframe(&mut self);
|
||||||
|
|
||||||
|
/// 重置编码器
|
||||||
|
fn reset(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取编码器信息
|
||||||
|
fn info(&self) -> EncoderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EncodedFrame {
|
||||||
|
pub data: Bytes,
|
||||||
|
pub codec: VideoCodec,
|
||||||
|
pub key_frame: bool,
|
||||||
|
pub pts: u64,
|
||||||
|
pub dts: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum VideoCodec {
|
||||||
|
H264,
|
||||||
|
H265,
|
||||||
|
VP8,
|
||||||
|
VP9,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 编码器优先级
|
||||||
|
|
||||||
|
```
|
||||||
|
H264 编码器选择顺序:
|
||||||
|
1. VAAPI (Intel/AMD GPU)
|
||||||
|
2. RKMPP (Rockchip)
|
||||||
|
3. V4L2 M2M
|
||||||
|
4. x264 (Software)
|
||||||
|
|
||||||
|
H265 编码器选择顺序:
|
||||||
|
1. VAAPI
|
||||||
|
2. RKMPP
|
||||||
|
(无软件后备)
|
||||||
|
|
||||||
|
VP8/VP9 编码器:
|
||||||
|
1. VAAPI only
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 EncoderRegistry (encoder/registry.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct EncoderRegistry {
|
||||||
|
/// 已注册的编码器工厂
|
||||||
|
factories: HashMap<VideoCodec, Vec<EncoderFactory>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncoderRegistry {
|
||||||
|
/// 创建注册表
|
||||||
|
pub fn new() -> Self;
|
||||||
|
|
||||||
|
/// 注册编码器工厂
|
||||||
|
pub fn register(&mut self, codec: VideoCodec, factory: EncoderFactory);
|
||||||
|
|
||||||
|
/// 创建最佳编码器
|
||||||
|
pub fn create_encoder(&self, codec: VideoCodec, config: EncoderConfig) -> Result<Box<dyn Encoder>>;
|
||||||
|
|
||||||
|
/// 列出可用编码器
|
||||||
|
pub fn list_available(&self, codec: VideoCodec) -> Vec<EncoderInfo>;
|
||||||
|
|
||||||
|
/// 探测硬件能力
|
||||||
|
pub fn probe_hardware() -> HardwareCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EncoderFactory {
|
||||||
|
pub name: String,
|
||||||
|
pub priority: u32,
|
||||||
|
pub create: Box<dyn Fn(EncoderConfig) -> Result<Box<dyn Encoder>>>,
|
||||||
|
pub probe: Box<dyn Fn() -> bool>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 格式转换
|
||||||
|
|
||||||
|
### 5.1 MjpegDecoder (convert.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MjpegDecoder {
|
||||||
|
/// turbojpeg 解压缩器
|
||||||
|
decompressor: Decompressor,
|
||||||
|
|
||||||
|
/// 输出缓冲区
|
||||||
|
output_buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MjpegDecoder {
|
||||||
|
/// 创建解码器
|
||||||
|
pub fn new() -> Result<Self>;
|
||||||
|
|
||||||
|
/// 解码 MJPEG 到 YUV420
|
||||||
|
pub fn decode(&mut self, jpeg_data: &[u8]) -> Result<YuvFrame>;
|
||||||
|
|
||||||
|
/// 获取图像信息
|
||||||
|
pub fn get_info(jpeg_data: &[u8]) -> Result<ImageInfo>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 YuvConverter (convert.rs)
|
||||||
|
|
||||||
|
使用 libyuv 进行高性能格式转换。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct YuvConverter;
|
||||||
|
|
||||||
|
impl YuvConverter {
|
||||||
|
/// YUYV → YUV420
|
||||||
|
pub fn yuyv_to_yuv420(src: &[u8], dst: &mut [u8], width: u32, height: u32);
|
||||||
|
|
||||||
|
/// NV12 → YUV420
|
||||||
|
pub fn nv12_to_yuv420(src: &[u8], dst: &mut [u8], width: u32, height: u32);
|
||||||
|
|
||||||
|
/// RGB24 → YUV420
|
||||||
|
pub fn rgb24_to_yuv420(src: &[u8], dst: &mut [u8], width: u32, height: u32);
|
||||||
|
|
||||||
|
/// YUV420 → NV12
|
||||||
|
pub fn yuv420_to_nv12(src: &[u8], dst: &mut [u8], width: u32, height: u32);
|
||||||
|
|
||||||
|
/// 缩放 YUV420
|
||||||
|
pub fn scale_yuv420(
|
||||||
|
src: &[u8], src_width: u32, src_height: u32,
|
||||||
|
dst: &mut [u8], dst_width: u32, dst_height: u32,
|
||||||
|
filter: ScaleFilter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ScaleFilter {
|
||||||
|
None, // 最近邻
|
||||||
|
Linear, // 双线性
|
||||||
|
Bilinear, // 双线性 (同 Linear)
|
||||||
|
Box, // 盒式滤波
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 配置说明
|
||||||
|
|
||||||
|
### 6.1 视频配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct VideoConfig {
|
||||||
|
/// 设备路径 (/dev/video0)
|
||||||
|
pub device: Option<String>,
|
||||||
|
|
||||||
|
/// 像素格式 (MJPEG/YUYV/NV12)
|
||||||
|
pub format: Option<String>,
|
||||||
|
|
||||||
|
/// 宽度
|
||||||
|
pub width: u32,
|
||||||
|
|
||||||
|
/// 高度
|
||||||
|
pub height: u32,
|
||||||
|
|
||||||
|
/// 帧率
|
||||||
|
pub fps: u32,
|
||||||
|
|
||||||
|
/// JPEG 质量 (1-100)
|
||||||
|
pub quality: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VideoConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
device: None,
|
||||||
|
format: None,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
fps: 30,
|
||||||
|
quality: 80,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 流配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct StreamConfig {
|
||||||
|
/// 流模式
|
||||||
|
pub mode: StreamMode,
|
||||||
|
|
||||||
|
/// 码率 (kbps)
|
||||||
|
pub bitrate_kbps: u32,
|
||||||
|
|
||||||
|
/// GOP 大小
|
||||||
|
pub gop_size: u32,
|
||||||
|
|
||||||
|
/// 编码器类型
|
||||||
|
pub encoder: EncoderType,
|
||||||
|
|
||||||
|
/// STUN 服务器
|
||||||
|
pub stun_server: Option<String>,
|
||||||
|
|
||||||
|
/// TURN 服务器
|
||||||
|
pub turn_server: Option<String>,
|
||||||
|
|
||||||
|
/// TURN 用户名
|
||||||
|
pub turn_username: Option<String>,
|
||||||
|
|
||||||
|
/// TURN 密码
|
||||||
|
pub turn_password: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 端点
|
||||||
|
|
||||||
|
### 7.1 流控制
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/stream/status` | GET | 获取流状态 |
|
||||||
|
| `/api/stream/start` | POST | 启动流 |
|
||||||
|
| `/api/stream/stop` | POST | 停止流 |
|
||||||
|
| `/api/stream/mode` | GET | 获取流模式 |
|
||||||
|
| `/api/stream/mode` | POST | 设置流模式 |
|
||||||
|
| `/api/stream/mjpeg` | GET | MJPEG 流 |
|
||||||
|
| `/api/stream/snapshot` | GET | 获取快照 |
|
||||||
|
|
||||||
|
### 7.2 设备管理
|
||||||
|
|
||||||
|
| 端点 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/devices/video` | GET | 列出视频设备 |
|
||||||
|
| `/api/devices/video/:id/formats` | GET | 列出设备格式 |
|
||||||
|
| `/api/devices/video/:id/resolutions` | GET | 列出分辨率 |
|
||||||
|
|
||||||
|
### 7.3 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/stream/status
|
||||||
|
{
|
||||||
|
"status": "streaming",
|
||||||
|
"device": "/dev/video0",
|
||||||
|
"resolution": { "width": 1920, "height": 1080 },
|
||||||
|
"format": "MJPEG",
|
||||||
|
"fps": 30.0,
|
||||||
|
"frame_count": 12345,
|
||||||
|
"mode": "mjpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/devices/video
|
||||||
|
{
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"path": "/dev/video0",
|
||||||
|
"name": "USB Capture",
|
||||||
|
"driver": "uvcvideo",
|
||||||
|
"bus": "usb-0000:00:14.0-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 事件
|
||||||
|
|
||||||
|
视频模块发布的事件:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SystemEvent {
|
||||||
|
/// 流状态变化
|
||||||
|
StreamStateChanged {
|
||||||
|
state: String, // "idle" | "starting" | "streaming" | "stopping" | "error"
|
||||||
|
device: Option<String>,
|
||||||
|
resolution: Option<Resolution>,
|
||||||
|
fps: Option<f32>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 设备变化
|
||||||
|
VideoDeviceChanged {
|
||||||
|
added: Vec<String>,
|
||||||
|
removed: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 编码器变化
|
||||||
|
EncoderChanged {
|
||||||
|
codec: String,
|
||||||
|
hardware: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum VideoError {
|
||||||
|
#[error("Device not found: {0}")]
|
||||||
|
DeviceNotFound(String),
|
||||||
|
|
||||||
|
#[error("Device busy: {0}")]
|
||||||
|
DeviceBusy(String),
|
||||||
|
|
||||||
|
#[error("Format not supported: {0:?}")]
|
||||||
|
FormatNotSupported(PixelFormat),
|
||||||
|
|
||||||
|
#[error("Resolution not supported: {0}x{1}")]
|
||||||
|
ResolutionNotSupported(u32, u32),
|
||||||
|
|
||||||
|
#[error("Capture error: {0}")]
|
||||||
|
CaptureError(String),
|
||||||
|
|
||||||
|
#[error("Encoder error: {0}")]
|
||||||
|
EncoderError(String),
|
||||||
|
|
||||||
|
#[error("No signal")]
|
||||||
|
NoSignal,
|
||||||
|
|
||||||
|
#[error("Device lost")]
|
||||||
|
DeviceLost,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 性能优化
|
||||||
|
|
||||||
|
### 10.1 零拷贝
|
||||||
|
|
||||||
|
- `Arc<Bytes>` 共享帧数据
|
||||||
|
- 引用计数避免复制
|
||||||
|
|
||||||
|
### 10.2 帧去重
|
||||||
|
|
||||||
|
- xxHash64 快速哈希
|
||||||
|
- 相同帧跳过编码
|
||||||
|
|
||||||
|
### 10.3 硬件加速
|
||||||
|
|
||||||
|
- VAAPI 优先
|
||||||
|
- 自动后备软件编码
|
||||||
|
|
||||||
|
### 10.4 内存池
|
||||||
|
|
||||||
|
- 预分配帧缓冲区
|
||||||
|
- 复用编码器缓冲区
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 常见问题
|
||||||
|
|
||||||
|
### Q: 如何添加新的视频格式?
|
||||||
|
|
||||||
|
1. 在 `format.rs` 添加枚举值
|
||||||
|
2. 实现 `to_fourcc()` 和 `from_fourcc()`
|
||||||
|
3. 在 `convert.rs` 添加转换函数
|
||||||
|
|
||||||
|
### Q: 如何添加新的编码器?
|
||||||
|
|
||||||
|
1. 实现 `Encoder` trait
|
||||||
|
2. 创建 `EncoderFactory`
|
||||||
|
3. 在 `EncoderRegistry` 注册
|
||||||
|
|
||||||
|
### Q: 帧率不稳定怎么办?
|
||||||
|
|
||||||
|
1. 检查 USB 带宽
|
||||||
|
2. 降低分辨率
|
||||||
|
3. 使用 MJPEG 格式
|
||||||
|
4. 启用硬件编码
|
||||||
428
docs/modules/web.md
Normal file
428
docs/modules/web.md
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
# Web 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
Web 模块提供 HTTP API 和静态文件服务。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- REST API
|
||||||
|
- WebSocket
|
||||||
|
- 静态文件服务
|
||||||
|
- 认证中间件
|
||||||
|
- CORS 支持
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/web/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── routes.rs # 路由定义 (9KB)
|
||||||
|
├── ws.rs # WebSocket (8KB)
|
||||||
|
├── audio_ws.rs # 音频 WebSocket (8KB)
|
||||||
|
├── static_files.rs # 静态文件 (6KB)
|
||||||
|
└── handlers/ # API 处理器
|
||||||
|
├── mod.rs
|
||||||
|
└── config/
|
||||||
|
├── mod.rs
|
||||||
|
├── apply.rs
|
||||||
|
├── types.rs
|
||||||
|
└── rustdesk.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 路由结构
|
||||||
|
|
||||||
|
### 2.1 公共路由 (无认证)
|
||||||
|
|
||||||
|
| 路由 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/health` | GET | 健康检查 |
|
||||||
|
| `/auth/login` | POST | 登录 |
|
||||||
|
| `/setup` | GET | 获取设置状态 |
|
||||||
|
| `/setup/init` | POST | 初始化设置 |
|
||||||
|
|
||||||
|
### 2.2 用户路由 (需认证)
|
||||||
|
|
||||||
|
| 路由 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/info` | GET | 系统信息 |
|
||||||
|
| `/devices` | GET | 设备列表 |
|
||||||
|
| `/stream/*` | * | 流控制 |
|
||||||
|
| `/webrtc/*` | * | WebRTC 信令 |
|
||||||
|
| `/hid/*` | * | HID 控制 |
|
||||||
|
| `/audio/*` | * | 音频控制 |
|
||||||
|
| `/ws` | WS | 事件 WebSocket |
|
||||||
|
| `/ws/audio` | WS | 音频 WebSocket |
|
||||||
|
|
||||||
|
### 2.3 管理员路由 (需 Admin)
|
||||||
|
|
||||||
|
| 路由 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/config/*` | * | 配置管理 |
|
||||||
|
| `/msd/*` | * | MSD 操作 |
|
||||||
|
| `/atx/*` | * | ATX 控制 |
|
||||||
|
| `/extensions/*` | * | 扩展管理 |
|
||||||
|
| `/rustdesk/*` | * | RustDesk |
|
||||||
|
| `/users/*` | * | 用户管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 路由定义
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn create_router(state: Arc<AppState>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
// 公共路由
|
||||||
|
.route("/health", get(handlers::health))
|
||||||
|
.route("/auth/login", post(handlers::login))
|
||||||
|
.route("/setup", get(handlers::setup_status))
|
||||||
|
.route("/setup/init", post(handlers::setup_init))
|
||||||
|
|
||||||
|
// 用户路由
|
||||||
|
.nest("/api", user_routes())
|
||||||
|
|
||||||
|
// 管理员路由
|
||||||
|
.nest("/api/admin", admin_routes())
|
||||||
|
|
||||||
|
// 静态文件
|
||||||
|
.fallback(static_files::serve)
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
.layer(CompressionLayer::new())
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/info", get(handlers::system_info))
|
||||||
|
.route("/devices", get(handlers::list_devices))
|
||||||
|
|
||||||
|
// 流控制
|
||||||
|
.route("/stream/status", get(handlers::stream_status))
|
||||||
|
.route("/stream/start", post(handlers::stream_start))
|
||||||
|
.route("/stream/stop", post(handlers::stream_stop))
|
||||||
|
.route("/stream/mjpeg", get(handlers::mjpeg_stream))
|
||||||
|
|
||||||
|
// WebRTC
|
||||||
|
.route("/webrtc/session", post(handlers::webrtc_create_session))
|
||||||
|
.route("/webrtc/offer", post(handlers::webrtc_offer))
|
||||||
|
.route("/webrtc/ice", post(handlers::webrtc_ice))
|
||||||
|
.route("/webrtc/close", post(handlers::webrtc_close))
|
||||||
|
|
||||||
|
// HID
|
||||||
|
.route("/hid/status", get(handlers::hid_status))
|
||||||
|
.route("/hid/reset", post(handlers::hid_reset))
|
||||||
|
|
||||||
|
// WebSocket
|
||||||
|
.route("/ws", get(handlers::ws_handler))
|
||||||
|
.route("/ws/audio", get(handlers::audio_ws_handler))
|
||||||
|
|
||||||
|
// 认证中间件
|
||||||
|
.layer(middleware::from_fn(auth_middleware))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
// 配置
|
||||||
|
.route("/config", get(handlers::config::get_config))
|
||||||
|
.route("/config", patch(handlers::config::update_config))
|
||||||
|
|
||||||
|
// MSD
|
||||||
|
.route("/msd/status", get(handlers::msd_status))
|
||||||
|
.route("/msd/connect", post(handlers::msd_connect))
|
||||||
|
|
||||||
|
// ATX
|
||||||
|
.route("/atx/status", get(handlers::atx_status))
|
||||||
|
.route("/atx/power/short", post(handlers::atx_power_short))
|
||||||
|
|
||||||
|
// 认证中间件
|
||||||
|
.layer(middleware::from_fn(auth_middleware))
|
||||||
|
.layer(middleware::from_fn(admin_middleware))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 静态文件服务
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "web/dist"]
|
||||||
|
#[include = "*.html"]
|
||||||
|
#[include = "*.js"]
|
||||||
|
#[include = "*.css"]
|
||||||
|
#[include = "assets/*"]
|
||||||
|
struct Assets;
|
||||||
|
|
||||||
|
pub async fn serve(uri: Uri) -> impl IntoResponse {
|
||||||
|
let path = uri.path().trim_start_matches('/');
|
||||||
|
|
||||||
|
// 尝试获取文件
|
||||||
|
if let Some(content) = Assets::get(path) {
|
||||||
|
let mime = mime_guess::from_path(path)
|
||||||
|
.first_or_octet_stream();
|
||||||
|
|
||||||
|
return (
|
||||||
|
[(header::CONTENT_TYPE, mime.as_ref())],
|
||||||
|
content.data.into_owned(),
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPA 回退到 index.html
|
||||||
|
if let Some(content) = Assets::get("index.html") {
|
||||||
|
return (
|
||||||
|
[(header::CONTENT_TYPE, "text/html")],
|
||||||
|
content.data.into_owned(),
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusCode::NOT_FOUND.into_response()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. WebSocket 处理
|
||||||
|
|
||||||
|
### 5.1 事件 WebSocket (ws.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn ws_handler(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
ws.on_upgrade(|socket| handle_ws(socket, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_ws(mut socket: WebSocket, state: Arc<AppState>) {
|
||||||
|
// 发送初始设备信息
|
||||||
|
let device_info = state.get_device_info().await;
|
||||||
|
let json = serde_json::to_string(&device_info).unwrap();
|
||||||
|
let _ = socket.send(Message::Text(json)).await;
|
||||||
|
|
||||||
|
// 订阅事件
|
||||||
|
let mut rx = state.events.subscribe();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// 发送事件
|
||||||
|
result = rx.recv() => {
|
||||||
|
if let Ok(event) = result {
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
if socket.send(Message::Text(json)).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收消息 (心跳/关闭)
|
||||||
|
msg = socket.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(Message::Ping(data))) => {
|
||||||
|
let _ = socket.send(Message::Pong(data)).await;
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Close(_))) | None => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 音频 WebSocket (audio_ws.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn audio_ws_handler(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
ws.on_upgrade(|socket| handle_audio_ws(socket, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_audio_ws(mut socket: WebSocket, state: Arc<AppState>) {
|
||||||
|
// 订阅音频帧
|
||||||
|
let mut rx = state.audio.subscribe();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// 发送音频帧
|
||||||
|
result = rx.recv() => {
|
||||||
|
if let Ok(frame) = result {
|
||||||
|
if socket.send(Message::Binary(frame.data.to_vec())).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理关闭
|
||||||
|
msg = socket.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(Message::Close(_))) | None => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. MJPEG 流
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn mjpeg_stream(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let boundary = "frame";
|
||||||
|
|
||||||
|
// 订阅视频帧
|
||||||
|
let rx = state.stream_manager.subscribe_mjpeg();
|
||||||
|
|
||||||
|
// 创建流
|
||||||
|
let stream = async_stream::stream! {
|
||||||
|
let mut rx = rx;
|
||||||
|
while let Ok(frame) = rx.recv().await {
|
||||||
|
let header = format!(
|
||||||
|
"--{}\r\nContent-Type: image/jpeg\r\nContent-Length: {}\r\n\r\n",
|
||||||
|
boundary,
|
||||||
|
frame.data.len()
|
||||||
|
);
|
||||||
|
yield Ok::<_, std::io::Error>(Bytes::from(header));
|
||||||
|
yield Ok(frame.data.clone());
|
||||||
|
yield Ok(Bytes::from("\r\n"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
[(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
format!("multipart/x-mixed-replace; boundary={}", boundary),
|
||||||
|
)],
|
||||||
|
Body::from_stream(stream),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, message) = match self {
|
||||||
|
AppError::AuthError => (StatusCode::UNAUTHORIZED, "Authentication failed"),
|
||||||
|
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||||
|
AppError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden"),
|
||||||
|
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.as_str()),
|
||||||
|
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
|
||||||
|
AppError::Internal(err) => {
|
||||||
|
tracing::error!("Internal error: {:?}", err);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
(status, Json(json!({ "error": message }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 请求提取器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 从 Cookie 获取会话
|
||||||
|
pub struct AuthUser(pub Session);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for AuthUser
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let cookies = Cookies::from_request_parts(parts, state).await?;
|
||||||
|
let token = cookies
|
||||||
|
.get("session_id")
|
||||||
|
.map(|c| c.value().to_string())
|
||||||
|
.ok_or(AppError::Unauthorized)?;
|
||||||
|
|
||||||
|
let state = parts.extensions.get::<Arc<AppState>>().unwrap();
|
||||||
|
let session = state.sessions
|
||||||
|
.get_session(&token)
|
||||||
|
.ok_or(AppError::Unauthorized)?;
|
||||||
|
|
||||||
|
Ok(AuthUser(session))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 中间件
|
||||||
|
|
||||||
|
### 9.1 认证中间件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn auth_middleware(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
cookies: Cookies,
|
||||||
|
mut request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
let token = cookies
|
||||||
|
.get("session_id")
|
||||||
|
.map(|c| c.value().to_string());
|
||||||
|
|
||||||
|
if let Some(session) = token.and_then(|t| state.sessions.get_session(&t)) {
|
||||||
|
request.extensions_mut().insert(session);
|
||||||
|
next.run(request).await
|
||||||
|
} else {
|
||||||
|
StatusCode::UNAUTHORIZED.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Admin 中间件
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn admin_middleware(
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
if session.role == UserRole::Admin {
|
||||||
|
next.run(request).await
|
||||||
|
} else {
|
||||||
|
StatusCode::FORBIDDEN.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. HTTPS 支持
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 使用 axum-server 提供 TLS
|
||||||
|
let tls_config = RustlsConfig::from_pem_file(cert_path, key_path).await?;
|
||||||
|
|
||||||
|
axum_server::bind_rustls(addr, tls_config)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 或自动生成自签名证书
|
||||||
|
let (cert, key) = generate_self_signed_cert()?;
|
||||||
|
let tls_config = RustlsConfig::from_pem(cert, key).await?;
|
||||||
|
```
|
||||||
731
docs/modules/webrtc.md
Normal file
731
docs/modules/webrtc.md
Normal file
@@ -0,0 +1,731 @@
|
|||||||
|
# WebRTC 模块文档
|
||||||
|
|
||||||
|
## 1. 模块概述
|
||||||
|
|
||||||
|
WebRTC 模块提供低延迟的实时音视频流传输,支持多种视频编码格式和 DataChannel HID 控制。
|
||||||
|
|
||||||
|
### 1.1 主要功能
|
||||||
|
|
||||||
|
- WebRTC 会话管理
|
||||||
|
- 多编码器支持 (H264/H265/VP8/VP9)
|
||||||
|
- 音频轨道 (Opus)
|
||||||
|
- DataChannel HID
|
||||||
|
- ICE/STUN/TURN 支持
|
||||||
|
|
||||||
|
### 1.2 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/webrtc/
|
||||||
|
├── mod.rs # 模块导出
|
||||||
|
├── webrtc_streamer.rs # 统一管理器 (34KB)
|
||||||
|
├── universal_session.rs # 会话管理 (32KB)
|
||||||
|
├── video_track.rs # 视频轨道 (19KB)
|
||||||
|
├── rtp.rs # RTP 打包 (24KB)
|
||||||
|
├── h265_payloader.rs # H265 RTP (15KB)
|
||||||
|
├── peer.rs # PeerConnection (17KB)
|
||||||
|
├── config.rs # 配置 (3KB)
|
||||||
|
├── signaling.rs # 信令 (5KB)
|
||||||
|
└── track.rs # 轨道基类 (11KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ WebRTC Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Browser
|
||||||
|
│
|
||||||
|
│ HTTP Signaling
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ WebRtcStreamer │
|
||||||
|
│(webrtc_streamer)│
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┼─────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌────────┐ ┌────────┐
|
||||||
|
│Session │ │Session │ │Session │
|
||||||
|
│ 1 │ │ 2 │ │ N │
|
||||||
|
└───┬────┘ └───┬────┘ └───┬────┘
|
||||||
|
│ │ │
|
||||||
|
├───────────┼─────────────┤
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ SharedVideoPipeline │
|
||||||
|
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||||
|
│ │H264 │ │H265 │ │VP8 │ │VP9 │ │
|
||||||
|
│ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────┐
|
||||||
|
│ VideoCapturer │
|
||||||
|
└────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 会话生命周期
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Session Lifecycle │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. 创建会话
|
||||||
|
POST /webrtc/session
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Create Session │
|
||||||
|
│ Generate ID │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
{ session_id: "..." }
|
||||||
|
|
||||||
|
2. 发送 Offer
|
||||||
|
POST /webrtc/offer
|
||||||
|
{ session_id, codec, offer_sdp }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Process Offer │
|
||||||
|
│ Create Answer │
|
||||||
|
│ Setup Tracks │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
{ answer_sdp, ice_candidates }
|
||||||
|
|
||||||
|
3. ICE 候选
|
||||||
|
POST /webrtc/ice
|
||||||
|
{ session_id, candidate }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Add ICE │
|
||||||
|
│ Candidate │
|
||||||
|
└─────────────────┘
|
||||||
|
|
||||||
|
4. 连接建立
|
||||||
|
┌─────────────────┐
|
||||||
|
│ DTLS Handshake │
|
||||||
|
│ SRTP Setup │
|
||||||
|
│ DataChannel │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
开始传输视频/音频
|
||||||
|
|
||||||
|
5. 关闭会话
|
||||||
|
POST /webrtc/close
|
||||||
|
{ session_id }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Cleanup │
|
||||||
|
│ Release │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 WebRtcStreamer (webrtc_streamer.rs)
|
||||||
|
|
||||||
|
WebRTC 服务主类。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct WebRtcStreamer {
|
||||||
|
/// 会话映射
|
||||||
|
sessions: Arc<RwLock<HashMap<String, Arc<UniversalSession>>>>,
|
||||||
|
|
||||||
|
/// 共享视频管道
|
||||||
|
video_pipeline: Arc<SharedVideoPipeline>,
|
||||||
|
|
||||||
|
/// 共享音频管道
|
||||||
|
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||||
|
|
||||||
|
/// HID 控制器
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
|
||||||
|
/// 配置
|
||||||
|
config: WebRtcConfig,
|
||||||
|
|
||||||
|
/// 事件总线
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebRtcStreamer {
|
||||||
|
/// 创建流服务
|
||||||
|
pub async fn new(
|
||||||
|
video_pipeline: Arc<SharedVideoPipeline>,
|
||||||
|
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
config: WebRtcConfig,
|
||||||
|
events: Arc<EventBus>,
|
||||||
|
) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 创建会话
|
||||||
|
pub async fn create_session(&self) -> Result<String>;
|
||||||
|
|
||||||
|
/// 处理 Offer
|
||||||
|
pub async fn process_offer(
|
||||||
|
&self,
|
||||||
|
session_id: &str,
|
||||||
|
offer: &str,
|
||||||
|
codec: VideoCodec,
|
||||||
|
) -> Result<OfferResponse>;
|
||||||
|
|
||||||
|
/// 添加 ICE 候选
|
||||||
|
pub async fn add_ice_candidate(
|
||||||
|
&self,
|
||||||
|
session_id: &str,
|
||||||
|
candidate: &str,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// 关闭会话
|
||||||
|
pub async fn close_session(&self, session_id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取会话列表
|
||||||
|
pub fn list_sessions(&self) -> Vec<SessionInfo>;
|
||||||
|
|
||||||
|
/// 获取统计信息
|
||||||
|
pub fn get_stats(&self) -> WebRtcStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OfferResponse {
|
||||||
|
pub answer_sdp: String,
|
||||||
|
pub ice_candidates: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WebRtcStats {
|
||||||
|
pub active_sessions: usize,
|
||||||
|
pub total_bytes_sent: u64,
|
||||||
|
pub avg_bitrate: u32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 UniversalSession (universal_session.rs)
|
||||||
|
|
||||||
|
单个 WebRTC 会话。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct UniversalSession {
|
||||||
|
/// 会话 ID
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
/// PeerConnection
|
||||||
|
peer: Arc<RTCPeerConnection>,
|
||||||
|
|
||||||
|
/// 视频轨道
|
||||||
|
video_track: Arc<UniversalVideoTrack>,
|
||||||
|
|
||||||
|
/// 音频轨道
|
||||||
|
audio_track: Option<Arc<dyn TrackLocal>>,
|
||||||
|
|
||||||
|
/// HID DataChannel
|
||||||
|
hid_channel: Arc<RwLock<Option<Arc<RTCDataChannel>>>>,
|
||||||
|
|
||||||
|
/// HID 处理器
|
||||||
|
hid_handler: Arc<HidDataChannelHandler>,
|
||||||
|
|
||||||
|
/// 状态
|
||||||
|
state: Arc<RwLock<SessionState>>,
|
||||||
|
|
||||||
|
/// 编码器类型
|
||||||
|
codec: VideoCodec,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSession {
|
||||||
|
/// 创建会话
|
||||||
|
pub async fn new(
|
||||||
|
id: String,
|
||||||
|
config: &WebRtcConfig,
|
||||||
|
video_pipeline: Arc<SharedVideoPipeline>,
|
||||||
|
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||||
|
hid_handler: Arc<HidDataChannelHandler>,
|
||||||
|
codec: VideoCodec,
|
||||||
|
) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 处理 Offer SDP
|
||||||
|
pub async fn handle_offer(&self, offer_sdp: &str) -> Result<String>;
|
||||||
|
|
||||||
|
/// 添加 ICE 候选
|
||||||
|
pub async fn add_ice_candidate(&self, candidate: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取 ICE 候选
|
||||||
|
pub fn get_ice_candidates(&self) -> Vec<String>;
|
||||||
|
|
||||||
|
/// 关闭会话
|
||||||
|
pub async fn close(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取状态
|
||||||
|
pub fn state(&self) -> SessionState;
|
||||||
|
|
||||||
|
/// 获取统计
|
||||||
|
pub fn stats(&self) -> SessionStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SessionState {
|
||||||
|
New,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Failed,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SessionStats {
|
||||||
|
pub bytes_sent: u64,
|
||||||
|
pub packets_sent: u64,
|
||||||
|
pub bitrate: u32,
|
||||||
|
pub frame_rate: f32,
|
||||||
|
pub round_trip_time: Duration,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 VideoTrack (video_track.rs)
|
||||||
|
|
||||||
|
视频轨道封装。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct UniversalVideoTrack {
|
||||||
|
/// 轨道 ID
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
/// 编码类型
|
||||||
|
codec: VideoCodec,
|
||||||
|
|
||||||
|
/// RTP 发送器
|
||||||
|
rtp_sender: Arc<RtpSender>,
|
||||||
|
|
||||||
|
/// 帧计数
|
||||||
|
frame_count: AtomicU64,
|
||||||
|
|
||||||
|
/// 统计
|
||||||
|
stats: Arc<RwLock<TrackStats>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalVideoTrack {
|
||||||
|
/// 创建轨道
|
||||||
|
pub fn new(id: &str, codec: VideoCodec) -> Result<Self>;
|
||||||
|
|
||||||
|
/// 发送编码帧
|
||||||
|
pub async fn send_frame(&self, frame: &EncodedFrame) -> Result<()>;
|
||||||
|
|
||||||
|
/// 获取 RTP 参数
|
||||||
|
pub fn rtp_params(&self) -> RtpParameters;
|
||||||
|
|
||||||
|
/// 获取统计
|
||||||
|
pub fn stats(&self) -> TrackStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TrackStats {
|
||||||
|
pub frames_sent: u64,
|
||||||
|
pub bytes_sent: u64,
|
||||||
|
pub packets_sent: u64,
|
||||||
|
pub packet_loss: f32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 RTP 打包 (rtp.rs)
|
||||||
|
|
||||||
|
RTP 协议实现。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct RtpPacketizer {
|
||||||
|
/// SSRC
|
||||||
|
ssrc: u32,
|
||||||
|
|
||||||
|
/// 序列号
|
||||||
|
sequence: u16,
|
||||||
|
|
||||||
|
/// 时间戳
|
||||||
|
timestamp: u32,
|
||||||
|
|
||||||
|
/// 负载类型
|
||||||
|
payload_type: u8,
|
||||||
|
|
||||||
|
/// 时钟频率
|
||||||
|
clock_rate: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtpPacketizer {
|
||||||
|
/// 创建打包器
|
||||||
|
pub fn new(codec: VideoCodec) -> Self;
|
||||||
|
|
||||||
|
/// 打包 H264 帧
|
||||||
|
pub fn packetize_h264(&mut self, frame: &[u8], keyframe: bool) -> Vec<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 打包 VP8 帧
|
||||||
|
pub fn packetize_vp8(&mut self, frame: &[u8], keyframe: bool) -> Vec<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 打包 VP9 帧
|
||||||
|
pub fn packetize_vp9(&mut self, frame: &[u8], keyframe: bool) -> Vec<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 打包 Opus 帧
|
||||||
|
pub fn packetize_opus(&mut self, frame: &[u8]) -> Vec<u8>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// H264 NAL 单元分片
|
||||||
|
pub struct H264Fragmenter;
|
||||||
|
|
||||||
|
impl H264Fragmenter {
|
||||||
|
/// 分片大于 MTU 的 NAL
|
||||||
|
pub fn fragment(nal: &[u8], mtu: usize) -> Vec<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 创建 STAP-A 聚合
|
||||||
|
pub fn aggregate(nals: &[&[u8]]) -> Vec<u8>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 H265 打包器 (h265_payloader.rs)
|
||||||
|
|
||||||
|
H265/HEVC RTP 打包。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct H265Payloader {
|
||||||
|
/// MTU 大小
|
||||||
|
mtu: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl H265Payloader {
|
||||||
|
/// 创建打包器
|
||||||
|
pub fn new(mtu: usize) -> Self;
|
||||||
|
|
||||||
|
/// 打包 H265 帧
|
||||||
|
pub fn packetize(&self, frame: &[u8]) -> Vec<Vec<u8>>;
|
||||||
|
|
||||||
|
/// 分析 NAL 单元类型
|
||||||
|
fn get_nal_type(nal: &[u8]) -> u8;
|
||||||
|
|
||||||
|
/// 是否需要分片
|
||||||
|
fn needs_fragmentation(&self, nal: &[u8]) -> bool;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 信令协议
|
||||||
|
|
||||||
|
### 4.1 创建会话
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/webrtc/session
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"session_id": "abc123-def456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 发送 Offer
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/webrtc/offer
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"session_id": "abc123-def456",
|
||||||
|
"video_codec": "h264",
|
||||||
|
"enable_audio": true,
|
||||||
|
"offer_sdp": "v=0\r\no=- ..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"answer_sdp": "v=0\r\no=- ...",
|
||||||
|
"ice_candidates": [
|
||||||
|
"candidate:1 1 UDP ...",
|
||||||
|
"candidate:2 1 TCP ..."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 ICE 候选
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/webrtc/ice
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"session_id": "abc123-def456",
|
||||||
|
"candidate": "candidate:1 1 UDP ..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 关闭会话
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/webrtc/close
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"session_id": "abc123-def456"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct WebRtcConfig {
|
||||||
|
/// STUN 服务器
|
||||||
|
pub stun_servers: Vec<String>,
|
||||||
|
|
||||||
|
/// TURN 服务器
|
||||||
|
pub turn_servers: Vec<TurnServer>,
|
||||||
|
|
||||||
|
/// 默认编码器
|
||||||
|
pub default_codec: VideoCodec,
|
||||||
|
|
||||||
|
/// 码率 (kbps)
|
||||||
|
pub bitrate_kbps: u32,
|
||||||
|
|
||||||
|
/// GOP 大小
|
||||||
|
pub gop_size: u32,
|
||||||
|
|
||||||
|
/// 启用音频
|
||||||
|
pub enable_audio: bool,
|
||||||
|
|
||||||
|
/// 启用 DataChannel HID
|
||||||
|
pub enable_datachannel_hid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TurnServer {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WebRtcConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
stun_servers: vec!["stun:stun.l.google.com:19302".to_string()],
|
||||||
|
turn_servers: vec![],
|
||||||
|
default_codec: VideoCodec::H264,
|
||||||
|
bitrate_kbps: 2000,
|
||||||
|
gop_size: 60,
|
||||||
|
enable_audio: true,
|
||||||
|
enable_datachannel_hid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. DataChannel HID
|
||||||
|
|
||||||
|
### 6.1 消息格式
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 键盘事件
|
||||||
|
{
|
||||||
|
"type": "keyboard",
|
||||||
|
"keys": ["KeyA", "KeyB"],
|
||||||
|
"modifiers": {
|
||||||
|
"ctrl": false,
|
||||||
|
"shift": true,
|
||||||
|
"alt": false,
|
||||||
|
"meta": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标事件
|
||||||
|
{
|
||||||
|
"type": "mouse",
|
||||||
|
"x": 16384,
|
||||||
|
"y": 16384,
|
||||||
|
"button": "left",
|
||||||
|
"event": "press"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标模式
|
||||||
|
{
|
||||||
|
"type": "mouse_mode",
|
||||||
|
"mode": "absolute"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 处理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
DataChannel Message
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│Parse JSON Event │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│HidDataChannel │
|
||||||
|
│ Handler │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ HidController │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
USB/Serial
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 支持的编码器
|
||||||
|
|
||||||
|
| 编码器 | RTP 负载类型 | 时钟频率 | 硬件加速 |
|
||||||
|
|--------|-------------|---------|---------|
|
||||||
|
| H264 | 96 (动态) | 90000 | VAAPI/RKMPP/V4L2 |
|
||||||
|
| H265 | 97 (动态) | 90000 | VAAPI |
|
||||||
|
| VP8 | 98 (动态) | 90000 | VAAPI |
|
||||||
|
| VP9 | 99 (动态) | 90000 | VAAPI |
|
||||||
|
| Opus | 111 (动态) | 48000 | 无 (软件) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum WebRtcError {
|
||||||
|
#[error("Session not found: {0}")]
|
||||||
|
SessionNotFound(String),
|
||||||
|
|
||||||
|
#[error("Session already exists")]
|
||||||
|
SessionExists,
|
||||||
|
|
||||||
|
#[error("Invalid SDP: {0}")]
|
||||||
|
InvalidSdp(String),
|
||||||
|
|
||||||
|
#[error("Codec not supported: {0}")]
|
||||||
|
CodecNotSupported(String),
|
||||||
|
|
||||||
|
#[error("ICE failed")]
|
||||||
|
IceFailed,
|
||||||
|
|
||||||
|
#[error("DTLS failed")]
|
||||||
|
DtlsFailed,
|
||||||
|
|
||||||
|
#[error("Track error: {0}")]
|
||||||
|
TrackError(String),
|
||||||
|
|
||||||
|
#[error("Connection closed")]
|
||||||
|
ConnectionClosed,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 使用示例
|
||||||
|
|
||||||
|
### 9.1 创建会话
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let streamer = WebRtcStreamer::new(
|
||||||
|
video_pipeline,
|
||||||
|
audio_pipeline,
|
||||||
|
hid,
|
||||||
|
WebRtcConfig::default(),
|
||||||
|
events,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 创建会话
|
||||||
|
let session_id = streamer.create_session().await?;
|
||||||
|
|
||||||
|
// 处理 Offer
|
||||||
|
let response = streamer.process_offer(
|
||||||
|
&session_id,
|
||||||
|
&offer_sdp,
|
||||||
|
VideoCodec::H264,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
println!("Answer: {}", response.answer_sdp);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 前端连接
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 创建 PeerConnection
|
||||||
|
const pc = new RTCPeerConnection({
|
||||||
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建 DataChannel
|
||||||
|
const hidChannel = pc.createDataChannel('hid');
|
||||||
|
|
||||||
|
// 创建 Offer
|
||||||
|
const offer = await pc.createOffer();
|
||||||
|
await pc.setLocalDescription(offer);
|
||||||
|
|
||||||
|
// 发送到服务器
|
||||||
|
const response = await fetch('/api/webrtc/offer', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
session_id,
|
||||||
|
video_codec: 'h264',
|
||||||
|
offer_sdp: offer.sdp
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const { answer_sdp, ice_candidates } = await response.json();
|
||||||
|
|
||||||
|
// 设置 Answer
|
||||||
|
await pc.setRemoteDescription({ type: 'answer', sdp: answer_sdp });
|
||||||
|
|
||||||
|
// 添加 ICE 候选
|
||||||
|
for (const candidate of ice_candidates) {
|
||||||
|
await pc.addIceCandidate({ candidate });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 常见问题
|
||||||
|
|
||||||
|
### Q: 连接超时?
|
||||||
|
|
||||||
|
1. 检查 STUN/TURN 配置
|
||||||
|
2. 检查防火墙设置
|
||||||
|
3. 尝试使用 TURN 中继
|
||||||
|
|
||||||
|
### Q: 视频卡顿?
|
||||||
|
|
||||||
|
1. 降低分辨率/码率
|
||||||
|
2. 检查网络带宽
|
||||||
|
3. 使用硬件编码
|
||||||
|
|
||||||
|
### Q: 音频不同步?
|
||||||
|
|
||||||
|
1. 检查时间戳同步
|
||||||
|
2. 调整缓冲区大小
|
||||||
|
3. 使用 NTP 同步
|
||||||
550
docs/report/hwcodec/00-architecture.md
Normal file
550
docs/report/hwcodec/00-architecture.md
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
# hwcodec 技术架构报告
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
hwcodec 是一个基于 FFmpeg 的硬件视频编解码库,来源于 RustDesk 项目并针对 One-KVM 进行了定制优化。该库提供跨平台的 GPU 加速视频编解码能力,支持多个 GPU 厂商和多种编码标准。
|
||||||
|
|
||||||
|
### 1.1 项目位置
|
||||||
|
|
||||||
|
```
|
||||||
|
libs/hwcodec/
|
||||||
|
├── src/ # Rust 源代码
|
||||||
|
├── cpp/ # C++ 源代码
|
||||||
|
├── externals/ # 外部依赖 (SDK)
|
||||||
|
├── dev/ # 开发工具
|
||||||
|
└── examples/ # 示例程序
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 核心特性
|
||||||
|
|
||||||
|
- **多编解码格式支持**: H.264, H.265 (HEVC), VP8, VP9, AV1, MJPEG
|
||||||
|
- **硬件加速**: NVENC/NVDEC, AMF, Intel QSV/MFX, VAAPI, RKMPP, V4L2 M2M, VideoToolbox
|
||||||
|
- **跨平台**: Windows, Linux, macOS, Android, iOS
|
||||||
|
- **低延迟优化**: 专为实时流媒体场景设计
|
||||||
|
- **Rust/C++ 混合架构**: Rust 提供安全的上层 API,C++ 实现底层编解码逻辑
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Rust API Layer │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ ffmpeg_ram │ │ vram │ │ mux │ │
|
||||||
|
│ │ module │ │ module │ │ module │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||||
|
├─────────┼────────────────┼───────────────────┼──────────────┤
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ FFI Bindings (bindgen) │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ C++ Core Layer │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ ffmpeg_ram │ │ ffmpeg_vram │ │ mux.cpp │ │
|
||||||
|
│ │ encode/ │ │ encode/ │ │ │ │
|
||||||
|
│ │ decode │ │ decode │ │ │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||||
|
├─────────┼────────────────┼───────────────────┼──────────────┤
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────────┴───────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ FFmpeg Libraries │ │
|
||||||
|
│ │ libavcodec │ libavutil │ libavformat │ libswscale │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
├──────────────────────────┼──────────────────────────────────┤
|
||||||
|
│ Hardware Acceleration Backends │
|
||||||
|
│ ┌────────┐ ┌─────┐ ┌─────┐ ┌───────┐ ┌───────┐ ┌───────┐ │
|
||||||
|
│ │ NVENC │ │ AMF │ │ MFX │ │ VAAPI │ │ RKMPP │ │V4L2M2M│ │
|
||||||
|
│ └────────┘ └─────┘ └─────┘ └───────┘ └───────┘ └───────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 模块职责
|
||||||
|
|
||||||
|
| 模块 | 职责 | 关键文件 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `ffmpeg_ram` | 基于 RAM 的软件/硬件编解码 | `src/ffmpeg_ram/` |
|
||||||
|
| `vram` | GPU 显存直接编解码 (Windows) | `src/vram/` |
|
||||||
|
| `mux` | 视频混流 (MP4/MKV) | `src/mux.rs` |
|
||||||
|
| `common` | 公共定义和 GPU 检测 | `src/common.rs` |
|
||||||
|
| `ffmpeg` | FFmpeg 日志和初始化 | `src/ffmpeg.rs` |
|
||||||
|
|
||||||
|
## 3. 模块详细分析
|
||||||
|
|
||||||
|
### 3.1 库入口 (lib.rs)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// libs/hwcodec/src/lib.rs
|
||||||
|
pub mod common;
|
||||||
|
pub mod ffmpeg;
|
||||||
|
pub mod ffmpeg_ram;
|
||||||
|
pub mod mux;
|
||||||
|
#[cfg(all(windows, feature = "vram"))]
|
||||||
|
pub mod vram;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub mod android;
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
- 导出所有子模块
|
||||||
|
- 提供 C 日志回调函数 `hwcodec_log`
|
||||||
|
- 条件编译: `vram` 模块仅在 Windows + vram feature 启用时编译
|
||||||
|
|
||||||
|
### 3.2 公共模块 (common.rs)
|
||||||
|
|
||||||
|
**核心类型**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum Driver {
|
||||||
|
NV, // NVIDIA
|
||||||
|
AMF, // AMD
|
||||||
|
MFX, // Intel
|
||||||
|
FFMPEG, // 软件编码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GPU 检测函数**:
|
||||||
|
|
||||||
|
| 平台 | 检测函数 | 检测方式 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| Linux | `linux_support_nv()` | 加载 CUDA/NVENC 动态库 |
|
||||||
|
| Linux | `linux_support_amd()` | 检查 `libamfrt64.so.1` |
|
||||||
|
| Linux | `linux_support_intel()` | 检查 `libvpl.so`/`libmfx.so` |
|
||||||
|
| Linux | `linux_support_rkmpp()` | 检查 `/dev/mpp_service` |
|
||||||
|
| Linux | `linux_support_v4l2m2m()` | 检查 `/dev/video*` 设备 |
|
||||||
|
| macOS | `get_video_toolbox_codec_support()` | 调用 VideoToolbox API |
|
||||||
|
| Windows | 通过 VRAM 模块检测 | 查询 D3D11 设备 |
|
||||||
|
|
||||||
|
### 3.3 FFmpeg RAM 编码模块
|
||||||
|
|
||||||
|
#### 3.3.1 Rust 层 (src/ffmpeg_ram/)
|
||||||
|
|
||||||
|
**CodecInfo 结构体**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct CodecInfo {
|
||||||
|
pub name: String, // 编码器名称如 "h264_nvenc"
|
||||||
|
pub mc_name: Option<String>, // MediaCodec 名称 (Android)
|
||||||
|
pub format: DataFormat, // H264/H265/VP8/VP9/AV1/MJPEG
|
||||||
|
pub priority: i32, // 优先级 (Best=0, Good=1, Normal=2, Soft=3, Bad=4)
|
||||||
|
pub hwdevice: AVHWDeviceType, // 硬件设备类型
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**EncodeContext 结构体**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct EncodeContext {
|
||||||
|
pub name: String, // 编码器名称
|
||||||
|
pub width: i32, // 视频宽度
|
||||||
|
pub height: i32, // 视频高度
|
||||||
|
pub pixfmt: AVPixelFormat, // 像素格式 (NV12/YUV420P)
|
||||||
|
pub align: i32, // 内存对齐
|
||||||
|
pub fps: i32, // 帧率
|
||||||
|
pub gop: i32, // GOP 大小
|
||||||
|
pub rc: RateControl, // 码率控制模式
|
||||||
|
pub quality: Quality, // 质量级别
|
||||||
|
pub kbs: i32, // 目标码率 (kbps)
|
||||||
|
pub q: i32, // 量化参数
|
||||||
|
pub thread_count: i32, // 线程数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Encoder 类**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Encoder {
|
||||||
|
codec: *mut c_void, // C++ 编码器指针
|
||||||
|
frames: *mut Vec<EncodeFrame>, // 编码输出帧
|
||||||
|
pub ctx: EncodeContext,
|
||||||
|
pub linesize: Vec<i32>, // 行大小
|
||||||
|
pub offset: Vec<i32>, // 平面偏移
|
||||||
|
pub length: i32, // 总数据长度
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心方法**:
|
||||||
|
|
||||||
|
| 方法 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `Encoder::new()` | 创建编码器实例 |
|
||||||
|
| `Encoder::encode()` | 编码一帧 YUV 数据 |
|
||||||
|
| `Encoder::set_bitrate()` | 动态调整码率 |
|
||||||
|
| `Encoder::request_keyframe()` | 请求下一帧为关键帧 |
|
||||||
|
| `Encoder::available_encoders()` | 检测系统可用编码器 |
|
||||||
|
|
||||||
|
#### 3.3.2 C++ 层 (cpp/ffmpeg_ram/)
|
||||||
|
|
||||||
|
**FFmpegRamEncoder 类** (ffmpeg_ram_encode.cpp:97-420):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class FFmpegRamEncoder {
|
||||||
|
AVCodecContext *c_ = NULL; // FFmpeg 编码上下文
|
||||||
|
AVFrame *frame_ = NULL; // 输入帧
|
||||||
|
AVPacket *pkt_ = NULL; // 编码输出包
|
||||||
|
AVBufferRef *hw_device_ctx_; // 硬件设备上下文
|
||||||
|
AVFrame *hw_frame_ = NULL; // 硬件帧
|
||||||
|
bool force_keyframe_ = false; // 强制关键帧标志
|
||||||
|
|
||||||
|
// 主要方法
|
||||||
|
bool init(int *linesize, int *offset, int *length);
|
||||||
|
int encode(const uint8_t *data, int length, const void *obj, uint64_t ms);
|
||||||
|
int do_encode(AVFrame *frame, const void *obj, int64_t ms);
|
||||||
|
int set_hwframe_ctx(); // 设置硬件帧上下文
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**编码流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
输入 YUV 数据
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
fill_frame() - 填充 AVFrame 数据指针
|
||||||
|
│
|
||||||
|
├──▶ (软件编码) 直接使用 frame_
|
||||||
|
│
|
||||||
|
└──▶ (硬件编码) av_hwframe_transfer_data() 传输到 GPU
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
使用 hw_frame_
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
avcodec_send_frame() - 发送帧到编码器
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
avcodec_receive_packet() - 获取编码数据
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
callback() - 回调输出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 FFmpeg RAM 解码模块
|
||||||
|
|
||||||
|
**Decoder 类**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Decoder {
|
||||||
|
codec: *mut c_void,
|
||||||
|
frames: *mut Vec<DecodeFrame>,
|
||||||
|
pub ctx: DecodeContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DecodeFrame {
|
||||||
|
pub pixfmt: AVPixelFormat,
|
||||||
|
pub width: i32,
|
||||||
|
pub height: i32,
|
||||||
|
pub data: Vec<Vec<u8>>, // Y, U, V 平面数据
|
||||||
|
pub linesize: Vec<i32>,
|
||||||
|
pub key: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C++ 实现** (ffmpeg_ram_decode.cpp):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class FFmpegRamDecoder {
|
||||||
|
AVCodecContext *c_ = NULL;
|
||||||
|
AVBufferRef *hw_device_ctx_ = NULL;
|
||||||
|
AVFrame *sw_frame_ = NULL; // 软件帧 (用于硬件→软件转换)
|
||||||
|
AVFrame *frame_ = NULL; // 解码输出帧
|
||||||
|
AVPacket *pkt_ = NULL;
|
||||||
|
bool hwaccel_ = true;
|
||||||
|
|
||||||
|
int do_decode(const void *obj);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**解码流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
输入编码数据
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
avcodec_send_packet() - 发送数据到解码器
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
avcodec_receive_frame() - 获取解码帧
|
||||||
|
│
|
||||||
|
├──▶ (软件解码) 直接使用 frame_
|
||||||
|
│
|
||||||
|
└──▶ (硬件解码) av_hwframe_transfer_data()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
sw_frame_ (GPU → CPU)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
callback() - 回调输出
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 硬件加速支持
|
||||||
|
|
||||||
|
### 4.1 支持的硬件加速后端
|
||||||
|
|
||||||
|
| 后端 | 厂商 | 平台 | 编码器名称 |
|
||||||
|
|------|------|------|-----------|
|
||||||
|
| NVENC | NVIDIA | Windows/Linux | h264_nvenc, hevc_nvenc |
|
||||||
|
| AMF | AMD | Windows/Linux | h264_amf, hevc_amf |
|
||||||
|
| QSV | Intel | Windows | h264_qsv, hevc_qsv |
|
||||||
|
| VAAPI | 通用 | Linux | h264_vaapi, hevc_vaapi, vp8_vaapi, vp9_vaapi |
|
||||||
|
| RKMPP | Rockchip | Linux | h264_rkmpp, hevc_rkmpp |
|
||||||
|
| V4L2 M2M | ARM SoC | Linux | h264_v4l2m2m, hevc_v4l2m2m |
|
||||||
|
| VideoToolbox | Apple | macOS/iOS | hevc_videotoolbox |
|
||||||
|
| MediaCodec | Google | Android | h264_mediacodec, hevc_mediacodec |
|
||||||
|
|
||||||
|
### 4.2 硬件检测逻辑 (Linux)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||||
|
|
||||||
|
// NVIDIA 检测 - 加载 CUDA 和 NVENC 动态库
|
||||||
|
int linux_support_nv() {
|
||||||
|
CudaFunctions *cuda_dl = NULL;
|
||||||
|
NvencFunctions *nvenc_dl = NULL;
|
||||||
|
CuvidFunctions *cvdl = NULL;
|
||||||
|
load_driver(&cuda_dl, &nvenc_dl, &cvdl);
|
||||||
|
// 成功加载则返回 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AMD 检测 - 检查 AMF 运行时库
|
||||||
|
int linux_support_amd() {
|
||||||
|
void *handle = dlopen("libamfrt64.so.1", RTLD_LAZY);
|
||||||
|
// 成功加载则返回 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intel 检测 - 检查 VPL/MFX 库
|
||||||
|
int linux_support_intel() {
|
||||||
|
const char *libs[] = {"libvpl.so", "libmfx.so", ...};
|
||||||
|
// 任一成功加载则返回 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rockchip MPP 检测 - 检查设备节点
|
||||||
|
int linux_support_rkmpp() {
|
||||||
|
if (access("/dev/mpp_service", F_OK) == 0) return 0;
|
||||||
|
if (access("/dev/rga", F_OK) == 0) return 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// V4L2 M2M 检测 - 检查视频设备
|
||||||
|
int linux_support_v4l2m2m() {
|
||||||
|
const char *devices[] = {"/dev/video10", "/dev/video11", ...};
|
||||||
|
// 任一设备可打开则返回 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 编码器优先级系统
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum Priority {
|
||||||
|
Best = 0, // 最高优先级 (硬件加速)
|
||||||
|
Good = 1, // 良好 (VAAPI, 部分硬件)
|
||||||
|
Normal = 2, // 普通
|
||||||
|
Soft = 3, // 软件编码
|
||||||
|
Bad = 4, // 最低优先级
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优先级分配**:
|
||||||
|
|
||||||
|
| 编码器 | 优先级 |
|
||||||
|
|--------|--------|
|
||||||
|
| h264_nvenc, hevc_nvenc | Best (0) |
|
||||||
|
| h264_amf, hevc_amf | Best (0) |
|
||||||
|
| h264_qsv, hevc_qsv | Best (0) |
|
||||||
|
| h264_rkmpp, hevc_rkmpp | Best (0) |
|
||||||
|
| h264_vaapi, hevc_vaapi | Good (1) |
|
||||||
|
| h264_v4l2m2m, hevc_v4l2m2m | Good (1) |
|
||||||
|
| h264 (x264), hevc (x265) | Soft (3) |
|
||||||
|
|
||||||
|
### 4.4 低延迟优化配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/util.cpp
|
||||||
|
|
||||||
|
bool set_lantency_free(void *priv_data, const std::string &name) {
|
||||||
|
// NVENC: 禁用延迟缓冲
|
||||||
|
if (name.find("nvenc") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "delay", "0", 0);
|
||||||
|
}
|
||||||
|
// AMF: 设置查询超时
|
||||||
|
if (name.find("amf") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "query_timeout", "1000", 0);
|
||||||
|
}
|
||||||
|
// QSV/VAAPI: 设置异步深度为 1
|
||||||
|
if (name.find("qsv") != std::string::npos ||
|
||||||
|
name.find("vaapi") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "async_depth", "1", 0);
|
||||||
|
}
|
||||||
|
// VideoToolbox: 实时模式
|
||||||
|
if (name.find("videotoolbox") != std::string::npos) {
|
||||||
|
av_opt_set_int(priv_data, "realtime", 1, 0);
|
||||||
|
av_opt_set_int(priv_data, "prio_speed", 1, 0);
|
||||||
|
}
|
||||||
|
// libvpx: 实时模式
|
||||||
|
if (name.find("libvpx") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "deadline", "realtime", 0);
|
||||||
|
av_opt_set_int(priv_data, "cpu-used", 6, 0);
|
||||||
|
av_opt_set_int(priv_data, "lag-in-frames", 0, 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 混流模块 (Mux)
|
||||||
|
|
||||||
|
### 5.1 功能概述
|
||||||
|
|
||||||
|
混流模块提供将编码后的视频流写入容器格式 (MP4/MKV) 的功能。
|
||||||
|
|
||||||
|
### 5.2 Rust API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// libs/hwcodec/src/mux.rs
|
||||||
|
|
||||||
|
pub struct MuxContext {
|
||||||
|
pub filename: String, // 输出文件名
|
||||||
|
pub width: usize, // 视频宽度
|
||||||
|
pub height: usize, // 视频高度
|
||||||
|
pub is265: bool, // 是否为 H.265
|
||||||
|
pub framerate: usize, // 帧率
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Muxer {
|
||||||
|
inner: *mut c_void, // C++ Muxer 指针
|
||||||
|
pub ctx: MuxContext,
|
||||||
|
start: Instant, // 开始时间
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Muxer {
|
||||||
|
pub fn new(ctx: MuxContext) -> Result<Self, ()>;
|
||||||
|
pub fn write_video(&mut self, data: &[u8], key: bool) -> Result<(), i32>;
|
||||||
|
pub fn write_tail(&mut self) -> Result<(), i32>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 C++ 实现
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/mux/mux.cpp
|
||||||
|
|
||||||
|
class Muxer {
|
||||||
|
OutputStream video_st; // 视频流
|
||||||
|
AVFormatContext *oc = NULL; // 格式上下文
|
||||||
|
int framerate;
|
||||||
|
int64_t start_ms; // 起始时间戳
|
||||||
|
int64_t last_pts; // 上一帧 PTS
|
||||||
|
int got_first; // 是否收到第一帧
|
||||||
|
|
||||||
|
bool init(const char *filename, int width, int height,
|
||||||
|
int is265, int framerate);
|
||||||
|
int write_video_frame(const uint8_t *data, int len,
|
||||||
|
int64_t pts_ms, int key);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**写入流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
write_video_frame()
|
||||||
|
│
|
||||||
|
├── 检查是否为关键帧 (第一帧必须是关键帧)
|
||||||
|
│
|
||||||
|
├── 计算 PTS (相对于 start_ms)
|
||||||
|
│
|
||||||
|
├── 填充 AVPacket
|
||||||
|
│
|
||||||
|
├── av_packet_rescale_ts() (ms → stream timebase)
|
||||||
|
│
|
||||||
|
└── av_write_frame() → 写入文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 构建系统
|
||||||
|
|
||||||
|
### 6.1 Cargo.toml 配置
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "hwcodec"
|
||||||
|
version = "0.7.1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
vram = [] # GPU VRAM 直接编解码 (Windows only)
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1.0" # C++ 编译
|
||||||
|
bindgen = "0.59" # FFI 绑定生成
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 构建流程 (build.rs)
|
||||||
|
|
||||||
|
```
|
||||||
|
build.rs
|
||||||
|
│
|
||||||
|
├── build_common()
|
||||||
|
│ ├── 生成 common_ffi.rs (bindgen)
|
||||||
|
│ ├── 编译平台相关 C++ 代码
|
||||||
|
│ └── 链接系统库 (d3d11, dxgi, stdc++)
|
||||||
|
│
|
||||||
|
├── ffmpeg::build_ffmpeg()
|
||||||
|
│ ├── 生成 ffmpeg_ffi.rs
|
||||||
|
│ ├── 链接 FFmpeg 库 (VCPKG 或 pkg-config)
|
||||||
|
│ ├── build_ffmpeg_ram()
|
||||||
|
│ │ └── 编译 ffmpeg_ram_encode.cpp, ffmpeg_ram_decode.cpp
|
||||||
|
│ ├── build_ffmpeg_vram() [vram feature]
|
||||||
|
│ │ └── 编译 ffmpeg_vram_encode.cpp, ffmpeg_vram_decode.cpp
|
||||||
|
│ └── build_mux()
|
||||||
|
│ └── 编译 mux.cpp
|
||||||
|
│
|
||||||
|
└── sdk::build_sdk() [Windows + vram feature]
|
||||||
|
├── build_nv() - NVIDIA SDK
|
||||||
|
├── build_amf() - AMD AMF
|
||||||
|
└── build_mfx() - Intel MFX
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 FFmpeg 链接方式
|
||||||
|
|
||||||
|
| 方式 | 平台 | 条件 |
|
||||||
|
|------|------|------|
|
||||||
|
| VCPKG 静态链接 | 跨平台 | 设置 `VCPKG_ROOT` 环境变量 |
|
||||||
|
| pkg-config 动态链接 | Linux | 默认方式 |
|
||||||
|
|
||||||
|
## 7. 外部依赖
|
||||||
|
|
||||||
|
### 7.1 SDK 版本
|
||||||
|
|
||||||
|
| SDK | 版本 | 用途 |
|
||||||
|
|-----|------|------|
|
||||||
|
| nv-codec-headers | n12.1.14.0 | NVIDIA 编码头文件 |
|
||||||
|
| Video_Codec_SDK | 12.1.14 | NVIDIA 编解码 SDK |
|
||||||
|
| AMF | v1.4.35 | AMD Advanced Media Framework |
|
||||||
|
| MediaSDK | 22.5.4 | Intel Media SDK |
|
||||||
|
|
||||||
|
### 7.2 FFmpeg 依赖库
|
||||||
|
|
||||||
|
```
|
||||||
|
libavcodec - 编解码核心
|
||||||
|
libavutil - 工具函数
|
||||||
|
libavformat - 容器格式
|
||||||
|
libswscale - 图像缩放转换
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 总结
|
||||||
|
|
||||||
|
hwcodec 库通过 Rust/C++ 混合架构,在保证内存安全的同时实现了高性能的视频编解码。其核心设计特点包括:
|
||||||
|
|
||||||
|
1. **统一的编解码器 API**: 无论使用硬件还是软件编解码,上层 API 保持一致
|
||||||
|
2. **自动硬件检测**: 运行时自动检测并选择最优的硬件加速后端
|
||||||
|
3. **优先级系统**: 基于质量和性能为不同编码器分配优先级
|
||||||
|
4. **低延迟优化**: 针对实时流媒体场景进行了专门优化
|
||||||
|
5. **跨平台支持**: 覆盖主流操作系统和 GPU 厂商
|
||||||
445
docs/report/hwcodec/01-api-reference.md
Normal file
445
docs/report/hwcodec/01-api-reference.md
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
# hwcodec 编解码器 API 详解
|
||||||
|
|
||||||
|
## 1. 编码器 API
|
||||||
|
|
||||||
|
### 1.1 编码器初始化
|
||||||
|
|
||||||
|
#### EncodeContext 参数
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct EncodeContext {
|
||||||
|
pub name: String, // 编码器名称
|
||||||
|
pub mc_name: Option<String>, // MediaCodec 名称 (Android)
|
||||||
|
pub width: i32, // 视频宽度 (必须为偶数)
|
||||||
|
pub height: i32, // 视频高度 (必须为偶数)
|
||||||
|
pub pixfmt: AVPixelFormat, // 像素格式
|
||||||
|
pub align: i32, // 内存对齐 (通常为 0 或 32)
|
||||||
|
pub fps: i32, // 帧率
|
||||||
|
pub gop: i32, // GOP 大小 (关键帧间隔)
|
||||||
|
pub rc: RateControl, // 码率控制模式
|
||||||
|
pub quality: Quality, // 编码质量
|
||||||
|
pub kbs: i32, // 目标码率 (kbps)
|
||||||
|
pub q: i32, // 量化参数 (CQ 模式)
|
||||||
|
pub thread_count: i32, // 编码线程数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 参数说明
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 | 推荐值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `name` | String | FFmpeg 编码器名称 | 见下表 |
|
||||||
|
| `width` | i32 | 视频宽度 | 1920 |
|
||||||
|
| `height` | i32 | 视频高度 | 1080 |
|
||||||
|
| `pixfmt` | AVPixelFormat | 像素格式 | NV12 / YUV420P |
|
||||||
|
| `align` | i32 | 内存对齐 | 0 (自动) |
|
||||||
|
| `fps` | i32 | 帧率 | 30 |
|
||||||
|
| `gop` | i32 | GOP 大小 | 30 (1秒) |
|
||||||
|
| `rc` | RateControl | 码率控制 | CBR / VBR |
|
||||||
|
| `quality` | Quality | 质量级别 | Medium |
|
||||||
|
| `kbs` | i32 | 码率 (kbps) | 2000-8000 |
|
||||||
|
| `thread_count` | i32 | 线程数 | 4 |
|
||||||
|
|
||||||
|
#### 编码器名称对照表
|
||||||
|
|
||||||
|
| 名称 | 格式 | 加速 | 平台 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `h264_nvenc` | H.264 | NVIDIA GPU | Windows/Linux |
|
||||||
|
| `hevc_nvenc` | H.265 | NVIDIA GPU | Windows/Linux |
|
||||||
|
| `h264_amf` | H.264 | AMD GPU | Windows/Linux |
|
||||||
|
| `hevc_amf` | H.265 | AMD GPU | Windows/Linux |
|
||||||
|
| `h264_qsv` | H.264 | Intel QSV | Windows |
|
||||||
|
| `hevc_qsv` | H.265 | Intel QSV | Windows |
|
||||||
|
| `h264_vaapi` | H.264 | VAAPI | Linux |
|
||||||
|
| `hevc_vaapi` | H.265 | VAAPI | Linux |
|
||||||
|
| `vp8_vaapi` | VP8 | VAAPI | Linux |
|
||||||
|
| `vp9_vaapi` | VP9 | VAAPI | Linux |
|
||||||
|
| `h264_rkmpp` | H.264 | Rockchip MPP | Linux |
|
||||||
|
| `hevc_rkmpp` | H.265 | Rockchip MPP | Linux |
|
||||||
|
| `h264_v4l2m2m` | H.264 | V4L2 M2M | Linux |
|
||||||
|
| `hevc_v4l2m2m` | H.265 | V4L2 M2M | Linux |
|
||||||
|
| `hevc_videotoolbox` | H.265 | VideoToolbox | macOS |
|
||||||
|
| `h264` | H.264 | 软件 (x264) | 全平台 |
|
||||||
|
| `hevc` | H.265 | 软件 (x265) | 全平台 |
|
||||||
|
| `libvpx` | VP8 | 软件 | 全平台 |
|
||||||
|
| `libvpx-vp9` | VP9 | 软件 | 全平台 |
|
||||||
|
| `mjpeg` | MJPEG | 软件 | 全平台 |
|
||||||
|
|
||||||
|
### 1.2 创建编码器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||||
|
use hwcodec::ffmpeg::{AVPixelFormat};
|
||||||
|
use hwcodec::common::{RateControl, Quality};
|
||||||
|
|
||||||
|
let ctx = EncodeContext {
|
||||||
|
name: "h264_vaapi".to_string(),
|
||||||
|
mc_name: None,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||||
|
align: 0,
|
||||||
|
fps: 30,
|
||||||
|
gop: 30,
|
||||||
|
rc: RateControl::RC_CBR,
|
||||||
|
quality: Quality::Quality_Medium,
|
||||||
|
kbs: 4000,
|
||||||
|
q: 0,
|
||||||
|
thread_count: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoder = Encoder::new(ctx)?;
|
||||||
|
println!("Linesize: {:?}", encoder.linesize);
|
||||||
|
println!("Offset: {:?}", encoder.offset);
|
||||||
|
println!("Buffer length: {}", encoder.length);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 编码帧
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 准备 YUV 数据
|
||||||
|
let yuv_data: Vec<u8> = prepare_yuv_frame();
|
||||||
|
|
||||||
|
// 编码
|
||||||
|
let pts_ms: i64 = 0; // 时间戳 (毫秒)
|
||||||
|
match encoder.encode(&yuv_data, pts_ms) {
|
||||||
|
Ok(frames) => {
|
||||||
|
for frame in frames.iter() {
|
||||||
|
println!("Encoded: {} bytes, pts={}, key={}",
|
||||||
|
frame.data.len(), frame.pts, frame.key);
|
||||||
|
// 发送 frame.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(code) => {
|
||||||
|
eprintln!("Encode error: {}", code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 动态调整码率
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 动态调整到 6000 kbps
|
||||||
|
encoder.set_bitrate(6000)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 请求关键帧
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 下一帧强制编码为 IDR 帧
|
||||||
|
encoder.request_keyframe();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.6 检测可用编码器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||||
|
use hwcodec::ffmpeg_ram::CodecInfo;
|
||||||
|
|
||||||
|
let ctx = EncodeContext {
|
||||||
|
name: String::new(),
|
||||||
|
mc_name: None,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||||
|
align: 0,
|
||||||
|
fps: 30,
|
||||||
|
gop: 30,
|
||||||
|
rc: RateControl::RC_DEFAULT,
|
||||||
|
quality: Quality::Quality_Default,
|
||||||
|
kbs: 4000,
|
||||||
|
q: 0,
|
||||||
|
thread_count: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
let available_encoders = Encoder::available_encoders(ctx, None);
|
||||||
|
for encoder in available_encoders {
|
||||||
|
println!("Available: {} (format: {:?}, priority: {})",
|
||||||
|
encoder.name, encoder.format, encoder.priority);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 解码器 API
|
||||||
|
|
||||||
|
### 2.1 解码器初始化
|
||||||
|
|
||||||
|
#### DecodeContext 参数
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct DecodeContext {
|
||||||
|
pub name: String, // 解码器名称
|
||||||
|
pub device_type: AVHWDeviceType, // 硬件设备类型
|
||||||
|
pub thread_count: i32, // 解码线程数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 硬件设备类型
|
||||||
|
|
||||||
|
| AVHWDeviceType | 说明 |
|
||||||
|
|----------------|------|
|
||||||
|
| `AV_HWDEVICE_TYPE_NONE` | 软件解码 |
|
||||||
|
| `AV_HWDEVICE_TYPE_CUDA` | NVIDIA CUDA |
|
||||||
|
| `AV_HWDEVICE_TYPE_VAAPI` | Linux VAAPI |
|
||||||
|
| `AV_HWDEVICE_TYPE_D3D11VA` | Windows D3D11 |
|
||||||
|
| `AV_HWDEVICE_TYPE_VIDEOTOOLBOX` | macOS VideoToolbox |
|
||||||
|
| `AV_HWDEVICE_TYPE_MEDIACODEC` | Android MediaCodec |
|
||||||
|
|
||||||
|
### 2.2 创建解码器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hwcodec::ffmpeg_ram::decode::{Decoder, DecodeContext};
|
||||||
|
use hwcodec::ffmpeg::AVHWDeviceType;
|
||||||
|
|
||||||
|
let ctx = DecodeContext {
|
||||||
|
name: "h264".to_string(),
|
||||||
|
device_type: AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
|
||||||
|
thread_count: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoder = Decoder::new(ctx)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 解码帧
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 输入编码数据
|
||||||
|
let encoded_packet: Vec<u8> = receive_encoded_data();
|
||||||
|
|
||||||
|
match decoder.decode(&encoded_packet) {
|
||||||
|
Ok(frames) => {
|
||||||
|
for frame in frames.iter() {
|
||||||
|
println!("Decoded: {}x{}, format={:?}, key={}",
|
||||||
|
frame.width, frame.height, frame.pixfmt, frame.key);
|
||||||
|
|
||||||
|
// 访问 YUV 数据
|
||||||
|
let y_plane = &frame.data[0];
|
||||||
|
let u_plane = &frame.data[1];
|
||||||
|
let v_plane = &frame.data[2]; // 仅 YUV420P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(code) => {
|
||||||
|
eprintln!("Decode error: {}", code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 DecodeFrame 结构体
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct DecodeFrame {
|
||||||
|
pub pixfmt: AVPixelFormat, // 输出像素格式
|
||||||
|
pub width: i32, // 帧宽度
|
||||||
|
pub height: i32, // 帧高度
|
||||||
|
pub data: Vec<Vec<u8>>, // 平面数据 [Y, U, V] 或 [Y, UV]
|
||||||
|
pub linesize: Vec<i32>, // 每个平面的行字节数
|
||||||
|
pub key: bool, // 是否为关键帧
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 像素格式与平面布局
|
||||||
|
|
||||||
|
| 像素格式 | 平面数 | data[0] | data[1] | data[2] |
|
||||||
|
|----------|--------|---------|---------|---------|
|
||||||
|
| `YUV420P` | 3 | Y | U | V |
|
||||||
|
| `YUVJ420P` | 3 | Y | U | V |
|
||||||
|
| `YUV422P` | 3 | Y | U | V |
|
||||||
|
| `NV12` | 2 | Y | UV (交错) | - |
|
||||||
|
| `NV21` | 2 | Y | VU (交错) | - |
|
||||||
|
|
||||||
|
### 2.5 检测可用解码器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hwcodec::ffmpeg_ram::decode::Decoder;
|
||||||
|
|
||||||
|
let available_decoders = Decoder::available_decoders();
|
||||||
|
for decoder in available_decoders {
|
||||||
|
println!("Available: {} (format: {:?}, hwdevice: {:?})",
|
||||||
|
decoder.name, decoder.format, decoder.hwdevice);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 码率控制模式
|
||||||
|
|
||||||
|
### 3.1 RateControl 枚举
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum RateControl {
|
||||||
|
RC_DEFAULT, // 使用编码器默认
|
||||||
|
RC_CBR, // 恒定码率
|
||||||
|
RC_VBR, // 可变码率
|
||||||
|
RC_CQ, // 恒定质量 (需设置 q 参数)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 模式说明
|
||||||
|
|
||||||
|
| 模式 | 说明 | 适用场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `RC_CBR` | 码率恒定,质量随场景变化 | 网络带宽受限 |
|
||||||
|
| `RC_VBR` | 质量优先,码率波动 | 本地存储 |
|
||||||
|
| `RC_CQ` | 恒定质量,码率波动大 | 质量敏感场景 |
|
||||||
|
|
||||||
|
### 3.3 各编码器支持情况
|
||||||
|
|
||||||
|
| 编码器 | CBR | VBR | CQ |
|
||||||
|
|--------|-----|-----|-----|
|
||||||
|
| nvenc | ✓ | ✓ | ✓ |
|
||||||
|
| amf | ✓ | ✓ (低延迟) | ✗ |
|
||||||
|
| qsv | ✓ | ✓ | ✗ |
|
||||||
|
| vaapi | ✓ | ✓ | ✗ |
|
||||||
|
| mediacodec | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
## 4. 质量等级
|
||||||
|
|
||||||
|
### 4.1 Quality 枚举
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum Quality {
|
||||||
|
Quality_Default, // 使用编码器默认
|
||||||
|
Quality_High, // 高质量 (慢速)
|
||||||
|
Quality_Medium, // 中等质量 (平衡)
|
||||||
|
Quality_Low, // 低质量 (快速)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 编码器预设映射
|
||||||
|
|
||||||
|
| 质量 | nvenc | amf | qsv |
|
||||||
|
|------|-------|-----|-----|
|
||||||
|
| High | - | quality | veryslow |
|
||||||
|
| Medium | p4 | balanced | medium |
|
||||||
|
| Low | p1 | speed | veryfast |
|
||||||
|
|
||||||
|
## 5. 混流器 API
|
||||||
|
|
||||||
|
### 5.1 创建混流器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hwcodec::mux::{Muxer, MuxContext};
|
||||||
|
|
||||||
|
let ctx = MuxContext {
|
||||||
|
filename: "/tmp/output.mp4".to_string(),
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
is265: false, // H.264
|
||||||
|
framerate: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let muxer = Muxer::new(ctx)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 写入视频帧
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 编码后的帧数据
|
||||||
|
let encoded_data: Vec<u8> = encoder.encode(...)?;
|
||||||
|
let is_keyframe = true;
|
||||||
|
|
||||||
|
muxer.write_video(&encoded_data, is_keyframe)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 完成写入
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 写入文件尾
|
||||||
|
muxer.write_tail()?;
|
||||||
|
// muxer 被 drop 时自动释放资源
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 错误处理
|
||||||
|
|
||||||
|
### 6.1 错误码
|
||||||
|
|
||||||
|
| 错误码 | 常量 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 0 | `HWCODEC_SUCCESS` | 成功 |
|
||||||
|
| -1 | `HWCODEC_ERR_COMMON` | 通用错误 |
|
||||||
|
| -2 | `HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC` | HEVC 解码参考帧丢失 |
|
||||||
|
|
||||||
|
### 6.2 常见错误处理
|
||||||
|
|
||||||
|
```rust
|
||||||
|
match encoder.encode(&yuv_data, pts) {
|
||||||
|
Ok(frames) => {
|
||||||
|
// 处理编码帧
|
||||||
|
}
|
||||||
|
Err(-1) => {
|
||||||
|
eprintln!("编码失败,可能是输入数据格式错误");
|
||||||
|
}
|
||||||
|
Err(code) => {
|
||||||
|
eprintln!("未知错误: {}", code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 最佳实践
|
||||||
|
|
||||||
|
### 7.1 编码器选择策略
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn select_best_encoder(
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
format: DataFormat
|
||||||
|
) -> Option<String> {
|
||||||
|
let ctx = EncodeContext {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||||
|
// ... 其他参数
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoders = Encoder::available_encoders(ctx, None);
|
||||||
|
|
||||||
|
// 按优先级排序,选择最佳
|
||||||
|
encoders.into_iter()
|
||||||
|
.filter(|e| e.format == format)
|
||||||
|
.min_by_key(|e| e.priority)
|
||||||
|
.map(|e| e.name)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 帧内存布局
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 获取 NV12 帧布局信息
|
||||||
|
let (linesize, offset, length) = ffmpeg_linesize_offset_length(
|
||||||
|
AVPixelFormat::AV_PIX_FMT_NV12,
|
||||||
|
1920,
|
||||||
|
1080,
|
||||||
|
0, // align
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 分配缓冲区
|
||||||
|
let mut buffer = vec![0u8; length as usize];
|
||||||
|
|
||||||
|
// 填充 Y 平面: buffer[0..offset[0]]
|
||||||
|
// 填充 UV 平面: buffer[offset[0]..length]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 关键帧控制
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut frame_count = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// 每 30 帧强制一个关键帧
|
||||||
|
if frame_count % 30 == 0 {
|
||||||
|
encoder.request_keyframe();
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.encode(&yuv_data, pts)?;
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 线程安全
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Decoder 实现了 Send + Sync
|
||||||
|
unsafe impl Send for Decoder {}
|
||||||
|
unsafe impl Sync for Decoder {}
|
||||||
|
|
||||||
|
// 可以安全地在多线程间传递
|
||||||
|
let decoder = Arc::new(Mutex::new(Decoder::new(ctx)?));
|
||||||
|
```
|
||||||
615
docs/report/hwcodec/02-hardware-acceleration.md
Normal file
615
docs/report/hwcodec/02-hardware-acceleration.md
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
# hwcodec 硬件加速详解
|
||||||
|
|
||||||
|
## 1. 硬件加速架构
|
||||||
|
|
||||||
|
### 1.1 整体流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 应用层 (Rust) │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐│
|
||||||
|
│ │ Encoder::available_encoders() → 自动检测可用硬件编码器 ││
|
||||||
|
│ └─────────────────────────────────────────────────────────┘│
|
||||||
|
└────────────────────────────┬────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 硬件检测层 (C++) │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
|
||||||
|
│ │linux_ │ │linux_ │ │linux_ │ │linux_support_ ││
|
||||||
|
│ │support_nv│ │support_ │ │support_ │ │rkmpp/v4l2m2m ││
|
||||||
|
│ └────┬─────┘ │amd │ │intel │ └─────────┬────────┘│
|
||||||
|
│ │ └────┬─────┘ └────┬─────┘ │ │
|
||||||
|
└───────┼────────────┼────────────┼─────────────────┼─────────┘
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐
|
||||||
|
│ CUDA/ │ │ AMF │ │ VPL/MFX │ │ 设备节点检测 │
|
||||||
|
│ NVENC │ │ Runtime │ │ Library │ │ /dev/mpp_service │
|
||||||
|
│ 动态库 │ │ 动态库 │ │ 动态库 │ │ /dev/video* │
|
||||||
|
└───────────┘ └───────────┘ └───────────┘ └───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 编码器测试验证
|
||||||
|
|
||||||
|
每个检测到的硬件编码器都会进行实际编码测试:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// libs/hwcodec/src/ffmpeg_ram/encode.rs:358-450
|
||||||
|
|
||||||
|
// 生成测试用 YUV 数据
|
||||||
|
let yuv = Encoder::dummy_yuv(ctx.clone())?;
|
||||||
|
|
||||||
|
// 尝试创建编码器并编码测试帧
|
||||||
|
match Encoder::new(c) {
|
||||||
|
Ok(mut encoder) => {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
match encoder.encode(&yuv, 0) {
|
||||||
|
Ok(frames) => {
|
||||||
|
let elapsed = start.elapsed().as_millis();
|
||||||
|
// 验证: 必须产生 1 帧且为关键帧,且在 1 秒内完成
|
||||||
|
if frames.len() == 1 && frames[0].key == 1
|
||||||
|
&& elapsed < TEST_TIMEOUT_MS {
|
||||||
|
res.push(codec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => { /* 编码失败,跳过 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => { /* 创建失败,跳过 */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. NVIDIA NVENC/NVDEC
|
||||||
|
|
||||||
|
### 2.1 检测机制 (Linux)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/platform/linux/linux.cpp:57-73
|
||||||
|
|
||||||
|
int linux_support_nv() {
|
||||||
|
CudaFunctions *cuda_dl = NULL;
|
||||||
|
NvencFunctions *nvenc_dl = NULL;
|
||||||
|
CuvidFunctions *cvdl = NULL;
|
||||||
|
|
||||||
|
// 加载 CUDA 动态库
|
||||||
|
if (cuda_load_functions(&cuda_dl, NULL) < 0)
|
||||||
|
throw "cuda_load_functions failed";
|
||||||
|
|
||||||
|
// 加载 NVENC 动态库
|
||||||
|
if (nvenc_load_functions(&nvenc_dl, NULL) < 0)
|
||||||
|
throw "nvenc_load_functions failed";
|
||||||
|
|
||||||
|
// 加载 CUVID (解码) 动态库
|
||||||
|
if (cuvid_load_functions(&cvdl, NULL) < 0)
|
||||||
|
throw "cuvid_load_functions failed";
|
||||||
|
|
||||||
|
// 全部成功则支持 NVIDIA 硬件加速
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 编码配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/util.cpp
|
||||||
|
|
||||||
|
// NVENC 低延迟配置
|
||||||
|
if (name.find("nvenc") != std::string::npos) {
|
||||||
|
// 禁用编码延迟
|
||||||
|
av_opt_set(priv_data, "delay", "0", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU 选择
|
||||||
|
if (name.find("nvenc") != std::string::npos) {
|
||||||
|
av_opt_set_int(priv_data, "gpu", gpu_index, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 质量预设
|
||||||
|
switch (quality) {
|
||||||
|
case Quality_Medium:
|
||||||
|
av_opt_set(priv_data, "preset", "p4", 0);
|
||||||
|
break;
|
||||||
|
case Quality_Low:
|
||||||
|
av_opt_set(priv_data, "preset", "p1", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 码率控制
|
||||||
|
av_opt_set(priv_data, "rc", "cbr", 0); // 或 "vbr"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 环境变量
|
||||||
|
|
||||||
|
| 变量 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `RUSTDESK_HWCODEC_NVENC_GPU` | 指定使用的 GPU 索引 (-1 = 自动) |
|
||||||
|
|
||||||
|
### 2.4 依赖库
|
||||||
|
|
||||||
|
- `libcuda.so` - CUDA 运行时
|
||||||
|
- `libnvidia-encode.so` - NVENC 编码器
|
||||||
|
- `libnvcuvid.so` - NVDEC 解码器
|
||||||
|
|
||||||
|
## 3. AMD AMF
|
||||||
|
|
||||||
|
### 3.1 检测机制 (Linux)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/platform/linux/linux.cpp:75-91
|
||||||
|
|
||||||
|
int linux_support_amd() {
|
||||||
|
#if defined(__x86_64__) || defined(__aarch64__)
|
||||||
|
#define AMF_DLL_NAMEA "libamfrt64.so.1"
|
||||||
|
#else
|
||||||
|
#define AMF_DLL_NAMEA "libamfrt32.so.1"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void *handle = dlopen(AMF_DLL_NAMEA, RTLD_LAZY);
|
||||||
|
if (!handle) {
|
||||||
|
return -1; // AMF 不可用
|
||||||
|
}
|
||||||
|
dlclose(handle);
|
||||||
|
return 0; // AMF 可用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 编码配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/util.cpp
|
||||||
|
|
||||||
|
// AMF 低延迟配置
|
||||||
|
if (name.find("amf") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "query_timeout", "1000", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 质量预设
|
||||||
|
switch (quality) {
|
||||||
|
case Quality_High:
|
||||||
|
av_opt_set(priv_data, "quality", "quality", 0);
|
||||||
|
break;
|
||||||
|
case Quality_Medium:
|
||||||
|
av_opt_set(priv_data, "quality", "balanced", 0);
|
||||||
|
break;
|
||||||
|
case Quality_Low:
|
||||||
|
av_opt_set(priv_data, "quality", "speed", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 码率控制
|
||||||
|
av_opt_set(priv_data, "rc", "cbr", 0); // 恒定码率
|
||||||
|
av_opt_set(priv_data, "rc", "vbr_latency", 0); // 低延迟 VBR
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 依赖库
|
||||||
|
|
||||||
|
- `libamfrt64.so.1` (64位) 或 `libamfrt32.so.1` (32位)
|
||||||
|
|
||||||
|
### 3.4 外部 SDK
|
||||||
|
|
||||||
|
```
|
||||||
|
externals/AMF_v1.4.35/
|
||||||
|
├── amf/
|
||||||
|
│ ├── public/common/ # 公共代码
|
||||||
|
│ │ ├── AMFFactory.cpp
|
||||||
|
│ │ ├── Thread.cpp
|
||||||
|
│ │ └── TraceAdapter.cpp
|
||||||
|
│ └── public/include/ # 头文件
|
||||||
|
│ ├── components/ # 组件定义
|
||||||
|
│ └── core/ # 核心定义
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Intel QSV/MFX
|
||||||
|
|
||||||
|
### 4.1 检测机制 (Linux)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/platform/linux/linux.cpp:93-107
|
||||||
|
|
||||||
|
int linux_support_intel() {
|
||||||
|
const char *libs[] = {
|
||||||
|
"libvpl.so", // oneVPL (新版)
|
||||||
|
"libmfx.so", // Media SDK
|
||||||
|
"libmfx-gen.so.1.2", // 新驱动
|
||||||
|
"libmfxhw64.so.1" // 旧版驱动
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(libs) / sizeof(libs[0]); i++) {
|
||||||
|
void *handle = dlopen(libs[i], RTLD_LAZY);
|
||||||
|
if (handle) {
|
||||||
|
dlclose(handle);
|
||||||
|
return 0; // 找到可用库
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1; // Intel MFX 不可用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 编码配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/util.cpp
|
||||||
|
|
||||||
|
// QSV 低延迟配置
|
||||||
|
if (name.find("qsv") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "async_depth", "1", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// QSV 特殊码率配置
|
||||||
|
if (name.find("qsv") != std::string::npos) {
|
||||||
|
c->rc_max_rate = c->bit_rate;
|
||||||
|
c->bit_rate--; // 实现 CBR 效果
|
||||||
|
}
|
||||||
|
|
||||||
|
// 质量预设
|
||||||
|
switch (quality) {
|
||||||
|
case Quality_High:
|
||||||
|
av_opt_set(priv_data, "preset", "veryslow", 0);
|
||||||
|
break;
|
||||||
|
case Quality_Medium:
|
||||||
|
av_opt_set(priv_data, "preset", "medium", 0);
|
||||||
|
break;
|
||||||
|
case Quality_Low:
|
||||||
|
av_opt_set(priv_data, "preset", "veryfast", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 严格标准兼容性 (用于某些特殊设置)
|
||||||
|
c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 限制
|
||||||
|
|
||||||
|
- QSV 不支持 `YUV420P` 像素格式,必须使用 `NV12`
|
||||||
|
- 仅在 Windows 平台完全支持
|
||||||
|
|
||||||
|
### 4.4 外部 SDK
|
||||||
|
|
||||||
|
```
|
||||||
|
externals/MediaSDK_22.5.4/
|
||||||
|
├── api/
|
||||||
|
│ ├── include/ # MFX 头文件
|
||||||
|
│ ├── mfx_dispatch/ # MFX 调度器
|
||||||
|
│ └── mediasdk_structures/ # 数据结构
|
||||||
|
└── samples/sample_common/ # 示例代码
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. VAAPI (Linux)
|
||||||
|
|
||||||
|
### 5.1 工作原理
|
||||||
|
|
||||||
|
VAAPI (Video Acceleration API) 是 Linux 上的通用硬件视频加速接口:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ FFmpeg libavcodec │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ VAAPI (libva) │
|
||||||
|
├──────────────┬──────────────┬──────────────┬────────────────┤
|
||||||
|
│ Intel i965 │ Intel iHD │ AMD radeonsi │ NVIDIA VDPAU │
|
||||||
|
│ (Gen8-) │ (Gen9+) │ │ (via wrapper) │
|
||||||
|
├──────────────┴──────────────┴──────────────┴────────────────┤
|
||||||
|
│ Kernel DRM Driver │
|
||||||
|
├──────────────┬──────────────┬──────────────┬────────────────┤
|
||||||
|
│ i915 │ amdgpu │ nvidia │ ... │
|
||||||
|
└──────────────┴──────────────┴──────────────┴────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 编码配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/util.cpp
|
||||||
|
|
||||||
|
// VAAPI 低延迟配置
|
||||||
|
if (name.find("vaapi") != std::string::npos) {
|
||||||
|
av_opt_set(priv_data, "async_depth", "1", 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 硬件上下文初始化
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp
|
||||||
|
|
||||||
|
// 检测 VAAPI 编码器
|
||||||
|
if (name_.find("vaapi") != std::string::npos) {
|
||||||
|
hw_device_type_ = AV_HWDEVICE_TYPE_VAAPI;
|
||||||
|
hw_pixfmt_ = AV_PIX_FMT_VAAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建硬件设备上下文
|
||||||
|
ret = av_hwdevice_ctx_create(&hw_device_ctx_, hw_device_type_,
|
||||||
|
NULL, // 使用默认设备
|
||||||
|
NULL, 0);
|
||||||
|
|
||||||
|
// 设置硬件帧上下文
|
||||||
|
set_hwframe_ctx();
|
||||||
|
|
||||||
|
// 分配硬件帧
|
||||||
|
hw_frame_ = av_frame_alloc();
|
||||||
|
av_hwframe_get_buffer(c_->hw_frames_ctx, hw_frame_, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 编码流程
|
||||||
|
|
||||||
|
```
|
||||||
|
输入 YUV (CPU 内存)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
av_hwframe_transfer_data(hw_frame_, frame_, 0) // CPU → GPU
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
avcodec_send_frame(c_, hw_frame_) // 发送 GPU 帧
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
avcodec_receive_packet(c_, pkt_) // 获取编码数据
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
编码数据 (CPU 内存)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 依赖库
|
||||||
|
|
||||||
|
- `libva.so` - VAAPI 核心库
|
||||||
|
- `libva-drm.so` - DRM 后端
|
||||||
|
- `libva-x11.so` - X11 后端 (可选)
|
||||||
|
|
||||||
|
## 6. Rockchip MPP
|
||||||
|
|
||||||
|
### 6.1 检测机制
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/platform/linux/linux.cpp:122-137
|
||||||
|
|
||||||
|
int linux_support_rkmpp() {
|
||||||
|
// 检测 MPP 服务设备
|
||||||
|
if (access("/dev/mpp_service", F_OK) == 0) {
|
||||||
|
return 0; // MPP 可用
|
||||||
|
}
|
||||||
|
// 备用: 检测 RGA 设备
|
||||||
|
if (access("/dev/rga", F_OK) == 0) {
|
||||||
|
return 0; // MPP 可能可用
|
||||||
|
}
|
||||||
|
return -1; // MPP 不可用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 支持的编码器
|
||||||
|
|
||||||
|
| 编码器 | 优先级 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `h264_rkmpp` | Best (0) | H.264 硬件编码 |
|
||||||
|
| `hevc_rkmpp` | Best (0) | H.265 硬件编码 |
|
||||||
|
|
||||||
|
### 6.3 适用设备
|
||||||
|
|
||||||
|
- Rockchip RK3328 (Onecloud, Chainedbox)
|
||||||
|
- Rockchip RK3399/RK3588 系列
|
||||||
|
- 其他 Rockchip SoC
|
||||||
|
|
||||||
|
## 7. V4L2 M2M
|
||||||
|
|
||||||
|
### 7.1 检测机制
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/platform/linux/linux.cpp:139-163
|
||||||
|
|
||||||
|
int linux_support_v4l2m2m() {
|
||||||
|
const char *m2m_devices[] = {
|
||||||
|
"/dev/video10", // 常见 M2M 编码设备
|
||||||
|
"/dev/video11", // 常见 M2M 解码设备
|
||||||
|
"/dev/video0", // 某些 SoC 使用
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
|
||||||
|
if (access(m2m_devices[i], F_OK) == 0) {
|
||||||
|
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
|
||||||
|
if (fd >= 0) {
|
||||||
|
close(fd);
|
||||||
|
return 0; // V4L2 M2M 可用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 支持的编码器
|
||||||
|
|
||||||
|
| 编码器 | 优先级 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `h264_v4l2m2m` | Good (1) | H.264 V4L2 编码 |
|
||||||
|
| `hevc_v4l2m2m` | Good (1) | H.265 V4L2 编码 |
|
||||||
|
|
||||||
|
### 7.3 适用设备
|
||||||
|
|
||||||
|
- 通用 ARM SoC (Allwinner, Amlogic 等)
|
||||||
|
- 支持 V4L2 M2M API 的设备
|
||||||
|
|
||||||
|
## 8. Apple VideoToolbox
|
||||||
|
|
||||||
|
### 8.1 检测机制 (macOS)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// libs/hwcodec/src/common.rs:57-87
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) fn get_video_toolbox_codec_support() -> (bool, bool, bool, bool) {
|
||||||
|
extern "C" {
|
||||||
|
fn checkVideoToolboxSupport(
|
||||||
|
h264_encode: *mut i32,
|
||||||
|
h265_encode: *mut i32,
|
||||||
|
h264_decode: *mut i32,
|
||||||
|
h265_decode: *mut i32,
|
||||||
|
) -> c_void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut h264_encode = 0;
|
||||||
|
let mut h265_encode = 0;
|
||||||
|
let mut h264_decode = 0;
|
||||||
|
let mut h265_decode = 0;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
checkVideoToolboxSupport(&mut h264_encode, &mut h265_encode,
|
||||||
|
&mut h264_decode, &mut h265_decode);
|
||||||
|
}
|
||||||
|
|
||||||
|
(h264_encode == 1, h265_encode == 1,
|
||||||
|
h264_decode == 1, h265_decode == 1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 编码配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// libs/hwcodec/cpp/common/util.cpp
|
||||||
|
|
||||||
|
// VideoToolbox 低延迟配置
|
||||||
|
if (name.find("videotoolbox") != std::string::npos) {
|
||||||
|
av_opt_set_int(priv_data, "realtime", 1, 0);
|
||||||
|
av_opt_set_int(priv_data, "prio_speed", 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制硬件编码
|
||||||
|
if (name.find("videotoolbox") != std::string::npos) {
|
||||||
|
av_opt_set_int(priv_data, "allow_sw", 0, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 限制
|
||||||
|
|
||||||
|
- H.264 编码不稳定,已禁用
|
||||||
|
- 仅支持 H.265 编码
|
||||||
|
- 完全支持 H.264/H.265 解码
|
||||||
|
|
||||||
|
### 8.4 依赖框架
|
||||||
|
|
||||||
|
```
|
||||||
|
CoreFoundation
|
||||||
|
CoreVideo
|
||||||
|
CoreMedia
|
||||||
|
VideoToolbox
|
||||||
|
AVFoundation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 硬件加速优先级
|
||||||
|
|
||||||
|
### 9.1 优先级定义
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum Priority {
|
||||||
|
Best = 0, // 专用硬件编码器
|
||||||
|
Good = 1, // 通用硬件加速
|
||||||
|
Normal = 2, // 基本硬件支持
|
||||||
|
Soft = 3, // 软件编码
|
||||||
|
Bad = 4, // 最低优先级
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 各编码器优先级
|
||||||
|
|
||||||
|
| 优先级 | 编码器 |
|
||||||
|
|--------|--------|
|
||||||
|
| Best (0) | nvenc, amf, qsv, rkmpp |
|
||||||
|
| Good (1) | vaapi, v4l2m2m |
|
||||||
|
| Soft (3) | x264, x265, libvpx |
|
||||||
|
|
||||||
|
### 9.3 选择策略
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// libs/hwcodec/src/ffmpeg_ram/mod.rs:49-117
|
||||||
|
|
||||||
|
pub fn prioritized(coders: Vec<CodecInfo>) -> CodecInfos {
|
||||||
|
// 对于每种格式,选择优先级最高的编码器
|
||||||
|
for coder in coders {
|
||||||
|
match coder.format {
|
||||||
|
DataFormat::H264 => {
|
||||||
|
if h264.is_none() || h264.priority > coder.priority {
|
||||||
|
h264 = Some(coder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ... 其他格式类似
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 故障排除
|
||||||
|
|
||||||
|
### 10.1 NVIDIA
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 NVIDIA 驱动
|
||||||
|
nvidia-smi
|
||||||
|
|
||||||
|
# 检查 NVENC 支持
|
||||||
|
ls /dev/nvidia*
|
||||||
|
|
||||||
|
# 检查 CUDA 库
|
||||||
|
ldconfig -p | grep cuda
|
||||||
|
ldconfig -p | grep nvidia-encode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 AMD
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 AMD 驱动
|
||||||
|
lspci | grep AMD
|
||||||
|
|
||||||
|
# 检查 AMF 库
|
||||||
|
ldconfig -p | grep amf
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 Intel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Intel 驱动
|
||||||
|
vainfo
|
||||||
|
|
||||||
|
# 检查 MFX 库
|
||||||
|
ldconfig -p | grep mfx
|
||||||
|
ldconfig -p | grep vpl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.4 VAAPI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 vainfo
|
||||||
|
sudo apt install vainfo
|
||||||
|
|
||||||
|
# 检查 VAAPI 支持
|
||||||
|
vainfo
|
||||||
|
|
||||||
|
# 输出示例:
|
||||||
|
# libva info: VA-API version 1.14.0
|
||||||
|
# libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
|
||||||
|
# vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics
|
||||||
|
# vainfo: Supported profile and entrypoints
|
||||||
|
# VAProfileH264Main : VAEntrypointVLD
|
||||||
|
# VAProfileH264Main : VAEntrypointEncSlice
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.5 Rockchip MPP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 MPP 设备
|
||||||
|
ls -la /dev/mpp_service
|
||||||
|
ls -la /dev/rga
|
||||||
|
|
||||||
|
# 检查 MPP 库
|
||||||
|
ldconfig -p | grep rockchip_mpp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.6 V4L2 M2M
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 列出 V4L2 设备
|
||||||
|
v4l2-ctl --list-devices
|
||||||
|
|
||||||
|
# 检查设备能力
|
||||||
|
v4l2-ctl -d /dev/video10 --all
|
||||||
|
```
|
||||||
539
docs/report/hwcodec/03-build-integration.md
Normal file
539
docs/report/hwcodec/03-build-integration.md
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
# hwcodec 构建系统与集成指南
|
||||||
|
|
||||||
|
## 1. 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
libs/hwcodec/
|
||||||
|
├── Cargo.toml # 包配置
|
||||||
|
├── Cargo.lock # 依赖锁定
|
||||||
|
├── build.rs # 构建脚本
|
||||||
|
├── src/ # Rust 源码
|
||||||
|
│ ├── lib.rs # 库入口
|
||||||
|
│ ├── common.rs # 公共定义
|
||||||
|
│ ├── ffmpeg.rs # FFmpeg 集成
|
||||||
|
│ ├── mux.rs # 混流器
|
||||||
|
│ ├── android.rs # Android 支持
|
||||||
|
│ ├── ffmpeg_ram/ # RAM 编解码
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── encode.rs
|
||||||
|
│ │ └── decode.rs
|
||||||
|
│ ├── vram/ # GPU 编解码 (Windows)
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── encode.rs
|
||||||
|
│ │ ├── decode.rs
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── res/ # 测试资源
|
||||||
|
│ ├── 720p.h264
|
||||||
|
│ └── 720p.h265
|
||||||
|
├── cpp/ # C++ 源码
|
||||||
|
│ ├── common/ # 公共代码
|
||||||
|
│ ├── ffmpeg_ram/ # FFmpeg RAM 实现
|
||||||
|
│ ├── ffmpeg_vram/ # FFmpeg VRAM 实现
|
||||||
|
│ ├── nv/ # NVIDIA 实现
|
||||||
|
│ ├── amf/ # AMD 实现
|
||||||
|
│ ├── mfx/ # Intel 实现
|
||||||
|
│ ├── mux/ # 混流实现
|
||||||
|
│ └── yuv/ # YUV 处理
|
||||||
|
├── externals/ # 外部 SDK (Git 子模块)
|
||||||
|
│ ├── nv-codec-headers_n12.1.14.0/
|
||||||
|
│ ├── Video_Codec_SDK_12.1.14/
|
||||||
|
│ ├── AMF_v1.4.35/
|
||||||
|
│ └── MediaSDK_22.5.4/
|
||||||
|
├── dev/ # 开发工具
|
||||||
|
│ ├── capture/ # 捕获工具
|
||||||
|
│ ├── render/ # 渲染工具
|
||||||
|
│ └── tool/ # 通用工具
|
||||||
|
└── examples/ # 示例程序
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Cargo 配置
|
||||||
|
|
||||||
|
### 2.1 Cargo.toml
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "hwcodec"
|
||||||
|
version = "0.7.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
vram = [] # GPU VRAM 直接编解码 (仅 Windows)
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4" # 日志
|
||||||
|
serde_derive = "1.0" # 序列化派生宏
|
||||||
|
serde = "1.0" # 序列化
|
||||||
|
serde_json = "1.0" # JSON 序列化
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1.0" # C++ 编译
|
||||||
|
bindgen = "0.59" # FFI 绑定生成
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.10" # 日志输出
|
||||||
|
rand = "0.8" # 随机数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Feature 说明
|
||||||
|
|
||||||
|
| Feature | 说明 | 平台 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `default` | 基础功能 | 全平台 |
|
||||||
|
| `vram` | GPU VRAM 直接编解码 | 仅 Windows |
|
||||||
|
|
||||||
|
### 2.3 使用方式
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# 基础使用
|
||||||
|
[dependencies]
|
||||||
|
hwcodec = { path = "libs/hwcodec" }
|
||||||
|
|
||||||
|
# 启用 VRAM 功能 (Windows)
|
||||||
|
[dependencies]
|
||||||
|
hwcodec = { path = "libs/hwcodec", features = ["vram"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 构建脚本详解 (build.rs)
|
||||||
|
|
||||||
|
### 3.1 主入口
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let mut builder = Build::new();
|
||||||
|
|
||||||
|
// 1. 构建公共模块
|
||||||
|
build_common(&mut builder);
|
||||||
|
|
||||||
|
// 2. 构建 FFmpeg 相关模块
|
||||||
|
ffmpeg::build_ffmpeg(&mut builder);
|
||||||
|
|
||||||
|
// 3. 构建 SDK 模块 (Windows + vram feature)
|
||||||
|
#[cfg(all(windows, feature = "vram"))]
|
||||||
|
sdk::build_sdk(&mut builder);
|
||||||
|
|
||||||
|
// 4. 编译生成静态库
|
||||||
|
builder.static_crt(true).compile("hwcodec");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 公共模块构建
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn build_common(builder: &mut Build) {
|
||||||
|
let common_dir = manifest_dir.join("cpp").join("common");
|
||||||
|
|
||||||
|
// 生成 FFI 绑定
|
||||||
|
bindgen::builder()
|
||||||
|
.header(common_dir.join("common.h"))
|
||||||
|
.header(common_dir.join("callback.h"))
|
||||||
|
.rustified_enum("*")
|
||||||
|
.generate()
|
||||||
|
.write_to_file(OUT_DIR.join("common_ffi.rs"));
|
||||||
|
|
||||||
|
// 平台相关代码
|
||||||
|
#[cfg(windows)]
|
||||||
|
builder.file(common_dir.join("platform/win/win.cpp"));
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
builder.file(common_dir.join("platform/linux/linux.cpp"));
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
builder.file(common_dir.join("platform/mac/mac.mm"));
|
||||||
|
|
||||||
|
// 工具代码
|
||||||
|
builder.files([
|
||||||
|
common_dir.join("log.cpp"),
|
||||||
|
common_dir.join("util.cpp"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 FFmpeg 模块构建
|
||||||
|
|
||||||
|
```rust
|
||||||
|
mod ffmpeg {
|
||||||
|
pub fn build_ffmpeg(builder: &mut Build) {
|
||||||
|
// 生成 FFmpeg FFI 绑定
|
||||||
|
ffmpeg_ffi();
|
||||||
|
|
||||||
|
// 链接 FFmpeg 库
|
||||||
|
if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") {
|
||||||
|
link_vcpkg(builder, vcpkg_root.into());
|
||||||
|
} else {
|
||||||
|
link_system_ffmpeg(builder); // pkg-config
|
||||||
|
}
|
||||||
|
|
||||||
|
// 链接系统库
|
||||||
|
link_os();
|
||||||
|
|
||||||
|
// 构建子模块
|
||||||
|
build_ffmpeg_ram(builder);
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
build_ffmpeg_vram(builder);
|
||||||
|
build_mux(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 FFmpeg 链接方式
|
||||||
|
|
||||||
|
#### VCPKG (跨平台静态链接)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn link_vcpkg(builder: &mut Build, path: PathBuf) -> PathBuf {
|
||||||
|
// 目标平台识别
|
||||||
|
let target = match (target_os, target_arch) {
|
||||||
|
("windows", "x86_64") => "x64-windows-static",
|
||||||
|
("macos", "x86_64") => "x64-osx",
|
||||||
|
("macos", "aarch64") => "arm64-osx",
|
||||||
|
("linux", arch) => format!("{}-linux", arch),
|
||||||
|
_ => panic!("unsupported platform"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let lib_path = path.join("installed").join(target).join("lib");
|
||||||
|
|
||||||
|
// 链接 FFmpeg 静态库
|
||||||
|
println!("cargo:rustc-link-search=native={}", lib_path);
|
||||||
|
["avcodec", "avutil", "avformat"].iter()
|
||||||
|
.for_each(|lib| println!("cargo:rustc-link-lib=static={}", lib));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pkg-config (Linux 动态链接)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn link_system_ffmpeg(builder: &mut Build) {
|
||||||
|
let libs = ["libavcodec", "libavutil", "libavformat", "libswscale"];
|
||||||
|
|
||||||
|
for lib in &libs {
|
||||||
|
// 获取编译标志
|
||||||
|
let cflags = Command::new("pkg-config")
|
||||||
|
.args(["--cflags", lib])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
// 获取链接标志
|
||||||
|
let libs = Command::new("pkg-config")
|
||||||
|
.args(["--libs", lib])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
// 解析并应用
|
||||||
|
for flag in libs.split_whitespace() {
|
||||||
|
if flag.starts_with("-L") {
|
||||||
|
println!("cargo:rustc-link-search=native={}", &flag[2..]);
|
||||||
|
} else if flag.starts_with("-l") {
|
||||||
|
println!("cargo:rustc-link-lib={}", &flag[2..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 系统库链接
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn link_os() {
|
||||||
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
|
|
||||||
|
let libs: Vec<&str> = match target_os.as_str() {
|
||||||
|
"windows" => vec!["User32", "bcrypt", "ole32", "advapi32"],
|
||||||
|
"linux" => vec!["drm", "X11", "stdc++", "z"],
|
||||||
|
"macos" | "ios" => vec!["c++", "m"],
|
||||||
|
"android" => vec!["z", "m", "android", "atomic", "mediandk"],
|
||||||
|
_ => panic!("unsupported os"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for lib in libs {
|
||||||
|
println!("cargo:rustc-link-lib={}", lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS 框架
|
||||||
|
if target_os == "macos" || target_os == "ios" {
|
||||||
|
for framework in ["CoreFoundation", "CoreVideo", "CoreMedia",
|
||||||
|
"VideoToolbox", "AVFoundation"] {
|
||||||
|
println!("cargo:rustc-link-lib=framework={}", framework);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 SDK 模块构建 (Windows)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(all(windows, feature = "vram"))]
|
||||||
|
mod sdk {
|
||||||
|
pub fn build_sdk(builder: &mut Build) {
|
||||||
|
build_amf(builder); // AMD AMF
|
||||||
|
build_nv(builder); // NVIDIA
|
||||||
|
build_mfx(builder); // Intel MFX
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_nv(builder: &mut Build) {
|
||||||
|
let sdk_path = externals_dir.join("Video_Codec_SDK_12.1.14");
|
||||||
|
|
||||||
|
// 包含 SDK 头文件
|
||||||
|
builder.includes([
|
||||||
|
sdk_path.join("Interface"),
|
||||||
|
sdk_path.join("Samples/Utils"),
|
||||||
|
sdk_path.join("Samples/NvCodec"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 编译 SDK 源文件
|
||||||
|
builder.file(sdk_path.join("Samples/NvCodec/NvEncoder/NvEncoder.cpp"));
|
||||||
|
builder.file(sdk_path.join("Samples/NvCodec/NvEncoder/NvEncoderD3D11.cpp"));
|
||||||
|
builder.file(sdk_path.join("Samples/NvCodec/NvDecoder/NvDecoder.cpp"));
|
||||||
|
|
||||||
|
// 编译封装代码
|
||||||
|
builder.files([
|
||||||
|
nv_dir.join("nv_encode.cpp"),
|
||||||
|
nv_dir.join("nv_decode.cpp"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. FFI 绑定生成
|
||||||
|
|
||||||
|
### 4.1 bindgen 配置
|
||||||
|
|
||||||
|
```rust
|
||||||
|
bindgen::builder()
|
||||||
|
.header("path/to/header.h")
|
||||||
|
.rustified_enum("*") // 生成 Rust 枚举
|
||||||
|
.parse_callbacks(Box::new(Callbacks)) // 自定义回调
|
||||||
|
.generate()
|
||||||
|
.write_to_file(OUT_DIR.join("ffi.rs"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 自定义派生
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CommonCallbacks;
|
||||||
|
|
||||||
|
impl bindgen::callbacks::ParseCallbacks for CommonCallbacks {
|
||||||
|
fn add_derives(&self, name: &str) -> Vec<String> {
|
||||||
|
// 为特定类型添加序列化支持
|
||||||
|
match name {
|
||||||
|
"DataFormat" | "SurfaceFormat" | "API" => {
|
||||||
|
vec!["Serialize".to_string(), "Deserialize".to_string()]
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 生成的文件
|
||||||
|
|
||||||
|
| 文件 | 来源 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| `common_ffi.rs` | `common.h`, `callback.h` | 枚举、常量、回调类型 |
|
||||||
|
| `ffmpeg_ffi.rs` | `ffmpeg_ffi.h` | FFmpeg 日志级别、函数 |
|
||||||
|
| `ffmpeg_ram_ffi.rs` | `ffmpeg_ram_ffi.h` | 编解码器函数 |
|
||||||
|
| `mux_ffi.rs` | `mux_ffi.h` | 混流器函数 |
|
||||||
|
|
||||||
|
## 5. 外部依赖管理
|
||||||
|
|
||||||
|
### 5.1 Git 子模块
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 初始化子模块
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# 更新子模块
|
||||||
|
git submodule update --remote externals
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 子模块配置 (.gitmodules)
|
||||||
|
|
||||||
|
```
|
||||||
|
[submodule "externals"]
|
||||||
|
path = libs/hwcodec/externals
|
||||||
|
url = https://github.com/rustdesk-org/externals.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 依赖版本
|
||||||
|
|
||||||
|
| 依赖 | 版本 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| nv-codec-headers | n12.1.14.0 | NVIDIA FFmpeg 编码头 |
|
||||||
|
| Video_Codec_SDK | 12.1.14 | NVIDIA 编解码 SDK |
|
||||||
|
| AMF | v1.4.35 | AMD Advanced Media Framework |
|
||||||
|
| MediaSDK | 22.5.4 | Intel Media SDK |
|
||||||
|
|
||||||
|
## 6. 平台构建指南
|
||||||
|
|
||||||
|
### 6.1 Linux 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 FFmpeg 开发库
|
||||||
|
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
|
||||||
|
|
||||||
|
# 安装其他依赖
|
||||||
|
sudo apt install libdrm-dev libx11-dev pkg-config
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
cargo build --release -p hwcodec
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Windows 构建 (VCPKG)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 安装 VCPKG
|
||||||
|
git clone https://github.com/microsoft/vcpkg
|
||||||
|
cd vcpkg
|
||||||
|
./bootstrap-vcpkg.bat
|
||||||
|
|
||||||
|
# 安装 FFmpeg
|
||||||
|
./vcpkg install ffmpeg:x64-windows-static
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
$env:VCPKG_ROOT = "C:\path\to\vcpkg"
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
cargo build --release -p hwcodec --features vram
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 macOS 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 FFmpeg (Homebrew)
|
||||||
|
brew install ffmpeg pkg-config
|
||||||
|
|
||||||
|
# 或使用 VCPKG
|
||||||
|
export VCPKG_ROOT=/path/to/vcpkg
|
||||||
|
vcpkg install ffmpeg:arm64-osx # Apple Silicon
|
||||||
|
vcpkg install ffmpeg:x64-osx # Intel
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
cargo build --release -p hwcodec
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 交叉编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 cross
|
||||||
|
cargo install cross --git https://github.com/cross-rs/cross
|
||||||
|
|
||||||
|
# ARM64 Linux
|
||||||
|
cross build --release -p hwcodec --target aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
# ARMv7 Linux
|
||||||
|
cross build --release -p hwcodec --target armv7-unknown-linux-gnueabihf
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 集成到 One-KVM
|
||||||
|
|
||||||
|
### 7.1 依赖配置
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
|
[dependencies]
|
||||||
|
hwcodec = { path = "libs/hwcodec" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 使用示例
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||||
|
use hwcodec::ffmpeg_ram::decode::{Decoder, DecodeContext};
|
||||||
|
use hwcodec::ffmpeg::AVPixelFormat;
|
||||||
|
|
||||||
|
// 检测可用编码器
|
||||||
|
let encoders = Encoder::available_encoders(ctx, None);
|
||||||
|
|
||||||
|
// 创建编码器
|
||||||
|
let encoder = Encoder::new(EncodeContext {
|
||||||
|
name: "h264_vaapi".to_string(),
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||||
|
fps: 30,
|
||||||
|
gop: 30,
|
||||||
|
kbs: 4000,
|
||||||
|
// ...
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 编码
|
||||||
|
let frames = encoder.encode(&yuv_data, pts_ms)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 日志集成
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// hwcodec 使用 log crate,与 One-KVM 日志系统兼容
|
||||||
|
use log::{debug, info, warn, error};
|
||||||
|
|
||||||
|
// C++ 层日志通过回调传递
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn hwcodec_log(level: i32, message: *const c_char) {
|
||||||
|
match level {
|
||||||
|
0 => error!("{}", message),
|
||||||
|
1 => warn!("{}", message),
|
||||||
|
2 => info!("{}", message),
|
||||||
|
3 => debug!("{}", message),
|
||||||
|
4 => trace!("{}", message),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 故障排除
|
||||||
|
|
||||||
|
### 8.1 编译错误
|
||||||
|
|
||||||
|
**FFmpeg 未找到**:
|
||||||
|
```
|
||||||
|
error: pkg-config failed for libavcodec
|
||||||
|
```
|
||||||
|
解决: 安装 FFmpeg 开发库
|
||||||
|
```bash
|
||||||
|
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**bindgen 错误**:
|
||||||
|
```
|
||||||
|
error: failed to run custom build command for `hwcodec`
|
||||||
|
```
|
||||||
|
解决: 安装 clang
|
||||||
|
```bash
|
||||||
|
sudo apt install clang libclang-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 链接错误
|
||||||
|
|
||||||
|
**符号未定义**:
|
||||||
|
```
|
||||||
|
undefined reference to `av_log_set_level'
|
||||||
|
```
|
||||||
|
解决: 检查 FFmpeg 库链接顺序,确保 pkg-config 正确配置
|
||||||
|
|
||||||
|
**动态库未找到**:
|
||||||
|
```
|
||||||
|
error while loading shared libraries: libavcodec.so.59
|
||||||
|
```
|
||||||
|
解决:
|
||||||
|
```bash
|
||||||
|
sudo ldconfig
|
||||||
|
# 或设置 LD_LIBRARY_PATH
|
||||||
|
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 运行时错误
|
||||||
|
|
||||||
|
**硬件编码器不可用**:
|
||||||
|
```
|
||||||
|
Encoder h264_vaapi test failed
|
||||||
|
```
|
||||||
|
检查:
|
||||||
|
1. 驱动是否正确安装: `vainfo`
|
||||||
|
2. 权限是否足够: `ls -la /dev/dri/`
|
||||||
|
3. 用户是否在 video 组: `groups`
|
||||||
|
|
||||||
|
**解码失败**:
|
||||||
|
```
|
||||||
|
avcodec_receive_frame failed, ret = -11
|
||||||
|
```
|
||||||
|
解决: 这通常表示需要更多输入数据 (EAGAIN),是正常行为
|
||||||
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. 客户端支持中继连接
|
||||||
876
docs/system-architecture.md
Normal file
876
docs/system-architecture.md
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
# One-KVM 系统架构文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
One-KVM 是一个用 Rust 编写的轻量级、开源 IP-KVM 解决方案。它提供 BIOS 级别的远程服务器管理能力,支持视频流、键鼠控制、虚拟存储、电源管理和音频等功能。
|
||||||
|
|
||||||
|
### 1.1 核心特性
|
||||||
|
|
||||||
|
- **单一二进制部署**:Web UI + 后端一体化,无需额外配置文件
|
||||||
|
- **双流模式**:支持 WebRTC(H264/H265/VP8/VP9)和 MJPEG 两种流模式
|
||||||
|
- **USB OTG**:虚拟键鼠、虚拟存储、虚拟网卡
|
||||||
|
- **ATX 电源控制**:GPIO/USB 继电器
|
||||||
|
- **RustDesk 协议集成**:支持跨平台访问
|
||||||
|
- **Vue3 SPA 前端**:支持中文/英文
|
||||||
|
- **SQLite 配置存储**:无需配置文件
|
||||||
|
|
||||||
|
### 1.2 目标平台
|
||||||
|
|
||||||
|
| 平台 | 架构 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| aarch64-unknown-linux-gnu | ARM64 | 主要目标(Rockchip RK3328 等) |
|
||||||
|
| armv7-unknown-linux-gnueabihf | ARMv7 | 备选平台 |
|
||||||
|
| x86_64-unknown-linux-gnu | x86-64 | 开发/测试环境 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 系统架构图
|
||||||
|
|
||||||
|
### 2.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ One-KVM System │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Web Frontend (Vue3) │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||||
|
│ │ │ Console │ │ Settings │ │ Login │ │ Setup │ │ Virtual │ │ │
|
||||||
|
│ │ │ View │ │ View │ │ View │ │ View │ │ Keyboard │ │ │
|
||||||
|
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌─────────────────┴─────────────────┐ │ │
|
||||||
|
│ │ │ Pinia State Store │ │ │
|
||||||
|
│ │ └─────────────────┬─────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ API Client Layer │ │ │
|
||||||
|
│ │ │ HTTP REST │ WebSocket │ WebRTC Signaling │ MJPEG │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ │ HTTP/WS/WebRTC │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Axum Web Server (routes.rs) │ │
|
||||||
|
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||||
|
│ │ │ Public │ │ User │ │ Admin │ │ Static │ │ │
|
||||||
|
│ │ │ Routes │ │ Routes │ │ Routes │ │ Files │ │ │
|
||||||
|
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ AppState (state.rs) │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Central State Hub │ │ │
|
||||||
|
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
|
||||||
|
│ │ │ │ConfigStore │ │SessionStore│ │ UserStore │ │ │ │
|
||||||
|
│ │ │ │ (SQLite) │ │ (Memory) │ │ (SQLite) │ │ │ │
|
||||||
|
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
|
||||||
|
│ │ │ │ EventBus │ │ OtgService │ │ Extensions │ │ │ │
|
||||||
|
│ │ │ │ (Broadcast)│ │ (USB) │ │ Manager │ │ │ │
|
||||||
|
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────────┼─────────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||||
|
│ │ Video │ │ HID │ │ Audio │ │
|
||||||
|
│ │ Module │ │ Module │ │ Module │ │
|
||||||
|
│ ├────────────┤ ├────────────┤ ├────────────┤ │
|
||||||
|
│ │ Capture │ │ Controller │ │ Capture │ │
|
||||||
|
│ │ Encoder │ │ OTG Backend│ │ Encoder │ │
|
||||||
|
│ │ Streamer │ │ CH9329 │ │ Pipeline │ │
|
||||||
|
│ │ Pipeline │ │ DataChannel│ │ (Opus) │ │
|
||||||
|
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └───────────────────────────┼──────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────────┼─────────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||||
|
│ │ MSD │ │ ATX │ │ RustDesk │ │
|
||||||
|
│ │ Module │ │ Module │ │ Module │ │
|
||||||
|
│ ├────────────┤ ├────────────┤ ├────────────┤ │
|
||||||
|
│ │ Controller │ │ Controller │ │ Service │ │
|
||||||
|
│ │ Image Mgr │ │ Executor │ │ Rendezvous │ │
|
||||||
|
│ │ Ventoy │ │ LED Monitor│ │ Connection │ │
|
||||||
|
│ │ Drive │ │ WOL │ │ Protocol │ │
|
||||||
|
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Hardware Layer │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||||
|
│ │ V4L2 Video │ │ USB OTG │ │ GPIO │ │ ALSA │ │
|
||||||
|
│ │ Device │ │ Gadget │ │ Sysfs │ │ Audio │ │
|
||||||
|
│ │/dev/video* │ │ ConfigFS │ │ │ │ │ │
|
||||||
|
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 数据流架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Flow Overview │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Target PC │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────┼────────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||||
|
│ HDMI Capture │ │ USB Port │ │ GPIO/Relay │
|
||||||
|
│ Card │ │ (OTG Mode) │ │ (ATX) │
|
||||||
|
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||||
|
│ /dev/video0 │ │ /dev/hidg* │ │ /sys/class/ │
|
||||||
|
│ (V4L2) │ │ (USB Gadget) │ │ gpio/gpio* │
|
||||||
|
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ One-KVM Application │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Video │ │ HID │ │ ATX │ │
|
||||||
|
│ │ Pipeline │ │ Controller │ │ Controller │ │
|
||||||
|
│ └─────┬───────┘ └─────┬───────┘ └─────┬───────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Event Bus │ │
|
||||||
|
│ │ (tokio broadcast channel) │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Web Server (Axum) │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||||
|
│ │ │ MJPEG │ │ WebRTC │ │WebSocket │ │ │
|
||||||
|
│ │ │ Stream │ │ Stream │ │ Events │ │ │
|
||||||
|
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Client Browser │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Video │ │ Input │ │ Control │ │
|
||||||
|
│ │ Display │ │ Events │ │ Panel │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 模块依赖关系
|
||||||
|
|
||||||
|
### 3.1 模块层次图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application Layer │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ main.rs ──► state.rs ──► web/routes.rs │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────┼───────────┬───────────┬───────────┬───────────┐ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||||
|
│ │video/│ │ hid/ │ │ msd/ │ │ atx/ │ │audio/│ │webrtc│ │
|
||||||
|
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ └──────────┼──────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ ┌─────▼─────┐ │ │ │
|
||||||
|
│ │ │ otg/ │ │ │ │
|
||||||
|
│ │ │ (OtgSvc) │ │ │ │
|
||||||
|
│ │ └───────────┘ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────────────────────────────────┼──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Infrastructure Layer │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ config/ │ │ auth/ │ │ events/ │ │extensions│ │
|
||||||
|
│ │(ConfigSt)│ │(Session) │ │(EventBus)│ │(ExtMgr) │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ rustdesk/ (RustDeskService) │ │
|
||||||
|
│ │ connection.rs │ rendezvous.rs │ crypto.rs │ protocol.rs │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 依赖矩阵
|
||||||
|
|
||||||
|
| 模块 | 依赖的模块 |
|
||||||
|
|------|-----------|
|
||||||
|
| `main.rs` | state, config, auth, video, hid, msd, atx, audio, webrtc, web, rustdesk, events |
|
||||||
|
| `state.rs` | config, auth, video, hid, msd, atx, audio, webrtc, rustdesk, events, otg |
|
||||||
|
| `video/` | events, hwcodec (外部) |
|
||||||
|
| `hid/` | otg, events |
|
||||||
|
| `msd/` | otg, events |
|
||||||
|
| `atx/` | events |
|
||||||
|
| `audio/` | events |
|
||||||
|
| `webrtc/` | video, audio, hid, events |
|
||||||
|
| `web/` | state, auth, config, video, hid, msd, atx, audio, webrtc, events |
|
||||||
|
| `rustdesk/` | video, audio, hid, events |
|
||||||
|
| `otg/` | (无内部依赖) |
|
||||||
|
| `config/` | (无内部依赖) |
|
||||||
|
| `auth/` | config |
|
||||||
|
| `events/` | (无内部依赖) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 核心组件详解
|
||||||
|
|
||||||
|
### 4.1 AppState (state.rs)
|
||||||
|
|
||||||
|
AppState 是整个应用的状态中枢,通过 `Arc` 包装的方式在所有 handler 之间共享。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AppState {
|
||||||
|
// 配置和存储
|
||||||
|
config: ConfigStore, // SQLite 配置存储
|
||||||
|
sessions: SessionStore, // 内存会话存储
|
||||||
|
users: UserStore, // SQLite 用户存储
|
||||||
|
|
||||||
|
// 核心服务
|
||||||
|
otg_service: Arc<OtgService>, // USB Gadget 统一管理
|
||||||
|
stream_manager: Arc<VideoStreamManager>, // 视频流管理器
|
||||||
|
hid: Arc<HidController>, // HID 控制器
|
||||||
|
msd: Arc<RwLock<Option<MsdController>>>, // MSD 控制器(可选)
|
||||||
|
atx: Arc<RwLock<Option<AtxController>>>, // ATX 控制器(可选)
|
||||||
|
audio: Arc<AudioController>, // 音频控制器
|
||||||
|
rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>, // RustDesk(可选)
|
||||||
|
extensions: Arc<ExtensionManager>,// 扩展管理器
|
||||||
|
|
||||||
|
// 通信和生命周期
|
||||||
|
events: Arc<EventBus>, // 事件总线
|
||||||
|
shutdown_tx: broadcast::Sender<()>, // 关闭信号
|
||||||
|
data_dir: PathBuf, // 数据目录
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 视频流管道
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Video Pipeline Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌───────────────────┐
|
||||||
|
│ V4L2 Device │
|
||||||
|
│ /dev/video0 │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│ Raw MJPEG/YUYV/NV12
|
||||||
|
▼
|
||||||
|
┌───────────────────┐
|
||||||
|
│ VideoCapturer │ ◄─── src/video/capture.rs
|
||||||
|
│ (capture.rs) │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│ VideoFrame
|
||||||
|
▼
|
||||||
|
┌───────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SharedVideoPipeline │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Decode Stage │ │
|
||||||
|
│ │ ┌─────────────┐ │ │
|
||||||
|
│ │ │ MJPEG → YUV │ turbojpeg / VAAPI │ │
|
||||||
|
│ │ └─────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Convert Stage │ │
|
||||||
|
│ │ ┌─────────────┐ │ │
|
||||||
|
│ │ │YUV → Target │ libyuv (SIMD accelerated) │ │
|
||||||
|
│ │ │ Format │ │ │
|
||||||
|
│ │ └─────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Encode Stage │ │
|
||||||
|
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||||
|
│ │ │ H264 │ │ H265 │ │ VP8 │ │ VP9 │ │ │
|
||||||
|
│ │ │Encoder │ │Encoder │ │Encoder │ │Encoder │ │ │
|
||||||
|
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||||
|
│ │ │ (VAAPI/RKMPP/V4L2 M2M/Software) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
├──────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────────┐ ┌───────────────────┐
|
||||||
|
│ MJPEG Streamer │ │ WebRTC Streamer │
|
||||||
|
│ (HTTP Stream) │ │ (RTP Packets) │
|
||||||
|
└───────────────────┘ └───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 OTG 服务架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ OTG Service Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ OtgService (service.rs) │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Public Interface │ │
|
||||||
|
│ │ enable_hid() │ disable_hid() │ enable_msd() │ disable_msd() │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ OtgGadgetManager (manager.rs) │ │
|
||||||
|
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Gadget Lifecycle │ │ │
|
||||||
|
│ │ │ create_gadget() │ destroy_gadget() │ bind_udc() │ unbind() │ │ │
|
||||||
|
│ │ └───────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────────────┼────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ HID Function │ │ MSD Function │ │ Endpoint Alloc │ │
|
||||||
|
│ │ (hid.rs) │ │ (msd.rs) │ │ (endpoint.rs) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ConfigFS Operations │ │
|
||||||
|
│ │ /sys/kernel/config/usb_gadget/one-kvm/ │ │
|
||||||
|
│ │ ├── idVendor, idProduct, strings/ │ │
|
||||||
|
│ │ ├── configs/c.1/ │ │
|
||||||
|
│ │ │ └── functions/ (symlinks) │ │
|
||||||
|
│ │ └── functions/ │ │
|
||||||
|
│ │ ├── hid.usb0, hid.usb1, hid.usb2 │ │
|
||||||
|
│ │ └── mass_storage.usb0 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Linux Kernel │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ /dev/hidg* │ │ Mass Storage │ │
|
||||||
|
│ │ (HID devices) │ │ Backend │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 事件系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Event System Architecture │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Event Producers │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Video │ │ HID │ │ MSD │ │ ATX │ │ Audio │ │
|
||||||
|
│ │ Module │ │ Module │ │ Module │ │ Module │ │ Module │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ └────────────┴────────────┼────────────┴────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ EventBus │ │
|
||||||
|
│ │ (tokio broadcast channel) │ │
|
||||||
|
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ SystemEvent Enum │ │ │
|
||||||
|
│ │ │ StreamStateChanged │ HidStateChanged │ MsdStateChanged │ │ │
|
||||||
|
│ │ │ AtxStateChanged │ AudioStateChanged │ DeviceInfo │ Error │ │ │
|
||||||
|
│ │ └───────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────┼─────────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │WebSocket │ │ DeviceInfo│ │ Internal │ │
|
||||||
|
│ │ Clients │ │Broadcaster│ │ Tasks │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 初始化流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application Startup Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
main()
|
||||||
|
│
|
||||||
|
├──► Parse CLI Arguments (clap)
|
||||||
|
│ - address, port, data_dir
|
||||||
|
│ - enable_https, ssl_cert, ssl_key
|
||||||
|
│ - verbosity (-v, -vv, -vvv)
|
||||||
|
│
|
||||||
|
├──► Initialize Logging (tracing)
|
||||||
|
│
|
||||||
|
├──► Create/Open SQLite Database
|
||||||
|
│ └─► ConfigStore::new()
|
||||||
|
│ └─► UserStore::new()
|
||||||
|
│ └─► SessionStore::new()
|
||||||
|
│
|
||||||
|
├──► Initialize Core Services
|
||||||
|
│ │
|
||||||
|
│ ├──► EventBus::new()
|
||||||
|
│ │
|
||||||
|
│ ├──► OtgService::new()
|
||||||
|
│ │ └─► Detect UDC device
|
||||||
|
│ │
|
||||||
|
│ ├──► HidController::init()
|
||||||
|
│ │ └─► Select backend (OTG/CH9329/None)
|
||||||
|
│ │ └─► Request HID function from OtgService
|
||||||
|
│ │
|
||||||
|
│ ├──► MsdController::init() (if configured)
|
||||||
|
│ │ └─► Request MSD function from OtgService
|
||||||
|
│ │ └─► Initialize Ventoy drive (if available)
|
||||||
|
│ │
|
||||||
|
│ ├──► AtxController::init() (if configured)
|
||||||
|
│ │ └─► Setup GPIO pins
|
||||||
|
│ │
|
||||||
|
│ ├──► AudioController::init()
|
||||||
|
│ │ └─► Open ALSA device
|
||||||
|
│ │ └─► Initialize Opus encoder
|
||||||
|
│ │
|
||||||
|
│ ├──► VideoStreamManager::new()
|
||||||
|
│ │ └─► Initialize SharedVideoPipeline
|
||||||
|
│ │ └─► Setup encoder registry
|
||||||
|
│ │
|
||||||
|
│ └──► RustDeskService::new() (if configured)
|
||||||
|
│ └─► Load/generate device ID and keys
|
||||||
|
│ └─► Connect to rendezvous server
|
||||||
|
│
|
||||||
|
├──► Create AppState
|
||||||
|
│ └─► Wrap all services in Arc<>
|
||||||
|
│
|
||||||
|
├──► Spawn Background Tasks
|
||||||
|
│ ├──► spawn_device_info_broadcaster()
|
||||||
|
│ ├──► extension_health_check_task()
|
||||||
|
│ └──► rustdesk_reconnect_task()
|
||||||
|
│
|
||||||
|
├──► Create Axum Router
|
||||||
|
│ └─► create_router(app_state)
|
||||||
|
│
|
||||||
|
└──► Start HTTP/HTTPS Server
|
||||||
|
└─► axum::serve() or axum_server with TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
One-KVM-RUST/
|
||||||
|
├── src/ # Rust 源代码
|
||||||
|
│ ├── main.rs # 应用入口点
|
||||||
|
│ ├── lib.rs # 库导出
|
||||||
|
│ ├── state.rs # AppState 定义
|
||||||
|
│ ├── error.rs # 错误类型定义
|
||||||
|
│ │
|
||||||
|
│ ├── video/ # 视频模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── capture.rs # V4L2 采集
|
||||||
|
│ │ ├── streamer.rs # 视频流服务
|
||||||
|
│ │ ├── stream_manager.rs # 流管理器
|
||||||
|
│ │ ├── shared_video_pipeline.rs # 共享视频管道
|
||||||
|
│ │ ├── format.rs # 像素格式
|
||||||
|
│ │ ├── frame.rs # 视频帧
|
||||||
|
│ │ ├── convert.rs # 格式转换
|
||||||
|
│ │ └── encoder/ # 编码器
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── traits.rs
|
||||||
|
│ │ ├── h264.rs
|
||||||
|
│ │ ├── h265.rs
|
||||||
|
│ │ ├── vp8.rs
|
||||||
|
│ │ ├── vp9.rs
|
||||||
|
│ │ └── jpeg.rs
|
||||||
|
│ │
|
||||||
|
│ ├── hid/ # HID 模块
|
||||||
|
│ │ ├── mod.rs # HidController
|
||||||
|
│ │ ├── backend.rs # 后端抽象
|
||||||
|
│ │ ├── otg.rs # OTG 后端
|
||||||
|
│ │ ├── ch9329.rs # CH9329 串口后端
|
||||||
|
│ │ ├── keymap.rs # 按键映射
|
||||||
|
│ │ ├── types.rs # 类型定义
|
||||||
|
│ │ ├── monitor.rs # 健康监视
|
||||||
|
│ │ ├── datachannel.rs # DataChannel 适配
|
||||||
|
│ │ └── websocket.rs # WebSocket 适配
|
||||||
|
│ │
|
||||||
|
│ ├── otg/ # USB OTG 模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── service.rs # OtgService
|
||||||
|
│ │ ├── manager.rs # GadgetManager
|
||||||
|
│ │ ├── hid.rs # HID Function
|
||||||
|
│ │ ├── msd.rs # MSD Function
|
||||||
|
│ │ ├── configfs.rs # ConfigFS 操作
|
||||||
|
│ │ ├── endpoint.rs # 端点分配
|
||||||
|
│ │ └── report_desc.rs # HID 报告描述符
|
||||||
|
│ │
|
||||||
|
│ ├── msd/ # MSD 模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── controller.rs # MsdController
|
||||||
|
│ │ ├── image.rs # 镜像管理
|
||||||
|
│ │ ├── ventoy_drive.rs # Ventoy 驱动
|
||||||
|
│ │ ├── monitor.rs # 健康监视
|
||||||
|
│ │ └── types.rs # 类型定义
|
||||||
|
│ │
|
||||||
|
│ ├── atx/ # ATX 模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── controller.rs # AtxController
|
||||||
|
│ │ ├── executor.rs # 动作执行器
|
||||||
|
│ │ ├── types.rs # 类型定义
|
||||||
|
│ │ ├── led.rs # LED 监视
|
||||||
|
│ │ └── wol.rs # Wake-on-LAN
|
||||||
|
│ │
|
||||||
|
│ ├── audio/ # 音频模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── controller.rs # AudioController
|
||||||
|
│ │ ├── capture.rs # ALSA 采集
|
||||||
|
│ │ ├── encoder.rs # Opus 编码
|
||||||
|
│ │ ├── shared_pipeline.rs # 共享管道
|
||||||
|
│ │ ├── monitor.rs # 健康监视
|
||||||
|
│ │ └── device.rs # 设备枚举
|
||||||
|
│ │
|
||||||
|
│ ├── webrtc/ # WebRTC 模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── webrtc_streamer.rs # WebRTC 管理器
|
||||||
|
│ │ ├── universal_session.rs # 会话管理
|
||||||
|
│ │ ├── video_track.rs # 视频轨道
|
||||||
|
│ │ ├── rtp.rs # RTP 打包
|
||||||
|
│ │ ├── h265_payloader.rs # H265 RTP
|
||||||
|
│ │ ├── peer.rs # PeerConnection
|
||||||
|
│ │ ├── config.rs # 配置
|
||||||
|
│ │ ├── signaling.rs # 信令
|
||||||
|
│ │ └── track.rs # 轨道基类
|
||||||
|
│ │
|
||||||
|
│ ├── auth/ # 认证模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── user.rs # 用户管理
|
||||||
|
│ │ ├── session.rs # 会话管理
|
||||||
|
│ │ ├── password.rs # 密码哈希
|
||||||
|
│ │ └── middleware.rs # Axum 中间件
|
||||||
|
│ │
|
||||||
|
│ ├── config/ # 配置模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── schema.rs # 配置结构定义
|
||||||
|
│ │ └── store.rs # SQLite 存储
|
||||||
|
│ │
|
||||||
|
│ ├── events/ # 事件模块
|
||||||
|
│ │ └── mod.rs # EventBus
|
||||||
|
│ │
|
||||||
|
│ ├── rustdesk/ # RustDesk 模块
|
||||||
|
│ │ ├── mod.rs # RustDeskService
|
||||||
|
│ │ ├── connection.rs # 连接管理
|
||||||
|
│ │ ├── rendezvous.rs # 渲染服务器通信
|
||||||
|
│ │ ├── crypto.rs # NaCl 加密
|
||||||
|
│ │ ├── config.rs # 配置
|
||||||
|
│ │ ├── hid_adapter.rs # HID 适配
|
||||||
|
│ │ ├── frame_adapters.rs # 帧格式转换
|
||||||
|
│ │ ├── protocol.rs # 协议包装
|
||||||
|
│ │ └── bytes_codec.rs # 帧编码
|
||||||
|
│ │
|
||||||
|
│ ├── extensions/ # 扩展模块
|
||||||
|
│ │ └── mod.rs # ExtensionManager
|
||||||
|
│ │
|
||||||
|
│ ├── web/ # Web 模块
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── routes.rs # 路由定义
|
||||||
|
│ │ ├── ws.rs # WebSocket
|
||||||
|
│ │ ├── audio_ws.rs # 音频 WebSocket
|
||||||
|
│ │ ├── static_files.rs # 静态文件
|
||||||
|
│ │ └── handlers/ # API 处理器
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ └── config/
|
||||||
|
│ │
|
||||||
|
│ ├── stream/ # MJPEG 流
|
||||||
|
│ │ └── mod.rs
|
||||||
|
│ │
|
||||||
|
│ └── utils/ # 工具函数
|
||||||
|
│ └── mod.rs
|
||||||
|
│
|
||||||
|
├── web/ # Vue3 前端
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── views/ # 页面组件
|
||||||
|
│ │ ├── components/ # UI 组件
|
||||||
|
│ │ ├── api/ # API 客户端
|
||||||
|
│ │ ├── stores/ # Pinia 状态
|
||||||
|
│ │ ├── router/ # 路由配置
|
||||||
|
│ │ ├── i18n/ # 国际化
|
||||||
|
│ │ └── types/ # TypeScript 类型
|
||||||
|
│ └── package.json
|
||||||
|
│
|
||||||
|
├── libs/ # 外部库
|
||||||
|
│ ├── hwcodec/ # 硬件视频编码
|
||||||
|
│ └── ventoy-img-rs/ # Ventoy 支持
|
||||||
|
│
|
||||||
|
├── protos/ # Protobuf 定义
|
||||||
|
│ ├── message.proto # RustDesk 消息
|
||||||
|
│ └── rendezvous.proto # RustDesk 渲染
|
||||||
|
│
|
||||||
|
├── docs/ # 文档
|
||||||
|
├── scripts/ # 脚本
|
||||||
|
├── Cargo.toml # Rust 配置
|
||||||
|
├── build.rs # 构建脚本
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 安全架构
|
||||||
|
|
||||||
|
### 7.1 认证流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Authentication Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||||
|
│ Client │ │ Axum │ │ Auth │ │ SQLite │
|
||||||
|
│ Browser │ │ Server │ │ Module │ │ Database │
|
||||||
|
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||||
|
│ │ │ │
|
||||||
|
│ POST /auth/login │ │ │
|
||||||
|
│ {username, pass} │ │ │
|
||||||
|
│───────────────────►│ │ │
|
||||||
|
│ │ verify_user() │ │
|
||||||
|
│ │───────────────────►│ │
|
||||||
|
│ │ │ SELECT user │
|
||||||
|
│ │ │───────────────────►│
|
||||||
|
│ │ │◄───────────────────│
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ Argon2 verify │
|
||||||
|
│ │ │ ────────────► │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ session_token │ │
|
||||||
|
│ │◄───────────────────│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ Set-Cookie: │ │ │
|
||||||
|
│ session_id=token │ │ │
|
||||||
|
│◄───────────────────│ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ GET /api/... │ │ │
|
||||||
|
│ Cookie: session │ │ │
|
||||||
|
│───────────────────►│ │ │
|
||||||
|
│ │ validate_session()│ │
|
||||||
|
│ │───────────────────►│ │
|
||||||
|
│ │ user_info │ │
|
||||||
|
│ │◄───────────────────│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ Response │ │ │
|
||||||
|
│◄───────────────────│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 权限层级
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Permission Levels │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Public (No Auth) │
|
||||||
|
│ ├── GET /health │
|
||||||
|
│ ├── POST /auth/login │
|
||||||
|
│ ├── GET /setup │
|
||||||
|
│ └── POST /setup/init │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ User (Authenticated) │
|
||||||
|
│ ├── GET /info (系统信息) │
|
||||||
|
│ ├── GET /devices (设备列表) │
|
||||||
|
│ ├── GET/POST /stream/* (流控制) │
|
||||||
|
│ ├── POST /webrtc/* (WebRTC 信令) │
|
||||||
|
│ ├── POST /hid/* (HID 控制) │
|
||||||
|
│ ├── POST /audio/* (音频控制) │
|
||||||
|
│ └── WebSocket endpoints (实时通信) │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Admin (Admin Role) │
|
||||||
|
│ ├── GET/PATCH /config/* (配置管理) │
|
||||||
|
│ ├── POST /msd/* (MSD 操作) │
|
||||||
|
│ ├── POST /atx/* (电源控制) │
|
||||||
|
│ ├── POST /extensions/* (扩展管理) │
|
||||||
|
│ ├── POST /rustdesk/* (RustDesk 配置) │
|
||||||
|
│ └── POST /users/* (用户管理) │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 部署架构
|
||||||
|
|
||||||
|
### 8.1 单机部署
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Single Binary Deployment │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ARM64 Device (e.g., Rockchip RK3328) │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ one-kvm (single binary, ~15MB) │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Embedded Assets (rust-embed, gzip compressed) │ │ │
|
||||||
|
│ │ │ - index.html, app.js, app.css, assets/* │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Runtime Data (data_dir) │ │ │
|
||||||
|
│ │ │ - one-kvm.db (SQLite) │ │ │
|
||||||
|
│ │ │ - images/ (MSD images) │ │ │
|
||||||
|
│ │ │ - certs/ (SSL certificates) │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Hardware Connections: │
|
||||||
|
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ HDMI Input │ │ USB OTG Port │ │ GPIO Header │ │
|
||||||
|
│ │ (/dev/video0) │ │ (USB Gadget) │ │ (ATX Control) │ │
|
||||||
|
│ └───────────────────┘ └───────────────────┘ └───────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ USB Cable
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Target PC │
|
||||||
|
│ - Receives USB HID events (keyboard/mouse) │
|
||||||
|
│ - Provides HDMI video output │
|
||||||
|
│ - Can boot from virtual USB drive │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 网络拓扑
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Network Topology │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐
|
||||||
|
│ RustDesk │ │ Client │
|
||||||
|
│ Server │ │ Browser │
|
||||||
|
│ (hbbs/hbbr) │ │ │
|
||||||
|
└───────────────┘ └───────────────┘
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└───────────┬───────────┘
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ Router │
|
||||||
|
│ NAT │
|
||||||
|
└────┬────┘
|
||||||
|
│
|
||||||
|
Local Network
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐
|
||||||
|
│ One-KVM │───────│ Target PC │
|
||||||
|
│ Device │ USB │ │
|
||||||
|
│ :8080/:8443 │ HID │ │
|
||||||
|
└───────────────┘ └───────────────┘
|
||||||
|
|
||||||
|
Access Methods:
|
||||||
|
1. Local: http://one-kvm.local:8080
|
||||||
|
2. HTTPS: https://one-kvm.local:8443
|
||||||
|
3. RustDesk: Via RustDesk client with device ID
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 扩展点
|
||||||
|
|
||||||
|
### 9.1 添加新编码器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 1. 实现 Encoder trait
|
||||||
|
impl Encoder for MyEncoder {
|
||||||
|
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>>;
|
||||||
|
fn codec(&self) -> Codec;
|
||||||
|
fn bitrate(&self) -> u32;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 在 registry 中注册
|
||||||
|
encoder_registry.register("my-encoder", || Box::new(MyEncoder::new()));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 添加新 HID 后端
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 1. 实现 HidBackend trait
|
||||||
|
impl HidBackend for MyBackend {
|
||||||
|
async fn send_keyboard(&self, event: &KeyboardEvent) -> Result<()>;
|
||||||
|
async fn send_mouse(&self, event: &MouseEvent) -> Result<()>;
|
||||||
|
fn info(&self) -> HidBackendInfo;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 在 HidController::init() 中添加分支
|
||||||
|
match config.backend {
|
||||||
|
HidBackendType::MyBackend => MyBackend::new(config),
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 添加新扩展
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 通过 ExtensionManager 管理外部进程
|
||||||
|
extension_manager.register("my-extension", ExtensionConfig {
|
||||||
|
command: "my-binary",
|
||||||
|
args: vec!["--port", "9000"],
|
||||||
|
health_check: HealthCheckConfig::Http { url: "http://localhost:9000/health" },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 参考资料
|
||||||
|
|
||||||
|
- [Axum Web Framework](https://github.com/tokio-rs/axum)
|
||||||
|
- [webrtc-rs](https://github.com/webrtc-rs/webrtc)
|
||||||
|
- [V4L2 Documentation](https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/v4l2.html)
|
||||||
|
- [Linux USB Gadget](https://www.kernel.org/doc/html/latest/usb/gadget_configfs.html)
|
||||||
|
- [RustDesk Protocol](https://github.com/rustdesk/rustdesk)
|
||||||
1007
docs/tech-stack.md
Normal file
1007
docs/tech-stack.md
Normal file
File diff suppressed because it is too large
Load Diff
875
protos/message.proto
Normal file
875
protos/message.proto
Normal file
@@ -0,0 +1,875 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package hbb;
|
||||||
|
|
||||||
|
message EncodedVideoFrame {
|
||||||
|
bytes data = 1;
|
||||||
|
bool key = 2;
|
||||||
|
int64 pts = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
|
||||||
|
|
||||||
|
message RGB { bool compress = 1; }
|
||||||
|
|
||||||
|
// planes data send directly in binary for better use arraybuffer on web
|
||||||
|
message YUV {
|
||||||
|
bool compress = 1;
|
||||||
|
int32 stride = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Chroma {
|
||||||
|
I420 = 0;
|
||||||
|
I444 = 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 IdPk {
|
||||||
|
string id = 1;
|
||||||
|
bytes pk = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PortForward {
|
||||||
|
string host = 1;
|
||||||
|
int32 port = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransfer {
|
||||||
|
string dir = 1;
|
||||||
|
bool show_hidden = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ViewCamera {}
|
||||||
|
|
||||||
|
message OSLogin {
|
||||||
|
string username = 1;
|
||||||
|
string password = 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 Auth2FA {
|
||||||
|
string code = 1;
|
||||||
|
bytes hwid = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatMessage { string text = 1; }
|
||||||
|
|
||||||
|
message Features {
|
||||||
|
bool privacy_mode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CodecAbility {
|
||||||
|
bool vp8 = 1;
|
||||||
|
bool vp9 = 2;
|
||||||
|
bool av1 = 3;
|
||||||
|
bool h264 = 4;
|
||||||
|
bool h265 = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SupportedEncoding {
|
||||||
|
bool h264 = 1;
|
||||||
|
bool h265 = 2;
|
||||||
|
bool vp8 = 3;
|
||||||
|
bool av1 = 4;
|
||||||
|
CodecAbility i444 = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Use JSON's key-value format which is friendly for peer to handle.
|
||||||
|
// NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string.
|
||||||
|
string platform_additions = 12;
|
||||||
|
WindowsSessions windows_sessions = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WindowsSession {
|
||||||
|
uint32 sid = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
oneof union {
|
||||||
|
string error = 1;
|
||||||
|
PeerInfo peer_info = 2;
|
||||||
|
}
|
||||||
|
bool enable_trusted_devices = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TouchScaleUpdate {
|
||||||
|
// The delta scale factor relative to the previous scale.
|
||||||
|
// delta * 1000
|
||||||
|
// 0 means scale end
|
||||||
|
int32 scale = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TouchPanStart {
|
||||||
|
int32 x = 1;
|
||||||
|
int32 y = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TouchPanUpdate {
|
||||||
|
// The delta x position relative to the previous position.
|
||||||
|
int32 x = 1;
|
||||||
|
// The delta y position relative to the previous position.
|
||||||
|
int32 y = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TouchPanEnd {
|
||||||
|
int32 x = 1;
|
||||||
|
int32 y = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TouchEvent {
|
||||||
|
oneof union {
|
||||||
|
TouchScaleUpdate scale_update = 1;
|
||||||
|
TouchPanStart pan_start = 2;
|
||||||
|
TouchPanUpdate pan_update = 3;
|
||||||
|
TouchPanEnd pan_end = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message PointerDeviceEvent {
|
||||||
|
oneof union {
|
||||||
|
TouchEvent touch_event = 1;
|
||||||
|
}
|
||||||
|
repeated ControlKey modifiers = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MouseEvent {
|
||||||
|
int32 mask = 1;
|
||||||
|
sint32 x = 2;
|
||||||
|
sint32 y = 3;
|
||||||
|
repeated ControlKey modifiers = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyboardMode{
|
||||||
|
Legacy = 0;
|
||||||
|
Map = 1;
|
||||||
|
Translate = 2;
|
||||||
|
Auto = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ControlKey {
|
||||||
|
Unknown = 0;
|
||||||
|
Alt = 1;
|
||||||
|
Backspace = 2;
|
||||||
|
CapsLock = 3;
|
||||||
|
Control = 4;
|
||||||
|
Delete = 5;
|
||||||
|
DownArrow = 6;
|
||||||
|
End = 7;
|
||||||
|
Escape = 8;
|
||||||
|
F1 = 9;
|
||||||
|
F10 = 10;
|
||||||
|
F11 = 11;
|
||||||
|
F12 = 12;
|
||||||
|
F2 = 13;
|
||||||
|
F3 = 14;
|
||||||
|
F4 = 15;
|
||||||
|
F5 = 16;
|
||||||
|
F6 = 17;
|
||||||
|
F7 = 18;
|
||||||
|
F8 = 19;
|
||||||
|
F9 = 20;
|
||||||
|
Home = 21;
|
||||||
|
LeftArrow = 22;
|
||||||
|
/// meta key (also known as "windows"; "super"; and "command")
|
||||||
|
Meta = 23;
|
||||||
|
/// option key on macOS (alt key on Linux and Windows)
|
||||||
|
Option = 24; // deprecated, use Alt instead
|
||||||
|
PageDown = 25;
|
||||||
|
PageUp = 26;
|
||||||
|
Return = 27;
|
||||||
|
RightArrow = 28;
|
||||||
|
Shift = 29;
|
||||||
|
Space = 30;
|
||||||
|
Tab = 31;
|
||||||
|
UpArrow = 32;
|
||||||
|
Numpad0 = 33;
|
||||||
|
Numpad1 = 34;
|
||||||
|
Numpad2 = 35;
|
||||||
|
Numpad3 = 36;
|
||||||
|
Numpad4 = 37;
|
||||||
|
Numpad5 = 38;
|
||||||
|
Numpad6 = 39;
|
||||||
|
Numpad7 = 40;
|
||||||
|
Numpad8 = 41;
|
||||||
|
Numpad9 = 42;
|
||||||
|
Cancel = 43;
|
||||||
|
Clear = 44;
|
||||||
|
Menu = 45; // deprecated, use Alt instead
|
||||||
|
Pause = 46;
|
||||||
|
Kana = 47;
|
||||||
|
Hangul = 48;
|
||||||
|
Junja = 49;
|
||||||
|
Final = 50;
|
||||||
|
Hanja = 51;
|
||||||
|
Kanji = 52;
|
||||||
|
Convert = 53;
|
||||||
|
Select = 54;
|
||||||
|
Print = 55;
|
||||||
|
Execute = 56;
|
||||||
|
Snapshot = 57;
|
||||||
|
Insert = 58;
|
||||||
|
Help = 59;
|
||||||
|
Sleep = 60;
|
||||||
|
Separator = 61;
|
||||||
|
Scroll = 62;
|
||||||
|
NumLock = 63;
|
||||||
|
RWin = 64;
|
||||||
|
Apps = 65;
|
||||||
|
Multiply = 66;
|
||||||
|
Add = 67;
|
||||||
|
Subtract = 68;
|
||||||
|
Decimal = 69;
|
||||||
|
Divide = 70;
|
||||||
|
Equals = 71;
|
||||||
|
NumpadEnter = 72;
|
||||||
|
RShift = 73;
|
||||||
|
RControl = 74;
|
||||||
|
RAlt = 75;
|
||||||
|
VolumeMute = 76; // mainly used on mobile devices as controlled side
|
||||||
|
VolumeUp = 77;
|
||||||
|
VolumeDown = 78;
|
||||||
|
Power = 79; // mainly used on mobile devices as controlled side
|
||||||
|
CtrlAltDel = 100;
|
||||||
|
LockScreen = 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyEvent {
|
||||||
|
// `down` indicates the key's state(down or up).
|
||||||
|
bool down = 1;
|
||||||
|
// `press` indicates a click event(down and up).
|
||||||
|
bool press = 2;
|
||||||
|
oneof union {
|
||||||
|
ControlKey control_key = 3;
|
||||||
|
// position key code. win: scancode, linux: key code, macos: key code
|
||||||
|
uint32 chr = 4;
|
||||||
|
uint32 unicode = 5;
|
||||||
|
string seq = 6;
|
||||||
|
// high word. virtual keycode
|
||||||
|
// low word. unicode
|
||||||
|
uint32 win2win_hotkey = 7;
|
||||||
|
}
|
||||||
|
repeated ControlKey modifiers = 8;
|
||||||
|
KeyboardMode mode = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CursorData {
|
||||||
|
uint64 id = 1;
|
||||||
|
sint32 hotx = 2;
|
||||||
|
sint32 hoty = 3;
|
||||||
|
int32 width = 4;
|
||||||
|
int32 height = 5;
|
||||||
|
bytes colors = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CursorPosition {
|
||||||
|
sint32 x = 1;
|
||||||
|
sint32 y = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Hash {
|
||||||
|
string salt = 1;
|
||||||
|
string challenge = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Special format name, only used when format is Special.
|
||||||
|
string special_name = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MultiClipboards { repeated Clipboard clipboards = 1; }
|
||||||
|
|
||||||
|
enum FileType {
|
||||||
|
Dir = 0;
|
||||||
|
DirLink = 2;
|
||||||
|
DirDrive = 3;
|
||||||
|
File = 4;
|
||||||
|
FileLink = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileEntry {
|
||||||
|
FileType entry_type = 1;
|
||||||
|
string name = 2;
|
||||||
|
bool is_hidden = 3;
|
||||||
|
uint64 size = 4;
|
||||||
|
uint64 modified_time = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileDirectory {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
repeated FileEntry entries = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReadDir {
|
||||||
|
string path = 1;
|
||||||
|
bool include_hidden = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReadEmptyDirs {
|
||||||
|
string path = 1;
|
||||||
|
bool include_hidden = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReadEmptyDirsResponse {
|
||||||
|
string path = 1;
|
||||||
|
repeated FileDirectory empty_dirs = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReadAllFiles {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
bool include_hidden = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileRename {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
string new_name = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 FileTransferCancel { int32 id = 1; }
|
||||||
|
|
||||||
|
message FileResponse {
|
||||||
|
oneof union {
|
||||||
|
FileDirectory dir = 1;
|
||||||
|
FileTransferBlock block = 2;
|
||||||
|
FileTransferError error = 3;
|
||||||
|
FileTransferDone done = 4;
|
||||||
|
FileTransferDigest digest = 5;
|
||||||
|
ReadEmptyDirsResponse empty_dirs = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferDigest {
|
||||||
|
int32 id = 1;
|
||||||
|
sint32 file_num = 2;
|
||||||
|
uint64 last_modified = 3;
|
||||||
|
uint64 file_size = 4;
|
||||||
|
bool is_upload = 5;
|
||||||
|
bool is_identical = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferBlock {
|
||||||
|
int32 id = 1;
|
||||||
|
sint32 file_num = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
bool compressed = 4;
|
||||||
|
uint32 blk_id = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferError {
|
||||||
|
int32 id = 1;
|
||||||
|
string error = 2;
|
||||||
|
sint32 file_num = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferSendRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
bool include_hidden = 3;
|
||||||
|
int32 file_num = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferSendConfirmRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
sint32 file_num = 2;
|
||||||
|
oneof union {
|
||||||
|
bool skip = 3;
|
||||||
|
uint32 offset_blk = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferDone {
|
||||||
|
int32 id = 1;
|
||||||
|
sint32 file_num = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileTransferReceiveRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2; // path written to
|
||||||
|
repeated FileEntry files = 3;
|
||||||
|
int32 file_num = 4;
|
||||||
|
uint64 total_size = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileRemoveDir {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
bool recursive = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileRemoveFile {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
sint32 file_num = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FileDirCreate {
|
||||||
|
int32 id = 1;
|
||||||
|
string path = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// main logic from freeRDP
|
||||||
|
message CliprdrMonitorReady {
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrFormat {
|
||||||
|
int32 id = 2;
|
||||||
|
string format = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrServerFormatList {
|
||||||
|
repeated CliprdrFormat formats = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrServerFormatListResponse {
|
||||||
|
int32 msg_flags = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrServerFormatDataRequest {
|
||||||
|
int32 requested_format_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrServerFormatDataResponse {
|
||||||
|
int32 msg_flags = 2;
|
||||||
|
bytes format_data = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrFileContentsRequest {
|
||||||
|
int32 stream_id = 2;
|
||||||
|
int32 list_index = 3;
|
||||||
|
int32 dw_flags = 4;
|
||||||
|
int32 n_position_low = 5;
|
||||||
|
int32 n_position_high = 6;
|
||||||
|
int32 cb_requested = 7;
|
||||||
|
bool have_clip_data_id = 8;
|
||||||
|
int32 clip_data_id = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CliprdrFileContentsResponse {
|
||||||
|
int32 msg_flags = 3;
|
||||||
|
int32 stream_id = 4;
|
||||||
|
bytes requested_data = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try empty clipboard in the following case(Windows only):
|
||||||
|
// 1. `A`(Windows) -> `B`, `C`
|
||||||
|
// 2. Copy in `A, file clipboards on `B` and `C` are updated.
|
||||||
|
// 3. Copy in `B`.
|
||||||
|
// `A` should tell `C` to empty the file clipboard.
|
||||||
|
message CliprdrTryEmpty {
|
||||||
|
}
|
||||||
|
|
||||||
|
message Cliprdr {
|
||||||
|
oneof union {
|
||||||
|
CliprdrMonitorReady ready = 1;
|
||||||
|
CliprdrServerFormatList format_list = 2;
|
||||||
|
CliprdrServerFormatListResponse format_list_response = 3;
|
||||||
|
CliprdrServerFormatDataRequest format_data_request = 4;
|
||||||
|
CliprdrServerFormatDataResponse format_data_response = 5;
|
||||||
|
CliprdrFileContentsRequest file_contents_request = 6;
|
||||||
|
CliprdrFileContentsResponse file_contents_response = 7;
|
||||||
|
CliprdrTryEmpty try_empty = 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Resolution {
|
||||||
|
int32 width = 1;
|
||||||
|
int32 height = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DisplayResolution {
|
||||||
|
int32 display = 1;
|
||||||
|
Resolution resolution = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SupportedResolutions { repeated Resolution resolutions = 1; }
|
||||||
|
|
||||||
|
message SwitchDisplay {
|
||||||
|
int32 display = 1;
|
||||||
|
sint32 x = 2;
|
||||||
|
sint32 y = 3;
|
||||||
|
int32 width = 4;
|
||||||
|
int32 height = 5;
|
||||||
|
bool cursor_embedded = 6;
|
||||||
|
SupportedResolutions resolutions = 7;
|
||||||
|
// Do not care about the origin point for now.
|
||||||
|
Resolution original_resolution = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CaptureDisplays {
|
||||||
|
repeated int32 add = 1;
|
||||||
|
repeated int32 sub = 2;
|
||||||
|
repeated int32 set = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ToggleVirtualDisplay {
|
||||||
|
int32 display = 1;
|
||||||
|
bool on = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TogglePrivacyMode {
|
||||||
|
string impl_key = 1;
|
||||||
|
bool on = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PermissionInfo {
|
||||||
|
enum Permission {
|
||||||
|
Keyboard = 0;
|
||||||
|
Clipboard = 2;
|
||||||
|
Audio = 3;
|
||||||
|
File = 4;
|
||||||
|
Restart = 5;
|
||||||
|
Recording = 6;
|
||||||
|
BlockInput = 7;
|
||||||
|
Camera = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
Permission permission = 1;
|
||||||
|
bool enabled = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ImageQuality {
|
||||||
|
NotSet = 0;
|
||||||
|
Low = 2;
|
||||||
|
Balanced = 3;
|
||||||
|
Best = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SupportedDecoding {
|
||||||
|
enum PreferCodec {
|
||||||
|
Auto = 0;
|
||||||
|
VP9 = 1;
|
||||||
|
H264 = 2;
|
||||||
|
H265 = 3;
|
||||||
|
VP8 = 4;
|
||||||
|
AV1 = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 ability_vp9 = 1;
|
||||||
|
int32 ability_h264 = 2;
|
||||||
|
int32 ability_h265 = 3;
|
||||||
|
PreferCodec prefer = 4;
|
||||||
|
int32 ability_vp8 = 5;
|
||||||
|
int32 ability_av1 = 6;
|
||||||
|
CodecAbility i444 = 7;
|
||||||
|
Chroma prefer_chroma = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
BoolOption disable_keyboard = 12;
|
||||||
|
// Position 13 is used for Resolution. Remove later.
|
||||||
|
// Resolution custom_resolution = 13;
|
||||||
|
// BoolOption support_windows_specific_session = 14;
|
||||||
|
// starting from 15 please, do not use removed fields
|
||||||
|
BoolOption follow_remote_cursor = 15;
|
||||||
|
BoolOption follow_remote_window = 16;
|
||||||
|
BoolOption disable_camera = 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TestDelay {
|
||||||
|
int64 time = 1;
|
||||||
|
bool from_client = 2;
|
||||||
|
uint32 last_delay = 3;
|
||||||
|
uint32 target_bitrate = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PublicKey {
|
||||||
|
bytes asymmetric_value = 1;
|
||||||
|
bytes symmetric_value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignedId { bytes id = 1; }
|
||||||
|
|
||||||
|
message AudioFormat {
|
||||||
|
uint32 sample_rate = 1;
|
||||||
|
uint32 channels = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AudioFrame {
|
||||||
|
bytes data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify peer to show message box.
|
||||||
|
message MessageBox {
|
||||||
|
// Message type. Refer to flutter/lib/common.dart/msgBox().
|
||||||
|
string msgtype = 1;
|
||||||
|
string title = 2;
|
||||||
|
// English
|
||||||
|
string text = 3;
|
||||||
|
// If not empty, msgbox provides a button to following the link.
|
||||||
|
// The link here can't be directly http url.
|
||||||
|
// It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
|
||||||
|
string link = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BackNotification {
|
||||||
|
// no need to consider block input by someone else
|
||||||
|
enum BlockInputState {
|
||||||
|
BlkStateUnknown = 0;
|
||||||
|
BlkOnSucceeded = 2;
|
||||||
|
BlkOnFailed = 3;
|
||||||
|
BlkOffSucceeded = 4;
|
||||||
|
BlkOffFailed = 5;
|
||||||
|
}
|
||||||
|
enum PrivacyModeState {
|
||||||
|
PrvStateUnknown = 0;
|
||||||
|
// Privacy mode on by someone else
|
||||||
|
PrvOnByOther = 2;
|
||||||
|
// Privacy mode is not supported on the remote side
|
||||||
|
PrvNotSupported = 3;
|
||||||
|
// Privacy mode on by self
|
||||||
|
PrvOnSucceeded = 4;
|
||||||
|
// Privacy mode on by self, but denied
|
||||||
|
PrvOnFailedDenied = 5;
|
||||||
|
// Some plugins are not found
|
||||||
|
PrvOnFailedPlugin = 6;
|
||||||
|
// Privacy mode on by self, but failed
|
||||||
|
PrvOnFailed = 7;
|
||||||
|
// Privacy mode off by self
|
||||||
|
PrvOffSucceeded = 8;
|
||||||
|
// Ctrl + P
|
||||||
|
PrvOffByPeer = 9;
|
||||||
|
// Privacy mode off by self, but failed
|
||||||
|
PrvOffFailed = 10;
|
||||||
|
PrvOffUnknown = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof union {
|
||||||
|
PrivacyModeState privacy_mode_state = 1;
|
||||||
|
BlockInputState block_input_state = 2;
|
||||||
|
}
|
||||||
|
// Supplementary message, for "PrvOnFailed" and "PrvOffFailed"
|
||||||
|
string details = 3;
|
||||||
|
// The key of the implementation
|
||||||
|
string impl_key = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ElevationRequestWithLogon {
|
||||||
|
string username = 1;
|
||||||
|
string password = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ElevationRequest {
|
||||||
|
oneof union {
|
||||||
|
bool direct = 1;
|
||||||
|
ElevationRequestWithLogon logon = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SwitchSidesRequest {
|
||||||
|
bytes uuid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SwitchSidesResponse {
|
||||||
|
bytes uuid = 1;
|
||||||
|
LoginRequest lr = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SwitchBack {}
|
||||||
|
|
||||||
|
message PluginRequest {
|
||||||
|
string id = 1;
|
||||||
|
bytes content = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PluginFailure {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string msg = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WindowsSessions {
|
||||||
|
repeated WindowsSession sessions = 1;
|
||||||
|
uint32 current_sid = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query messages from peer.
|
||||||
|
message MessageQuery {
|
||||||
|
// The SwitchDisplay message of the target display.
|
||||||
|
// If the target display is not found, the message will be ignored.
|
||||||
|
int32 switch_display = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
bool uac = 15;
|
||||||
|
bool foreground_window_elevated = 16;
|
||||||
|
bool stop_service = 17;
|
||||||
|
ElevationRequest elevation_request = 18;
|
||||||
|
string elevation_response = 19;
|
||||||
|
bool portable_service_running = 20;
|
||||||
|
SwitchSidesRequest switch_sides_request = 21;
|
||||||
|
SwitchBack switch_back = 22;
|
||||||
|
// Deprecated since 1.2.4, use `change_display_resolution` (36) instead.
|
||||||
|
// But we must keep it for compatibility when peer version < 1.2.4.
|
||||||
|
Resolution change_resolution = 24;
|
||||||
|
PluginRequest plugin_request = 25;
|
||||||
|
PluginFailure plugin_failure = 26;
|
||||||
|
uint32 full_speed_fps = 27; // deprecated
|
||||||
|
uint32 auto_adjust_fps = 28;
|
||||||
|
bool client_record_status = 29;
|
||||||
|
CaptureDisplays capture_displays = 30;
|
||||||
|
int32 refresh_video_display = 31;
|
||||||
|
ToggleVirtualDisplay toggle_virtual_display = 32;
|
||||||
|
TogglePrivacyMode toggle_privacy_mode = 33;
|
||||||
|
SupportedEncoding supported_encoding = 34;
|
||||||
|
uint32 selected_sid = 35;
|
||||||
|
DisplayResolution change_display_resolution = 36;
|
||||||
|
MessageQuery message_query = 37;
|
||||||
|
int32 follow_current_display = 38;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message VoiceCallRequest {
|
||||||
|
int64 req_timestamp = 1;
|
||||||
|
// Indicates whether the request is a connect action or a disconnect action.
|
||||||
|
bool is_connect = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VoiceCallResponse {
|
||||||
|
bool accepted = 1;
|
||||||
|
int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
|
||||||
|
int64 ack_timestamp = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
197
protos/rendezvous.proto
Normal file
197
protos/rendezvous.proto
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package hbb;
|
||||||
|
|
||||||
|
message RegisterPeer {
|
||||||
|
string id = 1;
|
||||||
|
int32 serial = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnType {
|
||||||
|
DEFAULT_CONN = 0;
|
||||||
|
FILE_TRANSFER = 1;
|
||||||
|
PORT_FORWARD = 2;
|
||||||
|
RDP = 3;
|
||||||
|
VIEW_CAMERA = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterPeerResponse { bool request_pk = 2; }
|
||||||
|
|
||||||
|
message PunchHoleRequest {
|
||||||
|
string id = 1;
|
||||||
|
NatType nat_type = 2;
|
||||||
|
string licence_key = 3;
|
||||||
|
ConnType conn_type = 4;
|
||||||
|
string token = 5;
|
||||||
|
string version = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PunchHole {
|
||||||
|
bytes socket_addr = 1;
|
||||||
|
string relay_server = 2;
|
||||||
|
NatType nat_type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TestNatRequest {
|
||||||
|
int32 serial = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative
|
||||||
|
message TestNatResponse {
|
||||||
|
int32 port = 1;
|
||||||
|
ConfigUpdate cu = 2; // for mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NatType {
|
||||||
|
UNKNOWN_NAT = 0;
|
||||||
|
ASYMMETRIC = 1;
|
||||||
|
SYMMETRIC = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PunchHoleSent {
|
||||||
|
bytes socket_addr = 1;
|
||||||
|
string id = 2;
|
||||||
|
string relay_server = 3;
|
||||||
|
NatType nat_type = 4;
|
||||||
|
string version = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterPk {
|
||||||
|
string id = 1;
|
||||||
|
bytes uuid = 2;
|
||||||
|
bytes pk = 3;
|
||||||
|
string old_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ConfigUpdate {
|
||||||
|
int32 serial = 1;
|
||||||
|
repeated string rendezvous_servers = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RequestRelay {
|
||||||
|
string id = 1;
|
||||||
|
string uuid = 2;
|
||||||
|
bytes socket_addr = 3;
|
||||||
|
string relay_server = 4;
|
||||||
|
bool secure = 5;
|
||||||
|
string licence_key = 6;
|
||||||
|
ConnType conn_type = 7;
|
||||||
|
string token = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 SoftwareUpdate { string url = 1; }
|
||||||
|
|
||||||
|
// if in same intranet, punch hole won't work both for udp and tcp,
|
||||||
|
// even some router has below connection error if we connect itself,
|
||||||
|
// { kind: Other, error: "could not resolve to any address" },
|
||||||
|
// so we request local address to connect.
|
||||||
|
message FetchLocalAddr {
|
||||||
|
bytes socket_addr = 1;
|
||||||
|
string relay_server = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LocalAddr {
|
||||||
|
bytes socket_addr = 1;
|
||||||
|
bytes local_addr = 2;
|
||||||
|
string relay_server = 3;
|
||||||
|
string id = 4;
|
||||||
|
string version = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PeerDiscovery {
|
||||||
|
string cmd = 1;
|
||||||
|
string mac = 2;
|
||||||
|
string id = 3;
|
||||||
|
string username = 4;
|
||||||
|
string hostname = 5;
|
||||||
|
string platform = 6;
|
||||||
|
string misc = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnlineRequest {
|
||||||
|
string id = 1;
|
||||||
|
repeated string peers = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnlineResponse {
|
||||||
|
bytes states = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyExchange {
|
||||||
|
repeated bytes keys = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthCheck {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ use typeshare::typeshare;
|
|||||||
|
|
||||||
// Re-export ExtensionsConfig from extensions module
|
// Re-export ExtensionsConfig from extensions module
|
||||||
pub use crate::extensions::ExtensionsConfig;
|
pub use crate::extensions::ExtensionsConfig;
|
||||||
|
// Re-export RustDeskConfig from rustdesk module
|
||||||
|
pub use crate::rustdesk::config::RustDeskConfig;
|
||||||
|
|
||||||
/// Main application configuration
|
/// Main application configuration
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
@@ -29,6 +31,8 @@ pub struct AppConfig {
|
|||||||
pub web: WebConfig,
|
pub web: WebConfig,
|
||||||
/// Extensions settings (ttyd, gostc, easytier)
|
/// Extensions settings (ttyd, gostc, easytier)
|
||||||
pub extensions: ExtensionsConfig,
|
pub extensions: ExtensionsConfig,
|
||||||
|
/// RustDesk remote access settings
|
||||||
|
pub rustdesk: RustDeskConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
@@ -44,6 +48,7 @@ impl Default for AppConfig {
|
|||||||
stream: StreamConfig::default(),
|
stream: StreamConfig::default(),
|
||||||
web: WebConfig::default(),
|
web: WebConfig::default(),
|
||||||
extensions: ExtensionsConfig::default(),
|
extensions: ExtensionsConfig::default(),
|
||||||
|
rustdesk: RustDeskConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod hid;
|
|||||||
pub mod modules;
|
pub mod modules;
|
||||||
pub mod msd;
|
pub mod msd;
|
||||||
pub mod otg;
|
pub mod otg;
|
||||||
|
pub mod rustdesk;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
56
src/main.rs
56
src/main.rs
@@ -17,6 +17,7 @@ use one_kvm::extensions::ExtensionManager;
|
|||||||
use one_kvm::hid::{HidBackendType, HidController};
|
use one_kvm::hid::{HidBackendType, HidController};
|
||||||
use one_kvm::msd::MsdController;
|
use one_kvm::msd::MsdController;
|
||||||
use one_kvm::otg::OtgService;
|
use one_kvm::otg::OtgService;
|
||||||
|
use one_kvm::rustdesk::RustDeskService;
|
||||||
use one_kvm::state::AppState;
|
use one_kvm::state::AppState;
|
||||||
use one_kvm::video::format::{PixelFormat, Resolution};
|
use one_kvm::video::format::{PixelFormat, Resolution};
|
||||||
use one_kvm::video::{Streamer, VideoStreamManager};
|
use one_kvm::video::{Streamer, VideoStreamManager};
|
||||||
@@ -374,6 +375,29 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
tracing::info!("Video stream manager initialized with mode: {:?}", initial_mode);
|
tracing::info!("Video stream manager initialized with mode: {:?}", initial_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create RustDesk service (optional, based on config)
|
||||||
|
let rustdesk = if config.rustdesk.is_valid() {
|
||||||
|
tracing::info!(
|
||||||
|
"Initializing RustDesk service: ID={} -> {}",
|
||||||
|
config.rustdesk.device_id,
|
||||||
|
config.rustdesk.rendezvous_addr()
|
||||||
|
);
|
||||||
|
let service = RustDeskService::new(
|
||||||
|
config.rustdesk.clone(),
|
||||||
|
stream_manager.clone(),
|
||||||
|
hid.clone(),
|
||||||
|
audio.clone(),
|
||||||
|
);
|
||||||
|
Some(Arc::new(service))
|
||||||
|
} else {
|
||||||
|
if config.rustdesk.enabled {
|
||||||
|
tracing::warn!("RustDesk enabled but configuration is incomplete (missing server or credentials)");
|
||||||
|
} else {
|
||||||
|
tracing::info!("RustDesk disabled in configuration");
|
||||||
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Create application state
|
// Create application state
|
||||||
let state = AppState::new(
|
let state = AppState::new(
|
||||||
config_store.clone(),
|
config_store.clone(),
|
||||||
@@ -385,12 +409,35 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
msd,
|
msd,
|
||||||
atx,
|
atx,
|
||||||
audio,
|
audio,
|
||||||
|
rustdesk.clone(),
|
||||||
extensions.clone(),
|
extensions.clone(),
|
||||||
events.clone(),
|
events.clone(),
|
||||||
shutdown_tx.clone(),
|
shutdown_tx.clone(),
|
||||||
data_dir.clone(),
|
data_dir.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start RustDesk service if enabled
|
||||||
|
if let Some(ref service) = rustdesk {
|
||||||
|
if let Err(e) = service.start().await {
|
||||||
|
tracing::error!("Failed to start RustDesk service: {}", e);
|
||||||
|
} else {
|
||||||
|
// Save generated keypair and UUID to config
|
||||||
|
if let Some(updated_config) = service.save_credentials() {
|
||||||
|
if let Err(e) = config_store
|
||||||
|
.update(|cfg| {
|
||||||
|
cfg.rustdesk.public_key = updated_config.public_key.clone();
|
||||||
|
cfg.rustdesk.private_key = updated_config.private_key.clone();
|
||||||
|
cfg.rustdesk.uuid = updated_config.uuid.clone();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("Failed to save RustDesk credentials: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!("RustDesk service started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start enabled extensions
|
// Start enabled extensions
|
||||||
{
|
{
|
||||||
let ext_config = config_store.get();
|
let ext_config = config_store.get();
|
||||||
@@ -636,6 +683,15 @@ async fn cleanup(state: &Arc<AppState>) {
|
|||||||
state.extensions.stop_all().await;
|
state.extensions.stop_all().await;
|
||||||
tracing::info!("Extensions stopped");
|
tracing::info!("Extensions stopped");
|
||||||
|
|
||||||
|
// Stop RustDesk service
|
||||||
|
if let Some(ref service) = *state.rustdesk.read().await {
|
||||||
|
if let Err(e) = service.stop().await {
|
||||||
|
tracing::warn!("Failed to stop RustDesk service: {}", e);
|
||||||
|
} else {
|
||||||
|
tracing::info!("RustDesk service stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stop video
|
// Stop video
|
||||||
if let Err(e) = state.stream_manager.stop().await {
|
if let Err(e) = state.stream_manager.stop().await {
|
||||||
tracing::warn!("Failed to stop streamer: {}", e);
|
tracing::warn!("Failed to stop streamer: {}", e);
|
||||||
|
|||||||
253
src/rustdesk/bytes_codec.rs
Normal file
253
src/rustdesk/bytes_codec.rs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
//! RustDesk BytesCodec - Variable-length framing for TCP messages
|
||||||
|
//!
|
||||||
|
//! RustDesk uses a custom variable-length encoding for message framing:
|
||||||
|
//! - Length <= 0x3F (63): 1-byte header, format `(len << 2)`
|
||||||
|
//! - Length <= 0x3FFF (16383): 2-byte LE header, format `(len << 2) | 0x1`
|
||||||
|
//! - Length <= 0x3FFFFF (4194303): 3-byte LE header, format `(len << 2) | 0x2`
|
||||||
|
//! - Length <= 0x3FFFFFFF (1073741823): 4-byte LE header, format `(len << 2) | 0x3`
|
||||||
|
//!
|
||||||
|
//! The low 2 bits of the first byte indicate the header length (+1).
|
||||||
|
|
||||||
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
|
use std::io;
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
/// Maximum packet length (1GB)
|
||||||
|
const MAX_PACKET_LENGTH: usize = 0x3FFFFFFF;
|
||||||
|
|
||||||
|
/// Encode a message with RustDesk's variable-length framing
|
||||||
|
pub fn encode_frame(data: &[u8]) -> io::Result<Vec<u8>> {
|
||||||
|
let len = data.len();
|
||||||
|
let mut buf = Vec::with_capacity(len + 4);
|
||||||
|
|
||||||
|
if len <= 0x3F {
|
||||||
|
buf.push((len << 2) as u8);
|
||||||
|
} else if len <= 0x3FFF {
|
||||||
|
let h = ((len << 2) as u16) | 0x1;
|
||||||
|
buf.extend_from_slice(&h.to_le_bytes());
|
||||||
|
} else if len <= 0x3FFFFF {
|
||||||
|
let h = ((len << 2) as u32) | 0x2;
|
||||||
|
buf.push((h & 0xFF) as u8);
|
||||||
|
buf.push(((h >> 8) & 0xFF) as u8);
|
||||||
|
buf.push(((h >> 16) & 0xFF) as u8);
|
||||||
|
} else if len <= MAX_PACKET_LENGTH {
|
||||||
|
let h = ((len << 2) as u32) | 0x3;
|
||||||
|
buf.extend_from_slice(&h.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_from_slice(data);
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode the header to get message length
|
||||||
|
/// Returns (header_length, message_length)
|
||||||
|
fn decode_header(first_byte: u8, header_bytes: &[u8]) -> (usize, usize) {
|
||||||
|
let head_len = ((first_byte & 0x3) + 1) as usize;
|
||||||
|
|
||||||
|
let mut n = first_byte as usize;
|
||||||
|
if head_len > 1 && header_bytes.len() >= 1 {
|
||||||
|
n |= (header_bytes[0] as usize) << 8;
|
||||||
|
}
|
||||||
|
if head_len > 2 && header_bytes.len() >= 2 {
|
||||||
|
n |= (header_bytes[1] as usize) << 16;
|
||||||
|
}
|
||||||
|
if head_len > 3 && header_bytes.len() >= 3 {
|
||||||
|
n |= (header_bytes[2] as usize) << 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg_len = n >> 2;
|
||||||
|
(head_len, msg_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a single framed message from an async reader
|
||||||
|
pub async fn read_frame<R: AsyncRead + Unpin>(reader: &mut R) -> io::Result<BytesMut> {
|
||||||
|
// Read first byte to determine header length
|
||||||
|
let mut first_byte = [0u8; 1];
|
||||||
|
reader.read_exact(&mut first_byte).await?;
|
||||||
|
|
||||||
|
let head_len = ((first_byte[0] & 0x3) + 1) as usize;
|
||||||
|
|
||||||
|
// Read remaining header bytes if needed
|
||||||
|
let mut header_rest = [0u8; 3];
|
||||||
|
if head_len > 1 {
|
||||||
|
reader.read_exact(&mut header_rest[..head_len - 1]).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate message length
|
||||||
|
let (_, msg_len) = decode_header(first_byte[0], &header_rest);
|
||||||
|
|
||||||
|
if msg_len > MAX_PACKET_LENGTH {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "Message too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read message body
|
||||||
|
let mut buf = BytesMut::with_capacity(msg_len);
|
||||||
|
buf.resize(msg_len, 0);
|
||||||
|
reader.read_exact(&mut buf).await?;
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a framed message to an async writer
|
||||||
|
pub async fn write_frame<W: AsyncWrite + Unpin>(writer: &mut W, data: &[u8]) -> io::Result<()> {
|
||||||
|
let frame = encode_frame(data)?;
|
||||||
|
writer.write_all(&frame).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BytesCodec for stateful decoding (compatible with tokio-util codec)
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct BytesCodec {
|
||||||
|
state: DecodeState,
|
||||||
|
max_packet_length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum DecodeState {
|
||||||
|
Head,
|
||||||
|
Data(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BytesCodec {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BytesCodec {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: DecodeState::Head,
|
||||||
|
max_packet_length: MAX_PACKET_LENGTH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_packet_length(&mut self, n: usize) {
|
||||||
|
self.max_packet_length = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from a BytesMut buffer (for use with Framed)
|
||||||
|
pub fn decode(&mut self, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
|
||||||
|
let n = match self.state {
|
||||||
|
DecodeState::Head => match self.decode_head(src)? {
|
||||||
|
Some(n) => {
|
||||||
|
self.state = DecodeState::Data(n);
|
||||||
|
n
|
||||||
|
}
|
||||||
|
None => return Ok(None),
|
||||||
|
},
|
||||||
|
DecodeState::Data(n) => n,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.decode_data(n, src)? {
|
||||||
|
Some(data) => {
|
||||||
|
self.state = DecodeState::Head;
|
||||||
|
Ok(Some(data))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_head(&mut self, src: &mut BytesMut) -> io::Result<Option<usize>> {
|
||||||
|
if src.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let head_len = ((src[0] & 0x3) + 1) as usize;
|
||||||
|
if src.len() < head_len {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut n = src[0] as usize;
|
||||||
|
if head_len > 1 {
|
||||||
|
n |= (src[1] as usize) << 8;
|
||||||
|
}
|
||||||
|
if head_len > 2 {
|
||||||
|
n |= (src[2] as usize) << 16;
|
||||||
|
}
|
||||||
|
if head_len > 3 {
|
||||||
|
n |= (src[3] as usize) << 24;
|
||||||
|
}
|
||||||
|
n >>= 2;
|
||||||
|
|
||||||
|
if n > self.max_packet_length {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "Message too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
src.advance(head_len);
|
||||||
|
Ok(Some(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
|
||||||
|
if src.len() < n {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(src.split_to(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a message into a BytesMut buffer
|
||||||
|
pub fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> io::Result<()> {
|
||||||
|
let len = data.len();
|
||||||
|
|
||||||
|
if len <= 0x3F {
|
||||||
|
buf.put_u8((len << 2) as u8);
|
||||||
|
} else if len <= 0x3FFF {
|
||||||
|
buf.put_u16_le(((len << 2) as u16) | 0x1);
|
||||||
|
} else if len <= 0x3FFFFF {
|
||||||
|
let h = ((len << 2) as u32) | 0x2;
|
||||||
|
buf.put_u16_le((h & 0xFFFF) as u16);
|
||||||
|
buf.put_u8((h >> 16) as u8);
|
||||||
|
} else if len <= MAX_PACKET_LENGTH {
|
||||||
|
buf.put_u32_le(((len << 2) as u32) | 0x3);
|
||||||
|
} else {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend(data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_small() {
|
||||||
|
let data = vec![1u8; 63];
|
||||||
|
let encoded = encode_frame(&data).unwrap();
|
||||||
|
assert_eq!(encoded.len(), 63 + 1); // 1 byte header
|
||||||
|
|
||||||
|
let mut codec = BytesCodec::new();
|
||||||
|
let mut buf = BytesMut::from(&encoded[..]);
|
||||||
|
let decoded = codec.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(decoded.len(), 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_medium() {
|
||||||
|
let data = vec![2u8; 1000];
|
||||||
|
let encoded = encode_frame(&data).unwrap();
|
||||||
|
assert_eq!(encoded.len(), 1000 + 2); // 2 byte header
|
||||||
|
|
||||||
|
let mut codec = BytesCodec::new();
|
||||||
|
let mut buf = BytesMut::from(&encoded[..]);
|
||||||
|
let decoded = codec.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(decoded.len(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_large() {
|
||||||
|
let data = vec![3u8; 100000];
|
||||||
|
let encoded = encode_frame(&data).unwrap();
|
||||||
|
assert_eq!(encoded.len(), 100000 + 3); // 3 byte header
|
||||||
|
|
||||||
|
let mut codec = BytesCodec::new();
|
||||||
|
let mut buf = BytesMut::from(&encoded[..]);
|
||||||
|
let decoded = codec.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(decoded.len(), 100000);
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/rustdesk/config.rs
Normal file
210
src/rustdesk/config.rs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
//! RustDesk Configuration
|
||||||
|
//!
|
||||||
|
//! Configuration types for the RustDesk protocol integration.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
/// RustDesk configuration
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct RustDeskConfig {
|
||||||
|
/// Enable RustDesk protocol
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// Rendezvous server address (hbbs), e.g., "rs.example.com" or "192.168.1.100"
|
||||||
|
/// Port defaults to 21116 if not specified
|
||||||
|
pub rendezvous_server: String,
|
||||||
|
|
||||||
|
/// Relay server address (hbbr), if different from rendezvous server
|
||||||
|
/// Usually the same host as rendezvous server but different port (21117)
|
||||||
|
pub relay_server: Option<String>,
|
||||||
|
|
||||||
|
/// Device ID (9-digit number), auto-generated if empty
|
||||||
|
pub device_id: String,
|
||||||
|
|
||||||
|
/// Device password for client authentication
|
||||||
|
#[typeshare(skip)]
|
||||||
|
pub device_password: String,
|
||||||
|
|
||||||
|
/// Public key for encryption (Curve25519, base64 encoded), auto-generated
|
||||||
|
#[typeshare(skip)]
|
||||||
|
pub public_key: Option<String>,
|
||||||
|
|
||||||
|
/// Private key for encryption (Curve25519, base64 encoded), auto-generated
|
||||||
|
#[typeshare(skip)]
|
||||||
|
pub private_key: Option<String>,
|
||||||
|
|
||||||
|
/// Signing public key (Ed25519, base64 encoded), auto-generated
|
||||||
|
/// Used for SignedId verification by clients
|
||||||
|
#[typeshare(skip)]
|
||||||
|
pub signing_public_key: Option<String>,
|
||||||
|
|
||||||
|
/// Signing private key (Ed25519, base64 encoded), auto-generated
|
||||||
|
/// Used for signing SignedId messages
|
||||||
|
#[typeshare(skip)]
|
||||||
|
pub signing_private_key: Option<String>,
|
||||||
|
|
||||||
|
/// UUID for rendezvous server registration (persisted to avoid UUID_MISMATCH)
|
||||||
|
#[typeshare(skip)]
|
||||||
|
pub uuid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RustDeskConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
rendezvous_server: String::new(),
|
||||||
|
relay_server: None,
|
||||||
|
device_id: generate_device_id(),
|
||||||
|
device_password: generate_random_password(),
|
||||||
|
public_key: None,
|
||||||
|
private_key: None,
|
||||||
|
signing_public_key: None,
|
||||||
|
signing_private_key: None,
|
||||||
|
uuid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustDeskConfig {
|
||||||
|
/// Check if the configuration is valid for starting the service
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
&& !self.rendezvous_server.is_empty()
|
||||||
|
&& !self.device_id.is_empty()
|
||||||
|
&& !self.device_password.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a new random device ID
|
||||||
|
pub fn generate_device_id() -> String {
|
||||||
|
generate_device_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a new random password
|
||||||
|
pub fn generate_password() -> String {
|
||||||
|
generate_random_password()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or generate the UUID for rendezvous registration
|
||||||
|
/// Returns (uuid_bytes, is_new) where is_new indicates if a new UUID was generated
|
||||||
|
pub fn ensure_uuid(&mut self) -> ([u8; 16], bool) {
|
||||||
|
if let Some(ref uuid_str) = self.uuid {
|
||||||
|
// Try to parse existing UUID
|
||||||
|
if let Ok(uuid) = uuid::Uuid::parse_str(uuid_str) {
|
||||||
|
return (*uuid.as_bytes(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate new UUID
|
||||||
|
let new_uuid = uuid::Uuid::new_v4();
|
||||||
|
self.uuid = Some(new_uuid.to_string());
|
||||||
|
(*new_uuid.as_bytes(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the UUID bytes (returns None if not set)
|
||||||
|
pub fn get_uuid_bytes(&self) -> Option<[u8; 16]> {
|
||||||
|
self.uuid.as_ref().and_then(|s| {
|
||||||
|
uuid::Uuid::parse_str(s).ok().map(|u| *u.as_bytes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the rendezvous server address with default port
|
||||||
|
pub fn rendezvous_addr(&self) -> String {
|
||||||
|
if self.rendezvous_server.contains(':') {
|
||||||
|
self.rendezvous_server.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}:21116", self.rendezvous_server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the relay server address with default port
|
||||||
|
pub fn relay_addr(&self) -> Option<String> {
|
||||||
|
self.relay_server.as_ref().map(|s| {
|
||||||
|
if s.contains(':') {
|
||||||
|
s.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}:21117", s)
|
||||||
|
}
|
||||||
|
}).or_else(|| {
|
||||||
|
// Default: same host as rendezvous server
|
||||||
|
if !self.rendezvous_server.is_empty() {
|
||||||
|
let host = self.rendezvous_server.split(':').next().unwrap_or("");
|
||||||
|
if !host.is_empty() {
|
||||||
|
Some(format!("{}:21117", host))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random 9-digit device ID
|
||||||
|
pub fn generate_device_id() -> String {
|
||||||
|
use rand::Rng;
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let id: u32 = rng.gen_range(100_000_000..999_999_999);
|
||||||
|
id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random 8-character password
|
||||||
|
pub fn generate_random_password() -> String {
|
||||||
|
use rand::Rng;
|
||||||
|
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
(0..8)
|
||||||
|
.map(|_| {
|
||||||
|
let idx = rng.gen_range(0..CHARSET.len());
|
||||||
|
CHARSET[idx] as char
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_device_id_generation() {
|
||||||
|
let id = generate_device_id();
|
||||||
|
assert_eq!(id.len(), 9);
|
||||||
|
assert!(id.chars().all(|c| c.is_ascii_digit()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_password_generation() {
|
||||||
|
let password = generate_random_password();
|
||||||
|
assert_eq!(password.len(), 8);
|
||||||
|
assert!(password.chars().all(|c| c.is_alphanumeric()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rendezvous_addr() {
|
||||||
|
let mut config = RustDeskConfig::default();
|
||||||
|
|
||||||
|
config.rendezvous_server = "example.com".to_string();
|
||||||
|
assert_eq!(config.rendezvous_addr(), "example.com:21116");
|
||||||
|
|
||||||
|
config.rendezvous_server = "example.com:21116".to_string();
|
||||||
|
assert_eq!(config.rendezvous_addr(), "example.com:21116");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relay_addr() {
|
||||||
|
let mut config = RustDeskConfig::default();
|
||||||
|
|
||||||
|
// No server configured
|
||||||
|
assert!(config.relay_addr().is_none());
|
||||||
|
|
||||||
|
// Rendezvous server configured, relay defaults to same host
|
||||||
|
config.rendezvous_server = "example.com".to_string();
|
||||||
|
assert_eq!(config.relay_addr(), Some("example.com:21117".to_string()));
|
||||||
|
|
||||||
|
// Explicit relay server
|
||||||
|
config.relay_server = Some("relay.example.com".to_string());
|
||||||
|
assert_eq!(config.relay_addr(), Some("relay.example.com:21117".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
1396
src/rustdesk/connection.rs
Normal file
1396
src/rustdesk/connection.rs
Normal file
File diff suppressed because it is too large
Load Diff
467
src/rustdesk/crypto.rs
Normal file
467
src/rustdesk/crypto.rs
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
//! RustDesk Cryptography
|
||||||
|
//!
|
||||||
|
//! This module implements the NaCl-based cryptography used by RustDesk:
|
||||||
|
//! - Curve25519 for key exchange
|
||||||
|
//! - XSalsa20-Poly1305 for authenticated encryption
|
||||||
|
//! - Ed25519 for signatures
|
||||||
|
//! - Ed25519 to Curve25519 key conversion for unified keypair usage
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
||||||
|
use sodiumoxide::crypto::box_::{self, Nonce, PublicKey, SecretKey};
|
||||||
|
use sodiumoxide::crypto::secretbox;
|
||||||
|
use sodiumoxide::crypto::sign::{self, ed25519};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Cryptography errors
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CryptoError {
|
||||||
|
#[error("Failed to initialize sodiumoxide")]
|
||||||
|
InitError,
|
||||||
|
#[error("Encryption failed")]
|
||||||
|
EncryptionFailed,
|
||||||
|
#[error("Decryption failed")]
|
||||||
|
DecryptionFailed,
|
||||||
|
#[error("Invalid key length")]
|
||||||
|
InvalidKeyLength,
|
||||||
|
#[error("Invalid nonce")]
|
||||||
|
InvalidNonce,
|
||||||
|
#[error("Signature verification failed")]
|
||||||
|
SignatureVerificationFailed,
|
||||||
|
#[error("Key conversion failed")]
|
||||||
|
KeyConversionFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the cryptography library
|
||||||
|
/// Must be called before using any crypto functions
|
||||||
|
pub fn init() -> Result<(), CryptoError> {
|
||||||
|
sodiumoxide::init().map_err(|_| CryptoError::InitError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A keypair for asymmetric encryption
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct KeyPair {
|
||||||
|
pub public_key: PublicKey,
|
||||||
|
pub secret_key: SecretKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyPair {
|
||||||
|
/// Generate a new random keypair
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
let (public_key, secret_key) = box_::gen_keypair();
|
||||||
|
Self {
|
||||||
|
public_key,
|
||||||
|
secret_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from existing keys
|
||||||
|
pub fn from_keys(public_key: &[u8], secret_key: &[u8]) -> Result<Self, CryptoError> {
|
||||||
|
let pk = PublicKey::from_slice(public_key).ok_or(CryptoError::InvalidKeyLength)?;
|
||||||
|
let sk = SecretKey::from_slice(secret_key).ok_or(CryptoError::InvalidKeyLength)?;
|
||||||
|
Ok(Self {
|
||||||
|
public_key: pk,
|
||||||
|
secret_key: sk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get public key as bytes
|
||||||
|
pub fn public_key_bytes(&self) -> &[u8] {
|
||||||
|
self.public_key.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get secret key as bytes
|
||||||
|
pub fn secret_key_bytes(&self) -> &[u8] {
|
||||||
|
self.secret_key.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode public key as base64
|
||||||
|
pub fn public_key_base64(&self) -> String {
|
||||||
|
BASE64.encode(self.public_key_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode secret key as base64
|
||||||
|
pub fn secret_key_base64(&self) -> String {
|
||||||
|
BASE64.encode(self.secret_key_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from base64-encoded keys
|
||||||
|
pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> {
|
||||||
|
let pk_bytes = BASE64.decode(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
let sk_bytes = BASE64.decode(secret_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
Self::from_keys(&pk_bytes, &sk_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random nonce for box encryption
|
||||||
|
pub fn generate_nonce() -> Nonce {
|
||||||
|
box_::gen_nonce()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt data using public-key cryptography (NaCl box)
|
||||||
|
///
|
||||||
|
/// Uses the sender's secret key and receiver's public key for encryption.
|
||||||
|
/// Returns (nonce, ciphertext).
|
||||||
|
pub fn encrypt_box(
|
||||||
|
data: &[u8],
|
||||||
|
their_public_key: &PublicKey,
|
||||||
|
our_secret_key: &SecretKey,
|
||||||
|
) -> (Nonce, Vec<u8>) {
|
||||||
|
let nonce = generate_nonce();
|
||||||
|
let ciphertext = box_::seal(data, &nonce, their_public_key, our_secret_key);
|
||||||
|
(nonce, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt data using public-key cryptography (NaCl box)
|
||||||
|
pub fn decrypt_box(
|
||||||
|
ciphertext: &[u8],
|
||||||
|
nonce: &Nonce,
|
||||||
|
their_public_key: &PublicKey,
|
||||||
|
our_secret_key: &SecretKey,
|
||||||
|
) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
box_::open(ciphertext, nonce, their_public_key, our_secret_key)
|
||||||
|
.map_err(|_| CryptoError::DecryptionFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt data with a precomputed shared key
|
||||||
|
pub fn encrypt_with_key(data: &[u8], key: &secretbox::Key) -> (secretbox::Nonce, Vec<u8>) {
|
||||||
|
let nonce = secretbox::gen_nonce();
|
||||||
|
let ciphertext = secretbox::seal(data, &nonce, key);
|
||||||
|
(nonce, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt data with a precomputed shared key
|
||||||
|
pub fn decrypt_with_key(
|
||||||
|
ciphertext: &[u8],
|
||||||
|
nonce: &secretbox::Nonce,
|
||||||
|
key: &secretbox::Key,
|
||||||
|
) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
secretbox::open(ciphertext, nonce, key).map_err(|_| CryptoError::DecryptionFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a shared symmetric key from public/private keypair
|
||||||
|
/// This is the precomputed key for the NaCl box
|
||||||
|
pub fn precompute_key(their_public_key: &PublicKey, our_secret_key: &SecretKey) -> box_::PrecomputedKey {
|
||||||
|
box_::precompute(their_public_key, our_secret_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a symmetric key from raw bytes
|
||||||
|
pub fn symmetric_key_from_slice(key: &[u8]) -> Result<secretbox::Key, CryptoError> {
|
||||||
|
secretbox::Key::from_slice(key).ok_or(CryptoError::InvalidKeyLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a nonce from bytes
|
||||||
|
pub fn nonce_from_slice(bytes: &[u8]) -> Result<Nonce, CryptoError> {
|
||||||
|
Nonce::from_slice(bytes).ok_or(CryptoError::InvalidNonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a public key from bytes
|
||||||
|
pub fn public_key_from_slice(bytes: &[u8]) -> Result<PublicKey, CryptoError> {
|
||||||
|
PublicKey::from_slice(bytes).ok_or(CryptoError::InvalidKeyLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash a password for storage/comparison
|
||||||
|
/// RustDesk uses simple SHA256 for password hashing
|
||||||
|
pub fn hash_password(password: &str, salt: &str) -> Vec<u8> {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(password.as_bytes());
|
||||||
|
hasher.update(salt.as_bytes());
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk double hash for password verification
|
||||||
|
/// Client calculates: SHA256(SHA256(password + salt) + challenge)
|
||||||
|
/// This matches what the client sends in LoginRequest
|
||||||
|
pub fn hash_password_double(password: &str, salt: &str, challenge: &str) -> Vec<u8> {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
// First hash: SHA256(password + salt)
|
||||||
|
let mut hasher1 = Sha256::new();
|
||||||
|
hasher1.update(password.as_bytes());
|
||||||
|
hasher1.update(salt.as_bytes());
|
||||||
|
let first_hash = hasher1.finalize();
|
||||||
|
|
||||||
|
// Second hash: SHA256(first_hash + challenge)
|
||||||
|
let mut hasher2 = Sha256::new();
|
||||||
|
hasher2.update(&first_hash);
|
||||||
|
hasher2.update(challenge.as_bytes());
|
||||||
|
hasher2.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a password hash
|
||||||
|
pub fn verify_password(password: &str, salt: &str, expected_hash: &[u8]) -> bool {
|
||||||
|
let computed = hash_password(password, salt);
|
||||||
|
// Constant-time comparison would be better, but for our use case this is acceptable
|
||||||
|
computed == expected_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk symmetric key negotiation result
|
||||||
|
pub struct SymmetricKeyNegotiation {
|
||||||
|
/// Our temporary public key (to send to peer)
|
||||||
|
pub our_public_key: Vec<u8>,
|
||||||
|
/// The sealed/encrypted symmetric key (to send to peer)
|
||||||
|
pub sealed_symmetric_key: Vec<u8>,
|
||||||
|
/// The actual symmetric key (for local use)
|
||||||
|
pub symmetric_key: secretbox::Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create symmetric key message for RustDesk encrypted handshake
|
||||||
|
///
|
||||||
|
/// This implements RustDesk's `create_symmetric_key_msg` protocol:
|
||||||
|
/// 1. Generate a temporary keypair
|
||||||
|
/// 2. Generate a symmetric key
|
||||||
|
/// 3. Encrypt the symmetric key using the peer's public key and our temp secret key
|
||||||
|
/// 4. Return (our_temp_public_key, sealed_symmetric_key, symmetric_key)
|
||||||
|
pub fn create_symmetric_key_msg(their_public_key_bytes: &[u8; 32]) -> SymmetricKeyNegotiation {
|
||||||
|
let their_pk = box_::PublicKey(*their_public_key_bytes);
|
||||||
|
let (our_temp_pk, our_temp_sk) = box_::gen_keypair();
|
||||||
|
let symmetric_key = secretbox::gen_key();
|
||||||
|
|
||||||
|
// Use zero nonce as per RustDesk protocol
|
||||||
|
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||||
|
let sealed_key = box_::seal(&symmetric_key.0, &nonce, &their_pk, &our_temp_sk);
|
||||||
|
|
||||||
|
SymmetricKeyNegotiation {
|
||||||
|
our_public_key: our_temp_pk.0.to_vec(),
|
||||||
|
sealed_symmetric_key: sealed_key,
|
||||||
|
symmetric_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt symmetric key received from peer during handshake
|
||||||
|
///
|
||||||
|
/// This is the server-side of RustDesk's encrypted handshake:
|
||||||
|
/// 1. Receive peer's temporary public key and sealed symmetric key
|
||||||
|
/// 2. Decrypt the symmetric key using our secret key
|
||||||
|
pub fn decrypt_symmetric_key_msg(
|
||||||
|
their_temp_public_key: &[u8],
|
||||||
|
sealed_symmetric_key: &[u8],
|
||||||
|
our_keypair: &KeyPair,
|
||||||
|
) -> Result<secretbox::Key, CryptoError> {
|
||||||
|
if their_temp_public_key.len() != box_::PUBLICKEYBYTES {
|
||||||
|
return Err(CryptoError::InvalidKeyLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
let their_pk = PublicKey::from_slice(their_temp_public_key)
|
||||||
|
.ok_or(CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
|
// Use zero nonce as per RustDesk protocol
|
||||||
|
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||||
|
|
||||||
|
let key_bytes = box_::open(sealed_symmetric_key, &nonce, &their_pk, &our_keypair.secret_key)
|
||||||
|
.map_err(|_| CryptoError::DecryptionFailed)?;
|
||||||
|
|
||||||
|
secretbox::Key::from_slice(&key_bytes).ok_or(CryptoError::InvalidKeyLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt symmetric key using Ed25519 signing keypair (converted to Curve25519)
|
||||||
|
///
|
||||||
|
/// RustDesk clients encrypt the symmetric key using the public key from IdPk,
|
||||||
|
/// which is our Ed25519 signing public key converted to Curve25519.
|
||||||
|
/// We must use the corresponding converted secret key to decrypt.
|
||||||
|
pub fn decrypt_symmetric_key_with_signing_keypair(
|
||||||
|
their_temp_public_key: &[u8],
|
||||||
|
sealed_symmetric_key: &[u8],
|
||||||
|
signing_keypair: &SigningKeyPair,
|
||||||
|
) -> Result<secretbox::Key, CryptoError> {
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
if their_temp_public_key.len() != box_::PUBLICKEYBYTES {
|
||||||
|
return Err(CryptoError::InvalidKeyLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
let their_pk = PublicKey::from_slice(their_temp_public_key)
|
||||||
|
.ok_or(CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
|
// Convert our Ed25519 secret key to Curve25519 for decryption
|
||||||
|
let our_curve25519_sk = signing_keypair.to_curve25519_sk()?;
|
||||||
|
|
||||||
|
// Also get our converted public key for debugging
|
||||||
|
let our_curve25519_pk = signing_keypair.to_curve25519_pk()?;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Decrypting with converted keys: our_curve25519_pk={:02x?}, their_temp_pk={:02x?}",
|
||||||
|
&our_curve25519_pk.as_ref()[..8],
|
||||||
|
&their_pk.as_ref()[..8]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use zero nonce as per RustDesk protocol
|
||||||
|
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||||
|
|
||||||
|
let key_bytes = box_::open(sealed_symmetric_key, &nonce, &their_pk, &our_curve25519_sk)
|
||||||
|
.map_err(|_| CryptoError::DecryptionFailed)?;
|
||||||
|
|
||||||
|
secretbox::Key::from_slice(&key_bytes).ok_or(CryptoError::InvalidKeyLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt a message using the negotiated symmetric key
|
||||||
|
///
|
||||||
|
/// RustDesk uses a specific nonce format for session encryption
|
||||||
|
pub fn encrypt_message(data: &[u8], key: &secretbox::Key, nonce_counter: u64) -> Vec<u8> {
|
||||||
|
// Create nonce from counter (little-endian, padded to 24 bytes)
|
||||||
|
let mut nonce_bytes = [0u8; secretbox::NONCEBYTES];
|
||||||
|
nonce_bytes[..8].copy_from_slice(&nonce_counter.to_le_bytes());
|
||||||
|
let nonce = secretbox::Nonce(nonce_bytes);
|
||||||
|
|
||||||
|
secretbox::seal(data, &nonce, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt a message using the negotiated symmetric key
|
||||||
|
pub fn decrypt_message(
|
||||||
|
ciphertext: &[u8],
|
||||||
|
key: &secretbox::Key,
|
||||||
|
nonce_counter: u64,
|
||||||
|
) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
// Create nonce from counter (little-endian, padded to 24 bytes)
|
||||||
|
let mut nonce_bytes = [0u8; secretbox::NONCEBYTES];
|
||||||
|
nonce_bytes[..8].copy_from_slice(&nonce_counter.to_le_bytes());
|
||||||
|
let nonce = secretbox::Nonce(nonce_bytes);
|
||||||
|
|
||||||
|
secretbox::open(ciphertext, &nonce, key).map_err(|_| CryptoError::DecryptionFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ed25519 signing keypair for RustDesk SignedId messages
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SigningKeyPair {
|
||||||
|
pub public_key: sign::PublicKey,
|
||||||
|
pub secret_key: sign::SecretKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SigningKeyPair {
|
||||||
|
/// Generate a new random signing keypair
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
let (public_key, secret_key) = sign::gen_keypair();
|
||||||
|
Self {
|
||||||
|
public_key,
|
||||||
|
secret_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from existing keys
|
||||||
|
pub fn from_keys(public_key: &[u8], secret_key: &[u8]) -> Result<Self, CryptoError> {
|
||||||
|
let pk = sign::PublicKey::from_slice(public_key).ok_or(CryptoError::InvalidKeyLength)?;
|
||||||
|
let sk = sign::SecretKey::from_slice(secret_key).ok_or(CryptoError::InvalidKeyLength)?;
|
||||||
|
Ok(Self {
|
||||||
|
public_key: pk,
|
||||||
|
secret_key: sk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get public key as bytes
|
||||||
|
pub fn public_key_bytes(&self) -> &[u8] {
|
||||||
|
self.public_key.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get secret key as bytes
|
||||||
|
pub fn secret_key_bytes(&self) -> &[u8] {
|
||||||
|
self.secret_key.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode public key as base64
|
||||||
|
pub fn public_key_base64(&self) -> String {
|
||||||
|
BASE64.encode(self.public_key_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode secret key as base64
|
||||||
|
pub fn secret_key_base64(&self) -> String {
|
||||||
|
BASE64.encode(self.secret_key_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from base64-encoded keys
|
||||||
|
pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> {
|
||||||
|
let pk_bytes = BASE64.decode(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
let sk_bytes = BASE64.decode(secret_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
Self::from_keys(&pk_bytes, &sk_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign a message
|
||||||
|
/// Returns the signature prepended to the message (as per RustDesk protocol)
|
||||||
|
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
||||||
|
sign::sign(message, &self.secret_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign a message and return just the signature (64 bytes)
|
||||||
|
pub fn sign_detached(&self, message: &[u8]) -> [u8; 64] {
|
||||||
|
let sig = sign::sign_detached(message, &self.secret_key);
|
||||||
|
// Use as_ref() to access the signature bytes since the inner field is private
|
||||||
|
let sig_bytes: &[u8] = sig.as_ref();
|
||||||
|
let mut result = [0u8; 64];
|
||||||
|
result.copy_from_slice(sig_bytes);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Ed25519 public key to Curve25519 public key for encryption
|
||||||
|
///
|
||||||
|
/// This allows using the same keypair for both signing and encryption,
|
||||||
|
/// which is required by RustDesk's protocol where clients encrypt the
|
||||||
|
/// symmetric key using the public key from IdPk.
|
||||||
|
pub fn to_curve25519_pk(&self) -> Result<PublicKey, CryptoError> {
|
||||||
|
ed25519::to_curve25519_pk(&self.public_key)
|
||||||
|
.map_err(|_| CryptoError::KeyConversionFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Ed25519 secret key to Curve25519 secret key for decryption
|
||||||
|
///
|
||||||
|
/// This allows decrypting messages that were encrypted using the
|
||||||
|
/// converted public key.
|
||||||
|
pub fn to_curve25519_sk(&self) -> Result<SecretKey, CryptoError> {
|
||||||
|
ed25519::to_curve25519_sk(&self.secret_key)
|
||||||
|
.map_err(|_| CryptoError::KeyConversionFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a signed message
|
||||||
|
/// Returns the original message if signature is valid
|
||||||
|
pub fn verify_signed(signed_message: &[u8], public_key: &sign::PublicKey) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
sign::verify(signed_message, public_key).map_err(|_| CryptoError::SignatureVerificationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keypair_generation() {
|
||||||
|
let _ = init();
|
||||||
|
let keypair = KeyPair::generate();
|
||||||
|
assert_eq!(keypair.public_key_bytes().len(), 32);
|
||||||
|
assert_eq!(keypair.secret_key_bytes().len(), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keypair_serialization() {
|
||||||
|
let _ = init();
|
||||||
|
let keypair1 = KeyPair::generate();
|
||||||
|
let pk_b64 = keypair1.public_key_base64();
|
||||||
|
let sk_b64 = keypair1.secret_key_base64();
|
||||||
|
|
||||||
|
let keypair2 = KeyPair::from_base64(&pk_b64, &sk_b64).unwrap();
|
||||||
|
assert_eq!(keypair1.public_key_bytes(), keypair2.public_key_bytes());
|
||||||
|
assert_eq!(keypair1.secret_key_bytes(), keypair2.secret_key_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_box_encryption() {
|
||||||
|
let _ = init();
|
||||||
|
let alice = KeyPair::generate();
|
||||||
|
let bob = KeyPair::generate();
|
||||||
|
|
||||||
|
let message = b"Hello, RustDesk!";
|
||||||
|
let (nonce, ciphertext) = encrypt_box(message, &bob.public_key, &alice.secret_key);
|
||||||
|
|
||||||
|
let plaintext = decrypt_box(&ciphertext, &nonce, &alice.public_key, &bob.secret_key).unwrap();
|
||||||
|
assert_eq!(plaintext, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_password_hashing() {
|
||||||
|
let password = "test_password";
|
||||||
|
let salt = "random_salt";
|
||||||
|
|
||||||
|
let hash1 = hash_password(password, salt);
|
||||||
|
let hash2 = hash_password(password, salt);
|
||||||
|
assert_eq!(hash1, hash2);
|
||||||
|
|
||||||
|
assert!(verify_password(password, salt, &hash1));
|
||||||
|
assert!(!verify_password("wrong_password", salt, &hash1));
|
||||||
|
}
|
||||||
|
}
|
||||||
315
src/rustdesk/frame_adapters.rs
Normal file
315
src/rustdesk/frame_adapters.rs
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
//! RustDesk Frame Adapters
|
||||||
|
//!
|
||||||
|
//! Converts One-KVM video/audio frames to RustDesk protocol format.
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use prost::Message as ProstMessage;
|
||||||
|
|
||||||
|
use super::protocol::hbb::{self, message, EncodedVideoFrame, EncodedVideoFrames, AudioFrame, AudioFormat, Misc};
|
||||||
|
|
||||||
|
/// Video codec type for RustDesk
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum VideoCodec {
|
||||||
|
H264,
|
||||||
|
H265,
|
||||||
|
VP8,
|
||||||
|
VP9,
|
||||||
|
AV1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoCodec {
|
||||||
|
/// Get the codec ID for the RustDesk protocol
|
||||||
|
pub fn to_codec_id(self) -> i32 {
|
||||||
|
match self {
|
||||||
|
VideoCodec::H264 => 0,
|
||||||
|
VideoCodec::H265 => 1,
|
||||||
|
VideoCodec::VP8 => 2,
|
||||||
|
VideoCodec::VP9 => 3,
|
||||||
|
VideoCodec::AV1 => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Video frame adapter for converting to RustDesk format
|
||||||
|
pub struct VideoFrameAdapter {
|
||||||
|
/// Current codec
|
||||||
|
codec: VideoCodec,
|
||||||
|
/// Frame sequence number
|
||||||
|
seq: u32,
|
||||||
|
/// Timestamp offset
|
||||||
|
timestamp_base: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoFrameAdapter {
|
||||||
|
/// Create a new video frame adapter
|
||||||
|
pub fn new(codec: VideoCodec) -> Self {
|
||||||
|
Self {
|
||||||
|
codec,
|
||||||
|
seq: 0,
|
||||||
|
timestamp_base: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set codec type
|
||||||
|
pub fn set_codec(&mut self, codec: VideoCodec) {
|
||||||
|
self.codec = codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert encoded video data to RustDesk Message
|
||||||
|
pub fn encode_frame(&mut self, data: &[u8], is_keyframe: bool, timestamp_ms: u64) -> hbb::Message {
|
||||||
|
// Calculate relative timestamp
|
||||||
|
if self.seq == 0 {
|
||||||
|
self.timestamp_base = timestamp_ms;
|
||||||
|
}
|
||||||
|
let pts = (timestamp_ms - self.timestamp_base) as i64;
|
||||||
|
|
||||||
|
let frame = EncodedVideoFrame {
|
||||||
|
data: data.to_vec(),
|
||||||
|
key: is_keyframe,
|
||||||
|
pts,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.seq = self.seq.wrapping_add(1);
|
||||||
|
|
||||||
|
// Wrap in EncodedVideoFrames container
|
||||||
|
let frames = EncodedVideoFrames {
|
||||||
|
frames: vec![frame],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the appropriate VideoFrame variant based on codec
|
||||||
|
let video_frame = match self.codec {
|
||||||
|
VideoCodec::H264 => hbb::VideoFrame {
|
||||||
|
union: Some(hbb::video_frame::Union::H264s(frames)),
|
||||||
|
display: 0,
|
||||||
|
},
|
||||||
|
VideoCodec::H265 => hbb::VideoFrame {
|
||||||
|
union: Some(hbb::video_frame::Union::H265s(frames)),
|
||||||
|
display: 0,
|
||||||
|
},
|
||||||
|
VideoCodec::VP8 => hbb::VideoFrame {
|
||||||
|
union: Some(hbb::video_frame::Union::Vp8s(frames)),
|
||||||
|
display: 0,
|
||||||
|
},
|
||||||
|
VideoCodec::VP9 => hbb::VideoFrame {
|
||||||
|
union: Some(hbb::video_frame::Union::Vp9s(frames)),
|
||||||
|
display: 0,
|
||||||
|
},
|
||||||
|
VideoCodec::AV1 => hbb::VideoFrame {
|
||||||
|
union: Some(hbb::video_frame::Union::Av1s(frames)),
|
||||||
|
display: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
hbb::Message {
|
||||||
|
union: Some(message::Union::VideoFrame(video_frame)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode frame to bytes for sending
|
||||||
|
pub fn encode_frame_bytes(&mut self, data: &[u8], is_keyframe: bool, timestamp_ms: u64) -> Bytes {
|
||||||
|
let msg = self.encode_frame(data, is_keyframe, timestamp_ms);
|
||||||
|
Bytes::from(ProstMessage::encode_to_vec(&msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current sequence number
|
||||||
|
pub fn seq(&self) -> u32 {
|
||||||
|
self.seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Audio frame adapter for converting to RustDesk format
|
||||||
|
pub struct AudioFrameAdapter {
|
||||||
|
/// Sample rate
|
||||||
|
sample_rate: u32,
|
||||||
|
/// Channels
|
||||||
|
channels: u8,
|
||||||
|
/// Format sent flag
|
||||||
|
format_sent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFrameAdapter {
|
||||||
|
/// Create a new audio frame adapter
|
||||||
|
pub fn new(sample_rate: u32, channels: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
sample_rate,
|
||||||
|
channels,
|
||||||
|
format_sent: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create audio format message (should be sent once before audio frames)
|
||||||
|
pub fn create_format_message(&mut self) -> hbb::Message {
|
||||||
|
self.format_sent = true;
|
||||||
|
|
||||||
|
let format = AudioFormat {
|
||||||
|
sample_rate: self.sample_rate,
|
||||||
|
channels: self.channels as u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
hbb::Message {
|
||||||
|
union: Some(message::Union::Misc(Misc {
|
||||||
|
union: Some(hbb::misc::Union::AudioFormat(format)),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if format message has been sent
|
||||||
|
pub fn format_sent(&self) -> bool {
|
||||||
|
self.format_sent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Opus audio data to RustDesk Message
|
||||||
|
pub fn encode_opus_frame(&self, data: &[u8]) -> hbb::Message {
|
||||||
|
let frame = AudioFrame {
|
||||||
|
data: data.to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
hbb::Message {
|
||||||
|
union: Some(message::Union::AudioFrame(frame)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Opus frame to bytes for sending
|
||||||
|
pub fn encode_opus_bytes(&self, data: &[u8]) -> Bytes {
|
||||||
|
let msg = self.encode_opus_frame(data);
|
||||||
|
Bytes::from(ProstMessage::encode_to_vec(&msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset state (call when restarting audio stream)
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.format_sent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cursor data adapter
|
||||||
|
pub struct CursorAdapter;
|
||||||
|
|
||||||
|
impl CursorAdapter {
|
||||||
|
/// Create cursor data message
|
||||||
|
pub fn encode_cursor(
|
||||||
|
id: u64,
|
||||||
|
hotx: i32,
|
||||||
|
hoty: i32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
colors: Vec<u8>,
|
||||||
|
) -> hbb::Message {
|
||||||
|
let cursor = hbb::CursorData {
|
||||||
|
id,
|
||||||
|
hotx,
|
||||||
|
hoty,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
colors,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
hbb::Message {
|
||||||
|
union: Some(message::Union::CursorData(cursor)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create cursor position message
|
||||||
|
pub fn encode_position(x: i32, y: i32) -> hbb::Message {
|
||||||
|
let pos = hbb::CursorPosition {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
|
||||||
|
hbb::Message {
|
||||||
|
union: Some(message::Union::CursorPosition(pos)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_video_frame_encoding() {
|
||||||
|
let mut adapter = VideoFrameAdapter::new(VideoCodec::H264);
|
||||||
|
|
||||||
|
// Encode a keyframe
|
||||||
|
let data = vec![0x00, 0x00, 0x00, 0x01, 0x67]; // H264 SPS NAL
|
||||||
|
let msg = adapter.encode_frame(&data, true, 0);
|
||||||
|
|
||||||
|
match msg.union {
|
||||||
|
Some(message::Union::VideoFrame(vf)) => {
|
||||||
|
match vf.union {
|
||||||
|
Some(hbb::video_frame::Union::H264s(frames)) => {
|
||||||
|
assert_eq!(frames.frames.len(), 1);
|
||||||
|
assert!(frames.frames[0].key);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected H264s"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected VideoFrame"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_audio_format_message() {
|
||||||
|
let mut adapter = AudioFrameAdapter::new(48000, 2);
|
||||||
|
assert!(!adapter.format_sent());
|
||||||
|
|
||||||
|
let msg = adapter.create_format_message();
|
||||||
|
assert!(adapter.format_sent());
|
||||||
|
|
||||||
|
match msg.union {
|
||||||
|
Some(message::Union::Misc(misc)) => {
|
||||||
|
match misc.union {
|
||||||
|
Some(hbb::misc::Union::AudioFormat(fmt)) => {
|
||||||
|
assert_eq!(fmt.sample_rate, 48000);
|
||||||
|
assert_eq!(fmt.channels, 2);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AudioFormat"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Misc"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_audio_frame_encoding() {
|
||||||
|
let adapter = AudioFrameAdapter::new(48000, 2);
|
||||||
|
|
||||||
|
// Encode an Opus frame
|
||||||
|
let opus_data = vec![0xFC, 0x01, 0x02]; // Fake Opus data
|
||||||
|
let msg = adapter.encode_opus_frame(&opus_data);
|
||||||
|
|
||||||
|
match msg.union {
|
||||||
|
Some(message::Union::AudioFrame(af)) => {
|
||||||
|
assert_eq!(af.data, opus_data);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AudioFrame"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cursor_encoding() {
|
||||||
|
let msg = CursorAdapter::encode_cursor(1, 0, 0, 16, 16, vec![0xFF; 16 * 16 * 4]);
|
||||||
|
|
||||||
|
match msg.union {
|
||||||
|
Some(message::Union::CursorData(cd)) => {
|
||||||
|
assert_eq!(cd.id, 1);
|
||||||
|
assert_eq!(cd.width, 16);
|
||||||
|
assert_eq!(cd.height, 16);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected CursorData"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sequence_increment() {
|
||||||
|
let mut adapter = VideoFrameAdapter::new(VideoCodec::H264);
|
||||||
|
|
||||||
|
assert_eq!(adapter.seq(), 0);
|
||||||
|
adapter.encode_frame(&[0], false, 0);
|
||||||
|
assert_eq!(adapter.seq(), 1);
|
||||||
|
adapter.encode_frame(&[0], false, 33);
|
||||||
|
assert_eq!(adapter.seq(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
385
src/rustdesk/hid_adapter.rs
Normal file
385
src/rustdesk/hid_adapter.rs
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
//! RustDesk HID Adapter
|
||||||
|
//!
|
||||||
|
//! Converts RustDesk HID events (KeyEvent, MouseEvent) to One-KVM HID events.
|
||||||
|
|
||||||
|
use crate::hid::{
|
||||||
|
KeyboardEvent, KeyboardModifiers, KeyEventType,
|
||||||
|
MouseButton, MouseEvent as OneKvmMouseEvent, MouseEventType,
|
||||||
|
};
|
||||||
|
use super::protocol::hbb::{self, ControlKey, KeyEvent, MouseEvent};
|
||||||
|
|
||||||
|
/// Mouse event types from RustDesk protocol
|
||||||
|
/// mask = (button << 3) | event_type
|
||||||
|
pub mod mouse_type {
|
||||||
|
pub const MOVE: i32 = 0;
|
||||||
|
pub const DOWN: i32 = 1;
|
||||||
|
pub const UP: i32 = 2;
|
||||||
|
pub const WHEEL: i32 = 3;
|
||||||
|
pub const TRACKPAD: i32 = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mouse button IDs from RustDesk protocol (before left shift by 3)
|
||||||
|
pub mod mouse_button {
|
||||||
|
pub const LEFT: i32 = 0x01;
|
||||||
|
pub const RIGHT: i32 = 0x02;
|
||||||
|
pub const WHEEL: i32 = 0x04;
|
||||||
|
pub const BACK: i32 = 0x08;
|
||||||
|
pub const FORWARD: i32 = 0x10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert RustDesk MouseEvent to One-KVM MouseEvent(s)
|
||||||
|
/// Returns a Vec because a single RustDesk event may need multiple One-KVM events
|
||||||
|
/// (e.g., move + button + scroll)
|
||||||
|
pub fn convert_mouse_event(event: &MouseEvent, screen_width: u32, screen_height: u32) -> Vec<OneKvmMouseEvent> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
// RustDesk uses absolute coordinates
|
||||||
|
let x = event.x.max(0) as u32;
|
||||||
|
let y = event.y.max(0) as u32;
|
||||||
|
|
||||||
|
// Normalize to 0-32767 range for absolute mouse (USB HID standard)
|
||||||
|
let abs_x = ((x as u64 * 32767) / screen_width.max(1) as u64) as i32;
|
||||||
|
let abs_y = ((y as u64 * 32767) / screen_height.max(1) as u64) as i32;
|
||||||
|
|
||||||
|
// Parse RustDesk mask format: (button << 3) | event_type
|
||||||
|
let event_type = event.mask & 0x07;
|
||||||
|
let button_id = event.mask >> 3;
|
||||||
|
|
||||||
|
match event_type {
|
||||||
|
mouse_type::MOVE => {
|
||||||
|
// Pure move event
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::MoveAbs,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: None,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mouse_type::DOWN => {
|
||||||
|
// Button down - first move, then press
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::MoveAbs,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: None,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(button) = button_id_to_button(button_id) {
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::Down,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: Some(button),
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouse_type::UP => {
|
||||||
|
// Button up - first move, then release
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::MoveAbs,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: None,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(button) = button_id_to_button(button_id) {
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::Up,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: Some(button),
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouse_type::WHEEL => {
|
||||||
|
// Scroll event - move first, then scroll
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::MoveAbs,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: None,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// For wheel events, button_id indicates scroll direction
|
||||||
|
// Positive = scroll up, Negative = scroll down
|
||||||
|
// The actual scroll amount may be encoded differently
|
||||||
|
let scroll = if button_id > 0 { 1i8 } else { -1i8 };
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::Scroll,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: None,
|
||||||
|
scroll,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Unknown event type, just move
|
||||||
|
events.push(OneKvmMouseEvent {
|
||||||
|
event_type: MouseEventType::MoveAbs,
|
||||||
|
x: abs_x,
|
||||||
|
y: abs_y,
|
||||||
|
button: None,
|
||||||
|
scroll: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert RustDesk button ID to One-KVM MouseButton
|
||||||
|
fn button_id_to_button(button_id: i32) -> Option<MouseButton> {
|
||||||
|
match button_id {
|
||||||
|
mouse_button::LEFT => Some(MouseButton::Left),
|
||||||
|
mouse_button::RIGHT => Some(MouseButton::Right),
|
||||||
|
mouse_button::WHEEL => Some(MouseButton::Middle),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert RustDesk KeyEvent to One-KVM KeyboardEvent
|
||||||
|
pub fn convert_key_event(event: &KeyEvent) -> Option<KeyboardEvent> {
|
||||||
|
let pressed = event.down || event.press;
|
||||||
|
let event_type = if pressed { KeyEventType::Down } else { KeyEventType::Up };
|
||||||
|
|
||||||
|
// Parse modifiers from the event
|
||||||
|
let modifiers = parse_modifiers(event);
|
||||||
|
|
||||||
|
// Handle control keys
|
||||||
|
if let Some(hbb::key_event::Union::ControlKey(ck)) = &event.union {
|
||||||
|
if let Some(key) = control_key_to_hid(*ck) {
|
||||||
|
return Some(KeyboardEvent {
|
||||||
|
event_type,
|
||||||
|
key,
|
||||||
|
modifiers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle character keys (chr field contains platform-specific keycode)
|
||||||
|
if let Some(hbb::key_event::Union::Chr(chr)) = &event.union {
|
||||||
|
// chr contains USB HID scancode on Windows, X11 keycode on Linux
|
||||||
|
if let Some(key) = keycode_to_hid(*chr) {
|
||||||
|
return Some(KeyboardEvent {
|
||||||
|
event_type,
|
||||||
|
key,
|
||||||
|
modifiers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unicode (for text input, we'd need to convert to scancodes)
|
||||||
|
// Unicode input requires more complex handling, skip for now
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse modifier keys from RustDesk KeyEvent into KeyboardModifiers
|
||||||
|
fn parse_modifiers(event: &KeyEvent) -> KeyboardModifiers {
|
||||||
|
let mut modifiers = KeyboardModifiers::default();
|
||||||
|
|
||||||
|
for modifier in &event.modifiers {
|
||||||
|
match *modifier {
|
||||||
|
x if x == ControlKey::Control as i32 => modifiers.left_ctrl = true,
|
||||||
|
x if x == ControlKey::Shift as i32 => modifiers.left_shift = true,
|
||||||
|
x if x == ControlKey::Alt as i32 => modifiers.left_alt = true,
|
||||||
|
x if x == ControlKey::Meta as i32 => modifiers.left_meta = true,
|
||||||
|
x if x == ControlKey::RControl as i32 => modifiers.right_ctrl = true,
|
||||||
|
x if x == ControlKey::RShift as i32 => modifiers.right_shift = true,
|
||||||
|
x if x == ControlKey::RAlt as i32 => modifiers.right_alt = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert RustDesk ControlKey to USB HID usage code
|
||||||
|
fn control_key_to_hid(key: i32) -> Option<u8> {
|
||||||
|
match key {
|
||||||
|
x if x == ControlKey::Alt as i32 => Some(0xE2), // Left Alt
|
||||||
|
x if x == ControlKey::Backspace as i32 => Some(0x2A),
|
||||||
|
x if x == ControlKey::CapsLock as i32 => Some(0x39),
|
||||||
|
x if x == ControlKey::Control as i32 => Some(0xE0), // Left Ctrl
|
||||||
|
x if x == ControlKey::Delete as i32 => Some(0x4C),
|
||||||
|
x if x == ControlKey::DownArrow as i32 => Some(0x51),
|
||||||
|
x if x == ControlKey::End as i32 => Some(0x4D),
|
||||||
|
x if x == ControlKey::Escape as i32 => Some(0x29),
|
||||||
|
x if x == ControlKey::F1 as i32 => Some(0x3A),
|
||||||
|
x if x == ControlKey::F2 as i32 => Some(0x3B),
|
||||||
|
x if x == ControlKey::F3 as i32 => Some(0x3C),
|
||||||
|
x if x == ControlKey::F4 as i32 => Some(0x3D),
|
||||||
|
x if x == ControlKey::F5 as i32 => Some(0x3E),
|
||||||
|
x if x == ControlKey::F6 as i32 => Some(0x3F),
|
||||||
|
x if x == ControlKey::F7 as i32 => Some(0x40),
|
||||||
|
x if x == ControlKey::F8 as i32 => Some(0x41),
|
||||||
|
x if x == ControlKey::F9 as i32 => Some(0x42),
|
||||||
|
x if x == ControlKey::F10 as i32 => Some(0x43),
|
||||||
|
x if x == ControlKey::F11 as i32 => Some(0x44),
|
||||||
|
x if x == ControlKey::F12 as i32 => Some(0x45),
|
||||||
|
x if x == ControlKey::Home as i32 => Some(0x4A),
|
||||||
|
x if x == ControlKey::LeftArrow as i32 => Some(0x50),
|
||||||
|
x if x == ControlKey::Meta as i32 => Some(0xE3), // Left GUI/Windows
|
||||||
|
x if x == ControlKey::PageDown as i32 => Some(0x4E),
|
||||||
|
x if x == ControlKey::PageUp as i32 => Some(0x4B),
|
||||||
|
x if x == ControlKey::Return as i32 => Some(0x28),
|
||||||
|
x if x == ControlKey::RightArrow as i32 => Some(0x4F),
|
||||||
|
x if x == ControlKey::Shift as i32 => Some(0xE1), // Left Shift
|
||||||
|
x if x == ControlKey::Space as i32 => Some(0x2C),
|
||||||
|
x if x == ControlKey::Tab as i32 => Some(0x2B),
|
||||||
|
x if x == ControlKey::UpArrow as i32 => Some(0x52),
|
||||||
|
x if x == ControlKey::Numpad0 as i32 => Some(0x62),
|
||||||
|
x if x == ControlKey::Numpad1 as i32 => Some(0x59),
|
||||||
|
x if x == ControlKey::Numpad2 as i32 => Some(0x5A),
|
||||||
|
x if x == ControlKey::Numpad3 as i32 => Some(0x5B),
|
||||||
|
x if x == ControlKey::Numpad4 as i32 => Some(0x5C),
|
||||||
|
x if x == ControlKey::Numpad5 as i32 => Some(0x5D),
|
||||||
|
x if x == ControlKey::Numpad6 as i32 => Some(0x5E),
|
||||||
|
x if x == ControlKey::Numpad7 as i32 => Some(0x5F),
|
||||||
|
x if x == ControlKey::Numpad8 as i32 => Some(0x60),
|
||||||
|
x if x == ControlKey::Numpad9 as i32 => Some(0x61),
|
||||||
|
x if x == ControlKey::Insert as i32 => Some(0x49),
|
||||||
|
x if x == ControlKey::Pause as i32 => Some(0x48),
|
||||||
|
x if x == ControlKey::Scroll as i32 => Some(0x47),
|
||||||
|
x if x == ControlKey::NumLock as i32 => Some(0x53),
|
||||||
|
x if x == ControlKey::RShift as i32 => Some(0xE5),
|
||||||
|
x if x == ControlKey::RControl as i32 => Some(0xE4),
|
||||||
|
x if x == ControlKey::RAlt as i32 => Some(0xE6),
|
||||||
|
x if x == ControlKey::Multiply as i32 => Some(0x55),
|
||||||
|
x if x == ControlKey::Add as i32 => Some(0x57),
|
||||||
|
x if x == ControlKey::Subtract as i32 => Some(0x56),
|
||||||
|
x if x == ControlKey::Decimal as i32 => Some(0x63),
|
||||||
|
x if x == ControlKey::Divide as i32 => Some(0x54),
|
||||||
|
x if x == ControlKey::NumpadEnter as i32 => Some(0x58),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert platform keycode to USB HID usage code
|
||||||
|
/// This is a simplified mapping for X11 keycodes (Linux)
|
||||||
|
fn keycode_to_hid(keycode: u32) -> Option<u8> {
|
||||||
|
match keycode {
|
||||||
|
// Numbers 1-9 then 0 (X11 keycodes 10-19)
|
||||||
|
10 => Some(0x27), // 0
|
||||||
|
11..=19 => Some((keycode - 11 + 0x1E) as u8), // 1-9
|
||||||
|
|
||||||
|
// Punctuation before letters block
|
||||||
|
20 => Some(0x2D), // -
|
||||||
|
21 => Some(0x2E), // =
|
||||||
|
34 => Some(0x2F), // [
|
||||||
|
35 => Some(0x30), // ]
|
||||||
|
|
||||||
|
// Letters A-Z (X11 keycodes 38-63 map to various letters, not strictly A-Z)
|
||||||
|
// Note: X11 keycodes are row-based, not alphabetical
|
||||||
|
// Row 1: q(24) w(25) e(26) r(27) t(28) y(29) u(30) i(31) o(32) p(33)
|
||||||
|
// Row 2: a(38) s(39) d(40) f(41) g(42) h(43) j(44) k(45) l(46)
|
||||||
|
// Row 3: z(52) x(53) c(54) v(55) b(56) n(57) m(58)
|
||||||
|
24 => Some(0x14), // q
|
||||||
|
25 => Some(0x1A), // w
|
||||||
|
26 => Some(0x08), // e
|
||||||
|
27 => Some(0x15), // r
|
||||||
|
28 => Some(0x17), // t
|
||||||
|
29 => Some(0x1C), // y
|
||||||
|
30 => Some(0x18), // u
|
||||||
|
31 => Some(0x0C), // i
|
||||||
|
32 => Some(0x12), // o
|
||||||
|
33 => Some(0x13), // p
|
||||||
|
38 => Some(0x04), // a
|
||||||
|
39 => Some(0x16), // s
|
||||||
|
40 => Some(0x07), // d
|
||||||
|
41 => Some(0x09), // f
|
||||||
|
42 => Some(0x0A), // g
|
||||||
|
43 => Some(0x0B), // h
|
||||||
|
44 => Some(0x0D), // j
|
||||||
|
45 => Some(0x0E), // k
|
||||||
|
46 => Some(0x0F), // l
|
||||||
|
47 => Some(0x33), // ; (semicolon)
|
||||||
|
48 => Some(0x34), // ' (apostrophe)
|
||||||
|
49 => Some(0x35), // ` (grave)
|
||||||
|
51 => Some(0x31), // \ (backslash)
|
||||||
|
52 => Some(0x1D), // z
|
||||||
|
53 => Some(0x1B), // x
|
||||||
|
54 => Some(0x06), // c
|
||||||
|
55 => Some(0x19), // v
|
||||||
|
56 => Some(0x05), // b
|
||||||
|
57 => Some(0x11), // n
|
||||||
|
58 => Some(0x10), // m
|
||||||
|
59 => Some(0x36), // , (comma)
|
||||||
|
60 => Some(0x37), // . (period)
|
||||||
|
61 => Some(0x38), // / (slash)
|
||||||
|
|
||||||
|
// Space
|
||||||
|
65 => Some(0x2C),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_mouse_buttons() {
|
||||||
|
let buttons = parse_mouse_buttons(mouse_mask::LEFT | mouse_mask::RIGHT);
|
||||||
|
assert!(buttons.contains(&MouseButton::Left));
|
||||||
|
assert!(buttons.contains(&MouseButton::Right));
|
||||||
|
assert!(!buttons.contains(&MouseButton::Middle));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_scroll() {
|
||||||
|
assert_eq!(parse_scroll(mouse_mask::SCROLL_UP), 1);
|
||||||
|
assert_eq!(parse_scroll(mouse_mask::SCROLL_DOWN), -1);
|
||||||
|
assert_eq!(parse_scroll(0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_control_key_mapping() {
|
||||||
|
assert_eq!(control_key_to_hid(ControlKey::Escape as i32), Some(0x29));
|
||||||
|
assert_eq!(control_key_to_hid(ControlKey::Return as i32), Some(0x28));
|
||||||
|
assert_eq!(control_key_to_hid(ControlKey::Space as i32), Some(0x2C));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convert_mouse_event() {
|
||||||
|
let rustdesk_event = MouseEvent {
|
||||||
|
x: 500,
|
||||||
|
y: 300,
|
||||||
|
mask: mouse_mask::LEFT,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = convert_mouse_event(&rustdesk_event, 1920, 1080);
|
||||||
|
assert!(!events.is_empty());
|
||||||
|
|
||||||
|
// First event should be MoveAbs
|
||||||
|
assert_eq!(events[0].event_type, MouseEventType::MoveAbs);
|
||||||
|
|
||||||
|
// Should have a button down event
|
||||||
|
assert!(events.iter().any(|e| e.event_type == MouseEventType::Down && e.button == Some(MouseButton::Left)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_convert_key_event() {
|
||||||
|
let key_event = KeyEvent {
|
||||||
|
down: true,
|
||||||
|
press: false,
|
||||||
|
union: Some(hbb::key_event::Union::ControlKey(ControlKey::Return as i32)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = convert_key_event(&key_event);
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let kb_event = result.unwrap();
|
||||||
|
assert_eq!(kb_event.event_type, KeyEventType::Down);
|
||||||
|
assert_eq!(kb_event.key, 0x28); // Return key USB HID code
|
||||||
|
}
|
||||||
|
}
|
||||||
587
src/rustdesk/mod.rs
Normal file
587
src/rustdesk/mod.rs
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
//! RustDesk Protocol Integration Module
|
||||||
|
//!
|
||||||
|
//! This module implements the RustDesk client protocol, enabling One-KVM devices
|
||||||
|
//! to be accessed via standard RustDesk clients through existing hbbs/hbbr servers.
|
||||||
|
//!
|
||||||
|
//! ## Architecture
|
||||||
|
//!
|
||||||
|
//! - `config`: Configuration types for RustDesk settings
|
||||||
|
//! - `protocol`: Protobuf message wrappers and serialization
|
||||||
|
//! - `crypto`: NaCl cryptography (key generation, encryption, signatures)
|
||||||
|
//! - `rendezvous`: Communication with hbbs rendezvous server
|
||||||
|
//! - `connection`: Client session handling
|
||||||
|
//! - `frame_adapters`: Video/audio frame conversion to RustDesk format
|
||||||
|
//! - `hid_adapter`: RustDesk HID events to One-KVM conversion
|
||||||
|
|
||||||
|
pub mod bytes_codec;
|
||||||
|
pub mod config;
|
||||||
|
pub mod connection;
|
||||||
|
pub mod crypto;
|
||||||
|
pub mod frame_adapters;
|
||||||
|
pub mod hid_adapter;
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod rendezvous;
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use prost::Message;
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use crate::audio::AudioController;
|
||||||
|
use crate::hid::HidController;
|
||||||
|
use crate::video::stream_manager::VideoStreamManager;
|
||||||
|
|
||||||
|
use self::config::RustDeskConfig;
|
||||||
|
use self::connection::ConnectionManager;
|
||||||
|
use self::protocol::hbb::rendezvous_message;
|
||||||
|
use self::protocol::{make_local_addr, make_relay_response, RendezvousMessage};
|
||||||
|
use self::rendezvous::{AddrMangle, RendezvousMediator, RendezvousStatus};
|
||||||
|
|
||||||
|
/// Relay connection timeout
|
||||||
|
const RELAY_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
||||||
|
|
||||||
|
/// RustDesk service status
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum ServiceStatus {
|
||||||
|
/// Service is stopped
|
||||||
|
Stopped,
|
||||||
|
/// Service is starting
|
||||||
|
Starting,
|
||||||
|
/// Service is running and registered with rendezvous server
|
||||||
|
Running,
|
||||||
|
/// Service encountered an error
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ServiceStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Stopped => write!(f, "stopped"),
|
||||||
|
Self::Starting => write!(f, "starting"),
|
||||||
|
Self::Running => write!(f, "running"),
|
||||||
|
Self::Error(e) => write!(f, "error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default port for direct TCP connections (same as RustDesk)
|
||||||
|
const DIRECT_LISTEN_PORT: u16 = 21118;
|
||||||
|
|
||||||
|
/// RustDesk Service
|
||||||
|
///
|
||||||
|
/// Manages the RustDesk protocol integration, including:
|
||||||
|
/// - Registration with hbbs rendezvous server
|
||||||
|
/// - Accepting connections from RustDesk clients
|
||||||
|
/// - Streaming video/audio and receiving HID input
|
||||||
|
pub struct RustDeskService {
|
||||||
|
config: Arc<RwLock<RustDeskConfig>>,
|
||||||
|
status: Arc<RwLock<ServiceStatus>>,
|
||||||
|
rendezvous: Arc<RwLock<Option<Arc<RendezvousMediator>>>>,
|
||||||
|
rendezvous_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||||
|
tcp_listener_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||||
|
listen_port: Arc<RwLock<u16>>,
|
||||||
|
connection_manager: Arc<ConnectionManager>,
|
||||||
|
video_manager: Arc<VideoStreamManager>,
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
audio: Arc<AudioController>,
|
||||||
|
shutdown_tx: broadcast::Sender<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustDeskService {
|
||||||
|
/// Create a new RustDesk service instance
|
||||||
|
pub fn new(
|
||||||
|
config: RustDeskConfig,
|
||||||
|
video_manager: Arc<VideoStreamManager>,
|
||||||
|
hid: Arc<HidController>,
|
||||||
|
audio: Arc<AudioController>,
|
||||||
|
) -> Self {
|
||||||
|
let (shutdown_tx, _) = broadcast::channel(1);
|
||||||
|
let connection_manager = Arc::new(ConnectionManager::new(config.clone()));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config: Arc::new(RwLock::new(config)),
|
||||||
|
status: Arc::new(RwLock::new(ServiceStatus::Stopped)),
|
||||||
|
rendezvous: Arc::new(RwLock::new(None)),
|
||||||
|
rendezvous_handle: Arc::new(RwLock::new(None)),
|
||||||
|
tcp_listener_handle: Arc::new(RwLock::new(None)),
|
||||||
|
listen_port: Arc::new(RwLock::new(DIRECT_LISTEN_PORT)),
|
||||||
|
connection_manager,
|
||||||
|
video_manager,
|
||||||
|
hid,
|
||||||
|
audio,
|
||||||
|
shutdown_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the port for direct TCP connections
|
||||||
|
pub fn listen_port(&self) -> u16 {
|
||||||
|
*self.listen_port.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current service status
|
||||||
|
pub fn status(&self) -> ServiceStatus {
|
||||||
|
self.status.read().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current configuration
|
||||||
|
pub fn config(&self) -> RustDeskConfig {
|
||||||
|
self.config.read().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update configuration
|
||||||
|
pub fn update_config(&self, config: RustDeskConfig) {
|
||||||
|
*self.config.write() = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get rendezvous status
|
||||||
|
pub fn rendezvous_status(&self) -> Option<RendezvousStatus> {
|
||||||
|
self.rendezvous.read().as_ref().map(|r| r.status())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get device ID
|
||||||
|
pub fn device_id(&self) -> String {
|
||||||
|
self.config.read().device_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get connection count
|
||||||
|
pub fn connection_count(&self) -> usize {
|
||||||
|
self.connection_manager.connection_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the RustDesk service
|
||||||
|
pub async fn start(&self) -> anyhow::Result<()> {
|
||||||
|
let config = self.config.read().clone();
|
||||||
|
|
||||||
|
if !config.enabled {
|
||||||
|
info!("RustDesk service is disabled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.is_valid() {
|
||||||
|
warn!("RustDesk configuration is incomplete");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.status() == ServiceStatus::Running {
|
||||||
|
warn!("RustDesk service is already running");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.status.write() = ServiceStatus::Starting;
|
||||||
|
info!(
|
||||||
|
"Starting RustDesk service with ID: {} -> {}",
|
||||||
|
config.device_id,
|
||||||
|
config.rendezvous_addr()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize crypto
|
||||||
|
if let Err(e) = crypto::init() {
|
||||||
|
error!("Failed to initialize crypto: {}", e);
|
||||||
|
*self.status.write() = ServiceStatus::Error(e.to_string());
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and start rendezvous mediator with relay callback
|
||||||
|
let mediator = Arc::new(RendezvousMediator::new(config.clone()));
|
||||||
|
|
||||||
|
// Set the keypair on connection manager (Curve25519 for encryption)
|
||||||
|
let keypair = mediator.ensure_keypair();
|
||||||
|
self.connection_manager.set_keypair(keypair);
|
||||||
|
|
||||||
|
// Set the signing keypair on connection manager (Ed25519 for SignedId)
|
||||||
|
let signing_keypair = mediator.ensure_signing_keypair();
|
||||||
|
self.connection_manager.set_signing_keypair(signing_keypair);
|
||||||
|
|
||||||
|
// Set the HID controller on connection manager
|
||||||
|
self.connection_manager.set_hid(self.hid.clone());
|
||||||
|
|
||||||
|
// Set the video manager on connection manager for video streaming
|
||||||
|
self.connection_manager.set_video_manager(self.video_manager.clone());
|
||||||
|
|
||||||
|
*self.rendezvous.write() = Some(mediator.clone());
|
||||||
|
|
||||||
|
// Start TCP listener BEFORE the rendezvous mediator to ensure port is set correctly
|
||||||
|
// This prevents race condition where mediator starts registration with wrong port
|
||||||
|
let (tcp_handle, listen_port) = self.start_tcp_listener_with_port().await?;
|
||||||
|
*self.tcp_listener_handle.write() = Some(tcp_handle);
|
||||||
|
|
||||||
|
// Set the listen port on mediator before starting the registration loop
|
||||||
|
mediator.set_listen_port(listen_port);
|
||||||
|
|
||||||
|
// Create relay request handler
|
||||||
|
let connection_manager = self.connection_manager.clone();
|
||||||
|
let video_manager = self.video_manager.clone();
|
||||||
|
let hid = self.hid.clone();
|
||||||
|
let audio = self.audio.clone();
|
||||||
|
let service_config = self.config.clone();
|
||||||
|
|
||||||
|
// Set the relay callback on the mediator
|
||||||
|
mediator.set_relay_callback(Arc::new(move |relay_server, uuid, peer_pk| {
|
||||||
|
let conn_mgr = connection_manager.clone();
|
||||||
|
let video = video_manager.clone();
|
||||||
|
let hid = hid.clone();
|
||||||
|
let audio = audio.clone();
|
||||||
|
let config = service_config.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_relay_request(
|
||||||
|
&relay_server,
|
||||||
|
&uuid,
|
||||||
|
&peer_pk,
|
||||||
|
conn_mgr,
|
||||||
|
video,
|
||||||
|
hid,
|
||||||
|
audio,
|
||||||
|
config,
|
||||||
|
).await {
|
||||||
|
error!("Failed to handle relay request: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set the intranet callback on the mediator for same-LAN connections
|
||||||
|
let connection_manager2 = self.connection_manager.clone();
|
||||||
|
mediator.set_intranet_callback(Arc::new(move |rendezvous_addr, peer_socket_addr, local_addr, relay_server, device_id| {
|
||||||
|
let conn_mgr = connection_manager2.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_intranet_request(
|
||||||
|
&rendezvous_addr,
|
||||||
|
&peer_socket_addr,
|
||||||
|
local_addr,
|
||||||
|
&relay_server,
|
||||||
|
&device_id,
|
||||||
|
conn_mgr,
|
||||||
|
).await {
|
||||||
|
error!("Failed to handle intranet request: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Spawn rendezvous task
|
||||||
|
let status = self.status.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match mediator.start().await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Rendezvous mediator stopped normally");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Rendezvous mediator error: {}", e);
|
||||||
|
*status.write() = ServiceStatus::Error(e.to_string());
|
||||||
|
// Wait before retry
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
*status.write() = ServiceStatus::Starting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
*self.rendezvous_handle.write() = Some(handle);
|
||||||
|
|
||||||
|
*self.status.write() = ServiceStatus::Running;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start TCP listener for direct peer connections
|
||||||
|
/// Returns the join handle and the port that was bound
|
||||||
|
async fn start_tcp_listener_with_port(&self) -> anyhow::Result<(JoinHandle<()>, u16)> {
|
||||||
|
// Try to bind to the default port, or find an available port
|
||||||
|
let listener = match TcpListener::bind(format!("0.0.0.0:{}", DIRECT_LISTEN_PORT)).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(_) => {
|
||||||
|
// Try binding to port 0 to get an available port
|
||||||
|
TcpListener::bind("0.0.0.0:0").await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let local_addr = listener.local_addr()?;
|
||||||
|
let listen_port = local_addr.port();
|
||||||
|
*self.listen_port.write() = listen_port;
|
||||||
|
info!("RustDesk TCP listener started on {}", local_addr);
|
||||||
|
|
||||||
|
let connection_manager = self.connection_manager.clone();
|
||||||
|
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = listener.accept() => {
|
||||||
|
match result {
|
||||||
|
Ok((stream, peer_addr)) => {
|
||||||
|
info!("Accepted direct connection from {}", peer_addr);
|
||||||
|
let conn_mgr = connection_manager.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = conn_mgr.accept_connection(stream, peer_addr).await {
|
||||||
|
error!("Failed to handle direct connection from {}: {}", peer_addr, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("TCP accept error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = shutdown_rx.recv() => {
|
||||||
|
info!("TCP listener shutting down");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((handle, listen_port))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the RustDesk service
|
||||||
|
pub async fn stop(&self) -> anyhow::Result<()> {
|
||||||
|
if self.status() == ServiceStatus::Stopped {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Stopping RustDesk service");
|
||||||
|
|
||||||
|
// Send shutdown signal (this will stop the TCP listener)
|
||||||
|
let _ = self.shutdown_tx.send(());
|
||||||
|
|
||||||
|
// Close all connections
|
||||||
|
self.connection_manager.close_all();
|
||||||
|
|
||||||
|
// Stop rendezvous mediator
|
||||||
|
if let Some(mediator) = self.rendezvous.read().as_ref() {
|
||||||
|
mediator.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for rendezvous task to finish
|
||||||
|
if let Some(handle) = self.rendezvous_handle.write().take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for TCP listener task to finish
|
||||||
|
if let Some(handle) = self.tcp_listener_handle.write().take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.rendezvous.write() = None;
|
||||||
|
*self.status.write() = ServiceStatus::Stopped;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restart the service with new configuration
|
||||||
|
pub async fn restart(&self, config: RustDeskConfig) -> anyhow::Result<()> {
|
||||||
|
self.stop().await?;
|
||||||
|
self.update_config(config);
|
||||||
|
self.start().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a shutdown receiver for graceful shutdown handling
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn shutdown_rx(&self) -> broadcast::Receiver<()> {
|
||||||
|
self.shutdown_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save keypair and UUID to config
|
||||||
|
/// Returns the updated config if changes were made
|
||||||
|
pub fn save_credentials(&self) -> Option<RustDeskConfig> {
|
||||||
|
if let Some(mediator) = self.rendezvous.read().as_ref() {
|
||||||
|
let kp = mediator.ensure_keypair();
|
||||||
|
let skp = mediator.ensure_signing_keypair();
|
||||||
|
let mut config = self.config.write();
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
// Save encryption keypair (Curve25519)
|
||||||
|
let pk = kp.public_key_base64();
|
||||||
|
let sk = kp.secret_key_base64();
|
||||||
|
if config.public_key.as_ref() != Some(&pk) || config.private_key.as_ref() != Some(&sk) {
|
||||||
|
config.public_key = Some(pk);
|
||||||
|
config.private_key = Some(sk);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save signing keypair (Ed25519)
|
||||||
|
let signing_pk = skp.public_key_base64();
|
||||||
|
let signing_sk = skp.secret_key_base64();
|
||||||
|
if config.signing_public_key.as_ref() != Some(&signing_pk) || config.signing_private_key.as_ref() != Some(&signing_sk) {
|
||||||
|
config.signing_public_key = Some(signing_pk);
|
||||||
|
config.signing_private_key = Some(signing_sk);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save UUID if it was newly generated
|
||||||
|
if mediator.uuid_needs_save() {
|
||||||
|
let mediator_config = mediator.config();
|
||||||
|
if let Some(uuid) = mediator_config.uuid {
|
||||||
|
if config.uuid.as_ref() != Some(&uuid) {
|
||||||
|
config.uuid = Some(uuid);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediator.mark_uuid_saved();
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
return Some(config.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save keypair to config (deprecated, use save_credentials instead)
|
||||||
|
#[deprecated(note = "Use save_credentials instead")]
|
||||||
|
pub fn save_keypair(&self) {
|
||||||
|
let _ = self.save_credentials();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle relay request from rendezvous server
|
||||||
|
async fn handle_relay_request(
|
||||||
|
relay_server: &str,
|
||||||
|
uuid: &str,
|
||||||
|
_peer_pk: &[u8],
|
||||||
|
connection_manager: Arc<ConnectionManager>,
|
||||||
|
_video_manager: Arc<VideoStreamManager>,
|
||||||
|
_hid: Arc<HidController>,
|
||||||
|
_audio: Arc<AudioController>,
|
||||||
|
_config: Arc<RwLock<RustDeskConfig>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
info!("Handling relay request: server={}, uuid={}", relay_server, uuid);
|
||||||
|
|
||||||
|
// Parse relay server address
|
||||||
|
let relay_addr: SocketAddr = tokio::net::lookup_host(relay_server)
|
||||||
|
.await?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Failed to resolve relay server: {}", relay_server))?;
|
||||||
|
|
||||||
|
// Connect to relay server with timeout
|
||||||
|
let mut stream = tokio::time::timeout(
|
||||||
|
Duration::from_millis(RELAY_CONNECT_TIMEOUT_MS),
|
||||||
|
TcpStream::connect(relay_addr),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("Relay connection timeout"))??;
|
||||||
|
|
||||||
|
info!("Connected to relay server at {}", relay_addr);
|
||||||
|
|
||||||
|
// Send relay response to establish the connection
|
||||||
|
let relay_response = make_relay_response(uuid, None);
|
||||||
|
let bytes = relay_response.encode_to_vec();
|
||||||
|
|
||||||
|
// Send using RustDesk's variable-length framing (NOT big-endian length prefix)
|
||||||
|
bytes_codec::write_frame(&mut stream, &bytes).await?;
|
||||||
|
|
||||||
|
debug!("Sent relay response for uuid={}", uuid);
|
||||||
|
|
||||||
|
// Read response from relay using variable-length framing
|
||||||
|
let msg_buf = bytes_codec::read_frame(&mut stream).await?;
|
||||||
|
|
||||||
|
// Parse relay response
|
||||||
|
if let Ok(msg) = RendezvousMessage::decode(&msg_buf[..]) {
|
||||||
|
match msg.union {
|
||||||
|
Some(rendezvous_message::Union::RelayResponse(rr)) => {
|
||||||
|
debug!("Received relay response: uuid={}, socket_addr_len={}", rr.uuid, rr.socket_addr.len());
|
||||||
|
|
||||||
|
// Try to decode peer address from the relay response
|
||||||
|
// The socket_addr field contains the actual peer's address (mangled)
|
||||||
|
let peer_addr = if !rr.socket_addr.is_empty() {
|
||||||
|
rendezvous::AddrMangle::decode(&rr.socket_addr)
|
||||||
|
.unwrap_or(relay_addr)
|
||||||
|
} else {
|
||||||
|
// If no socket_addr in response, use a placeholder
|
||||||
|
// Note: This is not ideal, but allows the connection to proceed
|
||||||
|
warn!("No peer socket_addr in relay response, using relay server address");
|
||||||
|
relay_addr
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Peer address from relay: {}", peer_addr);
|
||||||
|
|
||||||
|
// At this point, the relay has connected us to the peer
|
||||||
|
// The stream is now a direct connection to the client
|
||||||
|
// Accept the connection through connection manager
|
||||||
|
connection_manager.accept_connection(stream, peer_addr).await?;
|
||||||
|
info!("Relay connection established for uuid={}, peer={}", uuid, peer_addr);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Unexpected message from relay server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle intranet/same-LAN connection request
|
||||||
|
///
|
||||||
|
/// When the server determines that the client and peer are on the same intranet
|
||||||
|
/// (same public IP or both on LAN), it sends FetchLocalAddr to the peer.
|
||||||
|
/// The peer must:
|
||||||
|
/// 1. Open a TCP connection to the rendezvous server
|
||||||
|
/// 2. Send LocalAddr with our local address
|
||||||
|
/// 3. Accept the peer connection over that same TCP stream
|
||||||
|
async fn handle_intranet_request(
|
||||||
|
rendezvous_addr: &str,
|
||||||
|
peer_socket_addr: &[u8],
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
relay_server: &str,
|
||||||
|
device_id: &str,
|
||||||
|
connection_manager: Arc<ConnectionManager>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
info!(
|
||||||
|
"Handling intranet request: rendezvous={}, local_addr={}, device_id={}",
|
||||||
|
rendezvous_addr, local_addr, device_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decode peer address for logging
|
||||||
|
let peer_addr = AddrMangle::decode(peer_socket_addr);
|
||||||
|
debug!("Peer address from FetchLocalAddr: {:?}", peer_addr);
|
||||||
|
|
||||||
|
// Connect to rendezvous server via TCP with timeout
|
||||||
|
let mut stream = tokio::time::timeout(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
TcpStream::connect(rendezvous_addr),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("Timeout connecting to rendezvous server"))??;
|
||||||
|
|
||||||
|
info!("Connected to rendezvous server for intranet: {}", rendezvous_addr);
|
||||||
|
|
||||||
|
// Build LocalAddr message with our local address (mangled)
|
||||||
|
let local_addr_bytes = AddrMangle::encode(local_addr);
|
||||||
|
let msg = make_local_addr(
|
||||||
|
peer_socket_addr,
|
||||||
|
&local_addr_bytes,
|
||||||
|
relay_server,
|
||||||
|
device_id,
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
);
|
||||||
|
let bytes = msg.encode_to_vec();
|
||||||
|
|
||||||
|
// Send LocalAddr using RustDesk's variable-length framing
|
||||||
|
bytes_codec::write_frame(&mut stream, &bytes).await?;
|
||||||
|
|
||||||
|
info!("Sent LocalAddr to rendezvous server, waiting for peer connection");
|
||||||
|
|
||||||
|
// Now the rendezvous server will forward this to the client,
|
||||||
|
// and the client will connect to us through this same TCP stream.
|
||||||
|
// The server proxies the connection between client and peer.
|
||||||
|
|
||||||
|
// Get peer address for logging/connection tracking
|
||||||
|
let effective_peer_addr = peer_addr.unwrap_or_else(|| {
|
||||||
|
// If we can't decode the peer address, use the rendezvous server address
|
||||||
|
rendezvous_addr.parse().unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accept the connection - the stream is now a proxied connection to the client
|
||||||
|
connection_manager.accept_connection(stream, effective_peer_addr).await?;
|
||||||
|
info!("Intranet connection established via rendezvous server proxy");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
169
src/rustdesk/protocol.rs
Normal file
169
src/rustdesk/protocol.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
//! RustDesk Protocol Messages
|
||||||
|
//!
|
||||||
|
//! This module provides the compiled protobuf messages for the RustDesk protocol.
|
||||||
|
//! Messages are generated from rendezvous.proto and message.proto at build time.
|
||||||
|
|
||||||
|
use prost::Message;
|
||||||
|
|
||||||
|
// Include the generated protobuf code
|
||||||
|
pub mod hbb {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/hbb.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export commonly used types (except Message which conflicts with prost::Message)
|
||||||
|
pub use hbb::{
|
||||||
|
ConnType, ConfigUpdate, FetchLocalAddr, HealthCheck, KeyExchange, LocalAddr, NatType,
|
||||||
|
OnlineRequest, OnlineResponse, PeerDiscovery, PunchHole, PunchHoleRequest, PunchHoleResponse,
|
||||||
|
PunchHoleSent, RegisterPeer, RegisterPeerResponse, RegisterPk, RegisterPkResponse,
|
||||||
|
RelayResponse, RendezvousMessage, RequestRelay, SoftwareUpdate, TestNatRequest,
|
||||||
|
TestNatResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export message.proto types
|
||||||
|
pub use hbb::{
|
||||||
|
AudioFormat, AudioFrame, Auth2Fa, Clipboard, CursorData, CursorPosition, EncodedVideoFrame,
|
||||||
|
EncodedVideoFrames, Hash, IdPk, KeyEvent, LoginRequest, LoginResponse, MouseEvent, Misc,
|
||||||
|
OptionMessage, PeerInfo, PublicKey, SignedId, SupportedDecoding, VideoFrame,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Trait for encoding/decoding protobuf messages
|
||||||
|
pub trait ProtobufMessage: Message + Default {
|
||||||
|
/// Encode the message to bytes
|
||||||
|
fn encode_to_vec(&self) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::with_capacity(self.encoded_len());
|
||||||
|
self.encode(&mut buf).expect("Failed to encode message");
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from bytes
|
||||||
|
fn decode_from_slice(buf: &[u8]) -> Result<Self, prost::DecodeError> {
|
||||||
|
Self::decode(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement for all generated message types
|
||||||
|
impl<T: Message + Default> ProtobufMessage for T {}
|
||||||
|
|
||||||
|
/// Helper to create a RendezvousMessage with RegisterPeer
|
||||||
|
pub fn make_register_peer(id: &str, serial: i32) -> RendezvousMessage {
|
||||||
|
RendezvousMessage {
|
||||||
|
union: Some(hbb::rendezvous_message::Union::RegisterPeer(RegisterPeer {
|
||||||
|
id: id.to_string(),
|
||||||
|
serial,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a RendezvousMessage with RegisterPk
|
||||||
|
pub fn make_register_pk(id: &str, uuid: &[u8], pk: &[u8], old_id: &str) -> RendezvousMessage {
|
||||||
|
RendezvousMessage {
|
||||||
|
union: Some(hbb::rendezvous_message::Union::RegisterPk(RegisterPk {
|
||||||
|
id: id.to_string(),
|
||||||
|
uuid: uuid.to_vec(),
|
||||||
|
pk: pk.to_vec(),
|
||||||
|
old_id: old_id.to_string(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a PunchHoleSent message
|
||||||
|
pub fn make_punch_hole_sent(
|
||||||
|
socket_addr: &[u8],
|
||||||
|
id: &str,
|
||||||
|
relay_server: &str,
|
||||||
|
nat_type: NatType,
|
||||||
|
version: &str,
|
||||||
|
) -> RendezvousMessage {
|
||||||
|
RendezvousMessage {
|
||||||
|
union: Some(hbb::rendezvous_message::Union::PunchHoleSent(PunchHoleSent {
|
||||||
|
socket_addr: socket_addr.to_vec(),
|
||||||
|
id: id.to_string(),
|
||||||
|
relay_server: relay_server.to_string(),
|
||||||
|
nat_type: nat_type.into(),
|
||||||
|
version: version.to_string(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a RelayResponse message (sent to relay server)
|
||||||
|
pub fn make_relay_response(uuid: &str, _pk: Option<&[u8]>) -> RendezvousMessage {
|
||||||
|
RendezvousMessage {
|
||||||
|
union: Some(hbb::rendezvous_message::Union::RelayResponse(RelayResponse {
|
||||||
|
socket_addr: vec![],
|
||||||
|
uuid: uuid.to_string(),
|
||||||
|
relay_server: String::new(),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a LocalAddr response message
|
||||||
|
/// This is sent in response to FetchLocalAddr when a peer on the same LAN wants to connect
|
||||||
|
pub fn make_local_addr(
|
||||||
|
socket_addr: &[u8],
|
||||||
|
local_addr: &[u8],
|
||||||
|
relay_server: &str,
|
||||||
|
id: &str,
|
||||||
|
version: &str,
|
||||||
|
) -> RendezvousMessage {
|
||||||
|
RendezvousMessage {
|
||||||
|
union: Some(hbb::rendezvous_message::Union::LocalAddr(LocalAddr {
|
||||||
|
socket_addr: socket_addr.to_vec(),
|
||||||
|
local_addr: local_addr.to_vec(),
|
||||||
|
relay_server: relay_server.to_string(),
|
||||||
|
id: id.to_string(),
|
||||||
|
version: version.to_string(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a RendezvousMessage from bytes
|
||||||
|
pub fn decode_rendezvous_message(buf: &[u8]) -> Result<RendezvousMessage, prost::DecodeError> {
|
||||||
|
RendezvousMessage::decode(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a Message (session message) from bytes
|
||||||
|
pub fn decode_message(buf: &[u8]) -> Result<hbb::Message, prost::DecodeError> {
|
||||||
|
hbb::Message::decode(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use prost::Message as ProstMessage;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_register_peer_encoding() {
|
||||||
|
let msg = make_register_peer("123456789", 1);
|
||||||
|
let encoded = ProstMessage::encode_to_vec(&msg);
|
||||||
|
assert!(!encoded.is_empty());
|
||||||
|
|
||||||
|
let decoded = decode_rendezvous_message(&encoded).unwrap();
|
||||||
|
match decoded.union {
|
||||||
|
Some(hbb::rendezvous_message::Union::RegisterPeer(rp)) => {
|
||||||
|
assert_eq!(rp.id, "123456789");
|
||||||
|
assert_eq!(rp.serial, 1);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected RegisterPeer message"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_register_pk_encoding() {
|
||||||
|
let uuid = [1u8; 16];
|
||||||
|
let pk = [2u8; 32];
|
||||||
|
let msg = make_register_pk("123456789", &uuid, &pk, "");
|
||||||
|
let encoded = ProstMessage::encode_to_vec(&msg);
|
||||||
|
assert!(!encoded.is_empty());
|
||||||
|
|
||||||
|
let decoded = decode_rendezvous_message(&encoded).unwrap();
|
||||||
|
match decoded.union {
|
||||||
|
Some(hbb::rendezvous_message::Union::RegisterPk(rpk)) => {
|
||||||
|
assert_eq!(rpk.id, "123456789");
|
||||||
|
assert_eq!(rpk.uuid.len(), 16);
|
||||||
|
assert_eq!(rpk.pk.len(), 32);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected RegisterPk message"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
828
src/rustdesk/rendezvous.rs
Normal file
828
src/rustdesk/rendezvous.rs
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
//! RustDesk Rendezvous Mediator
|
||||||
|
//!
|
||||||
|
//! This module handles communication with the hbbs rendezvous server.
|
||||||
|
//! It registers the device ID and public key, handles punch hole requests,
|
||||||
|
//! and relay requests.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use prost::Message;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use tokio::time::interval;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use super::config::RustDeskConfig;
|
||||||
|
use super::crypto::{KeyPair, SigningKeyPair};
|
||||||
|
use super::protocol::{
|
||||||
|
hbb::rendezvous_message, make_punch_hole_sent, make_register_peer,
|
||||||
|
make_register_pk, NatType, RendezvousMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Registration interval in milliseconds
|
||||||
|
const REG_INTERVAL_MS: u64 = 12_000;
|
||||||
|
|
||||||
|
/// Minimum registration timeout
|
||||||
|
const MIN_REG_TIMEOUT_MS: u64 = 3_000;
|
||||||
|
|
||||||
|
/// Maximum registration timeout
|
||||||
|
const MAX_REG_TIMEOUT_MS: u64 = 30_000;
|
||||||
|
|
||||||
|
/// Connection timeout
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const CONNECT_TIMEOUT_MS: u64 = 18_000;
|
||||||
|
|
||||||
|
/// Timer interval for checking registration status
|
||||||
|
const TIMER_INTERVAL_MS: u64 = 300;
|
||||||
|
|
||||||
|
/// Rendezvous mediator status
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum RendezvousStatus {
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Registered,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RendezvousStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Disconnected => write!(f, "disconnected"),
|
||||||
|
Self::Connecting => write!(f, "connecting"),
|
||||||
|
Self::Connected => write!(f, "connected"),
|
||||||
|
Self::Registered => write!(f, "registered"),
|
||||||
|
Self::Error(e) => write!(f, "error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback for handling incoming connection requests
|
||||||
|
pub type ConnectionCallback = Arc<dyn Fn(ConnectionRequest) + Send + Sync>;
|
||||||
|
|
||||||
|
/// Incoming connection request from a RustDesk client
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConnectionRequest {
|
||||||
|
/// Peer socket address (encoded)
|
||||||
|
pub socket_addr: Vec<u8>,
|
||||||
|
/// Relay server to use
|
||||||
|
pub relay_server: String,
|
||||||
|
/// NAT type
|
||||||
|
pub nat_type: NatType,
|
||||||
|
/// Connection UUID
|
||||||
|
pub uuid: String,
|
||||||
|
/// Whether to use secure connection
|
||||||
|
pub secure: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback type for relay requests
|
||||||
|
/// Parameters: relay_server, uuid, peer_public_key
|
||||||
|
pub type RelayCallback = Arc<dyn Fn(String, String, Vec<u8>) + Send + Sync>;
|
||||||
|
|
||||||
|
/// Callback type for intranet/local address connections
|
||||||
|
/// Parameters: rendezvous_addr, peer_socket_addr (mangled), local_addr, relay_server, device_id
|
||||||
|
pub type IntranetCallback = Arc<dyn Fn(String, Vec<u8>, SocketAddr, String, String) + Send + Sync>;
|
||||||
|
|
||||||
|
/// Rendezvous Mediator
|
||||||
|
///
|
||||||
|
/// Handles communication with hbbs rendezvous server:
|
||||||
|
/// - Registers device ID and public key
|
||||||
|
/// - Maintains keep-alive with server
|
||||||
|
/// - Handles punch hole and relay requests
|
||||||
|
pub struct RendezvousMediator {
|
||||||
|
config: Arc<RwLock<RustDeskConfig>>,
|
||||||
|
keypair: Arc<RwLock<Option<KeyPair>>>,
|
||||||
|
signing_keypair: Arc<RwLock<Option<SigningKeyPair>>>,
|
||||||
|
status: Arc<RwLock<RendezvousStatus>>,
|
||||||
|
uuid: Arc<RwLock<[u8; 16]>>,
|
||||||
|
uuid_needs_save: Arc<RwLock<bool>>,
|
||||||
|
serial: Arc<RwLock<i32>>,
|
||||||
|
key_confirmed: Arc<RwLock<bool>>,
|
||||||
|
keep_alive_ms: Arc<RwLock<i32>>,
|
||||||
|
relay_callback: Arc<RwLock<Option<RelayCallback>>>,
|
||||||
|
intranet_callback: Arc<RwLock<Option<IntranetCallback>>>,
|
||||||
|
listen_port: Arc<RwLock<u16>>,
|
||||||
|
shutdown_tx: broadcast::Sender<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RendezvousMediator {
|
||||||
|
/// Create a new rendezvous mediator
|
||||||
|
pub fn new(mut config: RustDeskConfig) -> Self {
|
||||||
|
let (shutdown_tx, _) = broadcast::channel(1);
|
||||||
|
|
||||||
|
// Get or generate UUID from config (persisted)
|
||||||
|
let (uuid, uuid_needs_save) = config.ensure_uuid();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config: Arc::new(RwLock::new(config)),
|
||||||
|
keypair: Arc::new(RwLock::new(None)),
|
||||||
|
signing_keypair: Arc::new(RwLock::new(None)),
|
||||||
|
status: Arc::new(RwLock::new(RendezvousStatus::Disconnected)),
|
||||||
|
uuid: Arc::new(RwLock::new(uuid)),
|
||||||
|
uuid_needs_save: Arc::new(RwLock::new(uuid_needs_save)),
|
||||||
|
serial: Arc::new(RwLock::new(0)),
|
||||||
|
key_confirmed: Arc::new(RwLock::new(false)),
|
||||||
|
keep_alive_ms: Arc::new(RwLock::new(30_000)),
|
||||||
|
relay_callback: Arc::new(RwLock::new(None)),
|
||||||
|
intranet_callback: Arc::new(RwLock::new(None)),
|
||||||
|
listen_port: Arc::new(RwLock::new(21118)),
|
||||||
|
shutdown_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the TCP listen port for direct connections
|
||||||
|
pub fn set_listen_port(&self, port: u16) {
|
||||||
|
let old_port = *self.listen_port.read();
|
||||||
|
if old_port != port {
|
||||||
|
*self.listen_port.write() = port;
|
||||||
|
// Port changed, increment serial to notify server
|
||||||
|
self.increment_serial();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the TCP listen port
|
||||||
|
pub fn listen_port(&self) -> u16 {
|
||||||
|
*self.listen_port.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the serial number to indicate local state change
|
||||||
|
pub fn increment_serial(&self) {
|
||||||
|
let mut serial = self.serial.write();
|
||||||
|
*serial = serial.wrapping_add(1);
|
||||||
|
debug!("Serial incremented to {}", *serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current serial number
|
||||||
|
pub fn serial(&self) -> i32 {
|
||||||
|
*self.serial.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if UUID needs to be saved to persistent storage
|
||||||
|
pub fn uuid_needs_save(&self) -> bool {
|
||||||
|
*self.uuid_needs_save.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current config (with UUID set)
|
||||||
|
pub fn config(&self) -> RustDeskConfig {
|
||||||
|
self.config.read().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark UUID as saved
|
||||||
|
pub fn mark_uuid_saved(&self) {
|
||||||
|
*self.uuid_needs_save.write() = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the callback for relay requests
|
||||||
|
pub fn set_relay_callback(&self, callback: RelayCallback) {
|
||||||
|
*self.relay_callback.write() = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the callback for intranet/local address connections
|
||||||
|
pub fn set_intranet_callback(&self, callback: IntranetCallback) {
|
||||||
|
*self.intranet_callback.write() = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current status
|
||||||
|
pub fn status(&self) -> RendezvousStatus {
|
||||||
|
self.status.read().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update configuration
|
||||||
|
pub fn update_config(&self, config: RustDeskConfig) {
|
||||||
|
*self.config.write() = config;
|
||||||
|
// Config changed, increment serial to notify server
|
||||||
|
self.increment_serial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize or get keypair (Curve25519 for encryption)
|
||||||
|
pub fn ensure_keypair(&self) -> KeyPair {
|
||||||
|
let mut keypair_guard = self.keypair.write();
|
||||||
|
if keypair_guard.is_none() {
|
||||||
|
let config = self.config.read();
|
||||||
|
// Try to load from config first
|
||||||
|
if let (Some(pk), Some(sk)) = (&config.public_key, &config.private_key) {
|
||||||
|
if let Ok(kp) = KeyPair::from_base64(pk, sk) {
|
||||||
|
*keypair_guard = Some(kp.clone());
|
||||||
|
return kp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate new keypair
|
||||||
|
let kp = KeyPair::generate();
|
||||||
|
*keypair_guard = Some(kp.clone());
|
||||||
|
kp
|
||||||
|
} else {
|
||||||
|
keypair_guard.as_ref().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize or get signing keypair (Ed25519 for SignedId)
|
||||||
|
pub fn ensure_signing_keypair(&self) -> SigningKeyPair {
|
||||||
|
let mut signing_guard = self.signing_keypair.write();
|
||||||
|
if signing_guard.is_none() {
|
||||||
|
let config = self.config.read();
|
||||||
|
// Try to load from config first
|
||||||
|
if let (Some(pk), Some(sk)) = (&config.signing_public_key, &config.signing_private_key) {
|
||||||
|
if let Ok(skp) = SigningKeyPair::from_base64(pk, sk) {
|
||||||
|
*signing_guard = Some(skp.clone());
|
||||||
|
return skp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate new signing keypair
|
||||||
|
let skp = SigningKeyPair::generate();
|
||||||
|
*signing_guard = Some(skp.clone());
|
||||||
|
skp
|
||||||
|
} else {
|
||||||
|
signing_guard.as_ref().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the device ID
|
||||||
|
pub fn device_id(&self) -> String {
|
||||||
|
self.config.read().device_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the rendezvous mediator
|
||||||
|
pub async fn start(&self) -> anyhow::Result<()> {
|
||||||
|
let config = self.config.read().clone();
|
||||||
|
if !config.enabled || config.rendezvous_server.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.status.write() = RendezvousStatus::Connecting;
|
||||||
|
|
||||||
|
let addr = config.rendezvous_addr();
|
||||||
|
info!("Starting rendezvous mediator for {} to {}", config.device_id, addr);
|
||||||
|
|
||||||
|
// Resolve server address
|
||||||
|
let server_addr: SocketAddr = tokio::net::lookup_host(&addr)
|
||||||
|
.await?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Failed to resolve {}", addr))?;
|
||||||
|
|
||||||
|
// Create UDP socket
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
socket.connect(server_addr).await?;
|
||||||
|
|
||||||
|
info!("Connected to rendezvous server at {}", server_addr);
|
||||||
|
*self.status.write() = RendezvousStatus::Connected;
|
||||||
|
|
||||||
|
// Start registration loop
|
||||||
|
self.registration_loop(socket).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main registration loop
|
||||||
|
async fn registration_loop(&self, socket: UdpSocket) -> anyhow::Result<()> {
|
||||||
|
let mut timer = interval(Duration::from_millis(TIMER_INTERVAL_MS));
|
||||||
|
let mut recv_buf = vec![0u8; 65535];
|
||||||
|
let mut last_register_sent: Option<Instant> = None;
|
||||||
|
let mut last_register_resp: Option<Instant> = None;
|
||||||
|
let mut reg_timeout = MIN_REG_TIMEOUT_MS;
|
||||||
|
let mut fails = 0;
|
||||||
|
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// Handle incoming messages
|
||||||
|
result = socket.recv(&mut recv_buf) => {
|
||||||
|
match result {
|
||||||
|
Ok(len) => {
|
||||||
|
if let Ok(msg) = RendezvousMessage::decode(&recv_buf[..len]) {
|
||||||
|
self.handle_response(&socket, msg, &mut last_register_resp, &mut fails, &mut reg_timeout).await?;
|
||||||
|
} else {
|
||||||
|
debug!("Failed to decode rendezvous message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to receive from socket: {}", e);
|
||||||
|
*self.status.write() = RendezvousStatus::Error(e.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic registration
|
||||||
|
_ = timer.tick() => {
|
||||||
|
let now = Instant::now();
|
||||||
|
let expired = last_register_resp
|
||||||
|
.map(|x| x.elapsed().as_millis() as u64 >= REG_INTERVAL_MS)
|
||||||
|
.unwrap_or(true);
|
||||||
|
let timeout = last_register_sent
|
||||||
|
.map(|x| x.elapsed().as_millis() as u64 >= reg_timeout)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if timeout && reg_timeout < MAX_REG_TIMEOUT_MS {
|
||||||
|
reg_timeout += MIN_REG_TIMEOUT_MS;
|
||||||
|
fails += 1;
|
||||||
|
if fails >= 4 {
|
||||||
|
warn!("Registration timeout, {} consecutive failures", fails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeout || (last_register_sent.is_none() && expired) {
|
||||||
|
self.send_register(&socket).await?;
|
||||||
|
last_register_sent = Some(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown signal
|
||||||
|
_ = shutdown_rx.recv() => {
|
||||||
|
info!("Rendezvous mediator shutting down");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.status.write() = RendezvousStatus::Disconnected;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send registration message
|
||||||
|
async fn send_register(&self, socket: &UdpSocket) -> anyhow::Result<()> {
|
||||||
|
let key_confirmed = *self.key_confirmed.read();
|
||||||
|
|
||||||
|
if !key_confirmed {
|
||||||
|
// Send RegisterPk with public key
|
||||||
|
self.send_register_pk(socket).await
|
||||||
|
} else {
|
||||||
|
// Send RegisterPeer heartbeat
|
||||||
|
self.send_register_peer(socket).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send RegisterPeer message
|
||||||
|
async fn send_register_peer(&self, socket: &UdpSocket) -> anyhow::Result<()> {
|
||||||
|
let id = self.device_id();
|
||||||
|
let serial = *self.serial.read();
|
||||||
|
|
||||||
|
debug!("Sending RegisterPeer: id={}, serial={}", id, serial);
|
||||||
|
let msg = make_register_peer(&id, serial);
|
||||||
|
let bytes = msg.encode_to_vec();
|
||||||
|
socket.send(&bytes).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send RegisterPk message
|
||||||
|
/// Uses the Ed25519 signing public key for registration
|
||||||
|
async fn send_register_pk(&self, socket: &UdpSocket) -> anyhow::Result<()> {
|
||||||
|
let id = self.device_id();
|
||||||
|
// Use signing public key (Ed25519) for RegisterPk
|
||||||
|
// This is what clients will use to verify our SignedId signature
|
||||||
|
let signing_keypair = self.ensure_signing_keypair();
|
||||||
|
let pk = signing_keypair.public_key_bytes();
|
||||||
|
let uuid = *self.uuid.read();
|
||||||
|
|
||||||
|
debug!("Sending RegisterPk: id={}, signing_pk_len={}", id, pk.len());
|
||||||
|
let msg = make_register_pk(&id, &uuid, pk, "");
|
||||||
|
let bytes = msg.encode_to_vec();
|
||||||
|
socket.send(&bytes).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle FetchLocalAddr - send to callback for proper TCP handling
|
||||||
|
///
|
||||||
|
/// The intranet callback will:
|
||||||
|
/// 1. Open a TCP connection to the rendezvous server
|
||||||
|
/// 2. Send LocalAddr message
|
||||||
|
/// 3. Accept the peer connection over that same TCP stream
|
||||||
|
async fn send_local_addr(
|
||||||
|
&self,
|
||||||
|
_udp_socket: &UdpSocket,
|
||||||
|
peer_socket_addr: &[u8],
|
||||||
|
relay_server: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let id = self.device_id();
|
||||||
|
|
||||||
|
// Get our actual local IP addresses for same-LAN connection
|
||||||
|
let local_addrs = get_local_addresses();
|
||||||
|
if local_addrs.is_empty() {
|
||||||
|
debug!("No local addresses available for LocalAddr response");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the rendezvous server address for TCP connection
|
||||||
|
let config = self.config.read().clone();
|
||||||
|
let rendezvous_addr = config.rendezvous_addr();
|
||||||
|
|
||||||
|
// Use TCP listen port for direct connections
|
||||||
|
let listen_port = self.listen_port();
|
||||||
|
|
||||||
|
// Use the first local IP
|
||||||
|
let local_ip = local_addrs[0];
|
||||||
|
let local_sock_addr = SocketAddr::new(local_ip, listen_port);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"FetchLocalAddr: calling intranet callback with local_addr={}, rendezvous={}",
|
||||||
|
local_sock_addr, rendezvous_addr
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the intranet callback if set
|
||||||
|
if let Some(callback) = self.intranet_callback.read().as_ref() {
|
||||||
|
callback(
|
||||||
|
rendezvous_addr,
|
||||||
|
peer_socket_addr.to_vec(),
|
||||||
|
local_sock_addr,
|
||||||
|
relay_server.to_string(),
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!("No intranet callback set, cannot handle FetchLocalAddr properly");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle response from rendezvous server
|
||||||
|
async fn handle_response(
|
||||||
|
&self,
|
||||||
|
socket: &UdpSocket,
|
||||||
|
msg: RendezvousMessage,
|
||||||
|
last_resp: &mut Option<Instant>,
|
||||||
|
fails: &mut i32,
|
||||||
|
reg_timeout: &mut u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
*last_resp = Some(Instant::now());
|
||||||
|
*fails = 0;
|
||||||
|
*reg_timeout = MIN_REG_TIMEOUT_MS;
|
||||||
|
|
||||||
|
match msg.union {
|
||||||
|
Some(rendezvous_message::Union::RegisterPeerResponse(rpr)) => {
|
||||||
|
debug!("Received RegisterPeerResponse, request_pk={}", rpr.request_pk);
|
||||||
|
if rpr.request_pk {
|
||||||
|
// Server wants us to register our public key
|
||||||
|
info!("Server requested public key registration");
|
||||||
|
*self.key_confirmed.write() = false;
|
||||||
|
self.send_register_pk(socket).await?;
|
||||||
|
}
|
||||||
|
*self.status.write() = RendezvousStatus::Registered;
|
||||||
|
}
|
||||||
|
Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => {
|
||||||
|
debug!("Received RegisterPkResponse: result={}", rpr.result);
|
||||||
|
match rpr.result {
|
||||||
|
0 => {
|
||||||
|
// OK
|
||||||
|
info!("Public key registered successfully");
|
||||||
|
*self.key_confirmed.write() = true;
|
||||||
|
// Increment serial after successful registration
|
||||||
|
self.increment_serial();
|
||||||
|
*self.status.write() = RendezvousStatus::Registered;
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// UUID_MISMATCH
|
||||||
|
warn!("UUID mismatch, need to re-register");
|
||||||
|
*self.key_confirmed.write() = false;
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
// ID_EXISTS
|
||||||
|
error!("Device ID already exists on server");
|
||||||
|
*self.status.write() =
|
||||||
|
RendezvousStatus::Error("Device ID already exists".to_string());
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
// TOO_FREQUENT
|
||||||
|
warn!("Registration too frequent");
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
// INVALID_ID_FORMAT
|
||||||
|
error!("Invalid device ID format");
|
||||||
|
*self.status.write() =
|
||||||
|
RendezvousStatus::Error("Invalid ID format".to_string());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Unknown RegisterPkResponse result: {}", rpr.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rpr.keep_alive > 0 {
|
||||||
|
*self.keep_alive_ms.write() = rpr.keep_alive * 1000;
|
||||||
|
debug!("Keep alive set to {}ms", rpr.keep_alive * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(rendezvous_message::Union::PunchHole(ph)) => {
|
||||||
|
// Decode the peer's socket address
|
||||||
|
let peer_addr = if !ph.socket_addr.is_empty() {
|
||||||
|
AddrMangle::decode(&ph.socket_addr)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Received PunchHole request: peer_addr={:?}, socket_addr_len={}, relay_server={}, nat_type={:?}",
|
||||||
|
peer_addr, ph.socket_addr.len(), ph.relay_server, ph.nat_type
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send PunchHoleSent to acknowledge and provide our address
|
||||||
|
// Use the TCP listen port address, not the UDP socket's address
|
||||||
|
let listen_port = self.listen_port();
|
||||||
|
|
||||||
|
// Get our public-facing address from the UDP socket
|
||||||
|
if let Ok(local_addr) = socket.local_addr() {
|
||||||
|
// Use the same IP as UDP socket but with TCP listen port
|
||||||
|
let tcp_addr = SocketAddr::new(local_addr.ip(), listen_port);
|
||||||
|
let our_socket_addr = AddrMangle::encode(tcp_addr);
|
||||||
|
let id = self.device_id();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Sending PunchHoleSent: id={}, socket_addr={}, relay_server={}",
|
||||||
|
id, tcp_addr, ph.relay_server
|
||||||
|
);
|
||||||
|
|
||||||
|
let msg = make_punch_hole_sent(
|
||||||
|
&our_socket_addr,
|
||||||
|
&id,
|
||||||
|
&ph.relay_server,
|
||||||
|
NatType::try_from(ph.nat_type).unwrap_or(NatType::UnknownNat),
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
);
|
||||||
|
let bytes = msg.encode_to_vec();
|
||||||
|
if let Err(e) = socket.send(&bytes).await {
|
||||||
|
warn!("Failed to send PunchHoleSent: {}", e);
|
||||||
|
} else {
|
||||||
|
info!("Sent PunchHoleSent response successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, we fall back to relay since true UDP hole punching is complex
|
||||||
|
// and may not work through all NAT types
|
||||||
|
if !ph.relay_server.is_empty() {
|
||||||
|
if let Some(callback) = self.relay_callback.read().as_ref() {
|
||||||
|
let relay_server = if ph.relay_server.contains(':') {
|
||||||
|
ph.relay_server.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}:21117", ph.relay_server)
|
||||||
|
};
|
||||||
|
// Use peer's socket_addr to generate a deterministic UUID
|
||||||
|
// This ensures both sides use the same UUID for the relay
|
||||||
|
let uuid = if !ph.socket_addr.is_empty() {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
ph.socket_addr.hash(&mut hasher);
|
||||||
|
format!("{:016x}", hasher.finish())
|
||||||
|
} else {
|
||||||
|
uuid::Uuid::new_v4().to_string()
|
||||||
|
};
|
||||||
|
callback(relay_server, uuid, vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(rendezvous_message::Union::RequestRelay(rr)) => {
|
||||||
|
info!(
|
||||||
|
"Received RequestRelay, relay_server={}, uuid={}",
|
||||||
|
rr.relay_server, rr.uuid
|
||||||
|
);
|
||||||
|
// Call the relay callback to handle the connection
|
||||||
|
if let Some(callback) = self.relay_callback.read().as_ref() {
|
||||||
|
let relay_server = if rr.relay_server.contains(':') {
|
||||||
|
rr.relay_server.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}:21117", rr.relay_server)
|
||||||
|
};
|
||||||
|
callback(relay_server, rr.uuid.clone(), vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(rendezvous_message::Union::FetchLocalAddr(fla)) => {
|
||||||
|
// Decode the peer address for logging
|
||||||
|
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||||
|
info!(
|
||||||
|
"Received FetchLocalAddr request: peer_addr={:?}, socket_addr_len={}, relay_server={}",
|
||||||
|
peer_addr, fla.socket_addr.len(), fla.relay_server
|
||||||
|
);
|
||||||
|
// Respond with our local address for same-LAN direct connection
|
||||||
|
self.send_local_addr(socket, &fla.socket_addr, &fla.relay_server).await?;
|
||||||
|
}
|
||||||
|
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
|
||||||
|
info!("Received ConfigureUpdate, serial={}", cu.serial);
|
||||||
|
*self.serial.write() = cu.serial;
|
||||||
|
}
|
||||||
|
Some(other) => {
|
||||||
|
// Log the actual message type for debugging
|
||||||
|
let type_name = match other {
|
||||||
|
rendezvous_message::Union::PunchHoleRequest(_) => "PunchHoleRequest",
|
||||||
|
rendezvous_message::Union::PunchHoleResponse(_) => "PunchHoleResponse",
|
||||||
|
rendezvous_message::Union::SoftwareUpdate(_) => "SoftwareUpdate",
|
||||||
|
rendezvous_message::Union::TestNatRequest(_) => "TestNatRequest",
|
||||||
|
rendezvous_message::Union::TestNatResponse(_) => "TestNatResponse",
|
||||||
|
rendezvous_message::Union::PeerDiscovery(_) => "PeerDiscovery",
|
||||||
|
rendezvous_message::Union::OnlineRequest(_) => "OnlineRequest",
|
||||||
|
rendezvous_message::Union::OnlineResponse(_) => "OnlineResponse",
|
||||||
|
rendezvous_message::Union::KeyExchange(_) => "KeyExchange",
|
||||||
|
rendezvous_message::Union::Hc(_) => "HealthCheck",
|
||||||
|
rendezvous_message::Union::RelayResponse(_) => "RelayResponse",
|
||||||
|
_ => "Other",
|
||||||
|
};
|
||||||
|
info!("Received unhandled rendezvous message type: {}", type_name);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("Received empty rendezvous message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the rendezvous mediator
|
||||||
|
pub fn stop(&self) {
|
||||||
|
info!("Stopping rendezvous mediator");
|
||||||
|
let _ = self.shutdown_tx.send(());
|
||||||
|
*self.status.write() = RendezvousStatus::Disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a shutdown receiver
|
||||||
|
pub fn shutdown_rx(&self) -> broadcast::Receiver<()> {
|
||||||
|
self.shutdown_tx.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AddrMangle - RustDesk's address encoding scheme
|
||||||
|
///
|
||||||
|
/// Certain routers and firewalls scan packets and modify IP addresses.
|
||||||
|
/// This encoding mangles the address to avoid detection.
|
||||||
|
pub struct AddrMangle;
|
||||||
|
|
||||||
|
impl AddrMangle {
|
||||||
|
/// Encode a SocketAddr to bytes using RustDesk's mangle algorithm
|
||||||
|
pub fn encode(addr: SocketAddr) -> Vec<u8> {
|
||||||
|
// Try to convert IPv6-mapped IPv4 to plain IPv4
|
||||||
|
let addr = try_into_v4(addr);
|
||||||
|
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(addr_v4) => {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
let tm = (SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or(std::time::Duration::ZERO)
|
||||||
|
.as_micros() as u32) as u128;
|
||||||
|
let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128;
|
||||||
|
let port = addr.port() as u128;
|
||||||
|
let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF));
|
||||||
|
let bytes = v.to_le_bytes();
|
||||||
|
|
||||||
|
// Remove trailing zeros
|
||||||
|
let mut n_padding = 0;
|
||||||
|
for i in bytes.iter().rev() {
|
||||||
|
if *i == 0u8 {
|
||||||
|
n_padding += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes[..(16 - n_padding)].to_vec()
|
||||||
|
}
|
||||||
|
SocketAddr::V6(addr_v6) => {
|
||||||
|
let mut x = addr_v6.ip().octets().to_vec();
|
||||||
|
let port: [u8; 2] = addr_v6.port().to_le_bytes();
|
||||||
|
x.push(port[0]);
|
||||||
|
x.push(port[1]);
|
||||||
|
x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode bytes to SocketAddr using RustDesk's mangle algorithm
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn decode(bytes: &[u8]) -> Option<SocketAddr> {
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4};
|
||||||
|
|
||||||
|
if bytes.len() > 16 {
|
||||||
|
// IPv6 format: 16 bytes IP + 2 bytes port
|
||||||
|
if bytes.len() != 18 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let tmp: [u8; 2] = bytes[16..].try_into().ok()?;
|
||||||
|
let port = u16::from_le_bytes(tmp);
|
||||||
|
let tmp: [u8; 16] = bytes[..16].try_into().ok()?;
|
||||||
|
let ip = Ipv6Addr::from(tmp);
|
||||||
|
return Some(SocketAddr::new(std::net::IpAddr::V6(ip), port));
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4 mangled format
|
||||||
|
let mut padded = [0u8; 16];
|
||||||
|
padded[..bytes.len()].copy_from_slice(bytes);
|
||||||
|
let number = u128::from_le_bytes(padded);
|
||||||
|
let tm = (number >> 17) & (u32::MAX as u128);
|
||||||
|
let ip = (((number >> 49).wrapping_sub(tm)) as u32).to_le_bytes();
|
||||||
|
let port = ((number & 0xFFFFFF).wrapping_sub(tm & 0xFFFF)) as u16;
|
||||||
|
Some(SocketAddr::V4(SocketAddrV4::new(
|
||||||
|
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
|
||||||
|
port,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to convert IPv6-mapped IPv4 address to plain IPv4
|
||||||
|
fn try_into_v4(addr: SocketAddr) -> SocketAddr {
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V6(v6) if !addr.ip().is_loopback() => {
|
||||||
|
if let Some(ipv4) = v6.ip().to_ipv4_mapped() {
|
||||||
|
return SocketAddr::new(std::net::IpAddr::V4(ipv4), v6.port());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an interface name belongs to Docker or other virtual networks
|
||||||
|
fn is_virtual_interface(name: &str) -> bool {
|
||||||
|
// Docker interfaces
|
||||||
|
name.starts_with("docker")
|
||||||
|
|| name.starts_with("br-")
|
||||||
|
|| name.starts_with("veth")
|
||||||
|
// Kubernetes/container interfaces
|
||||||
|
|| name.starts_with("cni")
|
||||||
|
|| name.starts_with("flannel")
|
||||||
|
|| name.starts_with("calico")
|
||||||
|
|| name.starts_with("weave")
|
||||||
|
// Virtual bridge interfaces
|
||||||
|
|| name.starts_with("virbr")
|
||||||
|
|| name.starts_with("lxcbr")
|
||||||
|
|| name.starts_with("lxdbr")
|
||||||
|
// VPN interfaces (usually not useful for LAN discovery)
|
||||||
|
|| name.starts_with("tun")
|
||||||
|
|| name.starts_with("tap")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an IP address is in a Docker/container private range
|
||||||
|
fn is_docker_ip(ip: &std::net::IpAddr) -> bool {
|
||||||
|
if let std::net::IpAddr::V4(ipv4) = ip {
|
||||||
|
let octets = ipv4.octets();
|
||||||
|
// Docker default bridge: 172.17.0.0/16
|
||||||
|
if octets[0] == 172 && octets[1] == 17 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Docker user-defined networks: 172.18-31.0.0/16
|
||||||
|
if octets[0] == 172 && (18..=31).contains(&octets[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Docker overlay networks: 10.0.0.0/8 (common range)
|
||||||
|
// Note: 10.x.x.x is also used for corporate LANs, so we only filter
|
||||||
|
// specific Docker-like patterns (10.0.x.x with small third octet)
|
||||||
|
if octets[0] == 10 && octets[1] == 0 && octets[2] < 10 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get local IP addresses (non-loopback, non-Docker)
|
||||||
|
fn get_local_addresses() -> Vec<std::net::IpAddr> {
|
||||||
|
let mut addrs = Vec::new();
|
||||||
|
|
||||||
|
// Use pnet or network-interface crate if available, otherwise use simple method
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Ok(interfaces) = std::fs::read_dir("/sys/class/net") {
|
||||||
|
for entry in interfaces.flatten() {
|
||||||
|
let iface_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
// Skip loopback and virtual interfaces
|
||||||
|
if iface_name == "lo" || is_virtual_interface(&iface_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get IP via command (simple approach)
|
||||||
|
if let Ok(output) = std::process::Command::new("ip")
|
||||||
|
.args(["-4", "addr", "show", &iface_name])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if let Some(inet_pos) = line.find("inet ") {
|
||||||
|
let ip_part = &line[inet_pos + 5..];
|
||||||
|
if let Some(slash_pos) = ip_part.find('/') {
|
||||||
|
if let Ok(ip) = ip_part[..slash_pos].parse::<std::net::IpAddr>() {
|
||||||
|
// Skip loopback and Docker IPs
|
||||||
|
if !ip.is_loopback() && !is_docker_ip(&ip) {
|
||||||
|
addrs.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get default route interface IP
|
||||||
|
if addrs.is_empty() {
|
||||||
|
// Try using DNS lookup to get local IP (connects to external server)
|
||||||
|
if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") {
|
||||||
|
// Connect to a public DNS server (doesn't actually send data)
|
||||||
|
if socket.connect("8.8.8.8:53").is_ok() {
|
||||||
|
if let Ok(local_addr) = socket.local_addr() {
|
||||||
|
let ip = local_addr.ip();
|
||||||
|
// Skip loopback and Docker IPs
|
||||||
|
if !ip.is_loopback() && !is_docker_ip(&ip) {
|
||||||
|
addrs.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use crate::extensions::ExtensionManager;
|
|||||||
use crate::hid::HidController;
|
use crate::hid::HidController;
|
||||||
use crate::msd::MsdController;
|
use crate::msd::MsdController;
|
||||||
use crate::otg::OtgService;
|
use crate::otg::OtgService;
|
||||||
|
use crate::rustdesk::RustDeskService;
|
||||||
use crate::video::VideoStreamManager;
|
use crate::video::VideoStreamManager;
|
||||||
|
|
||||||
/// Application-wide state shared across handlers
|
/// Application-wide state shared across handlers
|
||||||
@@ -44,6 +45,8 @@ pub struct AppState {
|
|||||||
pub atx: Arc<RwLock<Option<AtxController>>>,
|
pub atx: Arc<RwLock<Option<AtxController>>>,
|
||||||
/// Audio controller
|
/// Audio controller
|
||||||
pub audio: Arc<AudioController>,
|
pub audio: Arc<AudioController>,
|
||||||
|
/// RustDesk remote access service (optional)
|
||||||
|
pub rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>,
|
||||||
/// Extension manager (ttyd, gostc, easytier)
|
/// Extension manager (ttyd, gostc, easytier)
|
||||||
pub extensions: Arc<ExtensionManager>,
|
pub extensions: Arc<ExtensionManager>,
|
||||||
/// Event bus for real-time notifications
|
/// Event bus for real-time notifications
|
||||||
@@ -66,6 +69,7 @@ impl AppState {
|
|||||||
msd: Option<MsdController>,
|
msd: Option<MsdController>,
|
||||||
atx: Option<AtxController>,
|
atx: Option<AtxController>,
|
||||||
audio: Arc<AudioController>,
|
audio: Arc<AudioController>,
|
||||||
|
rustdesk: Option<Arc<RustDeskService>>,
|
||||||
extensions: Arc<ExtensionManager>,
|
extensions: Arc<ExtensionManager>,
|
||||||
events: Arc<EventBus>,
|
events: Arc<EventBus>,
|
||||||
shutdown_tx: broadcast::Sender<()>,
|
shutdown_tx: broadcast::Sender<()>,
|
||||||
@@ -81,6 +85,7 @@ impl AppState {
|
|||||||
msd: Arc::new(RwLock::new(msd)),
|
msd: Arc::new(RwLock::new(msd)),
|
||||||
atx: Arc::new(RwLock::new(atx)),
|
atx: Arc::new(RwLock::new(atx)),
|
||||||
audio,
|
audio,
|
||||||
|
rustdesk: Arc::new(RwLock::new(rustdesk)),
|
||||||
extensions,
|
extensions,
|
||||||
events,
|
events,
|
||||||
shutdown_tx,
|
shutdown_tx,
|
||||||
|
|||||||
@@ -541,6 +541,78 @@ impl VideoStreamManager {
|
|||||||
self.streamer.frame_sender().await
|
self.streamer.frame_sender().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to encoded video frames from the shared video pipeline
|
||||||
|
///
|
||||||
|
/// This allows RustDesk (and other consumers) to receive H264/H265/VP8/VP9
|
||||||
|
/// encoded frames without running a separate encoder. The encoding is shared
|
||||||
|
/// with WebRTC sessions.
|
||||||
|
///
|
||||||
|
/// This method ensures video capture is running before subscribing.
|
||||||
|
/// Returns None if video capture cannot be started or pipeline creation fails.
|
||||||
|
pub async fn subscribe_encoded_frames(
|
||||||
|
&self,
|
||||||
|
) -> Option<tokio::sync::broadcast::Receiver<crate::video::shared_video_pipeline::EncodedVideoFrame>> {
|
||||||
|
// 1. Ensure video capture is initialized
|
||||||
|
if self.streamer.state().await == StreamerState::Uninitialized {
|
||||||
|
tracing::info!("Initializing video capture for encoded frame subscription");
|
||||||
|
if let Err(e) = self.streamer.init_auto().await {
|
||||||
|
tracing::error!("Failed to initialize video capture for encoded frames: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ensure video capture is running (streaming)
|
||||||
|
if self.streamer.state().await != StreamerState::Streaming {
|
||||||
|
tracing::info!("Starting video capture for encoded frame subscription");
|
||||||
|
if let Err(e) = self.streamer.start().await {
|
||||||
|
tracing::error!("Failed to start video capture for encoded frames: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Wait for capture to stabilize
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get frame sender from running capture
|
||||||
|
let frame_tx = match self.streamer.frame_sender().await {
|
||||||
|
Some(tx) => tx,
|
||||||
|
None => {
|
||||||
|
tracing::warn!("Cannot subscribe to encoded frames: no frame sender available");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Synchronize WebRTC config with actual capture format
|
||||||
|
let (format, resolution, fps) = self.streamer.current_video_config().await;
|
||||||
|
tracing::info!(
|
||||||
|
"Connecting encoded frame subscription: {}x{} {:?} @ {}fps",
|
||||||
|
resolution.width, resolution.height, format, fps
|
||||||
|
);
|
||||||
|
self.webrtc_streamer.update_video_config(resolution, format, fps).await;
|
||||||
|
|
||||||
|
// 5. Use WebRtcStreamer to ensure the shared video pipeline is running
|
||||||
|
// This will create the pipeline if needed
|
||||||
|
match self.webrtc_streamer.ensure_video_pipeline_for_external(frame_tx).await {
|
||||||
|
Ok(pipeline) => Some(pipeline.subscribe()),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to start shared video pipeline: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current video encoding configuration from the shared pipeline
|
||||||
|
pub async fn get_encoding_config(&self) -> Option<crate::video::shared_video_pipeline::SharedVideoPipelineConfig> {
|
||||||
|
self.webrtc_streamer.get_pipeline_config().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set video codec for the shared video pipeline
|
||||||
|
///
|
||||||
|
/// This allows external consumers (like RustDesk) to set the video codec
|
||||||
|
/// before subscribing to encoded frames.
|
||||||
|
pub async fn set_video_codec(&self, codec: crate::video::encoder::VideoCodecType) -> crate::error::Result<()> {
|
||||||
|
self.webrtc_streamer.set_video_codec(codec).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Publish event to event bus
|
/// Publish event to event bus
|
||||||
async fn publish_event(&self, event: SystemEvent) {
|
async fn publish_event(&self, event: SystemEvent) {
|
||||||
if let Some(ref events) = *self.events.read().await {
|
if let Some(ref events) = *self.events.read().await {
|
||||||
|
|||||||
@@ -360,3 +360,62 @@ pub async fn apply_audio_config(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 应用 RustDesk 配置变更
|
||||||
|
pub async fn apply_rustdesk_config(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
old_config: &crate::rustdesk::config::RustDeskConfig,
|
||||||
|
new_config: &crate::rustdesk::config::RustDeskConfig,
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::info!("Applying RustDesk config changes...");
|
||||||
|
|
||||||
|
let mut rustdesk_guard = state.rustdesk.write().await;
|
||||||
|
|
||||||
|
// Check if service needs to be stopped
|
||||||
|
if old_config.enabled && !new_config.enabled {
|
||||||
|
// Disable service
|
||||||
|
if let Some(ref service) = *rustdesk_guard {
|
||||||
|
if let Err(e) = service.stop().await {
|
||||||
|
tracing::error!("Failed to stop RustDesk service: {}", e);
|
||||||
|
}
|
||||||
|
tracing::info!("RustDesk service stopped");
|
||||||
|
}
|
||||||
|
*rustdesk_guard = None;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service needs to be started or restarted
|
||||||
|
if new_config.enabled {
|
||||||
|
let need_restart = old_config.rendezvous_server != new_config.rendezvous_server
|
||||||
|
|| old_config.device_id != new_config.device_id
|
||||||
|
|| old_config.device_password != new_config.device_password;
|
||||||
|
|
||||||
|
if rustdesk_guard.is_none() {
|
||||||
|
// Create new service
|
||||||
|
tracing::info!("Initializing RustDesk service...");
|
||||||
|
let service = crate::rustdesk::RustDeskService::new(
|
||||||
|
new_config.clone(),
|
||||||
|
state.stream_manager.clone(),
|
||||||
|
state.hid.clone(),
|
||||||
|
state.audio.clone(),
|
||||||
|
);
|
||||||
|
if let Err(e) = service.start().await {
|
||||||
|
tracing::error!("Failed to start RustDesk service: {}", e);
|
||||||
|
} else {
|
||||||
|
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
|
||||||
|
}
|
||||||
|
*rustdesk_guard = Some(std::sync::Arc::new(service));
|
||||||
|
} else if need_restart {
|
||||||
|
// Restart existing service with new config
|
||||||
|
if let Some(ref service) = *rustdesk_guard {
|
||||||
|
if let Err(e) = service.restart(new_config.clone()).await {
|
||||||
|
tracing::error!("Failed to restart RustDesk service: {}", e);
|
||||||
|
} else {
|
||||||
|
tracing::info!("RustDesk service restarted with ID: {}", new_config.device_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
//! - PATCH /api/config/atx - 更新 ATX 配置
|
//! - PATCH /api/config/atx - 更新 ATX 配置
|
||||||
//! - GET /api/config/audio - 获取音频配置
|
//! - GET /api/config/audio - 获取音频配置
|
||||||
//! - PATCH /api/config/audio - 更新音频配置
|
//! - PATCH /api/config/audio - 更新音频配置
|
||||||
|
//! - GET /api/config/rustdesk - 获取 RustDesk 配置
|
||||||
|
//! - PATCH /api/config/rustdesk - 更新 RustDesk 配置
|
||||||
|
|
||||||
mod apply;
|
mod apply;
|
||||||
mod types;
|
mod types;
|
||||||
@@ -23,6 +25,7 @@ mod hid;
|
|||||||
mod msd;
|
mod msd;
|
||||||
mod atx;
|
mod atx;
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod rustdesk;
|
||||||
|
|
||||||
// 导出 handler 函数
|
// 导出 handler 函数
|
||||||
pub use video::{get_video_config, update_video_config};
|
pub use video::{get_video_config, update_video_config};
|
||||||
@@ -31,6 +34,10 @@ pub use hid::{get_hid_config, update_hid_config};
|
|||||||
pub use msd::{get_msd_config, update_msd_config};
|
pub use msd::{get_msd_config, update_msd_config};
|
||||||
pub use atx::{get_atx_config, update_atx_config};
|
pub use atx::{get_atx_config, update_atx_config};
|
||||||
pub use audio::{get_audio_config, update_audio_config};
|
pub use audio::{get_audio_config, update_audio_config};
|
||||||
|
pub use rustdesk::{
|
||||||
|
get_rustdesk_config, get_rustdesk_status, update_rustdesk_config,
|
||||||
|
regenerate_device_id, regenerate_device_password, get_device_password,
|
||||||
|
};
|
||||||
|
|
||||||
// 保留全局配置查询(向后兼容)
|
// 保留全局配置查询(向后兼容)
|
||||||
use axum::{extract::State, Json};
|
use axum::{extract::State, Json};
|
||||||
|
|||||||
142
src/web/handlers/config/rustdesk.rs
Normal file
142
src/web/handlers/config/rustdesk.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//! RustDesk 配置 Handler
|
||||||
|
|
||||||
|
use axum::{extract::State, Json};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::rustdesk::config::RustDeskConfig;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
use super::apply::apply_rustdesk_config;
|
||||||
|
use super::types::RustDeskConfigUpdate;
|
||||||
|
|
||||||
|
/// RustDesk 配置响应(隐藏敏感信息)
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct RustDeskConfigResponse {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub rendezvous_server: String,
|
||||||
|
pub relay_server: Option<String>,
|
||||||
|
pub device_id: String,
|
||||||
|
/// 是否已设置密码
|
||||||
|
pub has_password: bool,
|
||||||
|
/// 是否已设置密钥对
|
||||||
|
pub has_keypair: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&RustDeskConfig> for RustDeskConfigResponse {
|
||||||
|
fn from(config: &RustDeskConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: config.enabled,
|
||||||
|
rendezvous_server: config.rendezvous_server.clone(),
|
||||||
|
relay_server: config.relay_server.clone(),
|
||||||
|
device_id: config.device_id.clone(),
|
||||||
|
has_password: !config.device_password.is_empty(),
|
||||||
|
has_keypair: config.public_key.is_some() && config.private_key.is_some(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RustDesk 状态响应
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct RustDeskStatusResponse {
|
||||||
|
pub config: RustDeskConfigResponse,
|
||||||
|
pub service_status: String,
|
||||||
|
pub rendezvous_status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 RustDesk 配置
|
||||||
|
pub async fn get_rustdesk_config(State(state): State<Arc<AppState>>) -> Json<RustDeskConfigResponse> {
|
||||||
|
Json(RustDeskConfigResponse::from(&state.config.get().rustdesk))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 RustDesk 完整状态(配置 + 服务状态)
|
||||||
|
pub async fn get_rustdesk_status(State(state): State<Arc<AppState>>) -> Json<RustDeskStatusResponse> {
|
||||||
|
let config = state.config.get().rustdesk.clone();
|
||||||
|
|
||||||
|
// 获取服务状态
|
||||||
|
let (service_status, rendezvous_status) = {
|
||||||
|
let guard = state.rustdesk.read().await;
|
||||||
|
if let Some(ref service) = *guard {
|
||||||
|
let status = format!("{}", service.status());
|
||||||
|
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
|
||||||
|
(status, rv_status)
|
||||||
|
} else {
|
||||||
|
("not_initialized".to_string(), None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(RustDeskStatusResponse {
|
||||||
|
config: RustDeskConfigResponse::from(&config),
|
||||||
|
service_status,
|
||||||
|
rendezvous_status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新 RustDesk 配置
|
||||||
|
pub async fn update_rustdesk_config(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(req): Json<RustDeskConfigUpdate>,
|
||||||
|
) -> Result<Json<RustDeskConfigResponse>> {
|
||||||
|
// 1. 验证请求
|
||||||
|
req.validate()?;
|
||||||
|
|
||||||
|
// 2. 获取旧配置
|
||||||
|
let old_config = state.config.get().rustdesk.clone();
|
||||||
|
|
||||||
|
// 3. 应用更新到配置存储
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.update(|config| {
|
||||||
|
req.apply_to(&mut config.rustdesk);
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 4. 获取新配置
|
||||||
|
let new_config = state.config.get().rustdesk.clone();
|
||||||
|
|
||||||
|
// 5. 应用到子系统(热重载)
|
||||||
|
if let Err(e) = apply_rustdesk_config(&state, &old_config, &new_config).await {
|
||||||
|
tracing::error!("Failed to apply RustDesk config: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(RustDeskConfigResponse::from(&new_config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新生成设备 ID
|
||||||
|
pub async fn regenerate_device_id(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> Result<Json<RustDeskConfigResponse>> {
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.update(|config| {
|
||||||
|
config.rustdesk.device_id = RustDeskConfig::generate_device_id();
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let new_config = state.config.get().rustdesk.clone();
|
||||||
|
Ok(Json(RustDeskConfigResponse::from(&new_config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新生成设备密码
|
||||||
|
pub async fn regenerate_device_password(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> Result<Json<RustDeskConfigResponse>> {
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.update(|config| {
|
||||||
|
config.rustdesk.device_password = RustDeskConfig::generate_password();
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let new_config = state.config.get().rustdesk.clone();
|
||||||
|
Ok(Json(RustDeskConfigResponse::from(&new_config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取设备密码(管理员专用)
|
||||||
|
pub async fn get_device_password(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||||
|
let config = state.config.get().rustdesk.clone();
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"device_id": config.device_id,
|
||||||
|
"device_password": config.device_password
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use serde::Deserialize;
|
|||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
use crate::rustdesk::config::RustDeskConfig;
|
||||||
|
|
||||||
// ===== Video Config =====
|
// ===== Video Config =====
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
@@ -394,3 +395,60 @@ impl AudioConfigUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== RustDesk Config =====
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct RustDeskConfigUpdate {
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
pub rendezvous_server: Option<String>,
|
||||||
|
pub relay_server: Option<String>,
|
||||||
|
pub device_password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustDeskConfigUpdate {
|
||||||
|
pub fn validate(&self) -> crate::error::Result<()> {
|
||||||
|
// Validate rendezvous server format (should be host:port)
|
||||||
|
if let Some(ref server) = self.rendezvous_server {
|
||||||
|
if !server.is_empty() && !server.contains(':') {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Rendezvous server must be in format 'host:port' (e.g., rs.example.com:21116)".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate relay server format if provided
|
||||||
|
if let Some(ref server) = self.relay_server {
|
||||||
|
if !server.is_empty() && !server.contains(':') {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Relay server must be in format 'host:port' (e.g., rs.example.com:21117)".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate password (minimum 6 characters if provided)
|
||||||
|
if let Some(ref password) = self.device_password {
|
||||||
|
if !password.is_empty() && password.len() < 6 {
|
||||||
|
return Err(AppError::BadRequest(
|
||||||
|
"Device password must be at least 6 characters".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to(&self, config: &mut RustDeskConfig) {
|
||||||
|
if let Some(enabled) = self.enabled {
|
||||||
|
config.enabled = enabled;
|
||||||
|
}
|
||||||
|
if let Some(ref server) = self.rendezvous_server {
|
||||||
|
config.rendezvous_server = server.clone();
|
||||||
|
}
|
||||||
|
if let Some(ref server) = self.relay_server {
|
||||||
|
config.relay_server = if server.is_empty() { None } else { Some(server.clone()) };
|
||||||
|
}
|
||||||
|
if let Some(ref password) = self.device_password {
|
||||||
|
if !password.is_empty() {
|
||||||
|
config.device_password = password.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|||||||
.route("/config/atx", patch(handlers::config::update_atx_config))
|
.route("/config/atx", patch(handlers::config::update_atx_config))
|
||||||
.route("/config/audio", get(handlers::config::get_audio_config))
|
.route("/config/audio", get(handlers::config::get_audio_config))
|
||||||
.route("/config/audio", patch(handlers::config::update_audio_config))
|
.route("/config/audio", patch(handlers::config::update_audio_config))
|
||||||
|
// RustDesk configuration endpoints
|
||||||
|
.route("/config/rustdesk", get(handlers::config::get_rustdesk_config))
|
||||||
|
.route("/config/rustdesk", patch(handlers::config::update_rustdesk_config))
|
||||||
|
.route("/config/rustdesk/status", get(handlers::config::get_rustdesk_status))
|
||||||
|
.route("/config/rustdesk/password", get(handlers::config::get_device_password))
|
||||||
|
.route("/config/rustdesk/regenerate-id", post(handlers::config::regenerate_device_id))
|
||||||
|
.route("/config/rustdesk/regenerate-password", post(handlers::config::regenerate_device_password))
|
||||||
// MSD (Mass Storage Device) endpoints
|
// MSD (Mass Storage Device) endpoints
|
||||||
.route("/msd/status", get(handlers::msd_status))
|
.route("/msd/status", get(handlers::msd_status))
|
||||||
.route("/msd/images", get(handlers::msd_images_list))
|
.route("/msd/images", get(handlers::msd_images_list))
|
||||||
|
|||||||
@@ -293,6 +293,26 @@ impl WebRtcStreamer {
|
|||||||
Ok(pipeline)
|
Ok(pipeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure video pipeline is running and return it for external consumers
|
||||||
|
///
|
||||||
|
/// This is a public wrapper around ensure_video_pipeline for external
|
||||||
|
/// components (like RustDesk) that need to share the encoded video stream.
|
||||||
|
pub async fn ensure_video_pipeline_for_external(
|
||||||
|
&self,
|
||||||
|
tx: broadcast::Sender<VideoFrame>,
|
||||||
|
) -> Result<Arc<SharedVideoPipeline>> {
|
||||||
|
self.ensure_video_pipeline(tx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current pipeline configuration (if pipeline is running)
|
||||||
|
pub async fn get_pipeline_config(&self) -> Option<SharedVideoPipelineConfig> {
|
||||||
|
if let Some(ref pipeline) = *self.video_pipeline.read().await {
|
||||||
|
Some(pipeline.config().await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Audio Management ===
|
// === Audio Management ===
|
||||||
|
|
||||||
/// Check if audio is enabled
|
/// Check if audio is enabled
|
||||||
|
|||||||
@@ -253,3 +253,78 @@ export const extensionsApi = {
|
|||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== RustDesk 配置 API =====
|
||||||
|
|
||||||
|
/** RustDesk 配置响应 */
|
||||||
|
export interface RustDeskConfigResponse {
|
||||||
|
enabled: boolean
|
||||||
|
rendezvous_server: string
|
||||||
|
relay_server: string | null
|
||||||
|
device_id: string
|
||||||
|
has_password: boolean
|
||||||
|
has_keypair: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RustDesk 状态响应 */
|
||||||
|
export interface RustDeskStatusResponse {
|
||||||
|
config: RustDeskConfigResponse
|
||||||
|
service_status: string
|
||||||
|
rendezvous_status: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RustDesk 配置更新 */
|
||||||
|
export interface RustDeskConfigUpdate {
|
||||||
|
enabled?: boolean
|
||||||
|
rendezvous_server?: string
|
||||||
|
relay_server?: string
|
||||||
|
device_password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RustDesk 密码响应 */
|
||||||
|
export interface RustDeskPasswordResponse {
|
||||||
|
device_id: string
|
||||||
|
device_password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rustdeskConfigApi = {
|
||||||
|
/**
|
||||||
|
* 获取 RustDesk 配置
|
||||||
|
*/
|
||||||
|
get: () => request<RustDeskConfigResponse>('/config/rustdesk'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 RustDesk 配置
|
||||||
|
*/
|
||||||
|
update: (config: RustDeskConfigUpdate) =>
|
||||||
|
request<RustDeskConfigResponse>('/config/rustdesk', {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RustDesk 完整状态
|
||||||
|
*/
|
||||||
|
getStatus: () => request<RustDeskStatusResponse>('/config/rustdesk/status'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备密码(管理员专用)
|
||||||
|
*/
|
||||||
|
getPassword: () => request<RustDeskPasswordResponse>('/config/rustdesk/password'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成设备 ID
|
||||||
|
*/
|
||||||
|
regenerateId: () =>
|
||||||
|
request<RustDeskConfigResponse>('/config/rustdesk/regenerate-id', {
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成设备密码
|
||||||
|
*/
|
||||||
|
regeneratePassword: () =>
|
||||||
|
request<RustDeskConfigResponse>('/config/rustdesk/regenerate-password', {
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|||||||
@@ -587,6 +587,11 @@ export {
|
|||||||
atxConfigApi,
|
atxConfigApi,
|
||||||
audioConfigApi,
|
audioConfigApi,
|
||||||
extensionsApi,
|
extensionsApi,
|
||||||
|
rustdeskConfigApi,
|
||||||
|
type RustDeskConfigResponse,
|
||||||
|
type RustDeskStatusResponse,
|
||||||
|
type RustDeskConfigUpdate,
|
||||||
|
type RustDeskPasswordResponse,
|
||||||
} from './config'
|
} from './config'
|
||||||
|
|
||||||
// 导出生成的类型
|
// 导出生成的类型
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ export default {
|
|||||||
available: 'Available',
|
available: 'Available',
|
||||||
unavailable: 'Unavailable',
|
unavailable: 'Unavailable',
|
||||||
running: 'Running',
|
running: 'Running',
|
||||||
|
starting: 'Starting',
|
||||||
stopped: 'Stopped',
|
stopped: 'Stopped',
|
||||||
failed: 'Failed',
|
failed: 'Failed',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
@@ -641,6 +642,41 @@ export default {
|
|||||||
virtualIp: 'Virtual IP',
|
virtualIp: 'Virtual IP',
|
||||||
virtualIpHint: 'Leave empty for DHCP, or specify with CIDR (e.g., 10.0.0.1/24)',
|
virtualIpHint: 'Leave empty for DHCP, or specify with CIDR (e.g., 10.0.0.1/24)',
|
||||||
},
|
},
|
||||||
|
// rustdesk
|
||||||
|
rustdesk: {
|
||||||
|
title: 'RustDesk Remote',
|
||||||
|
desc: 'Remote access via RustDesk client',
|
||||||
|
serverSettings: 'Server Settings',
|
||||||
|
rendezvousServer: 'ID Server',
|
||||||
|
rendezvousServerPlaceholder: 'hbbs.example.com:21116',
|
||||||
|
rendezvousServerHint: 'RustDesk ID server address (required)',
|
||||||
|
relayServer: 'Relay Server',
|
||||||
|
relayServerPlaceholder: 'hbbr.example.com:21117',
|
||||||
|
relayServerHint: 'Relay server address, auto-derived from ID server if empty',
|
||||||
|
deviceInfo: 'Device Info',
|
||||||
|
deviceId: 'Device ID',
|
||||||
|
deviceIdHint: 'Use this ID in RustDesk client to connect',
|
||||||
|
devicePassword: 'Device Password',
|
||||||
|
showPassword: 'Show Password',
|
||||||
|
hidePassword: 'Hide Password',
|
||||||
|
regenerateId: 'Regenerate ID',
|
||||||
|
regeneratePassword: 'Regenerate Password',
|
||||||
|
confirmRegenerateId: 'Are you sure you want to regenerate the device ID? Existing clients will need to reconnect with the new ID.',
|
||||||
|
confirmRegeneratePassword: 'Are you sure you want to regenerate the password? Existing clients will need to reconnect with the new password.',
|
||||||
|
serviceStatus: 'Service Status',
|
||||||
|
rendezvousStatus: 'Registration Status',
|
||||||
|
registered: 'Registered',
|
||||||
|
connected: 'Connected',
|
||||||
|
disconnected: 'Disconnected',
|
||||||
|
connecting: 'Connecting',
|
||||||
|
notConfigured: 'Not Configured',
|
||||||
|
notInitialized: 'Not Initialized',
|
||||||
|
copyId: 'Copy ID',
|
||||||
|
copyPassword: 'Copy Password',
|
||||||
|
copied: 'Copied',
|
||||||
|
keypairGenerated: 'Keypair Generated',
|
||||||
|
noKeypair: 'No Keypair',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
title: 'Connection Stats',
|
title: 'Connection Stats',
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ export default {
|
|||||||
available: '可用',
|
available: '可用',
|
||||||
unavailable: '不可用',
|
unavailable: '不可用',
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
|
starting: '启动中',
|
||||||
stopped: '已停止',
|
stopped: '已停止',
|
||||||
failed: '启动失败',
|
failed: '启动失败',
|
||||||
start: '启动',
|
start: '启动',
|
||||||
@@ -641,6 +642,41 @@ export default {
|
|||||||
virtualIp: '虚拟 IP',
|
virtualIp: '虚拟 IP',
|
||||||
virtualIpHint: '留空则自动分配,手动指定需包含网段(如 10.0.0.1/24)',
|
virtualIpHint: '留空则自动分配,手动指定需包含网段(如 10.0.0.1/24)',
|
||||||
},
|
},
|
||||||
|
// rustdesk
|
||||||
|
rustdesk: {
|
||||||
|
title: 'RustDesk 远程',
|
||||||
|
desc: '使用 RustDesk 客户端进行远程访问',
|
||||||
|
serverSettings: '服务器设置',
|
||||||
|
rendezvousServer: 'ID 服务器',
|
||||||
|
rendezvousServerPlaceholder: 'hbbs.example.com:21116',
|
||||||
|
rendezvousServerHint: 'RustDesk ID 服务器地址(必填)',
|
||||||
|
relayServer: '中继服务器',
|
||||||
|
relayServerPlaceholder: 'hbbr.example.com:21117',
|
||||||
|
relayServerHint: '中继服务器地址,留空则自动从 ID 服务器推导',
|
||||||
|
deviceInfo: '设备信息',
|
||||||
|
deviceId: '设备 ID',
|
||||||
|
deviceIdHint: '此 ID 用于 RustDesk 客户端连接',
|
||||||
|
devicePassword: '设备密码',
|
||||||
|
showPassword: '显示密码',
|
||||||
|
hidePassword: '隐藏密码',
|
||||||
|
regenerateId: '重新生成 ID',
|
||||||
|
regeneratePassword: '重新生成密码',
|
||||||
|
confirmRegenerateId: '确定要重新生成设备 ID 吗?现有客户端需要使用新 ID 重新连接。',
|
||||||
|
confirmRegeneratePassword: '确定要重新生成设备密码吗?现有客户端需要使用新密码重新连接。',
|
||||||
|
serviceStatus: '服务状态',
|
||||||
|
rendezvousStatus: '注册状态',
|
||||||
|
registered: '已注册',
|
||||||
|
connected: '已连接',
|
||||||
|
disconnected: '未连接',
|
||||||
|
connecting: '连接中',
|
||||||
|
notConfigured: '未配置',
|
||||||
|
notInitialized: '未初始化',
|
||||||
|
copyId: '复制 ID',
|
||||||
|
copyPassword: '复制密码',
|
||||||
|
copied: '已复制',
|
||||||
|
keypairGenerated: '密钥对已生成',
|
||||||
|
noKeypair: '密钥对未生成',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
title: '连接统计',
|
title: '连接统计',
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ import {
|
|||||||
msdConfigApi,
|
msdConfigApi,
|
||||||
atxConfigApi,
|
atxConfigApi,
|
||||||
extensionsApi,
|
extensionsApi,
|
||||||
|
rustdeskConfigApi,
|
||||||
type EncoderBackendInfo,
|
type EncoderBackendInfo,
|
||||||
type User as UserType,
|
type User as UserType,
|
||||||
|
type RustDeskConfigResponse,
|
||||||
|
type RustDeskStatusResponse,
|
||||||
|
type RustDeskPasswordResponse,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
import type {
|
import type {
|
||||||
ExtensionsStatus,
|
ExtensionsStatus,
|
||||||
@@ -66,6 +70,8 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Plus,
|
Plus,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Copy,
|
||||||
|
ScreenShare,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
@@ -97,6 +103,7 @@ const navGroups = computed(() => [
|
|||||||
{
|
{
|
||||||
title: t('settings.extensions'),
|
title: t('settings.extensions'),
|
||||||
items: [
|
items: [
|
||||||
|
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
|
||||||
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
|
||||||
{ id: 'ext-gostc', label: t('extensions.gostc.title'), icon: Globe },
|
{ id: 'ext-gostc', label: t('extensions.gostc.title'), icon: Globe },
|
||||||
{ id: 'ext-easytier', label: t('extensions.easytier.title'), icon: Network },
|
{ id: 'ext-easytier', label: t('extensions.easytier.title'), icon: Network },
|
||||||
@@ -160,6 +167,18 @@ const extConfig = ref({
|
|||||||
easytier: { enabled: false, network_name: '', network_secret: '', peer_urls: [] as string[], virtual_ip: '' },
|
easytier: { enabled: false, network_name: '', network_secret: '', peer_urls: [] as string[], virtual_ip: '' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// RustDesk config state
|
||||||
|
const rustdeskConfig = ref<RustDeskConfigResponse | null>(null)
|
||||||
|
const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
|
||||||
|
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(null)
|
||||||
|
const rustdeskLoading = ref(false)
|
||||||
|
const rustdeskCopied = ref<'id' | 'password' | null>(null)
|
||||||
|
const rustdeskLocalConfig = ref({
|
||||||
|
enabled: false,
|
||||||
|
rendezvous_server: '',
|
||||||
|
relay_server: '',
|
||||||
|
})
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
interface DeviceConfig {
|
interface DeviceConfig {
|
||||||
video: Array<{
|
video: Array<{
|
||||||
@@ -764,6 +783,135 @@ function getAtxDevicesForDriver(driver: string): string[] {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RustDesk management functions
|
||||||
|
async function loadRustdeskConfig() {
|
||||||
|
rustdeskLoading.value = true
|
||||||
|
try {
|
||||||
|
const [config, status] = await Promise.all([
|
||||||
|
rustdeskConfigApi.get(),
|
||||||
|
rustdeskConfigApi.getStatus(),
|
||||||
|
])
|
||||||
|
rustdeskConfig.value = config
|
||||||
|
rustdeskStatus.value = status
|
||||||
|
rustdeskLocalConfig.value = {
|
||||||
|
enabled: config.enabled,
|
||||||
|
rendezvous_server: config.rendezvous_server,
|
||||||
|
relay_server: config.relay_server || '',
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load RustDesk config:', e)
|
||||||
|
} finally {
|
||||||
|
rustdeskLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRustdeskPassword() {
|
||||||
|
try {
|
||||||
|
rustdeskPassword.value = await rustdeskConfigApi.getPassword()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load RustDesk password:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRustdeskConfig() {
|
||||||
|
loading.value = true
|
||||||
|
saved.value = false
|
||||||
|
try {
|
||||||
|
await rustdeskConfigApi.update({
|
||||||
|
enabled: rustdeskLocalConfig.value.enabled,
|
||||||
|
rendezvous_server: rustdeskLocalConfig.value.rendezvous_server || undefined,
|
||||||
|
relay_server: rustdeskLocalConfig.value.relay_server || undefined,
|
||||||
|
})
|
||||||
|
await loadRustdeskConfig()
|
||||||
|
saved.value = true
|
||||||
|
setTimeout(() => (saved.value = false), 2000)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save RustDesk config:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateRustdeskId() {
|
||||||
|
if (!confirm(t('extensions.rustdesk.confirmRegenerateId'))) return
|
||||||
|
rustdeskLoading.value = true
|
||||||
|
try {
|
||||||
|
await rustdeskConfigApi.regenerateId()
|
||||||
|
await loadRustdeskConfig()
|
||||||
|
await loadRustdeskPassword()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to regenerate RustDesk ID:', e)
|
||||||
|
} finally {
|
||||||
|
rustdeskLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateRustdeskPassword() {
|
||||||
|
if (!confirm(t('extensions.rustdesk.confirmRegeneratePassword'))) return
|
||||||
|
rustdeskLoading.value = true
|
||||||
|
try {
|
||||||
|
await rustdeskConfigApi.regeneratePassword()
|
||||||
|
await loadRustdeskConfig()
|
||||||
|
await loadRustdeskPassword()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to regenerate RustDesk password:', e)
|
||||||
|
} finally {
|
||||||
|
rustdeskLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text: string, type: 'id' | 'password') {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
rustdeskCopied.value = type
|
||||||
|
setTimeout(() => (rustdeskCopied.value = null), 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRustdeskServiceStatusText(status: string | undefined): string {
|
||||||
|
if (!status) return t('extensions.rustdesk.notConfigured')
|
||||||
|
switch (status) {
|
||||||
|
case 'running': return t('extensions.running')
|
||||||
|
case 'starting': return t('extensions.starting')
|
||||||
|
case 'stopped': return t('extensions.stopped')
|
||||||
|
case 'not_initialized': return t('extensions.rustdesk.notInitialized')
|
||||||
|
default:
|
||||||
|
// Handle "error: xxx" format
|
||||||
|
if (status.startsWith('error:')) return t('extensions.failed')
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRustdeskRendezvousStatusText(status: string | null | undefined): string {
|
||||||
|
if (!status) return '-'
|
||||||
|
switch (status) {
|
||||||
|
case 'registered': return t('extensions.rustdesk.registered')
|
||||||
|
case 'connected': return t('extensions.rustdesk.connected')
|
||||||
|
case 'connecting': return t('extensions.rustdesk.connecting')
|
||||||
|
case 'disconnected': return t('extensions.rustdesk.disconnected')
|
||||||
|
default:
|
||||||
|
// Handle "error: xxx" format
|
||||||
|
if (status.startsWith('error:')) return t('extensions.failed')
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRustdeskStatusClass(status: string | null | undefined): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
case 'registered':
|
||||||
|
case 'connected': return 'bg-green-500'
|
||||||
|
case 'starting':
|
||||||
|
case 'connecting': return 'bg-yellow-500'
|
||||||
|
case 'stopped':
|
||||||
|
case 'not_initialized':
|
||||||
|
case 'disconnected': return 'bg-gray-400'
|
||||||
|
default:
|
||||||
|
// Handle "error: xxx" format
|
||||||
|
if (status?.startsWith('error:')) return 'bg-red-500'
|
||||||
|
return 'bg-gray-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Load theme preference
|
// Load theme preference
|
||||||
@@ -781,6 +929,8 @@ onMounted(async () => {
|
|||||||
loadExtensions(),
|
loadExtensions(),
|
||||||
loadAtxConfig(),
|
loadAtxConfig(),
|
||||||
loadAtxDevices(),
|
loadAtxDevices(),
|
||||||
|
loadRustdeskConfig(),
|
||||||
|
loadRustdeskPassword(),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1625,6 +1775,137 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- RustDesk Section -->
|
||||||
|
<div v-show="activeSection === 'ext-rustdesk'" class="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<CardTitle>{{ t('extensions.rustdesk.title') }}</CardTitle>
|
||||||
|
<CardDescription>{{ t('extensions.rustdesk.desc') }}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge :variant="rustdeskStatus?.service_status === 'Running' ? 'default' : 'secondary'">
|
||||||
|
{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}
|
||||||
|
</Badge>
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
|
||||||
|
<RefreshCw :class="['h-4 w-4', rustdeskLoading ? 'animate-spin' : '']" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- Status and controls -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div :class="['w-2 h-2 rounded-full', getRustdeskStatusClass(rustdeskStatus?.service_status)]" />
|
||||||
|
<span class="text-sm">{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}</span>
|
||||||
|
<template v-if="rustdeskStatus?.rendezvous_status">
|
||||||
|
<span class="text-muted-foreground">|</span>
|
||||||
|
<div :class="['w-2 h-2 rounded-full', getRustdeskStatusClass(rustdeskStatus?.rendezvous_status)]" />
|
||||||
|
<span class="text-sm text-muted-foreground">{{ getRustdeskRendezvousStatusText(rustdeskStatus?.rendezvous_status) }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Config -->
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||||
|
<Switch v-model="rustdeskLocalConfig.enabled" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">{{ t('extensions.rustdesk.rendezvousServer') }}</Label>
|
||||||
|
<div class="col-span-3 space-y-1">
|
||||||
|
<Input
|
||||||
|
v-model="rustdeskLocalConfig.rendezvous_server"
|
||||||
|
:placeholder="t('extensions.rustdesk.rendezvousServerPlaceholder')"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.rendezvousServerHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">{{ t('extensions.rustdesk.relayServer') }}</Label>
|
||||||
|
<div class="col-span-3 space-y-1">
|
||||||
|
<Input
|
||||||
|
v-model="rustdeskLocalConfig.relay_server"
|
||||||
|
:placeholder="t('extensions.rustdesk.relayServerPlaceholder')"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.relayServerHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Device Info -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-sm font-medium">{{ t('extensions.rustdesk.deviceInfo') }}</h4>
|
||||||
|
|
||||||
|
<!-- Device ID -->
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">{{ t('extensions.rustdesk.deviceId') }}</Label>
|
||||||
|
<div class="col-span-3 flex items-center gap-2">
|
||||||
|
<code class="font-mono text-lg bg-muted px-3 py-1 rounded">{{ rustdeskConfig?.device_id || '-' }}</code>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')"
|
||||||
|
:disabled="!rustdeskConfig?.device_id"
|
||||||
|
>
|
||||||
|
<Check v-if="rustdeskCopied === 'id'" class="h-4 w-4 text-green-500" />
|
||||||
|
<Copy v-else class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" @click="regenerateRustdeskId" :disabled="rustdeskLoading">
|
||||||
|
<RefreshCw class="h-4 w-4 mr-1" />
|
||||||
|
{{ t('extensions.rustdesk.regenerateId') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Device Password (直接显示) -->
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">{{ t('extensions.rustdesk.devicePassword') }}</Label>
|
||||||
|
<div class="col-span-3 flex items-center gap-2">
|
||||||
|
<code class="font-mono text-lg bg-muted px-3 py-1 rounded">{{ rustdeskPassword?.device_password || '-' }}</code>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')"
|
||||||
|
:disabled="!rustdeskPassword?.device_password"
|
||||||
|
>
|
||||||
|
<Check v-if="rustdeskCopied === 'password'" class="h-4 w-4 text-green-500" />
|
||||||
|
<Copy v-else class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" @click="regenerateRustdeskPassword" :disabled="rustdeskLoading">
|
||||||
|
<RefreshCw class="h-4 w-4 mr-1" />
|
||||||
|
{{ t('extensions.rustdesk.regeneratePassword') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Keypair Status -->
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label class="text-right">{{ t('extensions.rustdesk.keypairGenerated') }}</Label>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<Badge :variant="rustdeskConfig?.has_keypair ? 'default' : 'secondary'">
|
||||||
|
{{ rustdeskConfig?.has_keypair ? t('common.yes') : t('common.no') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<!-- Save button -->
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button :disabled="loading" @click="saveRustdeskConfig">
|
||||||
|
<Check v-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ saved ? t('common.success') : t('common.save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<div v-show="activeSection === 'about'" class="space-y-6">
|
<div v-show="activeSection === 'about'" class="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user