mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-02-01 10:31:54 +08:00
feat(video): 事务化切换与前端统一编排,增强视频输入格式支持
- 后端:切换事务+transition_id,/stream/mode 返回 switching/transition_id 与实际 codec - 事件:新增 mode_switching/mode_ready,config/webrtc_ready/mode_changed 关联事务 - 编码/格式:扩展 NV21/NV16/NV24/RGB/BGR 输入与转换链路,RKMPP direct input 优化 - 前端:useVideoSession 统一切换,失败回退真实切回 MJPEG,菜单格式同步修复 - 清理:useVideoStream 降级为 MJPEG-only
This commit is contained in:
@@ -34,7 +34,10 @@ pub fn encode_frame(data: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let h = ((len << 2) as u32) | 0x3;
|
||||
buf.extend_from_slice(&h.to_le_bytes());
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Message too large",
|
||||
));
|
||||
}
|
||||
|
||||
buf.extend_from_slice(data);
|
||||
@@ -79,7 +82,10 @@ pub async fn read_frame<R: AsyncRead + Unpin>(reader: &mut R) -> io::Result<Byte
|
||||
let (_, msg_len) = decode_header(first_byte[0], &header_rest);
|
||||
|
||||
if msg_len > MAX_PACKET_LENGTH {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "Message too large"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Message too large",
|
||||
));
|
||||
}
|
||||
|
||||
// Read message body
|
||||
@@ -133,7 +139,10 @@ pub fn encode_frame_into(data: &[u8], buf: &mut BytesMut) -> io::Result<()> {
|
||||
} else if len <= MAX_PACKET_LENGTH {
|
||||
buf.put_u32_le(((len << 2) as u32) | 0x3);
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Message too large",
|
||||
));
|
||||
}
|
||||
|
||||
buf.extend_from_slice(data);
|
||||
@@ -216,7 +225,10 @@ impl BytesCodec {
|
||||
n >>= 2;
|
||||
|
||||
if n > self.max_packet_length {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "Message too large"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Message too large",
|
||||
));
|
||||
}
|
||||
|
||||
src.advance(head_len);
|
||||
@@ -245,7 +257,10 @@ impl BytesCodec {
|
||||
} else if len <= MAX_PACKET_LENGTH {
|
||||
buf.put_u32_le(((len << 2) as u32) | 0x3);
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Message too large",
|
||||
));
|
||||
}
|
||||
|
||||
buf.extend(data);
|
||||
|
||||
@@ -116,9 +116,9 @@ impl RustDeskConfig {
|
||||
|
||||
/// Get the UUID bytes (returns None if not set)
|
||||
pub fn get_uuid_bytes(&self) -> Option<[u8; 16]> {
|
||||
self.uuid.as_ref().and_then(|s| {
|
||||
uuid::Uuid::parse_str(s).ok().map(|u| *u.as_bytes())
|
||||
})
|
||||
self.uuid
|
||||
.as_ref()
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok().map(|u| *u.as_bytes()))
|
||||
}
|
||||
|
||||
/// Get the rendezvous server address with default port
|
||||
@@ -135,26 +135,29 @@ impl RustDeskConfig {
|
||||
|
||||
/// Get the relay server address with default port
|
||||
pub fn relay_addr(&self) -> Option<String> {
|
||||
self.relay_server.as_ref().map(|s| {
|
||||
if s.contains(':') {
|
||||
s.clone()
|
||||
} else {
|
||||
format!("{}:21117", s)
|
||||
}
|
||||
}).or_else(|| {
|
||||
// Default: same host as rendezvous server
|
||||
let server = &self.rendezvous_server;
|
||||
if !server.is_empty() {
|
||||
let host = server.split(':').next().unwrap_or("");
|
||||
if !host.is_empty() {
|
||||
Some(format!("{}:21117", host))
|
||||
self.relay_server
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
if s.contains(':') {
|
||||
s.clone()
|
||||
} else {
|
||||
format!("{}:21117", s)
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
// Default: same host as rendezvous server
|
||||
let server = &self.rendezvous_server;
|
||||
if !server.is_empty() {
|
||||
let host = server.split(':').next().unwrap_or("");
|
||||
if !host.is_empty() {
|
||||
Some(format!("{}:21117", host))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +225,10 @@ mod tests {
|
||||
|
||||
// Explicit relay server
|
||||
config.relay_server = Some("relay.example.com".to_string());
|
||||
assert_eq!(config.relay_addr(), Some("relay.example.com:21117".to_string()));
|
||||
assert_eq!(
|
||||
config.relay_addr(),
|
||||
Some("relay.example.com:21117".to_string())
|
||||
);
|
||||
|
||||
// No rendezvous server, relay is None
|
||||
config.rendezvous_server = String::new();
|
||||
|
||||
@@ -13,16 +13,16 @@ 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 protobuf::Message as ProtobufMessage;
|
||||
use tokio::net::TcpStream;
|
||||
use sodiumoxide::crypto::box_;
|
||||
use tokio::net::tcp::OwnedWriteHalf;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{broadcast, mpsc, Mutex};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::audio::AudioController;
|
||||
use crate::hid::{HidController, KeyboardEvent, KeyEventType, KeyboardModifiers};
|
||||
use crate::hid::{HidController, KeyEventType, KeyboardEvent, KeyboardModifiers};
|
||||
use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType};
|
||||
use crate::video::encoder::BitratePreset;
|
||||
use crate::video::stream_manager::VideoStreamManager;
|
||||
@@ -33,10 +33,9 @@ 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::{
|
||||
message, misc, login_response,
|
||||
KeyEvent, MouseEvent, Clipboard, Misc, LoginRequest, LoginResponse, PeerInfo,
|
||||
IdPk, SignedId, Hash, TestDelay, ControlKey,
|
||||
decode_message, HbbMessage, DisplayInfo, SupportedEncoding, OptionMessage, PublicKey,
|
||||
decode_message, login_response, message, misc, Clipboard, ControlKey, DisplayInfo, Hash,
|
||||
HbbMessage, IdPk, KeyEvent, LoginRequest, LoginResponse, Misc, MouseEvent, OptionMessage,
|
||||
PeerInfo, PublicKey, SignedId, SupportedEncoding, TestDelay,
|
||||
};
|
||||
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
@@ -268,7 +267,11 @@ impl Connection {
|
||||
}
|
||||
|
||||
/// Handle an incoming TCP connection
|
||||
pub async fn handle_tcp(&mut self, stream: TcpStream, peer_addr: SocketAddr) -> anyhow::Result<()> {
|
||||
pub async fn handle_tcp(
|
||||
&mut self,
|
||||
stream: TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
) -> anyhow::Result<()> {
|
||||
info!("New connection from {}", peer_addr);
|
||||
*self.state.write() = ConnectionState::Handshaking;
|
||||
|
||||
@@ -279,7 +282,9 @@ 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 = signed_id_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode SignedId: {}", e))?;
|
||||
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?;
|
||||
|
||||
@@ -402,7 +407,11 @@ impl Connection {
|
||||
}
|
||||
|
||||
/// Send framed message using Arc<Mutex<OwnedWriteHalf>> with RustDesk's variable-length encoding
|
||||
async fn send_framed_arc(&self, writer: &Arc<Mutex<OwnedWriteHalf>>, data: &[u8]) -> anyhow::Result<()> {
|
||||
async fn send_framed_arc(
|
||||
&self,
|
||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
||||
data: &[u8],
|
||||
) -> anyhow::Result<()> {
|
||||
let mut w = writer.lock().await;
|
||||
write_frame(&mut *w, data).await?;
|
||||
Ok(())
|
||||
@@ -480,7 +489,9 @@ impl Connection {
|
||||
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!");
|
||||
warn!(
|
||||
"Received EMPTY PublicKey - client may have failed signature verification!"
|
||||
);
|
||||
}
|
||||
self.handle_peer_public_key(pk, writer).await?;
|
||||
}
|
||||
@@ -535,7 +546,7 @@ impl Connection {
|
||||
info!("Received SignedId from peer, id_len={}", si.id.len());
|
||||
self.handle_signed_id(si, writer).await?;
|
||||
return Ok(());
|
||||
},
|
||||
}
|
||||
message::Union::Hash(_) => "Hash",
|
||||
message::Union::VideoFrame(_) => "VideoFrame",
|
||||
message::Union::CursorData(_) => "CursorData",
|
||||
@@ -564,16 +575,26 @@ impl Connection {
|
||||
lr: &LoginRequest,
|
||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
||||
) -> anyhow::Result<bool> {
|
||||
info!("Login request from {} ({}), password_len={}", lr.my_id, lr.my_name, lr.password.len());
|
||||
info!(
|
||||
"Login request from {} ({}), password_len={}",
|
||||
lr.my_id,
|
||||
lr.my_name,
|
||||
lr.password.len()
|
||||
);
|
||||
|
||||
// Check if our server requires a password
|
||||
if !self.password.is_empty() {
|
||||
// Server requires password
|
||||
if lr.password.is_empty() {
|
||||
// Client sent empty password - tell them to enter password
|
||||
info!("Empty password from {}, requesting password input", lr.my_id);
|
||||
info!(
|
||||
"Empty password from {}, requesting password input",
|
||||
lr.my_id
|
||||
);
|
||||
let error_response = self.create_login_error_response("Empty Password");
|
||||
let response_bytes = error_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
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);
|
||||
@@ -583,7 +604,9 @@ 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 = error_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
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);
|
||||
@@ -601,7 +624,9 @@ impl Connection {
|
||||
info!("Negotiated video codec: {:?}", negotiated);
|
||||
|
||||
let response = self.create_login_response(true);
|
||||
let response_bytes = response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
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)
|
||||
}
|
||||
@@ -679,7 +704,10 @@ impl Connection {
|
||||
};
|
||||
|
||||
if let Some(preset) = preset {
|
||||
info!("Client requested quality preset: {:?} (image_quality={})", preset, image_quality);
|
||||
info!(
|
||||
"Client requested quality preset: {:?} (image_quality={})",
|
||||
preset, image_quality
|
||||
);
|
||||
if let Some(ref video_manager) = self.video_manager {
|
||||
if let Err(e) = video_manager.set_bitrate_preset(preset).await {
|
||||
warn!("Failed to set bitrate preset: {}", e);
|
||||
@@ -729,7 +757,10 @@ impl Connection {
|
||||
|
||||
// Log custom_image_quality (accept but don't process)
|
||||
if opt.custom_image_quality > 0 {
|
||||
debug!("Client sent custom_image_quality: {} (ignored)", opt.custom_image_quality);
|
||||
debug!(
|
||||
"Client sent custom_image_quality: {} (ignored)",
|
||||
opt.custom_image_quality
|
||||
);
|
||||
}
|
||||
if opt.custom_fps > 0 {
|
||||
debug!("Client requested FPS: {}", opt.custom_fps);
|
||||
@@ -779,7 +810,10 @@ impl Connection {
|
||||
let negotiated_codec = self.negotiated_codec.unwrap_or(VideoEncoderType::H264);
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
info!("Starting video streaming for connection {} with codec {:?}", conn_id, negotiated_codec);
|
||||
info!(
|
||||
"Starting video streaming for connection {} with codec {:?}",
|
||||
conn_id, negotiated_codec
|
||||
);
|
||||
|
||||
if let Err(e) = run_video_streaming(
|
||||
conn_id,
|
||||
@@ -788,7 +822,9 @@ impl Connection {
|
||||
state,
|
||||
shutdown_tx,
|
||||
negotiated_codec,
|
||||
).await {
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Video streaming error for connection {}: {}", conn_id, e);
|
||||
}
|
||||
|
||||
@@ -815,13 +851,9 @@ impl Connection {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -894,7 +926,10 @@ impl Connection {
|
||||
self.encryption_enabled = true;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to decrypt session key: {:?}, falling back to unencrypted", e);
|
||||
warn!(
|
||||
"Failed to decrypt session key: {:?}, falling back to unencrypted",
|
||||
e
|
||||
);
|
||||
// Continue without encryption - some clients may not support it
|
||||
self.encryption_enabled = false;
|
||||
}
|
||||
@@ -917,8 +952,13 @@ 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 = 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);
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
@@ -971,7 +1011,9 @@ 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 = signed_id_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
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(())
|
||||
@@ -1073,7 +1115,8 @@ impl Connection {
|
||||
msg
|
||||
} else {
|
||||
let mut login_response = LoginResponse::new();
|
||||
login_response.union = Some(login_response::Union::Error("Invalid password".to_string()));
|
||||
login_response.union =
|
||||
Some(login_response::Union::Error("Invalid password".to_string()));
|
||||
login_response.enable_trusted_devices = false;
|
||||
|
||||
let mut msg = HbbMessage::new();
|
||||
@@ -1133,7 +1176,9 @@ impl Connection {
|
||||
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))?;
|
||||
let data = response
|
||||
.write_to_bytes()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
self.send_encrypted_arc(writer, &data).await?;
|
||||
|
||||
debug!(
|
||||
@@ -1161,10 +1206,7 @@ impl Connection {
|
||||
/// The client will echo this back, allowing us to calculate RTT.
|
||||
/// The measured delay is then included in future TestDelay messages
|
||||
/// for the client to display.
|
||||
async fn send_test_delay(
|
||||
&mut self,
|
||||
writer: &Arc<Mutex<OwnedWriteHalf>>,
|
||||
) -> anyhow::Result<()> {
|
||||
async fn send_test_delay(&mut self, writer: &Arc<Mutex<OwnedWriteHalf>>) -> anyhow::Result<()> {
|
||||
// Get current time in milliseconds since epoch
|
||||
let time_ms = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -1180,13 +1222,18 @@ impl Connection {
|
||||
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))?;
|
||||
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
|
||||
self.last_test_delay_sent = Some(Instant::now());
|
||||
|
||||
debug!("TestDelay sent: time={}, last_delay={}ms", time_ms, self.last_delay);
|
||||
debug!(
|
||||
"TestDelay sent: time={}, last_delay={}ms",
|
||||
time_ms, self.last_delay
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1208,7 +1255,10 @@ impl Connection {
|
||||
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);
|
||||
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
|
||||
@@ -1234,7 +1284,9 @@ impl Connection {
|
||||
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()
|
||||
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 {
|
||||
@@ -1393,7 +1445,11 @@ impl ConnectionManager {
|
||||
}
|
||||
|
||||
/// Accept a new connection
|
||||
pub async fn accept_connection(&self, stream: TcpStream, peer_addr: SocketAddr) -> anyhow::Result<u32> {
|
||||
pub async fn accept_connection(
|
||||
&self,
|
||||
stream: TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
) -> anyhow::Result<u32> {
|
||||
let id = {
|
||||
let mut next = self.next_id.write();
|
||||
let id = *next;
|
||||
@@ -1406,14 +1462,14 @@ impl ConnectionManager {
|
||||
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, signing_keypair, hid, audio, 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();
|
||||
self.connections.write().push(Arc::new(RwLock::new(ConnectionInfo {
|
||||
id,
|
||||
state,
|
||||
})));
|
||||
self.connections
|
||||
.write()
|
||||
.push(Arc::new(RwLock::new(ConnectionInfo { id, state })));
|
||||
|
||||
// Spawn connection handler - Connection is moved, not locked
|
||||
tokio::spawn(async move {
|
||||
@@ -1466,7 +1522,10 @@ async fn run_video_streaming(
|
||||
};
|
||||
|
||||
// Set the video codec on the shared pipeline before subscribing
|
||||
info!("Setting video codec to {:?} for connection {}", negotiated_codec, conn_id);
|
||||
info!(
|
||||
"Setting video codec to {:?} for connection {}",
|
||||
negotiated_codec, conn_id
|
||||
);
|
||||
if let Err(e) = video_manager.set_video_codec(webrtc_codec).await {
|
||||
error!("Failed to set video codec: {}", e);
|
||||
// Continue anyway, will use whatever codec the pipeline already has
|
||||
@@ -1485,7 +1544,10 @@ async fn run_video_streaming(
|
||||
let mut encoded_count: u64 = 0;
|
||||
let mut last_log_time = Instant::now();
|
||||
|
||||
info!("Started shared video streaming for connection {} (codec: {:?})", conn_id, codec);
|
||||
info!(
|
||||
"Started shared video streaming for connection {} (codec: {:?})",
|
||||
conn_id, codec
|
||||
);
|
||||
|
||||
// Outer loop: handles pipeline restarts by re-subscribing
|
||||
'subscribe_loop: loop {
|
||||
@@ -1500,7 +1562,10 @@ async fn run_video_streaming(
|
||||
Some(rx) => rx,
|
||||
None => {
|
||||
// Pipeline not ready yet, wait and retry
|
||||
debug!("No encoded frame source available for connection {}, retrying...", conn_id);
|
||||
debug!(
|
||||
"No encoded frame source available for connection {}, retrying...",
|
||||
conn_id
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
continue 'subscribe_loop;
|
||||
}
|
||||
@@ -1619,13 +1684,19 @@ async fn run_audio_streaming(
|
||||
Some(rx) => rx,
|
||||
None => {
|
||||
// Audio not available, wait and retry
|
||||
debug!("No audio source available for connection {}, retrying...", conn_id);
|
||||
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);
|
||||
info!(
|
||||
"RustDesk connection {} subscribed to audio pipeline",
|
||||
conn_id
|
||||
);
|
||||
|
||||
// Send audio format message once before sending frames
|
||||
if !audio_adapter.format_sent() {
|
||||
|
||||
@@ -86,8 +86,12 @@ impl KeyPair {
|
||||
|
||||
/// Create from base64-encoded keys
|
||||
pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> {
|
||||
let pk_bytes = BASE64.decode(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let sk_bytes = BASE64.decode(secret_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let pk_bytes = BASE64
|
||||
.decode(public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let sk_bytes = BASE64
|
||||
.decode(secret_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
Self::from_keys(&pk_bytes, &sk_bytes)
|
||||
}
|
||||
}
|
||||
@@ -140,7 +144,10 @@ pub fn decrypt_with_key(
|
||||
|
||||
/// Compute a shared symmetric key from public/private keypair
|
||||
/// This is the precomputed key for the NaCl box
|
||||
pub fn precompute_key(their_public_key: &PublicKey, our_secret_key: &SecretKey) -> box_::PrecomputedKey {
|
||||
pub fn precompute_key(
|
||||
their_public_key: &PublicKey,
|
||||
our_secret_key: &SecretKey,
|
||||
) -> box_::PrecomputedKey {
|
||||
box_::precompute(their_public_key, our_secret_key)
|
||||
}
|
||||
|
||||
@@ -207,8 +214,8 @@ pub fn decrypt_symmetric_key(
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
|
||||
let their_pk = PublicKey::from_slice(their_temp_public_key)
|
||||
.ok_or(CryptoError::InvalidKeyLength)?;
|
||||
let their_pk =
|
||||
PublicKey::from_slice(their_temp_public_key).ok_or(CryptoError::InvalidKeyLength)?;
|
||||
|
||||
// Use zero nonce as per RustDesk protocol
|
||||
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||
@@ -294,8 +301,12 @@ impl SigningKeyPair {
|
||||
|
||||
/// Create from base64-encoded keys
|
||||
pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> {
|
||||
let pk_bytes = BASE64.decode(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let sk_bytes = BASE64.decode(secret_key).map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let pk_bytes = BASE64
|
||||
.decode(public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let sk_bytes = BASE64
|
||||
.decode(secret_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
Self::from_keys(&pk_bytes, &sk_bytes)
|
||||
}
|
||||
|
||||
@@ -321,8 +332,7 @@ impl SigningKeyPair {
|
||||
/// which is required by RustDesk's protocol where clients encrypt the
|
||||
/// symmetric key using the public key from IdPk.
|
||||
pub fn to_curve25519_pk(&self) -> Result<PublicKey, CryptoError> {
|
||||
ed25519::to_curve25519_pk(&self.public_key)
|
||||
.map_err(|_| CryptoError::KeyConversionFailed)
|
||||
ed25519::to_curve25519_pk(&self.public_key).map_err(|_| CryptoError::KeyConversionFailed)
|
||||
}
|
||||
|
||||
/// Convert Ed25519 secret key to Curve25519 secret key for decryption
|
||||
@@ -330,14 +340,16 @@ impl SigningKeyPair {
|
||||
/// This allows decrypting messages that were encrypted using the
|
||||
/// converted public key.
|
||||
pub fn to_curve25519_sk(&self) -> Result<SecretKey, CryptoError> {
|
||||
ed25519::to_curve25519_sk(&self.secret_key)
|
||||
.map_err(|_| CryptoError::KeyConversionFailed)
|
||||
ed25519::to_curve25519_sk(&self.secret_key).map_err(|_| CryptoError::KeyConversionFailed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a signed message
|
||||
/// Returns the original message if signature is valid
|
||||
pub fn verify_signed(signed_message: &[u8], public_key: &sign::PublicKey) -> Result<Vec<u8>, CryptoError> {
|
||||
pub fn verify_signed(
|
||||
signed_message: &[u8],
|
||||
public_key: &sign::PublicKey,
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
sign::verify(signed_message, public_key).map_err(|_| CryptoError::SignatureVerificationFailed)
|
||||
}
|
||||
|
||||
@@ -374,7 +386,8 @@ mod tests {
|
||||
let message = b"Hello, RustDesk!";
|
||||
let (nonce, ciphertext) = encrypt_box(message, &bob.public_key, &alice.secret_key);
|
||||
|
||||
let plaintext = decrypt_box(&ciphertext, &nonce, &alice.public_key, &bob.secret_key).unwrap();
|
||||
let plaintext =
|
||||
decrypt_box(&ciphertext, &nonce, &alice.public_key, &bob.secret_key).unwrap();
|
||||
assert_eq!(plaintext, message);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ use bytes::Bytes;
|
||||
use protobuf::Message as ProtobufMessage;
|
||||
|
||||
use super::protocol::hbb::message::{
|
||||
message as msg_union, misc as misc_union, video_frame as vf_union,
|
||||
AudioFormat, AudioFrame, CursorData, CursorPosition,
|
||||
EncodedVideoFrame, EncodedVideoFrames, Message, Misc, VideoFrame,
|
||||
message as msg_union, misc as misc_union, video_frame as vf_union, AudioFormat, AudioFrame,
|
||||
CursorData, CursorPosition, EncodedVideoFrame, EncodedVideoFrames, Message, Misc, VideoFrame,
|
||||
};
|
||||
|
||||
/// Video codec type for RustDesk
|
||||
@@ -63,7 +62,12 @@ impl VideoFrameAdapter {
|
||||
/// Convert encoded video data to RustDesk Message (zero-copy version)
|
||||
///
|
||||
/// This version takes Bytes directly to avoid copying the frame data.
|
||||
pub fn encode_frame_from_bytes(&mut self, data: Bytes, is_keyframe: bool, timestamp_ms: u64) -> Message {
|
||||
pub fn encode_frame_from_bytes(
|
||||
&mut self,
|
||||
data: Bytes,
|
||||
is_keyframe: bool,
|
||||
timestamp_ms: u64,
|
||||
) -> Message {
|
||||
// Calculate relative timestamp
|
||||
if self.seq == 0 {
|
||||
self.timestamp_base = timestamp_ms;
|
||||
@@ -104,13 +108,23 @@ impl VideoFrameAdapter {
|
||||
/// Encode frame to bytes for sending (zero-copy version)
|
||||
///
|
||||
/// Takes Bytes directly to avoid copying the frame data.
|
||||
pub fn encode_frame_bytes_zero_copy(&mut self, data: Bytes, is_keyframe: bool, timestamp_ms: u64) -> Bytes {
|
||||
pub fn encode_frame_bytes_zero_copy(
|
||||
&mut self,
|
||||
data: Bytes,
|
||||
is_keyframe: bool,
|
||||
timestamp_ms: u64,
|
||||
) -> Bytes {
|
||||
let msg = self.encode_frame_from_bytes(data, is_keyframe, timestamp_ms);
|
||||
Bytes::from(msg.write_to_bytes().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Encode frame to bytes for sending
|
||||
pub fn encode_frame_bytes(&mut self, data: &[u8], is_keyframe: bool, timestamp_ms: u64) -> Bytes {
|
||||
pub fn encode_frame_bytes(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
is_keyframe: bool,
|
||||
timestamp_ms: u64,
|
||||
) -> Bytes {
|
||||
self.encode_frame_bytes_zero_copy(Bytes::copy_from_slice(data), is_keyframe, timestamp_ms)
|
||||
}
|
||||
|
||||
@@ -234,15 +248,13 @@ mod tests {
|
||||
let msg = adapter.encode_frame(&data, true, 0);
|
||||
|
||||
match &msg.union {
|
||||
Some(msg_union::Union::VideoFrame(vf)) => {
|
||||
match &vf.union {
|
||||
Some(vf_union::Union::H264s(frames)) => {
|
||||
assert_eq!(frames.frames.len(), 1);
|
||||
assert!(frames.frames[0].key);
|
||||
}
|
||||
_ => panic!("Expected H264s"),
|
||||
Some(msg_union::Union::VideoFrame(vf)) => match &vf.union {
|
||||
Some(vf_union::Union::H264s(frames)) => {
|
||||
assert_eq!(frames.frames.len(), 1);
|
||||
assert!(frames.frames[0].key);
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected H264s"),
|
||||
},
|
||||
_ => panic!("Expected VideoFrame"),
|
||||
}
|
||||
}
|
||||
@@ -256,15 +268,13 @@ mod tests {
|
||||
assert!(adapter.format_sent());
|
||||
|
||||
match &msg.union {
|
||||
Some(msg_union::Union::Misc(misc)) => {
|
||||
match &misc.union {
|
||||
Some(misc_union::Union::AudioFormat(fmt)) => {
|
||||
assert_eq!(fmt.sample_rate, 48000);
|
||||
assert_eq!(fmt.channels, 2);
|
||||
}
|
||||
_ => panic!("Expected AudioFormat"),
|
||||
Some(msg_union::Union::Misc(misc)) => match &misc.union {
|
||||
Some(misc_union::Union::AudioFormat(fmt)) => {
|
||||
assert_eq!(fmt.sample_rate, 48000);
|
||||
assert_eq!(fmt.channels, 2);
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected AudioFormat"),
|
||||
},
|
||||
_ => panic!("Expected Misc"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
//!
|
||||
//! Converts RustDesk HID events (KeyEvent, MouseEvent) to One-KVM HID events.
|
||||
|
||||
use protobuf::Enum;
|
||||
use crate::hid::{
|
||||
KeyboardEvent, KeyboardModifiers, KeyEventType,
|
||||
MouseButton, MouseEvent as OneKvmMouseEvent, MouseEventType,
|
||||
};
|
||||
use super::protocol::{KeyEvent, MouseEvent, ControlKey};
|
||||
use super::protocol::hbb::message::key_event as ke_union;
|
||||
use super::protocol::{ControlKey, KeyEvent, MouseEvent};
|
||||
use crate::hid::{
|
||||
KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent as OneKvmMouseEvent,
|
||||
MouseEventType,
|
||||
};
|
||||
use protobuf::Enum;
|
||||
|
||||
/// Mouse event types from RustDesk protocol
|
||||
/// mask = (button << 3) | event_type
|
||||
@@ -32,7 +32,11 @@ pub mod mouse_button {
|
||||
/// Convert RustDesk MouseEvent to One-KVM MouseEvent(s)
|
||||
/// Returns a Vec because a single RustDesk event may need multiple One-KVM events
|
||||
/// (e.g., move + button + scroll)
|
||||
pub fn convert_mouse_event(event: &MouseEvent, screen_width: u32, screen_height: u32) -> Vec<OneKvmMouseEvent> {
|
||||
pub fn convert_mouse_event(
|
||||
event: &MouseEvent,
|
||||
screen_width: u32,
|
||||
screen_height: u32,
|
||||
) -> Vec<OneKvmMouseEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
// RustDesk uses absolute coordinates
|
||||
@@ -243,10 +247,10 @@ fn parse_modifiers(event: &KeyEvent) -> KeyboardModifiers {
|
||||
/// Convert RustDesk ControlKey to USB HID usage code
|
||||
fn control_key_to_hid(key: i32) -> Option<u8> {
|
||||
match key {
|
||||
x if x == ControlKey::Alt as i32 => Some(0xE2), // Left Alt
|
||||
x if x == ControlKey::Alt as i32 => Some(0xE2), // Left Alt
|
||||
x if x == ControlKey::Backspace as i32 => Some(0x2A),
|
||||
x if x == ControlKey::CapsLock as i32 => Some(0x39),
|
||||
x if x == ControlKey::Control as i32 => Some(0xE0), // Left Ctrl
|
||||
x if x == ControlKey::Control as i32 => Some(0xE0), // Left Ctrl
|
||||
x if x == ControlKey::Delete as i32 => Some(0x4C),
|
||||
x if x == ControlKey::DownArrow as i32 => Some(0x51),
|
||||
x if x == ControlKey::End as i32 => Some(0x4D),
|
||||
@@ -265,12 +269,12 @@ fn control_key_to_hid(key: i32) -> Option<u8> {
|
||||
x if x == ControlKey::F12 as i32 => Some(0x45),
|
||||
x if x == ControlKey::Home as i32 => Some(0x4A),
|
||||
x if x == ControlKey::LeftArrow as i32 => Some(0x50),
|
||||
x if x == ControlKey::Meta as i32 => Some(0xE3), // Left GUI/Windows
|
||||
x if x == ControlKey::Meta as i32 => Some(0xE3), // Left GUI/Windows
|
||||
x if x == ControlKey::PageDown as i32 => Some(0x4E),
|
||||
x if x == ControlKey::PageUp as i32 => Some(0x4B),
|
||||
x if x == ControlKey::Return as i32 => Some(0x28),
|
||||
x if x == ControlKey::RightArrow as i32 => Some(0x4F),
|
||||
x if x == ControlKey::Shift as i32 => Some(0xE1), // Left Shift
|
||||
x if x == ControlKey::Shift as i32 => Some(0xE1), // Left Shift
|
||||
x if x == ControlKey::Space as i32 => Some(0x2C),
|
||||
x if x == ControlKey::Tab as i32 => Some(0x2B),
|
||||
x if x == ControlKey::UpArrow as i32 => Some(0x52),
|
||||
@@ -330,7 +334,7 @@ fn ascii_to_hid(ascii: u32) -> Option<u8> {
|
||||
Some((ascii - 65 + 0x04) as u8)
|
||||
}
|
||||
// Numbers 0-9 (ASCII 48-57)
|
||||
48 => Some(0x27), // 0
|
||||
48 => Some(0x27), // 0
|
||||
49..=57 => Some((ascii - 49 + 0x1E) as u8), // 1-9
|
||||
// Common punctuation
|
||||
32 => Some(0x2C), // Space
|
||||
@@ -341,17 +345,17 @@ fn ascii_to_hid(ascii: u32) -> Option<u8> {
|
||||
8 => Some(0x2A), // Backspace
|
||||
127 => Some(0x4C), // Delete
|
||||
// Symbols (US keyboard layout)
|
||||
45 => Some(0x2D), // -
|
||||
61 => Some(0x2E), // =
|
||||
91 => Some(0x2F), // [
|
||||
93 => Some(0x30), // ]
|
||||
92 => Some(0x31), // \
|
||||
59 => Some(0x33), // ;
|
||||
39 => Some(0x34), // '
|
||||
96 => Some(0x35), // `
|
||||
44 => Some(0x36), // ,
|
||||
46 => Some(0x37), // .
|
||||
47 => Some(0x38), // /
|
||||
45 => Some(0x2D), // -
|
||||
61 => Some(0x2E), // =
|
||||
91 => Some(0x2F), // [
|
||||
93 => Some(0x30), // ]
|
||||
92 => Some(0x31), // \
|
||||
59 => Some(0x33), // ;
|
||||
39 => Some(0x34), // '
|
||||
96 => Some(0x35), // `
|
||||
44 => Some(0x36), // ,
|
||||
46 => Some(0x37), // .
|
||||
47 => Some(0x38), // /
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -394,10 +398,10 @@ fn windows_vk_to_hid(vk: u32) -> Option<u8> {
|
||||
})
|
||||
}
|
||||
// Numbers 0-9 (VK_0=0x30 to VK_9=0x39)
|
||||
0x30 => Some(0x27), // 0
|
||||
0x30 => Some(0x27), // 0
|
||||
0x31..=0x39 => Some((vk - 0x31 + 0x1E) as u8), // 1-9
|
||||
// Numpad 0-9 (VK_NUMPAD0=0x60 to VK_NUMPAD9=0x69)
|
||||
0x60 => Some(0x62), // Numpad 0
|
||||
0x60 => Some(0x62), // Numpad 0
|
||||
0x61..=0x69 => Some((vk - 0x61 + 0x59) as u8), // Numpad 1-9
|
||||
// Numpad operators
|
||||
0x6A => Some(0x55), // Numpad *
|
||||
@@ -451,7 +455,7 @@ fn x11_keycode_to_hid(keycode: u32) -> Option<u8> {
|
||||
match keycode {
|
||||
// Numbers: X11 keycode 10="1", 11="2", ..., 18="9", 19="0"
|
||||
10..=18 => Some((keycode - 10 + 0x1E) as u8), // 1-9
|
||||
19 => Some(0x27), // 0
|
||||
19 => Some(0x27), // 0
|
||||
// Punctuation
|
||||
20 => Some(0x2D), // -
|
||||
21 => Some(0x2E), // =
|
||||
@@ -533,7 +537,9 @@ mod tests {
|
||||
let events = convert_mouse_event(&event, 1920, 1080);
|
||||
assert!(events.len() >= 2);
|
||||
// Should have a button down event
|
||||
assert!(events.iter().any(|e| e.event_type == MouseEventType::Down && e.button == Some(MouseButton::Left)));
|
||||
assert!(events
|
||||
.iter()
|
||||
.any(|e| e.event_type == MouseEventType::Down && e.button == Some(MouseButton::Left)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -542,7 +548,9 @@ mod tests {
|
||||
let mut key_event = KeyEvent::new();
|
||||
key_event.down = true;
|
||||
key_event.press = false;
|
||||
key_event.union = Some(ke_union::Union::ControlKey(EnumOrUnknown::new(ControlKey::Return)));
|
||||
key_event.union = Some(ke_union::Union::ControlKey(EnumOrUnknown::new(
|
||||
ControlKey::Return,
|
||||
)));
|
||||
|
||||
let result = convert_key_event(&key_event);
|
||||
assert!(result.is_some());
|
||||
|
||||
@@ -205,7 +205,8 @@ impl RustDeskService {
|
||||
self.connection_manager.set_audio(self.audio.clone());
|
||||
|
||||
// Set the video manager on connection manager for video streaming
|
||||
self.connection_manager.set_video_manager(self.video_manager.clone());
|
||||
self.connection_manager
|
||||
.set_video_manager(self.video_manager.clone());
|
||||
|
||||
*self.rendezvous.write() = Some(mediator.clone());
|
||||
|
||||
@@ -231,105 +232,117 @@ impl RustDeskService {
|
||||
let audio_punch = self.audio.clone();
|
||||
let service_config_punch = self.config.clone();
|
||||
|
||||
mediator.set_punch_callback(Arc::new(move |peer_addr, rendezvous_addr, relay_server, uuid, socket_addr, device_id| {
|
||||
let conn_mgr = connection_manager_punch.clone();
|
||||
let video = video_manager_punch.clone();
|
||||
let hid = hid_punch.clone();
|
||||
let audio = audio_punch.clone();
|
||||
let config = service_config_punch.clone();
|
||||
mediator.set_punch_callback(Arc::new(
|
||||
move |peer_addr, rendezvous_addr, relay_server, uuid, socket_addr, device_id| {
|
||||
let conn_mgr = connection_manager_punch.clone();
|
||||
let video = video_manager_punch.clone();
|
||||
let hid = hid_punch.clone();
|
||||
let audio = audio_punch.clone();
|
||||
let config = service_config_punch.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Get relay_key from config (no public server fallback)
|
||||
let relay_key = {
|
||||
let cfg = config.read();
|
||||
cfg.relay_key.clone().unwrap_or_default()
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
// Get relay_key from config (no public server fallback)
|
||||
let relay_key = {
|
||||
let cfg = config.read();
|
||||
cfg.relay_key.clone().unwrap_or_default()
|
||||
};
|
||||
|
||||
// Try P2P direct connection first
|
||||
if let Some(addr) = peer_addr {
|
||||
info!("Attempting P2P direct connection to {}", addr);
|
||||
match punch::try_direct_connection(addr).await {
|
||||
punch::PunchResult::DirectConnection(stream) => {
|
||||
info!("P2P direct connection succeeded to {}", addr);
|
||||
if let Err(e) = conn_mgr.accept_connection(stream, addr).await {
|
||||
error!("Failed to accept P2P connection: {}", e);
|
||||
// Try P2P direct connection first
|
||||
if let Some(addr) = peer_addr {
|
||||
info!("Attempting P2P direct connection to {}", addr);
|
||||
match punch::try_direct_connection(addr).await {
|
||||
punch::PunchResult::DirectConnection(stream) => {
|
||||
info!("P2P direct connection succeeded to {}", addr);
|
||||
if let Err(e) = conn_mgr.accept_connection(stream, addr).await {
|
||||
error!("Failed to accept P2P connection: {}", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
punch::PunchResult::NeedRelay => {
|
||||
info!("P2P direct connection failed, falling back to relay");
|
||||
}
|
||||
return;
|
||||
}
|
||||
punch::PunchResult::NeedRelay => {
|
||||
info!("P2P direct connection failed, falling back to relay");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to relay
|
||||
if let Err(e) = handle_relay_request(
|
||||
&rendezvous_addr,
|
||||
&relay_server,
|
||||
&uuid,
|
||||
&socket_addr,
|
||||
&device_id,
|
||||
&relay_key,
|
||||
conn_mgr,
|
||||
video,
|
||||
hid,
|
||||
audio,
|
||||
).await {
|
||||
error!("Failed to handle relay request: {}", e);
|
||||
}
|
||||
});
|
||||
}));
|
||||
// Fall back to relay
|
||||
if let Err(e) = handle_relay_request(
|
||||
&rendezvous_addr,
|
||||
&relay_server,
|
||||
&uuid,
|
||||
&socket_addr,
|
||||
&device_id,
|
||||
&relay_key,
|
||||
conn_mgr,
|
||||
video,
|
||||
hid,
|
||||
audio,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to handle relay request: {}", e);
|
||||
}
|
||||
});
|
||||
},
|
||||
));
|
||||
|
||||
// Set the relay callback on the mediator
|
||||
mediator.set_relay_callback(Arc::new(move |rendezvous_addr, relay_server, uuid, socket_addr, device_id| {
|
||||
let conn_mgr = connection_manager.clone();
|
||||
let video = video_manager.clone();
|
||||
let hid = hid.clone();
|
||||
let audio = audio.clone();
|
||||
let config = service_config.clone();
|
||||
mediator.set_relay_callback(Arc::new(
|
||||
move |rendezvous_addr, relay_server, uuid, socket_addr, device_id| {
|
||||
let conn_mgr = connection_manager.clone();
|
||||
let video = video_manager.clone();
|
||||
let hid = hid.clone();
|
||||
let audio = audio.clone();
|
||||
let config = service_config.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Get relay_key from config (no public server fallback)
|
||||
let relay_key = {
|
||||
let cfg = config.read();
|
||||
cfg.relay_key.clone().unwrap_or_default()
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
// Get relay_key from config (no public server fallback)
|
||||
let relay_key = {
|
||||
let cfg = config.read();
|
||||
cfg.relay_key.clone().unwrap_or_default()
|
||||
};
|
||||
|
||||
if let Err(e) = handle_relay_request(
|
||||
&rendezvous_addr,
|
||||
&relay_server,
|
||||
&uuid,
|
||||
&socket_addr,
|
||||
&device_id,
|
||||
&relay_key,
|
||||
conn_mgr,
|
||||
video,
|
||||
hid,
|
||||
audio,
|
||||
).await {
|
||||
error!("Failed to handle relay request: {}", e);
|
||||
}
|
||||
});
|
||||
}));
|
||||
if let Err(e) = handle_relay_request(
|
||||
&rendezvous_addr,
|
||||
&relay_server,
|
||||
&uuid,
|
||||
&socket_addr,
|
||||
&device_id,
|
||||
&relay_key,
|
||||
conn_mgr,
|
||||
video,
|
||||
hid,
|
||||
audio,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to handle relay request: {}", e);
|
||||
}
|
||||
});
|
||||
},
|
||||
));
|
||||
|
||||
// Set the intranet callback on the mediator for same-LAN connections
|
||||
let connection_manager2 = self.connection_manager.clone();
|
||||
mediator.set_intranet_callback(Arc::new(move |rendezvous_addr, peer_socket_addr, local_addr, relay_server, device_id| {
|
||||
let conn_mgr = connection_manager2.clone();
|
||||
mediator.set_intranet_callback(Arc::new(
|
||||
move |rendezvous_addr, peer_socket_addr, local_addr, relay_server, device_id| {
|
||||
let conn_mgr = connection_manager2.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_intranet_request(
|
||||
&rendezvous_addr,
|
||||
&peer_socket_addr,
|
||||
local_addr,
|
||||
&relay_server,
|
||||
&device_id,
|
||||
conn_mgr,
|
||||
).await {
|
||||
error!("Failed to handle intranet request: {}", e);
|
||||
}
|
||||
});
|
||||
}));
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_intranet_request(
|
||||
&rendezvous_addr,
|
||||
&peer_socket_addr,
|
||||
local_addr,
|
||||
&relay_server,
|
||||
&device_id,
|
||||
conn_mgr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to handle intranet request: {}", e);
|
||||
}
|
||||
});
|
||||
},
|
||||
));
|
||||
|
||||
// Spawn rendezvous task
|
||||
let status = self.status.clone();
|
||||
@@ -471,7 +484,9 @@ impl RustDeskService {
|
||||
// Save signing keypair (Ed25519)
|
||||
let signing_pk = skp.public_key_base64();
|
||||
let signing_sk = skp.secret_key_base64();
|
||||
if config.signing_public_key.as_ref() != Some(&signing_pk) || config.signing_private_key.as_ref() != Some(&signing_sk) {
|
||||
if config.signing_public_key.as_ref() != Some(&signing_pk)
|
||||
|| config.signing_private_key.as_ref() != Some(&signing_sk)
|
||||
{
|
||||
config.signing_public_key = Some(signing_pk);
|
||||
config.signing_private_key = Some(signing_sk);
|
||||
changed = true;
|
||||
@@ -522,13 +537,18 @@ async fn handle_relay_request(
|
||||
_hid: Arc<HidController>,
|
||||
_audio: Arc<AudioController>,
|
||||
) -> anyhow::Result<()> {
|
||||
info!("Handling relay request: rendezvous={}, relay={}, uuid={}", rendezvous_addr, relay_server, uuid);
|
||||
info!(
|
||||
"Handling relay request: rendezvous={}, relay={}, uuid={}",
|
||||
rendezvous_addr, relay_server, uuid
|
||||
);
|
||||
|
||||
// Step 1: Connect to RENDEZVOUS server and send RelayResponse
|
||||
let rendezvous_socket_addr: SocketAddr = tokio::net::lookup_host(rendezvous_addr)
|
||||
.await?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to resolve rendezvous server: {}", rendezvous_addr))?;
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Failed to resolve rendezvous server: {}", rendezvous_addr)
|
||||
})?;
|
||||
|
||||
let mut rendezvous_stream = tokio::time::timeout(
|
||||
Duration::from_millis(RELAY_CONNECT_TIMEOUT_MS),
|
||||
@@ -537,12 +557,17 @@ async fn handle_relay_request(
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Rendezvous connection timeout"))??;
|
||||
|
||||
debug!("Connected to rendezvous server at {}", rendezvous_socket_addr);
|
||||
debug!(
|
||||
"Connected to rendezvous server at {}",
|
||||
rendezvous_socket_addr
|
||||
);
|
||||
|
||||
// Send RelayResponse to rendezvous server with client's socket_addr
|
||||
// IMPORTANT: Include our device ID so rendezvous server can look up and sign our public key
|
||||
let relay_response = make_relay_response(uuid, socket_addr, relay_server, device_id);
|
||||
let bytes = relay_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
let bytes = relay_response
|
||||
.write_to_bytes()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
bytes_codec::write_frame(&mut rendezvous_stream, &bytes).await?;
|
||||
debug!("Sent RelayResponse to rendezvous server for uuid={}", uuid);
|
||||
|
||||
@@ -568,7 +593,9 @@ async fn handle_relay_request(
|
||||
// The licence_key is required if the relay server is configured with -k option
|
||||
// The socket_addr is CRITICAL - the relay server uses it to match us with the peer
|
||||
let request_relay = make_request_relay(uuid, relay_key, socket_addr);
|
||||
let bytes = request_relay.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
let bytes = request_relay
|
||||
.write_to_bytes()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
bytes_codec::write_frame(&mut stream, &bytes).await?;
|
||||
debug!("Sent RequestRelay to relay server for uuid={}", uuid);
|
||||
|
||||
@@ -576,8 +603,13 @@ async fn handle_relay_request(
|
||||
let peer_addr = rendezvous::AddrMangle::decode(socket_addr).unwrap_or(relay_addr);
|
||||
|
||||
// Step 3: Accept connection - relay server bridges the connection
|
||||
connection_manager.accept_connection(stream, peer_addr).await?;
|
||||
info!("Relay connection established for uuid={}, peer={}", uuid, peer_addr);
|
||||
connection_manager
|
||||
.accept_connection(stream, peer_addr)
|
||||
.await?;
|
||||
info!(
|
||||
"Relay connection established for uuid={}, peer={}",
|
||||
uuid, peer_addr
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -608,14 +640,15 @@ async fn handle_intranet_request(
|
||||
debug!("Peer address from FetchLocalAddr: {:?}", peer_addr);
|
||||
|
||||
// Connect to rendezvous server via TCP with timeout
|
||||
let mut stream = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
TcpStream::connect(rendezvous_addr),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Timeout connecting to rendezvous server"))??;
|
||||
let mut stream =
|
||||
tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(rendezvous_addr))
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Timeout connecting to rendezvous server"))??;
|
||||
|
||||
info!("Connected to rendezvous server for intranet: {}", rendezvous_addr);
|
||||
info!(
|
||||
"Connected to rendezvous server for intranet: {}",
|
||||
rendezvous_addr
|
||||
);
|
||||
|
||||
// Build LocalAddr message with our local address (mangled)
|
||||
let local_addr_bytes = AddrMangle::encode(local_addr);
|
||||
@@ -626,7 +659,9 @@ async fn handle_intranet_request(
|
||||
device_id,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
let bytes = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
let bytes = msg
|
||||
.write_to_bytes()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
|
||||
// Send LocalAddr using RustDesk's variable-length framing
|
||||
bytes_codec::write_frame(&mut stream, &bytes).await?;
|
||||
@@ -640,11 +675,15 @@ async fn handle_intranet_request(
|
||||
// Get peer address for logging/connection tracking
|
||||
let effective_peer_addr = peer_addr.unwrap_or_else(|| {
|
||||
// If we can't decode the peer address, use the rendezvous server address
|
||||
rendezvous_addr.parse().unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap())
|
||||
rendezvous_addr
|
||||
.parse()
|
||||
.unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap())
|
||||
});
|
||||
|
||||
// Accept the connection - the stream is now a proxied connection to the client
|
||||
connection_manager.accept_connection(stream, effective_peer_addr).await?;
|
||||
connection_manager
|
||||
.accept_connection(stream, effective_peer_addr)
|
||||
.await?;
|
||||
info!("Intranet connection established via rendezvous server proxy");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -14,22 +14,20 @@ pub mod hbb {
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use hbb::rendezvous::{
|
||||
rendezvous_message, relay_response, punch_hole_response,
|
||||
ConnType, ConfigUpdate, FetchLocalAddr, HealthCheck, KeyExchange, LocalAddr, NatType,
|
||||
OnlineRequest, OnlineResponse, PeerDiscovery, PunchHole, PunchHoleRequest, PunchHoleResponse,
|
||||
PunchHoleSent, RegisterPeer, RegisterPeerResponse, RegisterPk, RegisterPkResponse,
|
||||
RelayResponse, RendezvousMessage, RequestRelay, SoftwareUpdate, TestNatRequest,
|
||||
TestNatResponse,
|
||||
punch_hole_response, relay_response, rendezvous_message, ConfigUpdate, ConnType,
|
||||
FetchLocalAddr, HealthCheck, KeyExchange, LocalAddr, NatType, OnlineRequest, OnlineResponse,
|
||||
PeerDiscovery, PunchHole, PunchHoleRequest, PunchHoleResponse, PunchHoleSent, RegisterPeer,
|
||||
RegisterPeerResponse, RegisterPk, RegisterPkResponse, RelayResponse, RendezvousMessage,
|
||||
RequestRelay, SoftwareUpdate, TestNatRequest, TestNatResponse,
|
||||
};
|
||||
|
||||
// Re-export message.proto types
|
||||
pub use hbb::message::{
|
||||
message, misc, login_response, key_event,
|
||||
AudioFormat, AudioFrame, Auth2FA, Clipboard, CursorData, CursorPosition, EncodedVideoFrame,
|
||||
EncodedVideoFrames, Hash, IdPk, KeyEvent, LoginRequest, LoginResponse, MouseEvent, Misc,
|
||||
OptionMessage, PeerInfo, PublicKey, SignedId, SupportedDecoding, VideoFrame, TestDelay,
|
||||
Features, SupportedResolutions, WindowsSessions, Message as HbbMessage, ControlKey,
|
||||
DisplayInfo, SupportedEncoding,
|
||||
key_event, login_response, message, misc, AudioFormat, AudioFrame, Auth2FA, Clipboard,
|
||||
ControlKey, CursorData, CursorPosition, DisplayInfo, EncodedVideoFrame, EncodedVideoFrames,
|
||||
Features, Hash, IdPk, KeyEvent, LoginRequest, LoginResponse, Message as HbbMessage, Misc,
|
||||
MouseEvent, OptionMessage, PeerInfo, PublicKey, SignedId, SupportedDecoding, SupportedEncoding,
|
||||
SupportedResolutions, TestDelay, VideoFrame, WindowsSessions,
|
||||
};
|
||||
|
||||
/// Helper to create a RendezvousMessage with RegisterPeer
|
||||
@@ -80,7 +78,12 @@ pub fn make_punch_hole_sent(
|
||||
/// IMPORTANT: The union field should be `Id` (our device ID), NOT `Pk`.
|
||||
/// The rendezvous server will look up our registered public key using this ID,
|
||||
/// sign it with the server's private key, and set the `pk` field before forwarding to client.
|
||||
pub fn make_relay_response(uuid: &str, socket_addr: &[u8], relay_server: &str, device_id: &str) -> RendezvousMessage {
|
||||
pub fn make_relay_response(
|
||||
uuid: &str,
|
||||
socket_addr: &[u8],
|
||||
relay_server: &str,
|
||||
device_id: &str,
|
||||
) -> RendezvousMessage {
|
||||
let mut rr = RelayResponse::new();
|
||||
rr.socket_addr = socket_addr.to_vec().into();
|
||||
rr.uuid = uuid.to_string();
|
||||
|
||||
@@ -69,10 +69,7 @@ impl PunchHoleHandler {
|
||||
///
|
||||
/// Tries direct connection first, falls back to relay if needed.
|
||||
/// Returns true if direct connection succeeded, false if relay is needed.
|
||||
pub async fn handle_punch_hole(
|
||||
&self,
|
||||
peer_addr: Option<SocketAddr>,
|
||||
) -> bool {
|
||||
pub async fn handle_punch_hole(&self, peer_addr: Option<SocketAddr>) -> bool {
|
||||
let peer_addr = match peer_addr {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
@@ -84,7 +81,11 @@ impl PunchHoleHandler {
|
||||
match try_direct_connection(peer_addr).await {
|
||||
PunchResult::DirectConnection(stream) => {
|
||||
// Direct connection succeeded, accept it
|
||||
match self.connection_manager.accept_connection(stream, peer_addr).await {
|
||||
match self
|
||||
.connection_manager
|
||||
.accept_connection(stream, peer_addr)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("P2P direct connection established with {}", peer_addr);
|
||||
true
|
||||
|
||||
@@ -18,8 +18,8 @@ use tracing::{debug, error, info, warn};
|
||||
use super::config::RustDeskConfig;
|
||||
use super::crypto::{KeyPair, SigningKeyPair};
|
||||
use super::protocol::{
|
||||
rendezvous_message, make_punch_hole_sent, make_register_peer,
|
||||
make_register_pk, NatType, RendezvousMessage, decode_rendezvous_message,
|
||||
decode_rendezvous_message, make_punch_hole_sent, make_register_peer, make_register_pk,
|
||||
rendezvous_message, NatType, RendezvousMessage,
|
||||
};
|
||||
|
||||
/// Registration interval in milliseconds
|
||||
@@ -81,7 +81,8 @@ pub type RelayCallback = Arc<dyn Fn(String, String, String, Vec<u8>, String) + S
|
||||
/// Callback type for P2P punch hole requests
|
||||
/// Parameters: peer_addr (decoded), relay_callback_params (rendezvous_addr, relay_server, uuid, socket_addr, device_id)
|
||||
/// Returns: should call relay callback if P2P fails
|
||||
pub type PunchCallback = Arc<dyn Fn(Option<SocketAddr>, String, String, String, Vec<u8>, String) + Send + Sync>;
|
||||
pub type PunchCallback =
|
||||
Arc<dyn Fn(Option<SocketAddr>, String, String, String, Vec<u8>, String) + Send + Sync>;
|
||||
|
||||
/// Callback type for intranet/local address connections
|
||||
/// Parameters: rendezvous_addr, peer_socket_addr (mangled), local_addr, relay_server, device_id
|
||||
@@ -232,7 +233,8 @@ impl RendezvousMediator {
|
||||
if signing_guard.is_none() {
|
||||
let config = self.config.read();
|
||||
// Try to load from config first
|
||||
if let (Some(pk), Some(sk)) = (&config.signing_public_key, &config.signing_private_key) {
|
||||
if let (Some(pk), Some(sk)) = (&config.signing_public_key, &config.signing_private_key)
|
||||
{
|
||||
if let Ok(skp) = SigningKeyPair::from_base64(pk, sk) {
|
||||
debug!("Loaded signing keypair from config");
|
||||
*signing_guard = Some(skp.clone());
|
||||
@@ -265,14 +267,20 @@ impl RendezvousMediator {
|
||||
config.enabled, effective_server
|
||||
);
|
||||
if !config.enabled || effective_server.is_empty() {
|
||||
info!("Rendezvous mediator not starting: enabled={}, server='{}'", config.enabled, effective_server);
|
||||
info!(
|
||||
"Rendezvous mediator not starting: enabled={}, server='{}'",
|
||||
config.enabled, effective_server
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
*self.status.write() = RendezvousStatus::Connecting;
|
||||
|
||||
let addr = config.rendezvous_addr();
|
||||
info!("Starting rendezvous mediator for {} to {}", config.device_id, addr);
|
||||
info!(
|
||||
"Starting rendezvous mediator for {} to {}",
|
||||
config.device_id, addr
|
||||
);
|
||||
|
||||
// Resolve server address
|
||||
let server_addr: SocketAddr = tokio::net::lookup_host(&addr)
|
||||
@@ -376,7 +384,9 @@ impl RendezvousMediator {
|
||||
let serial = *self.serial.read();
|
||||
|
||||
let msg = make_register_peer(&id, serial);
|
||||
let bytes = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
let bytes = msg
|
||||
.write_to_bytes()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
socket.send(&bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -393,7 +403,9 @@ impl RendezvousMediator {
|
||||
|
||||
debug!("Sending RegisterPk: id={}", id);
|
||||
let msg = make_register_pk(&id, &uuid, pk, "");
|
||||
let bytes = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
let bytes = msg
|
||||
.write_to_bytes()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||
socket.send(&bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -540,7 +552,7 @@ impl RendezvousMediator {
|
||||
);
|
||||
|
||||
let msg = make_punch_hole_sent(
|
||||
&ph.socket_addr.to_vec(), // Use peer's socket_addr, not ours
|
||||
&ph.socket_addr.to_vec(), // Use peer's socket_addr, not ours
|
||||
&id,
|
||||
&ph.relay_server,
|
||||
ph.nat_type.enum_value().unwrap_or(NatType::UNKNOWN_NAT),
|
||||
@@ -570,9 +582,22 @@ impl RendezvousMediator {
|
||||
// Use punch callback if set (tries P2P first, then relay)
|
||||
// Otherwise fall back to relay callback directly
|
||||
if let Some(callback) = self.punch_callback.read().as_ref() {
|
||||
callback(peer_addr, rendezvous_addr, relay_server, uuid, ph.socket_addr.to_vec(), device_id);
|
||||
callback(
|
||||
peer_addr,
|
||||
rendezvous_addr,
|
||||
relay_server,
|
||||
uuid,
|
||||
ph.socket_addr.to_vec(),
|
||||
device_id,
|
||||
);
|
||||
} else if let Some(callback) = self.relay_callback.read().as_ref() {
|
||||
callback(rendezvous_addr, relay_server, uuid, ph.socket_addr.to_vec(), device_id);
|
||||
callback(
|
||||
rendezvous_addr,
|
||||
relay_server,
|
||||
uuid,
|
||||
ph.socket_addr.to_vec(),
|
||||
device_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,7 +616,13 @@ impl RendezvousMediator {
|
||||
let config = self.config.read().clone();
|
||||
let rendezvous_addr = config.rendezvous_addr();
|
||||
let device_id = config.device_id.clone();
|
||||
callback(rendezvous_addr, relay_server, rr.uuid.clone(), rr.socket_addr.to_vec(), device_id);
|
||||
callback(
|
||||
rendezvous_addr,
|
||||
relay_server,
|
||||
rr.uuid.clone(),
|
||||
rr.socket_addr.to_vec(),
|
||||
device_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(rendezvous_message::Union::FetchLocalAddr(fla)) => {
|
||||
@@ -602,7 +633,8 @@ impl RendezvousMediator {
|
||||
peer_addr, fla.socket_addr.len(), fla.relay_server
|
||||
);
|
||||
// Respond with our local address for same-LAN direct connection
|
||||
self.send_local_addr(socket, &fla.socket_addr, &fla.relay_server).await?;
|
||||
self.send_local_addr(socket, &fla.socket_addr, &fla.relay_server)
|
||||
.await?;
|
||||
}
|
||||
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
|
||||
info!("Received ConfigureUpdate, serial={}", cu.serial);
|
||||
|
||||
Reference in New Issue
Block a user