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

@@ -13,25 +13,31 @@ use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use bytes::{Bytes, BytesMut};
use sodiumoxide::crypto::box_;
use parking_lot::RwLock;
use prost::Message as ProstMessage;
use protobuf::Message as ProtobufMessage;
use tokio::net::TcpStream;
use tokio::net::tcp::OwnedWriteHalf;
use tokio::sync::{broadcast, mpsc, Mutex};
use tracing::{debug, error, info, warn};
use crate::hid::HidController;
use crate::audio::AudioController;
use crate::hid::{HidController, KeyboardEvent, KeyEventType, KeyboardModifiers};
use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType};
use crate::video::encoder::BitratePreset;
use crate::video::stream_manager::VideoStreamManager;
use super::bytes_codec::{read_frame, write_frame, write_frame_buffered};
use super::config::RustDeskConfig;
use super::crypto::{self, decrypt_symmetric_key_msg, KeyPair, SigningKeyPair};
use super::frame_adapters::{VideoCodec, VideoFrameAdapter};
use super::crypto::{self, KeyPair, SigningKeyPair};
use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter};
use super::hid_adapter::{convert_key_event, convert_mouse_event, mouse_type};
use super::protocol::hbb::{self, message};
use super::protocol::{LoginRequest, LoginResponse, PeerInfo};
use super::protocol::{
message, misc, login_response,
KeyEvent, MouseEvent, Clipboard, Misc, LoginRequest, LoginResponse, PeerInfo,
IdPk, SignedId, Hash, TestDelay, ControlKey,
decode_message, HbbMessage, DisplayInfo, SupportedEncoding, OptionMessage, PublicKey,
};
use sodiumoxide::crypto::secretbox;
@@ -39,8 +45,8 @@ use sodiumoxide::crypto::secretbox;
const DEFAULT_SCREEN_WIDTH: u32 = 1920;
const DEFAULT_SCREEN_HEIGHT: u32 = 1080;
/// Default mouse event throttle interval (10ms = 100Hz, matches USB HID polling rate)
const DEFAULT_MOUSE_THROTTLE_MS: u64 = 10;
/// Default mouse event throttle interval (16ms ≈ 60Hz)
const DEFAULT_MOUSE_THROTTLE_MS: u64 = 16;
/// Input event throttler
///
@@ -115,14 +121,17 @@ pub struct Connection {
peer_name: String,
/// Connection state
state: Arc<RwLock<ConnectionState>>,
/// Our encryption keypair (Curve25519)
keypair: KeyPair,
/// Our signing keypair (Ed25519) for SignedId messages
/// Our signing keypair (Ed25519) for signing SignedId messages
signing_keypair: SigningKeyPair,
/// Temporary Curve25519 keypair for this connection (used for encryption)
/// Generated fresh for each connection, public key goes in IdPk.pk
temp_keypair: (box_::PublicKey, box_::SecretKey),
/// Device password
password: String,
/// HID controller for keyboard/mouse events
hid: Option<Arc<HidController>>,
/// Audio controller for audio streaming
audio: Option<Arc<AudioController>>,
/// Video stream manager for frame subscription
video_manager: Option<Arc<VideoStreamManager>>,
/// Screen dimensions for mouse coordinate conversion
@@ -134,6 +143,8 @@ pub struct Connection {
shutdown_tx: broadcast::Sender<()>,
/// Video streaming task handle
video_task: Option<tokio::task::JoinHandle<()>>,
/// Audio streaming task handle
audio_task: Option<tokio::task::JoinHandle<()>>,
/// Session encryption key (negotiated during handshake)
session_key: Option<secretbox::Key>,
/// Encryption enabled flag
@@ -152,6 +163,8 @@ pub struct Connection {
last_delay: u32,
/// Time when we last sent a TestDelay to the client (for RTT calculation)
last_test_delay_sent: Option<Instant>,
/// Last known CapsLock state from RustDesk modifiers (for detecting toggle)
last_caps_lock: bool,
}
/// Messages sent to connection handler
@@ -173,13 +186,13 @@ pub enum ClientMessage {
/// Login request
Login(LoginRequest),
/// Key event
KeyEvent(hbb::KeyEvent),
KeyEvent(KeyEvent),
/// Mouse event
MouseEvent(hbb::MouseEvent),
MouseEvent(MouseEvent),
/// Clipboard
Clipboard(hbb::Clipboard),
Clipboard(Clipboard),
/// Misc message
Misc(hbb::Misc),
Misc(Misc),
/// Unknown/unhandled
Unknown,
}
@@ -189,30 +202,36 @@ impl Connection {
pub fn new(
id: u32,
config: &RustDeskConfig,
keypair: KeyPair,
signing_keypair: SigningKeyPair,
hid: Option<Arc<HidController>>,
audio: Option<Arc<AudioController>>,
video_manager: Option<Arc<VideoStreamManager>>,
) -> (Self, mpsc::UnboundedReceiver<ConnectionMessage>) {
let (tx, rx) = mpsc::unbounded_channel();
let (shutdown_tx, _) = broadcast::channel(1);
// Generate fresh Curve25519 keypair for this connection
// This is used for encrypting the symmetric key exchange
let temp_keypair = box_::gen_keypair();
let conn = Self {
id,
device_id: config.device_id.clone(),
peer_id: String::new(),
peer_name: String::new(),
state: Arc::new(RwLock::new(ConnectionState::Pending)),
keypair,
signing_keypair,
temp_keypair,
password: config.device_password.clone(),
hid,
audio,
video_manager,
screen_width: DEFAULT_SCREEN_WIDTH,
screen_height: DEFAULT_SCREEN_HEIGHT,
tx,
shutdown_tx,
video_task: None,
audio_task: None,
session_key: None,
encryption_enabled: false,
enc_seqnum: 0,
@@ -222,6 +241,7 @@ impl Connection {
input_throttler: InputThrottler::new(),
last_delay: 0,
last_test_delay_sent: None,
last_caps_lock: false,
};
(conn, rx)
@@ -259,14 +279,18 @@ impl Connection {
// Send our SignedId first (this is what RustDesk protocol expects)
// The SignedId contains our device ID and temporary public key
let signed_id_msg = self.create_signed_id_message(&self.device_id.clone());
let signed_id_bytes = ProstMessage::encode_to_vec(&signed_id_msg);
info!("Sending SignedId with device_id={}", self.device_id);
let signed_id_bytes = signed_id_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode SignedId: {}", e))?;
debug!("Sending SignedId with device_id={}", self.device_id);
self.send_framed_arc(&writer, &signed_id_bytes).await?;
// Channel for receiving video frames to send (bounded to provide backpressure)
let (video_tx, mut video_rx) = mpsc::channel::<Bytes>(4);
let mut video_streaming = false;
// Channel for receiving audio frames to send (bounded to provide backpressure)
let (audio_tx, mut audio_rx) = mpsc::channel::<Bytes>(8);
let mut audio_streaming = false;
// Timer for sending TestDelay to measure round-trip latency
// RustDesk clients display this delay information
let mut test_delay_interval = tokio::time::interval(Duration::from_secs(1));
@@ -282,13 +306,17 @@ impl Connection {
result = read_frame(&mut reader) => {
match result {
Ok(msg_buf) => {
if let Err(e) = self.handle_message_arc(&msg_buf, &writer, &video_tx, &mut video_streaming).await {
if let Err(e) = self.handle_message_arc(&msg_buf, &writer, &video_tx, &mut video_streaming, &audio_tx, &mut audio_streaming).await {
error!("Error handling message: {}", e);
break;
}
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
info!("Connection closed by peer");
if self.state() == ConnectionState::Handshaking {
warn!("Connection closed by peer DURING HANDSHAKE - signature verification likely failed on client side");
} else {
info!("Connection closed by peer");
}
break;
}
Err(e) => {
@@ -321,6 +349,27 @@ impl Connection {
}
}
// Send audio frames (encrypted if session key is set)
Some(frame_data) = audio_rx.recv() => {
let send_result = if let Some(ref key) = self.session_key {
// Encrypt the frame
self.enc_seqnum += 1;
let nonce = Self::get_nonce(self.enc_seqnum);
let ciphertext = secretbox::seal(&frame_data, &nonce, key);
let mut w = writer.lock().await;
write_frame_buffered(&mut *w, &ciphertext, &mut frame_buf).await
} else {
// No encryption, send plain
let mut w = writer.lock().await;
write_frame_buffered(&mut *w, &frame_data, &mut frame_buf).await
};
if let Err(e) = send_result {
error!("Error sending audio frame: {}", e);
break;
}
}
// Send TestDelay periodically to measure latency
_ = test_delay_interval.tick() => {
if self.state() == ConnectionState::Active && self.last_test_delay_sent.is_none() {
@@ -343,6 +392,11 @@ impl Connection {
task.abort();
}
// Stop audio streaming task if running
if let Some(task) = self.audio_task.take() {
task.abort();
}
*self.state.write() = ConnectionState::Closed;
Ok(())
}
@@ -389,6 +443,8 @@ impl Connection {
writer: &Arc<Mutex<OwnedWriteHalf>>,
video_tx: &mpsc::Sender<Bytes>,
video_streaming: &mut bool,
audio_tx: &mpsc::Sender<Bytes>,
audio_streaming: &mut bool,
) -> anyhow::Result<()> {
// Try to decrypt if we have a session key
// RustDesk uses sequence-based nonce, NOT nonce prefix in message
@@ -414,19 +470,26 @@ impl Connection {
data
};
let msg = hbb::Message::decode(msg_data)?;
let msg = decode_message(msg_data)?;
match msg.union {
Some(message::Union::PublicKey(pk)) => {
debug!("Received public key from peer");
self.handle_peer_public_key(&pk, writer).await?;
Some(message::Union::PublicKey(ref pk)) => {
info!(
"Received PublicKey from peer: asymmetric_len={}, symmetric_len={}",
pk.asymmetric_value.len(),
pk.symmetric_value.len()
);
if pk.asymmetric_value.is_empty() && pk.symmetric_value.is_empty() {
warn!("Received EMPTY PublicKey - client may have failed signature verification!");
}
self.handle_peer_public_key(pk, writer).await?;
}
Some(message::Union::LoginRequest(lr)) => {
debug!("Received login request from {}", lr.my_id);
self.peer_id = lr.my_id.clone();
self.peer_name = lr.my_name.clone();
// Handle login and start video streaming if successful
// Handle login and start video/audio streaming if successful
if self.handle_login_request_arc(&lr, writer).await? {
// Store video_tx for potential codec switching
self.video_frame_tx = Some(video_tx.clone());
@@ -435,6 +498,11 @@ impl Connection {
self.start_video_streaming(video_tx.clone());
*video_streaming = true;
}
// Start audio streaming
if !*audio_streaming {
self.start_audio_streaming(audio_tx.clone());
*audio_streaming = true;
}
}
}
Some(message::Union::KeyEvent(ke)) => {
@@ -505,7 +573,7 @@ impl Connection {
// Client sent empty password - tell them to enter password
info!("Empty password from {}, requesting password input", lr.my_id);
let error_response = self.create_login_error_response("Empty Password");
let response_bytes = ProstMessage::encode_to_vec(&error_response);
let response_bytes = error_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &response_bytes).await?;
// Don't close connection - wait for retry with password
return Ok(false);
@@ -515,7 +583,7 @@ impl Connection {
if !self.verify_password(&lr.password) {
warn!("Wrong password from {}", lr.my_id);
let error_response = self.create_login_error_response("Wrong Password");
let response_bytes = ProstMessage::encode_to_vec(&error_response);
let response_bytes = error_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &response_bytes).await?;
// Don't close connection - wait for retry with correct password
return Ok(false);
@@ -533,7 +601,7 @@ impl Connection {
info!("Negotiated video codec: {:?}", negotiated);
let response = self.create_login_response(true);
let response_bytes = ProstMessage::encode_to_vec(&response);
let response_bytes = response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &response_bytes).await?;
Ok(true)
}
@@ -567,23 +635,23 @@ impl Connection {
/// Handle misc message with Arc writer
async fn handle_misc_arc(
&mut self,
misc: &hbb::Misc,
misc: &Misc,
_writer: &Arc<Mutex<OwnedWriteHalf>>,
) -> anyhow::Result<()> {
match &misc.union {
Some(hbb::misc::Union::SwitchDisplay(sd)) => {
Some(misc::Union::SwitchDisplay(sd)) => {
debug!("Switch display request: {}", sd.display);
}
Some(hbb::misc::Union::Option(opt)) => {
Some(misc::Union::Option(opt)) => {
self.handle_option_message(opt).await?;
}
Some(hbb::misc::Union::RefreshVideo(refresh)) => {
Some(misc::Union::RefreshVideo(refresh)) => {
if *refresh {
debug!("Video refresh requested");
// TODO: Request keyframe from encoder
}
}
Some(hbb::misc::Union::VideoReceived(received)) => {
Some(misc::Union::VideoReceived(received)) => {
if *received {
debug!("Video received acknowledgement");
}
@@ -597,11 +665,11 @@ impl Connection {
}
/// Handle Option message from client (includes codec and quality preferences)
async fn handle_option_message(&mut self, opt: &hbb::OptionMessage) -> anyhow::Result<()> {
async fn handle_option_message(&mut self, opt: &OptionMessage) -> anyhow::Result<()> {
// Handle image quality preset
// RustDesk ImageQuality: NotSet=0, Low=2, Balanced=3, Best=4
// Map to One-KVM BitratePreset: Low->Speed, Balanced->Balanced, Best->Quality
let image_quality = opt.image_quality;
let image_quality = opt.image_quality.value();
if image_quality != 0 {
let preset = match image_quality {
2 => Some(BitratePreset::Speed), // Low -> Speed (1 Mbps)
@@ -621,8 +689,8 @@ impl Connection {
}
// Check if client sent supported_decoding with a codec preference
if let Some(ref supported_decoding) = opt.supported_decoding {
let prefer = supported_decoding.prefer;
if let Some(ref supported_decoding) = opt.supported_decoding.as_ref() {
let prefer = supported_decoding.prefer.value();
debug!("Client codec preference: prefer={}", prefer);
// Map RustDesk PreferCodec enum to our VideoEncoderType
@@ -730,47 +798,75 @@ impl Connection {
self.video_task = Some(task);
}
/// Start audio streaming task
fn start_audio_streaming(&mut self, audio_tx: mpsc::Sender<Bytes>) {
let audio_controller = match &self.audio {
Some(ac) => ac.clone(),
None => {
debug!("No audio controller available, skipping audio streaming");
return;
}
};
let state = self.state.clone();
let conn_id = self.id;
let shutdown_tx = self.shutdown_tx.clone();
let task = tokio::spawn(async move {
info!("Starting audio streaming for connection {}", conn_id);
if let Err(e) = run_audio_streaming(
conn_id,
audio_controller,
audio_tx,
state,
shutdown_tx,
).await {
error!("Audio streaming error for connection {}: {}", conn_id, e);
}
info!("Audio streaming stopped for connection {}", conn_id);
});
self.audio_task = Some(task);
}
/// Create SignedId message for initial handshake
///
/// RustDesk protocol:
/// - IdPk contains device ID and our Curve25519 encryption public key
/// - IdPk contains device ID and a fresh Curve25519 public key for this connection
/// - The IdPk is signed with Ed25519 to prove ownership of the device
/// - Client verifies the Ed25519 signature using public key from hbbs
/// - Client then encrypts symmetric key using our Curve25519 public key from IdPk
fn create_signed_id_message(&self, device_id: &str) -> hbb::Message {
// Create IdPk with our device ID and Curve25519 encryption public key
// The client will use this Curve25519 key to encrypt the symmetric session key
let id_pk = hbb::IdPk {
id: device_id.to_string(),
pk: self.keypair.public_key_bytes().to_vec().into(),
};
/// - Client then encrypts symmetric key using the Curve25519 public key from IdPk
fn create_signed_id_message(&self, device_id: &str) -> HbbMessage {
// Create IdPk with our device ID and temporary Curve25519 public key
// IMPORTANT: Use the fresh Curve25519 public key, NOT Ed25519!
// The client will use this directly for encryption (no conversion needed)
let pk_bytes = self.temp_keypair.0.as_ref();
let mut id_pk = IdPk::new();
id_pk.id = device_id.to_string();
id_pk.pk = pk_bytes.to_vec().into();
// Encode IdPk to bytes
let id_pk_bytes = ProstMessage::encode_to_vec(&id_pk);
let id_pk_bytes = id_pk.write_to_bytes().unwrap_or_default();
// Sign the IdPk bytes with Ed25519
// RustDesk's sign::sign() prepends the 64-byte signature to the message
let signed_id_pk = self.signing_keypair.sign(&id_pk_bytes);
debug!(
"Created SignedId: id={}, curve25519_pk_len={}, signature_len=64, total_len={}",
device_id,
self.keypair.public_key_bytes().len(),
signed_id_pk.len()
);
let mut signed_id = SignedId::new();
signed_id.id = signed_id_pk.into();
hbb::Message {
union: Some(message::Union::SignedId(hbb::SignedId {
id: signed_id_pk.into(),
})),
}
let mut msg = HbbMessage::new();
msg.union = Some(message::Union::SignedId(signed_id));
msg
}
/// Handle peer's public key and negotiate session encryption
/// After successful negotiation, send Hash message for password authentication
async fn handle_peer_public_key(
&mut self,
pk: &hbb::PublicKey,
pk: &PublicKey,
writer: &Arc<Mutex<OwnedWriteHalf>>,
) -> anyhow::Result<()> {
// RustDesk's PublicKey message has two parts:
@@ -785,12 +881,12 @@ impl Connection {
pk.symmetric_value.len()
);
// Decrypt the symmetric key using our Curve25519 keypair
// Decrypt the symmetric key using our temporary Curve25519 keypair
// The client encrypted it using our Curve25519 public key from IdPk
match decrypt_symmetric_key_msg(
match crypto::decrypt_symmetric_key(
&pk.asymmetric_value,
&pk.symmetric_value,
&self.keypair,
&self.temp_keypair.1,
) {
Ok(session_key) => {
info!("Session key negotiated successfully");
@@ -821,7 +917,7 @@ impl Connection {
// This tells the client what salt to use for password hashing
// Must be encrypted if session key was negotiated
let hash_msg = self.create_hash_message();
let hash_bytes = ProstMessage::encode_to_vec(&hash_msg);
let hash_bytes = hash_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
debug!("Sending Hash message for password authentication (encrypted={})", self.encryption_enabled);
self.send_encrypted_arc(writer, &hash_bytes).await?;
@@ -835,7 +931,7 @@ impl Connection {
/// or proceed with the connection.
async fn handle_signed_id(
&mut self,
si: &hbb::SignedId,
si: &SignedId,
writer: &Arc<Mutex<OwnedWriteHalf>>,
) -> anyhow::Result<()> {
// The SignedId contains a signed IdPk message
@@ -853,7 +949,7 @@ impl Connection {
&signed_data[..]
};
if let Ok(id_pk) = hbb::IdPk::decode(id_pk_bytes) {
if let Ok(id_pk) = IdPk::parse_from_bytes(id_pk_bytes) {
info!(
"Received SignedId from peer: id={}, pk_len={}",
id_pk.id,
@@ -875,7 +971,7 @@ impl Connection {
// If we haven't sent our SignedId yet, send it now
// (This handles the case where client sends SignedId before we do)
let signed_id_msg = self.create_signed_id_message(&self.device_id.clone());
let signed_id_bytes = ProstMessage::encode_to_vec(&signed_id_msg);
let signed_id_bytes = signed_id_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_framed_arc(writer, &signed_id_bytes).await?;
Ok(())
@@ -926,7 +1022,7 @@ impl Connection {
}
/// Create login response with dynamically detected encoder capabilities
fn create_login_response(&self, success: bool) -> hbb::Message {
fn create_login_response(&self, success: bool) -> HbbMessage {
if success {
// Dynamically detect available encoders
let registry = EncoderRegistry::global();
@@ -942,50 +1038,47 @@ impl Connection {
h264_available, h265_available, vp8_available, vp9_available
);
hbb::Message {
union: Some(message::Union::LoginResponse(LoginResponse {
union: Some(hbb::login_response::Union::PeerInfo(PeerInfo {
username: "one-kvm".to_string(),
hostname: get_hostname(),
platform: "Linux".to_string(),
displays: vec![hbb::DisplayInfo {
x: 0,
y: 0,
width: 1920,
height: 1080,
name: "KVM Display".to_string(),
online: true,
cursor_embedded: false,
original_resolution: None,
scale: 1.0,
}],
current_display: 0,
sas_enabled: false,
version: env!("CARGO_PKG_VERSION").to_string(),
features: None,
encoding: Some(hbb::SupportedEncoding {
h264: h264_available,
h265: h265_available,
vp8: vp8_available,
av1: false, // AV1 not supported yet
i444: None,
}),
resolutions: None,
platform_additions: String::new(),
windows_sessions: None,
})),
enable_trusted_devices: false,
})),
}
let mut display_info = DisplayInfo::new();
display_info.x = 0;
display_info.y = 0;
display_info.width = 1920;
display_info.height = 1080;
display_info.name = "KVM Display".to_string();
display_info.online = true;
display_info.cursor_embedded = false;
display_info.scale = 1.0;
let mut encoding = SupportedEncoding::new();
encoding.h264 = h264_available;
encoding.h265 = h265_available;
encoding.vp8 = vp8_available;
encoding.av1 = false; // AV1 not supported yet
let mut peer_info = PeerInfo::new();
peer_info.username = "one-kvm".to_string();
peer_info.hostname = get_hostname();
peer_info.platform = "Linux".to_string();
peer_info.displays.push(display_info);
peer_info.current_display = 0;
peer_info.sas_enabled = false;
peer_info.version = env!("CARGO_PKG_VERSION").to_string();
peer_info.encoding = protobuf::MessageField::some(encoding);
let mut login_response = LoginResponse::new();
login_response.union = Some(login_response::Union::PeerInfo(peer_info));
login_response.enable_trusted_devices = false;
let mut msg = HbbMessage::new();
msg.union = Some(message::Union::LoginResponse(login_response));
msg
} else {
hbb::Message {
union: Some(message::Union::LoginResponse(LoginResponse {
union: Some(hbb::login_response::Union::Error(
"Invalid password".to_string(),
)),
enable_trusted_devices: false,
})),
}
let mut login_response = LoginResponse::new();
login_response.union = Some(login_response::Union::Error("Invalid password".to_string()));
login_response.enable_trusted_devices = false;
let mut msg = HbbMessage::new();
msg.union = Some(message::Union::LoginResponse(login_response));
msg
}
}
@@ -993,26 +1086,28 @@ impl Connection {
/// RustDesk client recognizes specific error strings:
/// - "Empty Password" -> prompts for password input
/// - "Wrong Password" -> prompts for password re-entry
fn create_login_error_response(&self, error: &str) -> hbb::Message {
hbb::Message {
union: Some(message::Union::LoginResponse(LoginResponse {
union: Some(hbb::login_response::Union::Error(error.to_string())),
enable_trusted_devices: false,
})),
}
fn create_login_error_response(&self, error: &str) -> HbbMessage {
let mut login_response = LoginResponse::new();
login_response.union = Some(login_response::Union::Error(error.to_string()));
login_response.enable_trusted_devices = false;
let mut msg = HbbMessage::new();
msg.union = Some(message::Union::LoginResponse(login_response));
msg
}
/// Create Hash message for password authentication
/// The client will hash the password with the salt and send it back in LoginRequest
fn create_hash_message(&self) -> hbb::Message {
fn create_hash_message(&self) -> HbbMessage {
// Use device_id as salt for simplicity (RustDesk uses Config::get_salt())
// The challenge field is not used for our password verification
hbb::Message {
union: Some(message::Union::Hash(hbb::Hash {
salt: self.device_id.clone(),
challenge: String::new(),
})),
}
let mut hash = Hash::new();
hash.salt = self.device_id.clone();
hash.challenge = String::new();
let mut msg = HbbMessage::new();
msg.union = Some(message::Union::Hash(hash));
msg
}
/// Handle TestDelay message for round-trip latency measurement
@@ -1024,21 +1119,21 @@ impl Connection {
/// 4. Server includes last_delay in next TestDelay for client display
async fn handle_test_delay(
&mut self,
td: &hbb::TestDelay,
td: &TestDelay,
writer: &Arc<Mutex<OwnedWriteHalf>>,
) -> anyhow::Result<()> {
if td.from_client {
// Client initiated the delay test, respond with the same time
let response = hbb::Message {
union: Some(message::Union::TestDelay(hbb::TestDelay {
time: td.time,
from_client: false,
last_delay: self.last_delay,
target_bitrate: 0, // We don't do adaptive bitrate yet
})),
};
let mut test_delay = TestDelay::new();
test_delay.time = td.time;
test_delay.from_client = false;
test_delay.last_delay = self.last_delay;
test_delay.target_bitrate = 0; // We don't do adaptive bitrate yet
let data = prost::Message::encode_to_vec(&response);
let mut response = HbbMessage::new();
response.union = Some(message::Union::TestDelay(test_delay));
let data = response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &data).await?;
debug!(
@@ -1076,16 +1171,16 @@ impl Connection {
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
let msg = hbb::Message {
union: Some(message::Union::TestDelay(hbb::TestDelay {
time: time_ms,
from_client: false,
last_delay: self.last_delay,
target_bitrate: 0,
})),
};
let mut test_delay = TestDelay::new();
test_delay.time = time_ms;
test_delay.from_client = false;
test_delay.last_delay = self.last_delay;
test_delay.target_bitrate = 0;
let data = prost::Message::encode_to_vec(&msg);
let mut msg = HbbMessage::new();
msg.union = Some(message::Union::TestDelay(test_delay));
let data = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &data).await?;
// Record when we sent this, so we can calculate RTT when client echoes back
@@ -1096,14 +1191,51 @@ impl Connection {
}
/// Handle key event
async fn handle_key_event(&self, ke: &hbb::KeyEvent) -> anyhow::Result<()> {
async fn handle_key_event(&mut self, ke: &KeyEvent) -> anyhow::Result<()> {
debug!(
"Key event: down={}, press={}, chr={:?}",
ke.down, ke.press, ke.union
"Key event: down={}, press={}, chr={:?}, modifiers={:?}",
ke.down, ke.press, ke.union, ke.modifiers
);
// Check for CapsLock state change in modifiers
// RustDesk doesn't send CapsLock key events, only includes it in modifiers
let caps_lock_in_modifiers = ke.modifiers.iter().any(|m| {
use protobuf::Enum;
m.value() == ControlKey::CapsLock.value()
});
if caps_lock_in_modifiers != self.last_caps_lock {
self.last_caps_lock = caps_lock_in_modifiers;
// Send CapsLock key press (down + up) to toggle state on target
if let Some(ref hid) = self.hid {
debug!("CapsLock state changed to {}, sending CapsLock key", caps_lock_in_modifiers);
let caps_down = KeyboardEvent {
event_type: KeyEventType::Down,
key: 0x39, // USB HID CapsLock
modifiers: KeyboardModifiers::default(),
is_usb_hid: true,
};
let caps_up = KeyboardEvent {
event_type: KeyEventType::Up,
key: 0x39,
modifiers: KeyboardModifiers::default(),
is_usb_hid: true,
};
if let Err(e) = hid.send_keyboard(caps_down).await {
warn!("Failed to send CapsLock down: {}", e);
}
if let Err(e) = hid.send_keyboard(caps_up).await {
warn!("Failed to send CapsLock up: {}", e);
}
}
}
// Convert RustDesk key event to One-KVM key event
if let Some(kb_event) = convert_key_event(ke) {
debug!(
"Converted to HID: key=0x{:02X}, event_type={:?}, modifiers={:02X}",
kb_event.key, kb_event.event_type, kb_event.modifiers.to_hid_byte()
);
// Send to HID controller if available
if let Some(ref hid) = self.hid {
if let Err(e) = hid.send_keyboard(kb_event).await {
@@ -1113,7 +1245,7 @@ impl Connection {
debug!("HID controller not available, skipping key event");
}
} else {
debug!("Could not convert key event to HID");
warn!("Could not convert key event to HID: chr={:?}", ke.union);
}
Ok(())
@@ -1123,7 +1255,7 @@ impl Connection {
///
/// Pure move events (no button/scroll) are throttled to prevent HID EAGAIN errors.
/// Button down/up and scroll events are always sent immediately.
async fn handle_mouse_event(&mut self, me: &hbb::MouseEvent) -> anyhow::Result<()> {
async fn handle_mouse_event(&mut self, me: &MouseEvent) -> anyhow::Result<()> {
// Parse RustDesk mask format: (button << 3) | event_type
let event_type = me.mask & 0x07;
@@ -1195,6 +1327,8 @@ pub struct ConnectionManager {
signing_keypair: Arc<RwLock<Option<SigningKeyPair>>>,
/// HID controller for keyboard/mouse
hid: Arc<RwLock<Option<Arc<HidController>>>>,
/// Audio controller for audio streaming
audio: Arc<RwLock<Option<Arc<AudioController>>>>,
/// Video stream manager for frame subscription
video_manager: Arc<RwLock<Option<Arc<VideoStreamManager>>>>,
}
@@ -1209,6 +1343,7 @@ impl ConnectionManager {
keypair: Arc::new(RwLock::new(None)),
signing_keypair: Arc::new(RwLock::new(None)),
hid: Arc::new(RwLock::new(None)),
audio: Arc::new(RwLock::new(None)),
video_manager: Arc::new(RwLock::new(None)),
}
}
@@ -1218,6 +1353,11 @@ impl ConnectionManager {
*self.hid.write() = Some(hid);
}
/// Set audio controller
pub fn set_audio(&self, audio: Arc<AudioController>) {
*self.audio.write() = Some(audio);
}
/// Set video stream manager
pub fn set_video_manager(&self, video_manager: Arc<VideoStreamManager>) {
*self.video_manager.write() = Some(video_manager);
@@ -1246,6 +1386,7 @@ impl ConnectionManager {
pub fn ensure_signing_keypair(&self) -> SigningKeyPair {
let mut skp = self.signing_keypair.write();
if skp.is_none() {
warn!("ConnectionManager: signing_keypair not set, generating new one! This may cause signature verification failure.");
*skp = Some(SigningKeyPair::generate());
}
skp.as_ref().unwrap().clone()
@@ -1261,11 +1402,11 @@ impl ConnectionManager {
};
let config = self.config.read().clone();
let keypair = self.ensure_keypair();
let signing_keypair = self.ensure_signing_keypair();
let hid = self.hid.read().clone();
let audio = self.audio.read().clone();
let video_manager = self.video_manager.read().clone();
let (mut conn, _rx) = Connection::new(id, &config, keypair, signing_keypair, hid, video_manager);
let (mut conn, _rx) = Connection::new(id, &config, signing_keypair, hid, audio, video_manager);
// Track connection state for external access
let state = conn.state.clone();
@@ -1444,3 +1585,118 @@ async fn run_video_streaming(
Ok(())
}
/// Run audio streaming loop for a connection
///
/// This function subscribes to the audio controller's Opus stream
/// and forwards encoded audio frames to the RustDesk client.
async fn run_audio_streaming(
conn_id: u32,
audio_controller: Arc<AudioController>,
audio_tx: mpsc::Sender<Bytes>,
state: Arc<RwLock<ConnectionState>>,
shutdown_tx: broadcast::Sender<()>,
) -> anyhow::Result<()> {
// Audio format: 48kHz stereo Opus
let mut audio_adapter = AudioFrameAdapter::new(48000, 2);
let mut shutdown_rx = shutdown_tx.subscribe();
let mut frame_count: u64 = 0;
let mut last_log_time = Instant::now();
info!("Started audio streaming for connection {}", conn_id);
// Outer loop: handles pipeline restarts by re-subscribing
'subscribe_loop: loop {
// Check if connection is still active before subscribing
if *state.read() != ConnectionState::Active {
debug!("Connection {} no longer active, stopping audio", conn_id);
break;
}
// Subscribe to the audio Opus stream
let mut opus_rx = match audio_controller.subscribe_opus_async().await {
Some(rx) => rx,
None => {
// Audio not available, wait and retry
debug!("No audio source available for connection {}, retrying...", conn_id);
tokio::time::sleep(Duration::from_millis(500)).await;
continue 'subscribe_loop;
}
};
info!("RustDesk connection {} subscribed to audio pipeline", conn_id);
// Send audio format message once before sending frames
if !audio_adapter.format_sent() {
let format_msg = audio_adapter.create_format_message();
let format_bytes = Bytes::from(format_msg.write_to_bytes().unwrap_or_default());
if audio_tx.send(format_bytes).await.is_err() {
debug!("Audio channel closed for connection {}", conn_id);
break 'subscribe_loop;
}
debug!("Sent audio format message for connection {}", conn_id);
}
// Inner loop: receives frames from current subscription
loop {
// Check if connection is still active
if *state.read() != ConnectionState::Active {
debug!("Connection {} no longer active, stopping audio", conn_id);
break 'subscribe_loop;
}
tokio::select! {
biased;
_ = shutdown_rx.recv() => {
debug!("Shutdown signal received, stopping audio for connection {}", conn_id);
break 'subscribe_loop;
}
result = opus_rx.recv() => {
match result {
Ok(opus_frame) => {
// Convert OpusFrame to RustDesk AudioFrame message
let msg_bytes = audio_adapter.encode_opus_bytes(&opus_frame.data);
// Send to connection (blocks if channel is full, providing backpressure)
if audio_tx.send(msg_bytes).await.is_err() {
debug!("Audio channel closed for connection {}", conn_id);
break 'subscribe_loop;
}
frame_count += 1;
// Log stats periodically
if last_log_time.elapsed().as_secs() >= 30 {
info!(
"Audio streaming stats for connection {}: {} frames forwarded",
conn_id, frame_count
);
last_log_time = Instant::now();
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
debug!("Connection {} lagged {} audio frames", conn_id, n);
}
Err(broadcast::error::RecvError::Closed) => {
// Pipeline was restarted
info!("Audio pipeline closed for connection {}, re-subscribing...", conn_id);
audio_adapter.reset();
tokio::time::sleep(Duration::from_millis(100)).await;
continue 'subscribe_loop;
}
}
}
}
}
}
info!(
"Audio streaming ended for connection {}: {} total frames forwarded",
conn_id, frame_count
);
Ok(())
}