feat(rustdesk): 完整实现RustDesk协议和P2P连接

重大变更:
- 从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模块文档
This commit is contained in:
mofeng-git
2026-01-03 19:34:07 +08:00
parent cb7d9882a2
commit 0c82d1a840
49 changed files with 5470 additions and 1983 deletions

View File

@@ -156,11 +156,25 @@ pub async fn apply_hid_config(
old_config: &HidConfig,
new_config: &HidConfig,
) -> Result<()> {
// 检查是否需要重载
// 检查 OTG 描述符是否变更
let descriptor_changed = old_config.otg_descriptor != new_config.otg_descriptor;
// 如果描述符变更且当前使用 OTG 后端,需要重建 Gadget
if descriptor_changed && new_config.backend == HidBackend::Otg {
tracing::info!("OTG descriptor changed, updating gadget...");
if let Err(e) = state.otg_service.update_descriptor(&new_config.otg_descriptor).await {
tracing::error!("Failed to update OTG descriptor: {}", e);
return Err(AppError::Config(format!("OTG descriptor update failed: {}", e)));
}
tracing::info!("OTG descriptor updated successfully");
}
// 检查是否需要重载 HID 后端
if old_config.backend == new_config.backend
&& old_config.ch9329_port == new_config.ch9329_port
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
&& old_config.otg_udc == new_config.otg_udc
&& !descriptor_changed
{
tracing::info!("HID config unchanged, skipping reload");
return Ok(());
@@ -390,6 +404,8 @@ pub async fn apply_rustdesk_config(
|| old_config.device_id != new_config.device_id
|| old_config.device_password != new_config.device_password;
let mut credentials_to_save = None;
if rustdesk_guard.is_none() {
// Create new service
tracing::info!("Initializing RustDesk service...");
@@ -403,6 +419,8 @@ pub async fn apply_rustdesk_config(
tracing::error!("Failed to start RustDesk service: {}", e);
} else {
tracing::info!("RustDesk service started with ID: {}", new_config.device_id);
// Save generated keypair and UUID to config
credentials_to_save = service.save_credentials();
}
*rustdesk_guard = Some(std::sync::Arc::new(service));
} else if need_restart {
@@ -412,9 +430,32 @@ pub async fn apply_rustdesk_config(
tracing::error!("Failed to restart RustDesk service: {}", e);
} else {
tracing::info!("RustDesk service restarted with ID: {}", new_config.device_id);
// Save generated keypair and UUID to config
credentials_to_save = service.save_credentials();
}
}
}
// Save credentials to persistent config store (outside the lock)
drop(rustdesk_guard);
if let Some(updated_config) = credentials_to_save {
tracing::info!("Saving RustDesk credentials to config store...");
if let Err(e) = state
.config
.update(|cfg| {
cfg.rustdesk.public_key = updated_config.public_key.clone();
cfg.rustdesk.private_key = updated_config.private_key.clone();
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone();
cfg.rustdesk.uuid = updated_config.uuid.clone();
})
.await
{
tracing::warn!("Failed to save RustDesk credentials: {}", e);
} else {
tracing::info!("RustDesk credentials saved successfully");
}
}
}
Ok(())

View File

@@ -16,16 +16,17 @@
//! - GET /api/config/rustdesk - 获取 RustDesk 配置
//! - PATCH /api/config/rustdesk - 更新 RustDesk 配置
mod apply;
pub(crate) mod apply;
mod types;
mod video;
pub(crate) mod video;
mod stream;
mod hid;
mod msd;
mod atx;
mod audio;
mod rustdesk;
mod web;
// 导出 handler 函数
pub use video::{get_video_config, update_video_config};
@@ -38,6 +39,7 @@ pub use rustdesk::{
get_rustdesk_config, get_rustdesk_status, update_rustdesk_config,
regenerate_device_id, regenerate_device_password, get_device_password,
};
pub use web::{get_web_config, update_web_config};
// 保留全局配置查询(向后兼容)
use axum::{extract::State, Json};

View File

@@ -21,6 +21,8 @@ pub struct RustDeskConfigResponse {
pub has_password: bool,
/// 是否已设置密钥对
pub has_keypair: bool,
/// 是否已设置 relay key
pub has_relay_key: bool,
/// 是否使用公共服务器(用户留空时)
pub using_public_server: bool,
}
@@ -34,6 +36,7 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse {
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(),
has_relay_key: config.relay_key.is_some(),
using_public_server: config.is_using_public_server(),
}
}

View File

