重大变更: - 从prost切换到protobuf 3.4实现完整的RustDesk协议栈 - 新增P2P打洞模块(punch.rs)支持直连和中继回退 - 重构加密系统:临时Curve25519密钥对+Ed25519签名 - 完善HID适配器:支持CapsLock状态同步和修饰键映射 - 添加音频流支持:Opus编码+音频帧适配器 - 优化视频流:改进帧适配器和编码器协商 - 移除pacer.rs简化视频管道 扩展系统: - 在设置向导中添加扩展步骤(ttyd/rustdesk切换) - 扩展可用性检测和自动启动 - 新增WebConfig handler用于Web服务器配置 前端改进: - SetupView增加第4步扩展配置 - 音频设备列表和配置界面 - 新增多语言支持(en-US/zh-CN) - TypeScript类型生成更新 文档: - 更新系统架构文档 - 完善config/hid/rustdesk/video/webrtc模块文档
38 KiB
RustDesk 模块文档
1. 模块概述
RustDesk 模块实现 RustDesk 协议集成,允许使用标准 RustDesk 客户端访问 One-KVM 设备。
1.1 主要功能
- RustDesk 协议实现 (Protobuf + NaCl 加密)
- Rendezvous 服务器 (hbbs) 通信
- 中继服务器 (hbbr) 通信
- P2P 直连与中继回退
- 局域网直连支持
- 视频/音频/HID 转换
- 端到端加密 (Curve25519 + XSalsa20-Poly1305)
- 签名认证 (Ed25519)
- 公共服务器支持 (通过 secrets.toml)
- 动态编码器协商 (H264/H265/VP8/VP9)
- 输入节流 (防止 HID EAGAIN)
- CapsLock 状态同步
- 管道自动重订阅 (支持热更新)
1.2 文件结构
src/rustdesk/
├── mod.rs # RustDeskService 主服务类
├── connection.rs # 客户端连接处理 (Connection, ConnectionManager)
├── rendezvous.rs # Rendezvous 中介者 (RendezvousMediator)
├── punch.rs # P2P 直连尝试与中继回退
├── crypto.rs # NaCl 加密 (Curve25519 + Ed25519)
├── config.rs # 配置管理 (RustDeskConfig)
├── hid_adapter.rs # HID 事件转换 (RustDesk → One-KVM)
├── frame_adapters.rs # 音视频帧转换 (零拷贝优化)
├── protocol.rs # Protobuf 协议包装
└── bytes_codec.rs # 变长帧编解码
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 │
└────────┬────────┘
│
┌───────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐
│ Rendezvous │ │ Connection │ │ Crypto │
│ Mediator │ │ Manager │ │ (Keys) │
└────────┬─────────┘ └────────┬─────────┘ └───────────────┘
│ │
│ UDP │ TCP (P2P/Relay/Intranet)
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ hbbs Server │ │ Connections │
│ (Registration) │ │ ┌──────────────┐ │
└──────────────────┘ │ │ Connection 1 │ │
│ │ Connection 2 │ │
│ └──────────────┘ │
└────────┬─────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ HID Adapter │ │ Frame Adapters │ │ Input Throttler │
│ (Event Conv) │ │ (Zero-Copy) │ │ (Anti-EAGAIN) │
└───────┬───────┘ └────────┬─────────┘ └─────────────────┘
│ │
▼ ▼
┌───────────────┐ ┌─────────────────────────────┐
│ HID │ │ Video/Audio Manager │
│ Controller │ │ (Shared Pipeline) │
└───────────────┘ └─────────────────────────────┘
3. 核心组件
3.1 RustDeskService (mod.rs)
RustDesk 服务主类,管理整个 RustDesk 协议集成。
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 {
/// 创建新服务实例
pub fn new(
config: RustDeskConfig,
video_manager: Arc<VideoStreamManager>,
hid: Arc<HidController>,
audio: Arc<AudioController>,
) -> Self;
/// 启动服务 (启动 Rendezvous 注册和 TCP 监听)
pub async fn start(&self) -> anyhow::Result<()>;
/// 停止服务
pub async fn stop(&self) -> anyhow::Result<()>;
/// 重启服务 (用于配置更新)
pub async fn restart(&self, config: RustDeskConfig) -> anyhow::Result<()>;
/// 获取设备 ID
pub fn device_id(&self) -> String;
/// 获取服务状态
pub fn status(&self) -> ServiceStatus;
/// 获取 Rendezvous 状态
pub fn rendezvous_status(&self) -> Option<RendezvousStatus>;
/// 获取连接数量
pub fn connection_count(&self) -> usize;
/// 获取 TCP 监听端口
pub fn listen_port(&self) -> u16;
/// 保存凭据 (密钥和 UUID) 到配置
/// 返回更新后的配置 (如果有变更)
pub fn save_credentials(&self) -> Option<RustDeskConfig>;
}
pub enum ServiceStatus {
Stopped,
Starting,
Running,
Error(String),
}
主要流程:
- 启动流程: 初始化加密 → 启动 TCP 监听器 → 创建 Rendezvous Mediator → 设置回调 → 开始注册循环
- 连接处理: P2P 直连尝试 → 中继回退 → 局域网直连
- 停止流程: 发送停止信号 → 关闭所有连接 → 停止 Rendezvous → 等待任务结束
3.2 RendezvousMediator (rendezvous.rs)
Rendezvous 服务器通信中介者,处理设备注册、心跳维护和连接请求。
pub struct RendezvousMediator {
config: Arc<RwLock<RustDeskConfig>>,
keypair: Arc<RwLock<Option<KeyPair>>>, // Curve25519 加密密钥
signing_keypair: Arc<RwLock<Option<SigningKeyPair>>>, // Ed25519 签名密钥
status: Arc<RwLock<RendezvousStatus>>,
uuid: Arc<RwLock<[u8; 16]>>, // 设备 UUID (持久化)
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>>>,
punch_callback: Arc<RwLock<Option<PunchCallback>>>,
intranet_callback: Arc<RwLock<Option<IntranetCallback>>>,
listen_port: Arc<RwLock<u16>>, // TCP 监听端口
shutdown_tx: broadcast::Sender<()>,
}
impl RendezvousMediator {
/// 创建新的 Rendezvous 中介者
pub fn new(config: RustDeskConfig) -> Self;
/// 启动注册循环
pub async fn start(&self) -> anyhow::Result<()>;
/// 停止中介者
pub fn stop(&self);
/// 获取或生成加密密钥对
pub fn ensure_keypair(&self) -> KeyPair;
/// 获取或生成签名密钥对
pub fn ensure_signing_keypair(&self) -> SigningKeyPair;
/// 设置 TCP 监听端口
pub fn set_listen_port(&self, port: u16);
/// 设置中继请求回调
pub fn set_relay_callback(&self, callback: RelayCallback);
/// 设置 P2P 穿孔回调
pub fn set_punch_callback(&self, callback: PunchCallback);
/// 设置局域网连接回调
pub fn set_intranet_callback(&self, callback: IntranetCallback);
/// 获取当前状态
pub fn status(&self) -> RendezvousStatus;
/// 检查 UUID 是否需要保存
pub fn uuid_needs_save(&self) -> bool;
/// 标记 UUID 已保存
pub fn mark_uuid_saved(&self);
}
pub enum RendezvousStatus {
Disconnected, // 未连接
Connecting, // 正在连接
Connected, // 已连接但未注册
Registered, // 已注册
Error(String), // 错误状态
}
/// 中继请求回调
/// 参数: (rendezvous_addr, relay_server, uuid, socket_addr, device_id)
pub type RelayCallback = Arc<dyn Fn(String, String, String, Vec<u8>, String) + Send + Sync>;
/// P2P 穿孔回调
/// 参数: (peer_addr, rendezvous_addr, relay_server, uuid, socket_addr, device_id)
pub type PunchCallback = Arc<dyn Fn(Option<SocketAddr>, String, String, String, Vec<u8>, String) + Send + Sync>;
/// 局域网连接回调
/// 参数: (rendezvous_addr, peer_socket_addr, local_addr, relay_server, device_id)
pub type IntranetCallback = Arc<dyn Fn(String, Vec<u8>, SocketAddr, String, String) + Send + Sync>;
关键机制:
- UDP 通信: 使用 UDP 与 hbbs 服务器通信
- 双密钥系统: Curve25519 用于加密,Ed25519 用于签名
- UUID 持久化: 避免重新注册时的 UUID_MISMATCH 错误
- 地址混淆: 使用
AddrMangle编码地址避免防火墙篡改 - 三种连接模式: P2P 直连、中继连接、局域网直连
3.3 Connection & ConnectionManager (connection.rs)
客户端连接处理,包含连接生命周期管理和数据传输。
/// 单个客户端连接
pub struct Connection {
id: u32,
device_id: String,
peer_id: String,
state: Arc<RwLock<ConnectionState>>,
signing_keypair: SigningKeyPair,
temp_keypair: (box_::PublicKey, box_::SecretKey), // 每连接临时密钥
password: String,
hid: Option<Arc<HidController>>,
audio: Option<Arc<AudioController>>,
video_manager: Option<Arc<VideoStreamManager>>,
session_key: Option<secretbox::Key>,
encryption_enabled: bool,
negotiated_codec: Option<VideoEncoderType>,
input_throttler: InputThrottler, // 输入节流防止 EAGAIN
last_caps_lock: bool, // CapsLock 状态跟踪
// ... 更多字段
}
impl Connection {
/// 创建新连接
pub fn new(
id: u32,
config: &RustDeskConfig,
signing_keypair: SigningKeyPair,
hid: Option<Arc<HidController>>,
audio: Option<Arc<AudioController>>,
video_manager: Option<Arc<VideoStreamManager>>,
) -> (Self, mpsc::UnboundedReceiver<ConnectionMessage>);
/// 处理 TCP 连接
pub async fn handle_tcp(&mut self, stream: TcpStream, peer_addr: SocketAddr) -> anyhow::Result<()>;
/// 关闭连接
pub fn close(&self);
}
/// 连接管理器
pub struct ConnectionManager {
connections: Arc<RwLock<Vec<Arc<RwLock<ConnectionInfo>>>>>,
next_id: Arc<RwLock<u32>>,
config: Arc<RwLock<RustDeskConfig>>,
keypair: Arc<RwLock<Option<KeyPair>>>,
signing_keypair: Arc<RwLock<Option<SigningKeyPair>>>,
hid: Arc<RwLock<Option<Arc<HidController>>>>,
audio: Arc<RwLock<Option<Arc<AudioController>>>>,
video_manager: Arc<RwLock<Option<Arc<VideoStreamManager>>>>,
}
impl ConnectionManager {
pub fn new(config: RustDeskConfig) -> Self;
pub async fn accept_connection(&self, stream: TcpStream, peer_addr: SocketAddr) -> anyhow::Result<u32>;
pub fn connection_count(&self) -> usize;
pub fn close_all(&self);
}
pub enum ConnectionState {
Pending, // 等待连接
Handshaking, // 握手中
Active, // 活跃
Closed, // 已关闭
Error(String), // 错误
}
/// 输入节流器 (防止 HID EAGAIN 错误)
struct InputThrottler {
last_mouse_time: Instant,
mouse_interval: Duration, // 默认 16ms (≈60Hz)
}
连接流程:
- 握手: 发送 SignedId (含临时公钥) → 接收 PublicKey (含对称密钥) → 解密对称密钥
- 认证: 发送 Hash (密码盐) → 接收 LoginRequest (密码哈希) → 验证密码
- 编码协商: 根据可用编码器选择最优编解码器(H264 > H265 > VP8 > VP9)
- 流传输: 订阅共享视频/音频管道 → 转换为 RustDesk 格式 → 加密发送
- 输入处理: 接收 KeyEvent/MouseEvent → 节流 → 转换为 USB HID → 发送到 HID 控制器
3.4 RustDeskKeys (crypto.rs)
加密密钥管理。
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 事件转换。
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)
帧格式转换,支持零拷贝。
pub struct VideoFrameAdapter {
codec: VideoCodec,
seq: u32,
timestamp_base: u64,
}
impl VideoFrameAdapter {
/// 创建适配器
pub fn new(codec: VideoCodec) -> Self;
/// 零拷贝转换 (推荐)
/// Bytes 是引用计数类型,clone 只增加引用计数
pub fn encode_frame_bytes_zero_copy(
&mut self,
data: Bytes,
is_keyframe: bool,
timestamp_ms: u64,
) -> Bytes;
/// 转换视频帧到 RustDesk 格式 (会拷贝数据)
pub fn encode_frame_bytes(
&mut self,
data: &[u8],
is_keyframe: bool,
timestamp_ms: u64,
) -> Bytes;
}
/// RustDesk 视频帧 (protobuf 生成)
/// 注意: data 字段使用 bytes::Bytes 类型以支持零拷贝
pub struct EncodedVideoFrame {
pub data: Bytes, // 零拷贝: 引用计数共享
pub key: bool,
pub pts: i64,
}
pub enum VideoCodec {
H264,
H265,
VP8,
VP9,
AV1,
}
3.7 协议消息 (protocol.rs)
Protobuf 消息包装,使用 protobuf-rust(与 RustDesk 服务器一致)。
/// 使用 protobuf-codegen 生成的 protobuf 消息
pub mod hbb {
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
}
// Re-export commonly used types
pub use hbb::rendezvous::{...};
pub use hbb::message::{...};
/// 解码 RendezvousMessage
pub fn decode_rendezvous_message(buf: &[u8]) -> Result<RendezvousMessage, protobuf::Error> {
RendezvousMessage::parse_from_bytes(buf)
}
/// 解码 Message (session message)
pub fn decode_message(buf: &[u8]) -> Result<Message, protobuf::Error> {
Message::parse_from_bytes(buf)
}
3.8 帧编码 (bytes_codec.rs)
变长帧协议。
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 定义
// 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. 配置
#[derive(Serialize, Deserialize)]
#[typeshare]
pub struct RustDeskConfig {
/// 是否启用
pub enabled: bool,
/// Rendezvous 服务器地址 (hbbs)
/// 格式: "rs.example.com" 或 "192.168.1.100:21116"
/// 如果为空,使用 secrets.toml 中配置的公共服务器
pub rendezvous_server: String,
/// 中继服务器地址 (hbbr),默认与 rendezvous 同主机
/// 格式: "relay.example.com" 或 "192.168.1.100:21117"
pub relay_server: Option<String>,
/// 中继服务器认证密钥 (如果中继服务器使用 -k 选项)
#[typeshare(skip)]
pub relay_key: Option<String>,
/// 设备 ID (9 位数字),自动生成
pub device_id: String,
/// 设备密码 (客户端连接认证)
#[typeshare(skip)]
pub device_password: String,
/// Curve25519 公钥 (Base64 编码),用于加密
#[typeshare(skip)]
pub public_key: Option<String>,
/// Curve25519 私钥 (Base64 编码),用于加密
#[typeshare(skip)]
pub private_key: Option<String>,
/// Ed25519 签名公钥 (Base64 编码),用于 SignedId 验证
#[typeshare(skip)]
pub signing_public_key: Option<String>,
/// Ed25519 签名私钥 (Base64 编码),用于签名 SignedId
#[typeshare(skip)]
pub signing_private_key: Option<String>,
/// UUID (持久化,避免 UUID_MISMATCH 错误)
#[typeshare(skip)]
pub uuid: Option<String>,
}
impl RustDeskConfig {
/// 检查配置是否有效
pub fn is_valid(&self) -> bool;
/// 检查是否使用公共服务器
pub fn is_using_public_server(&self) -> bool;
/// 获取有效的 Rendezvous 服务器地址
pub fn effective_rendezvous_server(&self) -> &str;
/// 获取公共服务器信息 (如果配置了)
pub fn public_server_info() -> Option<PublicServerInfo>;
/// 获取带默认端口的 Rendezvous 地址
pub fn rendezvous_addr(&self) -> String;
/// 获取带默认端口的中继服务器地址
pub fn relay_addr(&self) -> Option<String>;
/// 确保 UUID 存在 (自动生成并标记需要保存)
pub fn ensure_uuid(&mut self) -> ([u8; 16], bool);
}
/// 公共服务器信息
#[derive(Serialize, Deserialize)]
#[typeshare]
pub struct PublicServerInfo {
pub server: String, // 服务器地址
pub public_key: String, // 公钥 (Base64)
}
配置文件示例
使用自建服务器:
[rustdesk]
enabled = true
rendezvous_server = "192.168.1.100:21116"
relay_server = "192.168.1.100:21117"
device_id = "123456789"
device_password = "mypassword"
# 密钥和 UUID 由程序自动生成和保存
使用公共服务器:
[rustdesk]
enabled = true
rendezvous_server = "" # 留空使用 secrets.toml 中的公共服务器
device_id = "123456789"
device_password = "mypassword"
secrets.toml 公共服务器配置:
[rustdesk]
# 公共服务器配置 (可选)
public_server = "rs-ny.rustdesk.com"
public_key = "xxx...base64...xxx"
relay_key = "xxx...key...xxx"
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 | 断开连接 |
响应格式
// 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. 事件
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. 错误处理
#[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 启动服务
let config = RustDeskConfig {
enabled: true,
rendezvous_server: "".to_string(), // 使用公共服务器
device_id: "123456789".to_string(),
device_password: "mypassword".to_string(),
..Default::default()
};
let service = RustDeskService::new(
config,
video_manager,
hid,
audio,
);
service.start().await?;
println!("Device ID: {}", service.device_id());
println!("Listen Port: {}", service.listen_port());
println!("Status: {:?}", service.status());
9.2 使用自建服务器
let config = RustDeskConfig {
enabled: true,
rendezvous_server: "192.168.1.100:21116".to_string(),
relay_server: Some("192.168.1.100:21117".to_string()),
relay_key: Some("your_licence_key".to_string()), // 如果使用 -k 选项
device_id: "123456789".to_string(),
device_password: "mypassword".to_string(),
..Default::default()
};
let service = RustDeskService::new(config, video_manager, hid, audio);
service.start().await?;
9.3 客户端连接
使用 RustDesk 客户端:
- 下载并安装 RustDesk 客户端 (https://rustdesk.com)
- 如果使用自建服务器,在设置中配置 ID 服务器地址
- 输入设备 ID (9 位数字)
- 输入密码 (如果设置)
- 点击连接
连接过程:
客户端 One-KVM
│ │
│ 1. 查询设备 (hbbs) │
│──────────────►┌─────────┐◄──────│
│ │ hbbs │ │
│◄──────────────└─────────┘ │
│ 2. 返回地址信息 │
│ │
│ 3a. 尝试 P2P 直连 (3s 超时) │
│─────────────────────────────────│
│ │
│ 3b. 失败则通过中继 (hbbr) │
│──────────►┌─────────┐◄──────────│
│ │ hbbr │ │
│◄──────────└─────────┘───────────│
│ │
│ 4. 握手 + 认证 │
│◄────────────────────────────────│
│ │
│ 5. 视频/音频/HID 传输 │
│◄────────────────────────────────│
9.4 保存凭据
// 启动后,凭据会自动生成
// 定期保存凭据到配置文件,避免重启后 UUID 变化
if let Some(updated_config) = service.save_credentials() {
// 保存到配置存储
config_manager.save_rustdesk_config(&updated_config).await?;
}
10. 性能优化
10.1 零拷贝设计
RustDesk 模块使用 bytes::Bytes 类型实现零拷贝:
// build.rs 配置 protobuf 使用 Bytes
protobuf_codegen::Codegen::new()
.pure()
.out_dir(&protos_dir)
.inputs(["protos/rendezvous.proto", "protos/message.proto"])
.include("protos")
.customize(protobuf_codegen::Customize::default().tokio_bytes(true))
.run()
.expect("Failed to compile protobuf files");
// 帧转换时直接传递 Bytes,只增加引用计数
let msg_bytes = video_adapter.encode_frame_bytes_zero_copy(
frame.data.clone(), // clone 只增加引用计数,不拷贝数据
frame.is_keyframe,
frame.pts_ms as u64,
);
// TCP 发送也使用 Bytes,避免拷贝
writer.write_all(&msg_bytes).await?;
10.2 输入节流 (Input Throttling)
防止 HID 设备 EAGAIN 错误和提高性能:
pub struct InputThrottler {
last_mouse_time: Instant,
mouse_interval: Duration, // 默认 16ms (≈60Hz)
}
impl InputThrottler {
/// 检查是否应该发送鼠标事件
pub fn should_send_mouse(&mut self) -> bool {
let now = Instant::now();
if now.duration_since(self.last_mouse_time) >= self.mouse_interval {
self.last_mouse_time = now;
true
} else {
false // 跳过此事件,避免 HID 缓冲区溢出
}
}
}
效果:
- 防止 HID write() 返回 EAGAIN (资源暂时不可用)
- 减少 CPU 使用率 (过滤冗余的鼠标移动事件)
- 保持流畅的鼠标体验 (60Hz 已足够)
10.3 共享管道架构
视频/音频使用共享管道,多个连接订阅同一数据流:
// 视频管道 (broadcast channel)
let (tx, _rx) = broadcast::channel(4); // 容量 4 帧
// 连接 1 订阅
let mut rx1 = tx.subscribe();
// 连接 2 订阅 (共享同一编码数据)
let mut rx2 = tx.subscribe();
// 发送帧时,所有订阅者都会收到 (零拷贝)
tx.send(frame).unwrap();
优点:
- 单次编码,多次使用 (减少 CPU/GPU 负载)
- 零拷贝共享 (使用
Bytes引用计数) - 自动背压控制 (慢客户端会 lag,不影响快客户端)
10.4 管道重订阅机制
当视频管道重启时 (如切换码率),连接自动重新订阅:
async fn run_video_streaming(...) {
// 外层循环: 处理管道重启
'subscribe_loop: loop {
// 订阅视频管道
let mut encoded_frame_rx = video_manager.subscribe_encoded_frames().await;
// 内层循环: 接收帧
loop {
match encoded_frame_rx.recv().await {
Ok(frame) => { /* 发送帧 */ }
Err(RecvError::Lagged(n)) => {
warn!("Video lagged {} frames", n);
}
Err(RecvError::Closed) => {
// 管道重启,重新订阅
info!("Video pipeline closed, re-subscribing...");
tokio::time::sleep(Duration::from_millis(100)).await;
continue 'subscribe_loop;
}
}
}
}
}
10.5 预分配缓冲区
TCP 发送使用预分配缓冲区减少内存分配:
// 预分配 128KB 缓冲区 (足够大部分视频帧)
let mut frame_buf = BytesMut::with_capacity(128 * 1024);
// 复用缓冲区发送多个帧
loop {
frame_buf.clear();
write_frame_buffered(&mut writer, &data, &mut frame_buf).await?;
}
10.6 编码器协商
根据硬件能力动态选择最优编码器:
// 优先级: H264 > H265 > VP8 > VP9
let available_encoders = video_manager.available_encoders();
let preferred_order = [
VideoEncoderType::H264,
VideoEncoderType::H265,
VideoEncoderType::VP8,
VideoEncoderType::VP9,
];
for codec in preferred_order {
if available_encoders.contains(&codec) {
// 使用此编码器
negotiated_codec = Some(codec);
break;
}
}
效果:
- 优先使用硬件加速 (H264/H265)
- 回退到软件编码 (VP8/VP9) 如果硬件不可用
- 客户端自动适配 (RustDesk 支持所有编码器)
11. P2P 直连与中继回退
11.1 连接策略
当收到 PunchHole 请求时,系统会先尝试 P2P 直连,失败后自动回退到中继:
PunchHole 请求
│
▼
┌─────────────────┐
│ 尝试 P2P 直连 │ ◄── 3秒超时
│ (TCP connect) │
└────────┬────────┘
│
┌────┴────┐
│ 成功? │
└────┬────┘
Yes │ No
▼ │ ▼
┌───────┐│┌─────────────┐
│ 直连 │││ 中继回退 │
│ 通信 │││ (hbbr) │
└───────┘│└─────────────┘
11.2 punch.rs 模块
/// P2P 直连超时时间
const DIRECT_CONNECT_TIMEOUT_MS: u64 = 3000;
/// 直连结果
pub enum PunchResult {
DirectConnection(TcpStream),
NeedRelay,
}
/// 尝试 P2P 直连
pub async fn try_direct_connection(peer_addr: SocketAddr) -> PunchResult;
/// Punch hole 处理器
pub struct PunchHoleHandler {
connection_manager: Arc<ConnectionManager>,
}
11.3 中继认证
如果中继服务器配置了 -k 选项,需<EFBC8C><E99C80>在配置中设置 relay_key:
// 发送 RequestRelay 时包含 licence_key
let request_relay = make_request_relay(uuid, relay_key, socket_addr);
12. 常见问题
Q: 无法连接到渲染服务器?
- 检查网络连接
- 检查服务器地址
- 检查防火墙
Q: 客户端连接失败?
- 检查设备 ID
- 检查密码
- 检查 NAT 穿透
Q: 视频延迟高?
- 使用更近的中继服务器
- 检查网络带宽
- 降低视频质量
Q: 切换画质后视频静止?
- 检查日志是否有 "re-subscribing" 信息
- 确认管道重启后成功重新订阅
- 检查 broadcast channel 是否正常关闭和重建
Q: 如何自建服务器?
参考 RustDesk Server 部署文档:
- hbbs: Rendezvous 服务器 (默认端口 21116)
- hbbr: 中继服务器 (默认端口 21117)
Docker 快速部署:
docker run -d --name hbbs \
-p 21116:21116 \
-p 21116:21116/udp \
-p 21118:21118 \
-v ./hbbs:/root \
rustdesk/rustdesk-server hbbs
docker run -d --name hbbr \
-p 21117:21117 \
-p 21119:21119 \
-v ./hbbr:/root \
rustdesk/rustdesk-server hbbr
Q: 输入节流是什么?
输入节流限制鼠标事件发送频率为 60Hz (16ms),防止:
- HID 设备写入错误 (EAGAIN)
- CPU 使用率过高
- 网络带宽浪费
这对用户体验几乎无影响,因为 60Hz 已经足够流畅。
13. 实现亮点
13.1 双密钥系统
- Curve25519: 用于 ECDH 密钥交换和加密
- Ed25519: 用于 SignedId 签名和验证
- 每个连接使用临时 Curve25519 密钥对,提高安全性
13.2 三种连接模式
- P2P 直连: 最快,延迟最低,优先尝试
- 中继连接: 通过 hbbr 中继,适用于 NAT 环境
- 局域网直连: 同一局域网内的优化路径
13.3 零拷贝架构
- 使用
bytes::Bytes引用计数,避免内存拷贝 - 视频/音频数据在管道中共享,单次编码多次使用
- Protobuf 消息直接使用
Bytes字段 (tokio_bytes = true)
13.4 容错与恢复
- 管道重订阅: 视频/音频管道重启时自动重新连接
- UUID 持久化: 避免重启后 UUID_MISMATCH 错误
- 连接重试: Rendezvous 连接失败时自动重试,指数退避
13.5 性能优化
- 预分配缓冲区 (128KB)
- 输入节流 (60Hz 鼠标)
- 共享管道 (broadcast channel)
- 编码器协商 (硬件优先)
- 零拷贝传输 (Bytes)
14. 与原版 RustDesk 的差异
| 特性 | One-KVM RustDesk | 原版 RustDesk Server |
|---|---|---|
| 角色 | 被控端 (受控设备) | 服务端 (中继/注册) |
| 视频源 | V4L2 硬件捕获 | 屏幕捕获 (桌面) |
| HID | USB OTG Gadget | 操作系统 API |
| 加密 | NaCl (Curve25519) | 同 |
| 协议 | RustDesk Protocol | 同 |
| P2P | 支持 | 支持 |
| 中继 | 支持 | 提供中继服务 |
| 公共服务器 | 可配置 (secrets.toml) | N/A |
| 多连接 | 支持 | N/A |
| 输入节流 | 60Hz 限流 | 无限制 |
关键区别: One-KVM 实现的是 RustDesk 被控端 (类似 RustDesk Desktop 的服务器模式),而不是 RustDesk Server (hbbs/hbbr)。