Files
One-KVM/docs/modules/rustdesk.md
mofeng-git 3fa91772f0 feat!: 移除内置公共服务器
- 移除公共 RustDesk ID 服务器 (用户需自行配置)
- 移除公共 TURN 服务器 (仅保留 Google STUN)
- 清理废弃代码: PublicServerInfo, is_using_public_server 等
- 更新前端 UI 和国际化文本
- 重新生成 TypeScript 类型

破坏性变更: 不再提供内置公共服务器。用户必须配置自己的
RustDesk 服务器和 TURN 服务器才能在生产环境中使用。
2026-01-08 16:53:19 +08:00

37 KiB
Raw Permalink Blame History

RustDesk 模块文档

1. 模块概述

RustDesk 模块实现 RustDesk 协议集成,允许使用标准 RustDesk 客户端访问 One-KVM 设备。

1.1 主要功能

  • RustDesk 协议实现 (Protobuf + NaCl 加密)
  • Rendezvous 服务器 (hbbs) 通信
  • 中继服务器 (hbbr) 通信
  • P2P 直连与中继回退
  • 局域网直连支持
  • 视频/音频/HID 转换
  • 端到端加密 (Curve25519 + XSalsa20-Poly1305)
  • 签名认证 (Ed25519)
  • 动态编码器协商 (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),
}

主要流程:

  1. 启动流程: 初始化加密 → 启动 TCP 监听器 → 创建 Rendezvous Mediator → 设置回调 → 开始注册循环
  2. 连接处理: P2P 直连尝试 → 中继回退 → 局域网直连
  3. 停止流程: 发送停止信号 → 关闭所有连接 → 停止 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)
}

连接流程:

  1. 握手: 发送 SignedId (含临时公钥) → 接收 PublicKey (含对称密钥) → 解密对称密钥
  2. 认证: 发送 Hash (密码盐) → 接收 LoginRequest (密码哈希) → 验证密码
  3. 编码协商: 根据可用编码器选择最优编解码器(H264 > H265 > VP8 > VP9)
  4. 流传输: 订阅共享视频/音频管道 → 转换为 RustDesk 格式 → 加密发送
  5. 输入处理: 接收 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"
    /// 必填项 - 不再提供公共服务器,需自行配置
    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;

    /// 获取有效的 Rendezvous 服务器地址
    pub fn effective_rendezvous_server(&self) -> &str;

    /// 获取带默认端口的 Rendezvous 地址
    pub fn rendezvous_addr(&self) -> String;

    /// 获取带默认端口的中继服务器地址
    pub fn relay_addr(&self) -> Option<String>;

    /// 确保 UUID 存在 (自动生成并标记需要保存)
    pub fn ensure_uuid(&mut self) -> ([u8; 16], bool);
}

配置文件示例

使用自建服务器:

[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 服务器。


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: "hbbs.example.com:21116".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 客户端:

  1. 下载并安装 RustDesk 客户端 (https://rustdesk.com)
  2. 如果使用自建服务器,在设置中配置 ID 服务器地址
  3. 输入设备 ID (9 位数字)
  4. 输入密码 (如果设置)
  5. 点击连接

连接过程:

客户端                           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: 无法连接到渲染服务器?

  1. 检查网络连接
  2. 检查服务器地址
  3. 检查防火墙

Q: 客户端连接失败?

  1. 检查设备 ID
  2. 检查密码
  3. 检查 NAT 穿透

Q: 视频延迟高?

  1. 使用更近的中继服务器
  2. 检查网络带宽
  3. 降低视频质量

Q: 切换画质后视频静止?

  1. 检查日志是否有 "re-subscribing" 信息
  2. 确认管道重启后成功重新订阅
  3. 检查 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 三种连接模式

  1. P2P 直连: 最快,延迟最低,优先尝试
  2. 中继连接: 通过 hbbr 中继,适用于 NAT 环境
  3. 局域网直连: 同一局域网内的优化路径

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 支持 支持
中继 支持 提供中继服务
公共服务器 不提供,需自建 N/A
多连接 支持 N/A
输入节流 60Hz 限流 无限制

关键区别: One-KVM 实现的是 RustDesk 被控端 (类似 RustDesk Desktop 的服务器模式),而不是 RustDesk Server (hbbs/hbbr)。


15. 参考资料