@@ -159,6 +159,60 @@ impl StreamConfigUpdate {
}
// ===== HID Config =====
/// OTG USB device descriptor configuration update
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct OtgDescriptorConfigUpdate {
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial_number: Option<String>,
}
impl OtgDescriptorConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
// Validate manufacturer string length
if let Some(ref s) = self.manufacturer {
if s.len() > 126 {
return Err(AppError::BadRequest("Manufacturer string too long (max 126 chars)".into()));
}
}
// Validate product string length
if let Some(ref s) = self.product {
if s.len() > 126 {
return Err(AppError::BadRequest("Product string too long (max 126 chars)".into()));
}
}
// Validate serial number string length
if let Some(ref s) = self.serial_number {
if s.len() > 126 {
return Err(AppError::BadRequest("Serial number string too long (max 126 chars)".into()));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut crate::config::OtgDescriptorConfig) {
if let Some(v) = self.vendor_id {
config.vendor_id = v;
}
if let Some(v) = self.product_id {
config.product_id = v;
}
if let Some(ref v) = self.manufacturer {
config.manufacturer = v.clone();
}
if let Some(ref v) = self.product {
config.product = v.clone();
}
if let Some(ref v) = self.serial_number {
config.serial_number = Some(v.clone());
}
}
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct HidConfigUpdate {
@@ -166,6 +220,7 @@ pub struct HidConfigUpdate {
pub ch9329_port: Option<String>,
pub ch9329_baudrate: Option<u32>,
pub otg_udc: Option<String>,
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
pub mouse_absolute: Option<bool>,
}
@@ -179,6 +234,9 @@ impl HidConfigUpdate {
));
}
}
if let Some(ref desc) = self.otg_descriptor {
desc.validate()?;
}
Ok(())
}
@@ -195,6 +253,9 @@ impl HidConfigUpdate {
if let Some(ref udc) = self.otg_udc {
config.otg_udc = Some(udc.clone());
}
if let Some(ref desc) = self.otg_descriptor {
desc.apply_to(&mut config.otg_descriptor);
}
if let Some(absolute) = self.mouse_absolute {
config.mouse_absolute = absolute;
}
@@ -389,6 +450,7 @@ pub struct RustDeskConfigUpdate {
pub enabled: Option<bool>,
pub rendezvous_server: Option<String>,
pub relay_server: Option<String>,
pub relay_key: Option<String>,
pub device_password: Option<String>,
}
@@ -431,6 +493,9 @@ impl RustDeskConfigUpdate {
if let Some(ref server) = self.relay_server {
config.relay_server = if server.is_empty() { None } else { Some(server.clone()) };
}
if let Some(ref key) = self.relay_key {
config.relay_key = if key.is_empty() { None } else { Some(key.clone()) };
}
if let Some(ref password) = self.device_password {
if !password.is_empty() {
config.device_password = password.clone();
@@ -438,3 +503,49 @@ impl RustDeskConfigUpdate {
}
}
}
// ===== Web Config =====
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct WebConfigUpdate {
pub http_port: Option<u16>,
pub https_port: Option<u16>,
pub bind_address: Option<String>,
pub https_enabled: Option<bool>,
}
impl WebConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> {
if let Some(port) = self.http_port {
if port == 0 {
return Err(AppError::BadRequest("HTTP port cannot be 0".into()));
}
}
if let Some(port) = self.https_port {
if port == 0 {
return Err(AppError::BadRequest("HTTPS port cannot be 0".into()));
}
}
if let Some(ref addr) = self.bind_address {
if addr.parse::<std::net::IpAddr>().is_err() {
return Err(AppError::BadRequest("Invalid bind address".into()));
}
}
Ok(())
}
pub fn apply_to(&self, config: &mut crate::config::WebConfig) {
if let Some(port) = self.http_port {
config.http_port = port;
}
if let Some(port) = self.https_port {
config.https_port = port;
}
if let Some(ref addr) = self.bind_address {
config.bind_address = addr.clone();
}
if let Some(enabled) = self.https_enabled {
config.https_enabled = enabled;
}
}
}

View File

@@ -0,0 +1,32 @@
//! Web 服务器配置 Handler
use axum::{extract::State, Json};
use std::sync::Arc;
use crate::config::WebConfig;
use crate::error::Result;
use crate::state::AppState;
use super::types::WebConfigUpdate;
/// 获取 Web 配置
pub async fn get_web_config(State(state): State<Arc<AppState>>) -> Json<WebConfig> {
Json(state.config.get().web.clone())
}
/// 更新 Web 配置
pub async fn update_web_config(
State(state): State<Arc<AppState>>,
Json(req): Json<WebConfigUpdate>,
) -> Result<Json<WebConfig>> {
req.validate()?;
state
.config
.update(|config| {
req.apply_to(&mut config.web);
})
.await?;
Ok(Json(state.config.get().web.clone()))
}

View File

@@ -185,13 +185,26 @@ fn get_cpu_model() -> String {
std::fs::read_to_string("/proc/cpuinfo")
.ok()
.and_then(|content| {
content
// Try to get model name
let model = content
.lines()
.find(|line| line.starts_with("model name") || line.starts_with("Model"))
.and_then(|line| line.split(':').nth(1))
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
if model.is_some() {
return model;
}
// Fallback: show arch and core count
let cores = content
.lines()
.filter(|line| line.starts_with("processor"))
.count();
Some(format!("{} {}C", std::env::consts::ARCH, cores))
})
.unwrap_or_else(|| "Unknown CPU".to_string())
.unwrap_or_else(|| format!("{}", std::env::consts::ARCH))
}
/// CPU usage state for calculating usage between samples
@@ -482,11 +495,16 @@ pub struct SetupRequest {
pub video_width: Option<u32>,
pub video_height: Option<u32>,
pub video_fps: Option<u32>,
// Audio settings
pub audio_device: Option<String>,
// HID settings
pub hid_backend: Option<String>,
pub hid_ch9329_port: Option<String>,
pub hid_ch9329_baudrate: Option<u32>,
pub hid_otg_udc: Option<String>,
// Extension settings
pub ttyd_enabled: Option<bool>,
pub rustdesk_enabled: Option<bool>,
}
pub async fn setup_init(
@@ -541,6 +559,12 @@ pub async fn setup_init(
config.video.fps = fps;
}
// Audio settings
if let Some(device) = req.audio_device.clone() {
config.audio.device = device;
config.audio.enabled = true;
}
// HID settings
if let Some(backend) = req.hid_backend.clone() {
config.hid.backend = match backend.as_str() {
@@ -558,12 +582,26 @@ pub async fn setup_init(
if let Some(udc) = req.hid_otg_udc.clone() {
config.hid.otg_udc = Some(udc);
}
// Extension settings
if let Some(enabled) = req.ttyd_enabled {
config.extensions.ttyd.enabled = enabled;
}
if let Some(enabled) = req.rustdesk_enabled {
config.rustdesk.enabled = enabled;
}
})
.await?;
// Get updated config for HID reload
let new_config = state.config.get();
tracing::info!(
"Extension config after save: ttyd.enabled={}, rustdesk.enabled={}",
new_config.extensions.ttyd.enabled,
new_config.rustdesk.enabled
);
// Initialize HID backend with new config
let new_hid_backend = match new_config.hid.backend {
crate::config::HidBackend::Otg => crate::hid::HidBackendType::Otg,
@@ -582,6 +620,34 @@ pub async fn setup_init(
tracing::info!("HID backend initialized: {:?}", new_config.hid.backend);
}
// Start extensions if enabled
if new_config.extensions.ttyd.enabled {
if let Err(e) = state
.extensions
.start(
crate::extensions::ExtensionId::Ttyd,
&new_config.extensions,
)
.await
{
tracing::warn!("Failed to start ttyd during setup: {}", e);
} else {
tracing::info!("ttyd started during setup");
}
}
// Start RustDesk if enabled
if new_config.rustdesk.enabled {
let empty_config = crate::rustdesk::config::RustDeskConfig::default();
if let Err(e) =
config::apply::apply_rustdesk_config(&state, &empty_config, &new_config.rustdesk).await
{
tracing::warn!("Failed to start RustDesk during setup: {}", e);
} else {
tracing::info!("RustDesk started during setup");
}
}
tracing::info!("System initialized successfully with admin user: {}", req.username);
Ok(Json(LoginResponse {
@@ -908,6 +974,13 @@ pub struct DeviceList {
pub serial: Vec<SerialDevice>,
pub audio: Vec<AudioDevice>,
pub udc: Vec<UdcDevice>,
pub extensions: ExtensionsAvailability,
}
#[derive(Serialize)]
pub struct ExtensionsAvailability {
pub ttyd_available: bool,
pub rustdesk_available: bool,
}
#[derive(Serialize)]
@@ -916,6 +989,7 @@ pub struct VideoDevice {
pub name: String,
pub driver: String,
pub formats: Vec<VideoFormat>,
pub usb_bus: Option<String>,
}
#[derive(Serialize)]
@@ -942,6 +1016,8 @@ pub struct SerialDevice {
pub struct AudioDevice {
pub name: String,
pub description: String,
pub is_hdmi: bool,
pub usb_bus: Option<String>,
}
#[derive(Serialize)]
@@ -949,32 +1025,62 @@ pub struct UdcDevice {
pub name: String,
}
/// Extract USB bus port from V4L2 bus_info string
/// Examples:
/// - "usb-0000:00:14.0-1" -> Some("1")
/// - "usb-xhci-hcd.0-1.2" -> Some("1.2")
/// - "usb-0000:00:14.0-1.3.2" -> Some("1.3.2")
/// - "platform:..." -> None
fn extract_usb_bus_from_bus_info(bus_info: &str) -> Option<String> {
if !bus_info.starts_with("usb-") {
return None;
}
// Find the last '-' which separates the USB port
// e.g., "usb-0000:00:14.0-1" -> "1"
// e.g., "usb-xhci-hcd.0-1.2" -> "1.2"
let parts: Vec<&str> = bus_info.rsplitn(2, '-').collect();
if parts.len() == 2 {
let port = parts[0];
// Verify it looks like a USB port (starts with digit)
if port.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
return Some(port.to_string());
}
}
None
}
pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList> {
// Detect video devices
let video_devices = match state.stream_manager.list_devices().await {
Ok(devices) => devices
.into_iter()
.map(|d| VideoDevice {
path: d.path.to_string_lossy().to_string(),
name: d.name,
driver: d.driver,
formats: d
.formats
.iter()
.map(|f| VideoFormat {
format: format!("{}", f.format),
description: f.description.clone(),
resolutions: f
.resolutions
.iter()
.map(|r| VideoResolution {
width: r.width,
height: r.height,
fps: r.fps.clone(),
})
.collect(),
})
.collect(),
.map(|d| {
// Extract USB bus from bus_info (e.g., "usb-0000:00:14.0-1" -> "1")
// or "usb-xhci-hcd.0-1.2" -> "1.2"
let usb_bus = extract_usb_bus_from_bus_info(&d.bus_info);
VideoDevice {
path: d.path.to_string_lossy().to_string(),
name: d.name,
driver: d.driver,
formats: d
.formats
.iter()
.map(|f| VideoFormat {
format: format!("{}", f.format),
description: f.description.clone(),
resolutions: f
.resolutions
.iter()
.map(|r| VideoResolution {
width: r.width,
height: r.height,
fps: r.fps.clone(),
})
.collect(),
})
.collect(),
usb_bus,
}
})
.collect(),
Err(_) => vec![],
@@ -1024,16 +1130,25 @@ pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList
.map(|d| AudioDevice {
name: d.name,
description: d.description,
is_hdmi: d.is_hdmi,
usb_bus: d.usb_bus,
})
.collect(),
Err(_) => vec![],
};
// Check extension availability
let ttyd_available = state.extensions.check_available(crate::extensions::ExtensionId::Ttyd);
Json(DeviceList {
video: video_devices,
serial: serial_devices,
audio: audio_devices,
udc: udc_devices,
extensions: ExtensionsAvailability {
ttyd_available,
rustdesk_available: true, // RustDesk is built-in
},
})
}
@@ -2574,3 +2689,53 @@ pub async fn change_user_password(
message: Some("Password changed successfully".to_string()),
}))
}
// ============================================================================
// System Control
// ============================================================================
/// Restart the application
pub async fn system_restart(State(state): State<Arc<AppState>>) -> Json<LoginResponse> {
info!("System restart requested via API");
// Send shutdown signal
let _ = state.shutdown_tx.send(());
// Spawn restart task in background
tokio::spawn(async {
// Wait for resources to be released (OTG, video, etc.)
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Get current executable and args
let exe = match std::env::current_exe() {
Ok(e) => e,
Err(e) => {
tracing::error!("Failed to get current exe: {}", e);
std::process::exit(1);
}
};
let args: Vec<String> = std::env::args().skip(1).collect();
info!("Restarting: {:?} {:?}", exe, args);
// Use exec to replace current process (Unix)
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = std::process::Command::new(&exe).args(&args).exec();
tracing::error!("Failed to restart: {}", err);
std::process::exit(1);
}
#[cfg(not(unix))]
{
let _ = std::process::Command::new(&exe).args(&args).spawn();
std::process::exit(0);
}
});
Json(LoginResponse {
success: true,
message: Some("Restarting...".to_string()),
})
}