mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-27 02:34:56 +03:00
fix(ipc): harden local IPC authorization and portable-service bootstrap flow (#14671)
* fix(ipc): harden ipc access Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): full cmd path, comments, simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): portable service, ipc exit Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Remove unused logs Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Use SetEntriesInAclW instead of icacls Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): check is_reparse_point Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): shmem name, no fallback Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): better exit and clear Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): portable service, better exit Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): comments, id -u Signed-off-by: fufesou <linlong1266@gmail.com> * fix: comments linux headless, rx desktop ready Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): magic number Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): update deps Signed-off-by: fufesou <linlong1266@gmail.com> * Update Cargo.lock * Update Cargo.lock * fix(ipc): harden ipc, test `identity_unavailable` Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): portable service, check dir of shmem Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): macos, better check exe allowed Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): update hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): update hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): harden ipc, better active uid for uinput Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): harden portable service token validation Compare portable service IPC tokens in constant time and document the CSPRNG source used for one-time token generation. Clarify Windows IPC authorization comments around canonical path matching and partial peer identity lookup. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): harden portable service token handling Generate the portable service IPC token directly from OsRng, keep token comparison in the IPC layer as a fixed-length byte-wise check, and document the malformed-frame behavior for protected service IPC. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): comments Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
@@ -22,8 +22,6 @@ use crate::{
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
|
||||
use cidr_utils::cidr::IpCidr;
|
||||
#[cfg(target_os = "linux")]
|
||||
use hbb_common::platform::linux::run_cmds;
|
||||
#[cfg(target_os = "android")]
|
||||
use hbb_common::protobuf::EnumOrUnknown;
|
||||
use hbb_common::{
|
||||
@@ -4983,6 +4981,9 @@ pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
// IPC bootstrap summary:
|
||||
// - Resolve target CM socket (headless/non-headless, optional UID-scoped path on Linux).
|
||||
// - Start CM when missing, then bridge bidirectional messages between this task and CM IPC.
|
||||
async fn start_ipc(
|
||||
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
|
||||
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
|
||||
@@ -4997,10 +4998,19 @@ async fn start_ipc(
|
||||
}
|
||||
sleep(1.).await;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
let headless_cm = crate::is_server()
|
||||
&& crate::platform::is_headless_allowed()
|
||||
&& linux_desktop_manager::is_headless();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let headless_cm = false;
|
||||
let mut stream = None;
|
||||
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
|
||||
stream = Some(s);
|
||||
} else {
|
||||
if !headless_cm {
|
||||
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
|
||||
stream = Some(s);
|
||||
}
|
||||
}
|
||||
if stream.is_none() {
|
||||
#[allow(unused_mut)]
|
||||
#[allow(unused_assignments)]
|
||||
let mut args = vec!["--cm"];
|
||||
@@ -5010,75 +5020,123 @@ async fn start_ipc(
|
||||
|
||||
// Cm run as user, wait until desktop session is ready.
|
||||
#[cfg(target_os = "linux")]
|
||||
if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() {
|
||||
if headless_cm {
|
||||
let mut username = linux_desktop_manager::get_username();
|
||||
loop {
|
||||
if !username.is_empty() {
|
||||
break;
|
||||
}
|
||||
// `_rx_desktop_ready` is used as a wake-up signal from desktop/session state changes
|
||||
// (for example wait_desktop_cm_ready paths). It is not itself a proof of CM readiness.
|
||||
// TODO:
|
||||
// When `_rx_desktop_ready` is closed, `recv()` returns
|
||||
// `None` immediately and this loop may spin if `username` remains empty.
|
||||
// Keep behavior unchanged for now; if field reports appear, handle `Ok(None)` by
|
||||
// breaking/returning to avoid hot-looping.
|
||||
let _res = timeout(1_000, _rx_desktop_ready.recv()).await;
|
||||
username = linux_desktop_manager::get_username();
|
||||
}
|
||||
let uid = {
|
||||
let output = run_cmds(&format!("id -u {}", &username))?;
|
||||
let username_for_cmd = username.clone();
|
||||
let mut uid_cmd = hbb_common::tokio::process::Command::new("id");
|
||||
// TODO:
|
||||
// Keep current behavior for now to minimize change risk.
|
||||
// If usernames starting with '-' are observed in the field, prefer:
|
||||
// `id -u -- <username>` to avoid option-parsing ambiguity.
|
||||
// Already verified that `id -u -- <username>` works as expected on macOS and Ubuntu 24.04.
|
||||
uid_cmd.arg("-u").arg(&username_for_cmd).kill_on_drop(true);
|
||||
let output = timeout(10_000, uid_cmd.output())
|
||||
.await
|
||||
.map_err(|_| anyhow!("Timed out querying uid for {}", username))?
|
||||
.map_err(|e| anyhow!("Failed to run `id -u {}`: {}", username, e))?;
|
||||
if !output.status.success() {
|
||||
bail!("Failed to query uid for {}", username);
|
||||
}
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
let output = output.trim();
|
||||
if output.is_empty() || !output.parse::<i32>().is_ok() {
|
||||
bail!("Invalid username {}", &username);
|
||||
if output.parse::<u32>().is_err() {
|
||||
bail!("Invalid uid {}", output);
|
||||
}
|
||||
output.to_string()
|
||||
};
|
||||
user = Some((uid, username));
|
||||
args = vec!["--cm-no-ui"];
|
||||
}
|
||||
let run_done;
|
||||
if crate::platform::is_root() {
|
||||
let mut res = Ok(None);
|
||||
for _ in 0..10 {
|
||||
#[cfg(not(any(target_os = "linux")))]
|
||||
{
|
||||
log::debug!("Start cm");
|
||||
res = crate::platform::run_as_user(args.clone());
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
log::debug!("Start cm");
|
||||
res = crate::platform::run_as_user(
|
||||
args.clone(),
|
||||
user.clone(),
|
||||
None::<(&str, &str)>,
|
||||
);
|
||||
}
|
||||
if res.is_ok() {
|
||||
break;
|
||||
}
|
||||
log::error!("Failed to run cm: {res:?}");
|
||||
sleep(1.).await;
|
||||
}
|
||||
if let Some(task) = res? {
|
||||
super::CHILD_PROCESS.lock().unwrap().push(task);
|
||||
}
|
||||
run_done = true;
|
||||
} else {
|
||||
run_done = false;
|
||||
}
|
||||
if !run_done {
|
||||
log::debug!("Start cm");
|
||||
super::CHILD_PROCESS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(crate::run_me(args)?);
|
||||
}
|
||||
for _ in 0..20 {
|
||||
sleep(0.3).await;
|
||||
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
|
||||
#[cfg(target_os = "linux")]
|
||||
let cm_uid: Option<u32> = match &user {
|
||||
Some((uid, _)) => Some(
|
||||
uid.parse::<u32>()
|
||||
.map_err(|_| anyhow!("Invalid uid {}", uid))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(uid) = cm_uid {
|
||||
if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await {
|
||||
stream = Some(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if stream.is_none() {
|
||||
bail!("Failed to connect to connection manager");
|
||||
let run_done;
|
||||
if crate::platform::is_root() {
|
||||
let mut res = Ok(None);
|
||||
for _ in 0..10 {
|
||||
#[cfg(not(any(target_os = "linux")))]
|
||||
{
|
||||
log::debug!("Start cm");
|
||||
res = crate::platform::run_as_user(args.clone());
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
log::debug!("Start cm");
|
||||
res = crate::platform::run_as_user(
|
||||
args.clone(),
|
||||
user.clone(),
|
||||
None::<(&str, &str)>,
|
||||
);
|
||||
}
|
||||
if res.is_ok() {
|
||||
break;
|
||||
}
|
||||
log::error!("Failed to run cm: {res:?}");
|
||||
sleep(1.).await;
|
||||
}
|
||||
if let Some(task) = res? {
|
||||
super::CHILD_PROCESS.lock().unwrap().push(task);
|
||||
}
|
||||
run_done = true;
|
||||
} else {
|
||||
run_done = false;
|
||||
}
|
||||
if !run_done {
|
||||
log::debug!("Start cm");
|
||||
super::CHILD_PROCESS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(crate::run_me(args)?);
|
||||
}
|
||||
for _ in 0..20 {
|
||||
sleep(0.3).await;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(uid) = cm_uid {
|
||||
if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await {
|
||||
stream = Some(s);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
|
||||
stream = Some(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if stream.is_none() {
|
||||
bail!("Failed to connect to connection manager");
|
||||
}
|
||||
|
||||
let _res = tx_stream_ready.send(()).await;
|
||||
let mut stream = stream.ok_or(anyhow!("none stream"))?;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
use crate::{
|
||||
ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN},
|
||||
platform::{
|
||||
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
|
||||
set_path_permission_for_portable_service_shmem_file,
|
||||
validate_path_for_portable_service_shmem_dir,
|
||||
},
|
||||
};
|
||||
use core::slice;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
@@ -15,26 +23,26 @@ use shared_memory::*;
|
||||
use std::{
|
||||
mem::size_of,
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use winapi::{
|
||||
shared::minwindef::{BOOL, FALSE, TRUE},
|
||||
um::winuser::{self, CURSORINFO, PCURSORINFO},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ipc::{self, new_listener, Connection, Data, DataPortableService},
|
||||
platform::set_path_permission,
|
||||
};
|
||||
use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ};
|
||||
|
||||
use super::video_qos;
|
||||
|
||||
const SIZE_COUNTER: usize = size_of::<i32>() * 2;
|
||||
const FRAME_ALIGN: usize = 64;
|
||||
|
||||
const ADDR_CURSOR_PARA: usize = 0;
|
||||
const ADDR_IPC_TOKEN: usize = 0;
|
||||
const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN;
|
||||
const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::<CURSORINFO>();
|
||||
|
||||
const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER;
|
||||
@@ -44,12 +52,186 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of::<i3
|
||||
|
||||
const ADDR_CAPTURE_FRAME: usize =
|
||||
(ADDR_CAPTURE_FRAME_COUNTER + SIZE_COUNTER + FRAME_ALIGN - 1) / FRAME_ALIGN * FRAME_ALIGN;
|
||||
const MIN_RUNTIME_SHMEM_LEN: usize = ADDR_CAPTURE_FRAME + FRAME_ALIGN;
|
||||
|
||||
const IPC_SUFFIX: &str = "_portable_service";
|
||||
pub const SHMEM_NAME: &str = "_portable_service";
|
||||
pub const SHMEM_ARG_PREFIX: &str = "--portable-service-shmem-name=";
|
||||
const SHMEM_PARENT_DIR: &str = "portable_service_shmem";
|
||||
const SHMEM_NAME_MAX_LEN: usize = 64;
|
||||
const MAX_NACK: usize = 3;
|
||||
const PORTABLE_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const MAX_DXGI_FAIL_TIME: usize = 5;
|
||||
|
||||
#[inline]
|
||||
fn is_valid_portable_service_shmem_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name.len() <= SHMEM_NAME_MAX_LEN
|
||||
&& name
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn portable_service_shmem_arg(name: &str) -> String {
|
||||
format!("{SHMEM_ARG_PREFIX}{name}")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_valid_portable_service_ipc_token(token: &str) -> bool {
|
||||
token.len() == IPC_TOKEN_LEN
|
||||
&& token
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option<String> {
|
||||
if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN {
|
||||
log::error!(
|
||||
"Portable service shared memory too small: len={}, need>={}",
|
||||
shmem.len(),
|
||||
ADDR_IPC_TOKEN + IPC_TOKEN_LEN
|
||||
);
|
||||
return None;
|
||||
}
|
||||
unsafe {
|
||||
let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN);
|
||||
let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN);
|
||||
let end = bytes
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.unwrap_or(IPC_TOKEN_LEN);
|
||||
if end == 0 {
|
||||
return None;
|
||||
}
|
||||
let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned();
|
||||
if is_valid_portable_service_ipc_token(&token) {
|
||||
Some(token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> {
|
||||
if shmem.len() < MIN_RUNTIME_SHMEM_LEN {
|
||||
bail!(
|
||||
"Portable service shared memory too small for runtime layout: len={}, need>={}",
|
||||
shmem.len(),
|
||||
MIN_RUNTIME_SHMEM_LEN
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool {
|
||||
let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME);
|
||||
frame_len > 0 && frame_len <= frame_capacity
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn shared_memory_flink_path_by_name(name: &str) -> ResultType<PathBuf> {
|
||||
let mut dir = crate::platform::user_accessible_folder()?;
|
||||
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
|
||||
dir = dir.join(SHMEM_PARENT_DIR);
|
||||
Ok(dir.join(format!("shared_memory{}", name)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool {
|
||||
let flink = match shared_memory_flink_path_by_name(name) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
if log_on_error {
|
||||
log::warn!(
|
||||
"{} failed to resolve portable service shared-memory flink path for '{}': {}",
|
||||
log_context,
|
||||
name,
|
||||
err
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match std::fs::remove_file(&flink) {
|
||||
Ok(()) => {
|
||||
log::info!(
|
||||
"{} removed portable service shared-memory flink artifact: {:?}",
|
||||
log_context,
|
||||
flink
|
||||
);
|
||||
true
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
|
||||
Err(err) => {
|
||||
if log_on_error {
|
||||
log::warn!(
|
||||
"{} failed to remove portable service shared-memory flink artifact {:?}: {}",
|
||||
log_context,
|
||||
flink,
|
||||
err
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> {
|
||||
if !is_valid_portable_service_ipc_token(token) {
|
||||
bail!("Invalid portable service ipc token");
|
||||
}
|
||||
shmem.write(ADDR_IPC_TOKEN, token.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_ipc_token_in_shmem(shmem: &SharedMemory) {
|
||||
shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn portable_service_arg_value_candidate_from_arg<'a>(
|
||||
arg: &'a str,
|
||||
prefix: &str,
|
||||
) -> Option<&'a str> {
|
||||
let mut value = arg.strip_prefix(prefix)?;
|
||||
value = value.trim_start();
|
||||
value = value
|
||||
.strip_prefix('"')
|
||||
.or_else(|| value.strip_prefix('\''))
|
||||
.unwrap_or(value);
|
||||
value = value.split_whitespace().next().unwrap_or_default();
|
||||
value = value.trim_matches(|c| c == '"' || c == '\'');
|
||||
Some(value)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn portable_service_shmem_name_from_args() -> Option<String> {
|
||||
for arg in std::env::args() {
|
||||
if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) {
|
||||
if is_valid_portable_service_shmem_name(value) {
|
||||
return Some(value.to_owned());
|
||||
}
|
||||
log::error!(
|
||||
"Invalid portable service shared memory name argument: '{}'",
|
||||
value
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_portable_service_shmem_arg() -> bool {
|
||||
std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX))
|
||||
}
|
||||
|
||||
pub struct SharedMemory {
|
||||
inner: Shmem,
|
||||
}
|
||||
@@ -92,7 +274,27 @@ impl SharedMemory {
|
||||
}
|
||||
};
|
||||
log::info!("Create shared memory, size: {}, flink: {}", size, flink);
|
||||
set_path_permission(Path::new(&flink), "F").ok();
|
||||
if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) {
|
||||
// Release shmem handle first so best-effort flink cleanup has a chance to succeed.
|
||||
drop(shmem);
|
||||
match std::fs::remove_file(&flink) {
|
||||
Ok(()) => {
|
||||
log::info!(
|
||||
"Create cleanup removed portable service shared-memory flink artifact: {}",
|
||||
flink
|
||||
);
|
||||
}
|
||||
Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(remove_err) => {
|
||||
log::warn!(
|
||||
"Create cleanup failed to remove portable service shared-memory flink artifact {}: {}",
|
||||
flink,
|
||||
remove_err
|
||||
);
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
Ok(SharedMemory { inner: shmem })
|
||||
}
|
||||
|
||||
@@ -120,9 +322,18 @@ impl SharedMemory {
|
||||
fn flink(name: String) -> ResultType<String> {
|
||||
let mut dir = crate::platform::user_accessible_folder()?;
|
||||
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir(&dir)?;
|
||||
set_path_permission(&dir, "F").ok();
|
||||
dir = dir.join(SHMEM_PARENT_DIR);
|
||||
let parent_created = !dir.exists();
|
||||
if parent_created {
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
if parent_created || crate::platform::is_root() {
|
||||
// Harden parent ACL on first provisioning and periodically on SYSTEM path.
|
||||
set_path_permission_for_portable_service_shmem_dir(&dir)?;
|
||||
} else {
|
||||
// Existing parents still need type/reparse validation. Non-SYSTEM callers may lack
|
||||
// WRITE_DAC on a valid parent, so avoid rebuilding the ACL here.
|
||||
validate_path_for_portable_service_shmem_dir(&dir)?;
|
||||
}
|
||||
Ok(dir
|
||||
.join(format!("shared_memory{}", name))
|
||||
@@ -232,16 +443,45 @@ pub mod server {
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref EXIT: Arc<Mutex<bool>> = Default::default();
|
||||
static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
pub fn run_portable_service() {
|
||||
let shmem = match SharedMemory::open_existing(SHMEM_NAME) {
|
||||
let shmem_name = match portable_service_shmem_name_from_args() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
if has_portable_service_shmem_arg() {
|
||||
log::error!(
|
||||
"Invalid portable service shared memory argument, aborting startup"
|
||||
);
|
||||
} else {
|
||||
log::error!(
|
||||
"Missing portable service shared memory argument, aborting startup"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shmem = match SharedMemory::open_existing(&shmem_name) {
|
||||
Ok(shmem) => Arc::new(shmem),
|
||||
Err(e) => {
|
||||
log::error!("Failed to open existing shared memory: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) {
|
||||
log::error!("{}", e);
|
||||
return;
|
||||
}
|
||||
let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
log::error!(
|
||||
"Missing portable service ipc token in shared memory, aborting startup"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shmem1 = shmem.clone();
|
||||
let shmem2 = shmem.clone();
|
||||
let mut threads = vec![];
|
||||
@@ -251,17 +491,24 @@ pub mod server {
|
||||
threads.push(std::thread::spawn(|| {
|
||||
run_capture(shmem2);
|
||||
}));
|
||||
threads.push(std::thread::spawn(|| {
|
||||
run_ipc_client();
|
||||
threads.push(std::thread::spawn(move || {
|
||||
run_ipc_client(ipc_token);
|
||||
}));
|
||||
threads.push(std::thread::spawn(|| {
|
||||
// Detached shutdown watchdog:
|
||||
// - gives graceful shutdown/cleanup a short window
|
||||
// - force-exits the process if workers are still stuck
|
||||
std::thread::spawn(|| {
|
||||
run_exit_check();
|
||||
}));
|
||||
});
|
||||
let record_pos_handle = crate::input_service::try_start_record_cursor_pos();
|
||||
// Arm forced-exit watchdog only for worker join phase.
|
||||
// Once join phase completes, cleanup should not be interrupted by forced exit.
|
||||
FORCE_EXIT_ARMED.store(true, Ordering::SeqCst);
|
||||
for th in threads.drain(..) {
|
||||
th.join().ok();
|
||||
log::info!("thread joined");
|
||||
}
|
||||
FORCE_EXIT_ARMED.store(false, Ordering::SeqCst);
|
||||
|
||||
crate::input_service::try_stop_record_cursor_pos();
|
||||
if let Some(handle) = record_pos_handle {
|
||||
@@ -270,16 +517,47 @@ pub mod server {
|
||||
Err(e) => log::error!("record_pos_handle join error {:?}", &e),
|
||||
}
|
||||
}
|
||||
drop(shmem);
|
||||
remove_shared_memory_flink_with_retry(&shmem_name);
|
||||
}
|
||||
|
||||
fn run_exit_check() {
|
||||
const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3);
|
||||
loop {
|
||||
if EXIT.lock().unwrap().clone() {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
std::process::exit(0);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
// Fallback only: normal shutdown path should complete and process should exit naturally.
|
||||
// This forced exit is a last resort when worker threads are stuck and graceful teardown
|
||||
// does not finish in time.
|
||||
std::thread::sleep(FORCED_EXIT_DELAY);
|
||||
if FORCE_EXIT_ARMED.load(Ordering::SeqCst) {
|
||||
log::warn!(
|
||||
"Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}",
|
||||
FORCED_EXIT_DELAY
|
||||
);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_shared_memory_flink_with_retry(name: &str) {
|
||||
const MAX_RETRY: usize = 20;
|
||||
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
|
||||
for attempt in 0..MAX_RETRY {
|
||||
let is_last_attempt = attempt + 1 == MAX_RETRY;
|
||||
if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") {
|
||||
return;
|
||||
}
|
||||
if !is_last_attempt {
|
||||
std::thread::sleep(RETRY_INTERVAL);
|
||||
}
|
||||
}
|
||||
log::warn!(
|
||||
"SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
fn run_get_cursor_info(shmem: Arc<SharedMemory>) {
|
||||
@@ -386,6 +664,17 @@ pub mod server {
|
||||
match c.as_mut().map(|f| f.frame(spf)) {
|
||||
Some(Ok(f)) => match f {
|
||||
Frame::PixelBuffer(f) => {
|
||||
let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME);
|
||||
if f.data().len() > frame_capacity {
|
||||
log::error!(
|
||||
"Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}",
|
||||
f.data().len(),
|
||||
frame_capacity,
|
||||
shmem.len()
|
||||
);
|
||||
*EXIT.lock().unwrap() = true;
|
||||
return;
|
||||
}
|
||||
utils::set_frame_info(
|
||||
&shmem,
|
||||
FrameInfo {
|
||||
@@ -436,17 +725,33 @@ pub mod server {
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn run_ipc_client() {
|
||||
async fn run_ipc_client(ipc_token: String) {
|
||||
use DataPortableService::*;
|
||||
|
||||
let postfix = IPC_SUFFIX;
|
||||
|
||||
match ipc::connect(1000, postfix).await {
|
||||
Ok(mut stream) => {
|
||||
if let Err(err) =
|
||||
ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await
|
||||
{
|
||||
log::error!("portable service ipc handshake failed: {}", err);
|
||||
*EXIT.lock().unwrap() = true;
|
||||
return;
|
||||
}
|
||||
let mut timer =
|
||||
crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
|
||||
let mut nack = 0;
|
||||
loop {
|
||||
if *EXIT.lock().unwrap() {
|
||||
log::info!("Portable service EXIT signaled, closing ipc client loop");
|
||||
stream
|
||||
.send(&Data::DataPortableService(WillClose))
|
||||
.await
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
res = stream.next() => {
|
||||
match res {
|
||||
@@ -526,7 +831,11 @@ pub mod client {
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RUNNING: Arc<Mutex<bool>> = Default::default();
|
||||
static ref STARTING: Arc<Mutex<bool>> = Default::default();
|
||||
static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0);
|
||||
static ref SHMEM: Arc<Mutex<Option<SharedMemory>>> = Default::default();
|
||||
static ref SHMEM_RUNTIME_NAME: Arc<Mutex<Option<String>>> = Default::default();
|
||||
static ref IPC_RUNTIME_TOKEN: Arc<Mutex<Option<String>>> = Default::default();
|
||||
static ref SENDER : Mutex<mpsc::UnboundedSender<ipc::Data>> = Mutex::new(client::start_ipc_server());
|
||||
static ref QUICK_SUPPORT: Arc<Mutex<bool>> = Default::default();
|
||||
}
|
||||
@@ -536,12 +845,176 @@ pub mod client {
|
||||
Logon(String, String),
|
||||
}
|
||||
|
||||
fn has_running_portable_service_process() -> bool {
|
||||
let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase());
|
||||
!crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service")
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn next_portable_service_shmem_name() -> String {
|
||||
format!(
|
||||
"{}_{}_{:08x}",
|
||||
crate::portable_service::SHMEM_NAME,
|
||||
std::process::id(),
|
||||
hbb_common::rand::random::<u32>()
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_runtime_ipc_token(token: String) {
|
||||
*IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn schedule_remove_runtime_shmem_flink_retry(name: String) {
|
||||
std::thread::spawn(move || {
|
||||
const MAX_RETRY: usize = 20;
|
||||
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
|
||||
for _ in 0..MAX_RETRY {
|
||||
std::thread::sleep(RETRY_INTERVAL);
|
||||
if remove_shared_memory_flink_once(&name, false, "Client cleanup") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
log::warn!(
|
||||
"Failed to remove portable service shared-memory flink artifact '{}' after retry",
|
||||
name
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_runtime_shmem_state() {
|
||||
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
|
||||
let mut shmem_lock = SHMEM.lock().unwrap();
|
||||
if let Some(shmem) = shmem_lock.as_mut() {
|
||||
clear_ipc_token_in_shmem(shmem);
|
||||
}
|
||||
*shmem_lock = None;
|
||||
let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take();
|
||||
*runtime_token = None;
|
||||
drop(runtime_token);
|
||||
drop(shmem_lock);
|
||||
if let Some(name) = runtime_name.as_deref() {
|
||||
if !remove_shared_memory_flink_once(name, true, "Client cleanup") {
|
||||
schedule_remove_runtime_shmem_flink_retry(name.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option<String>) {
|
||||
let mut token = IPC_RUNTIME_TOKEN.lock().unwrap();
|
||||
if !token
|
||||
.as_deref()
|
||||
.is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate))
|
||||
{
|
||||
return (false, None);
|
||||
}
|
||||
let mut shmem_lock = SHMEM.lock().unwrap();
|
||||
let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
|
||||
*token = None;
|
||||
if let Some(shmem) = shmem_lock.as_mut() {
|
||||
clear_ipc_token_in_shmem(shmem);
|
||||
}
|
||||
(true, matched_shmem_name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn restore_runtime_ipc_token_after_failed_handshake(
|
||||
token: &str,
|
||||
expected_shmem_name: Option<&str>,
|
||||
) {
|
||||
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
|
||||
if let Some(current) = runtime_token.as_deref() {
|
||||
if current != token {
|
||||
log::debug!(
|
||||
"Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let mut shmem_lock = SHMEM.lock().unwrap();
|
||||
let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
|
||||
if current_shmem_name.as_deref() != expected_shmem_name {
|
||||
if runtime_token.as_deref() == Some(token) {
|
||||
*runtime_token = None;
|
||||
}
|
||||
log::debug!(
|
||||
"Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() {
|
||||
write_ipc_token_to_shmem(shmem, token)
|
||||
.err()
|
||||
.map(|err| err.to_string())
|
||||
} else {
|
||||
Some("shared memory unavailable".to_owned())
|
||||
};
|
||||
if let Some(err) = shmem_write_error {
|
||||
if runtime_token.as_deref() == Some(token) {
|
||||
*runtime_token = None;
|
||||
}
|
||||
log::warn!(
|
||||
"Failed to restore portable service ipc token after handshake failure: {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
*runtime_token = Some(token.to_owned());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn schedule_starting_timeout_reset(launch_token: u64) {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT);
|
||||
let should_reset = {
|
||||
// Guard against stale watchdogs from previous launches:
|
||||
// only the watchdog that matches the latest STARTING_TOKEN may reset STARTING.
|
||||
let current_token = STARTING_TOKEN.load(Ordering::SeqCst);
|
||||
// Keep lock guards in explicit short scopes to make it obvious
|
||||
// there is no nested lock ordering (and to avoid Copilot false positives).
|
||||
let starting = { *STARTING.lock().unwrap() };
|
||||
let running = { *RUNNING.lock().unwrap() };
|
||||
current_token == launch_token && starting && !running
|
||||
};
|
||||
if should_reset {
|
||||
log::warn!(
|
||||
"Portable service startup timeout before IPC ready, reset STARTING state"
|
||||
);
|
||||
*STARTING.lock().unwrap() = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Launch flow summary:
|
||||
// 1) Prepare/reset runtime shared memory + IPC token.
|
||||
// 2) Start helper process (direct or logon) with shmem argument.
|
||||
// 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it.
|
||||
pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> {
|
||||
log::info!("start portable service");
|
||||
if RUNNING.lock().unwrap().clone() {
|
||||
bail!("already running");
|
||||
}
|
||||
if SHMEM.lock().unwrap().is_none() {
|
||||
let launch_token = {
|
||||
// Keep lock guards in explicit short scopes to make it obvious
|
||||
// there is no nested lock ordering (and to avoid Copilot false positives).
|
||||
let running = { *RUNNING.lock().unwrap() };
|
||||
let mut starting = STARTING.lock().unwrap();
|
||||
if *starting && !running && !has_running_portable_service_process() {
|
||||
log::warn!(
|
||||
"Detected stale portable service STARTING state without running process, reset it"
|
||||
);
|
||||
*starting = false;
|
||||
}
|
||||
if *starting || running {
|
||||
bail!("already running");
|
||||
}
|
||||
*starting = true;
|
||||
STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1
|
||||
};
|
||||
let start_result = (|| -> ResultType<()> {
|
||||
clear_runtime_shmem_state();
|
||||
let mut shmem_lock = SHMEM.lock().unwrap();
|
||||
let displays = scrap::Display::all()?;
|
||||
if displays.is_empty() {
|
||||
bail!("no display available!");
|
||||
@@ -558,84 +1031,153 @@ pub mod client {
|
||||
}
|
||||
}
|
||||
}
|
||||
let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align);
|
||||
let shmem_size =
|
||||
utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN);
|
||||
let shmem_name = next_portable_service_shmem_name();
|
||||
if !is_valid_portable_service_shmem_name(&shmem_name) {
|
||||
bail!("Generated invalid portable service shared memory name");
|
||||
}
|
||||
let ipc_token = ipc::generate_one_time_ipc_token()?;
|
||||
// os error 112, no enough space
|
||||
*SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create(
|
||||
crate::portable_service::SHMEM_NAME,
|
||||
*shmem_lock = Some(crate::portable_service::SharedMemory::create(
|
||||
&shmem_name,
|
||||
shmem_size,
|
||||
)?);
|
||||
*SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name);
|
||||
shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory);
|
||||
}
|
||||
if let Some(shmem) = SHMEM.lock().unwrap().as_mut() {
|
||||
unsafe {
|
||||
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
|
||||
}
|
||||
}
|
||||
match para {
|
||||
StartPara::Direct => {
|
||||
if let Err(e) = crate::platform::run_background(
|
||||
&std::env::current_exe()?.to_string_lossy().to_string(),
|
||||
"--portable-service",
|
||||
) {
|
||||
*SHMEM.lock().unwrap() = None;
|
||||
bail!("Failed to run portable service process: {}", e);
|
||||
let shmem_name = SHMEM_RUNTIME_NAME
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?;
|
||||
let init_token_result = if let Some(shmem) = shmem_lock.as_mut() {
|
||||
unsafe {
|
||||
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
|
||||
}
|
||||
write_ipc_token_to_shmem(shmem, &ipc_token)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = init_token_result {
|
||||
drop(shmem_lock);
|
||||
clear_runtime_shmem_state();
|
||||
bail!(
|
||||
"Failed to initialize portable service ipc token in shared memory: {}",
|
||||
e
|
||||
);
|
||||
};
|
||||
drop(shmem_lock);
|
||||
set_runtime_ipc_token(ipc_token.clone());
|
||||
let portable_service_arg = format!(
|
||||
"--portable-service {}",
|
||||
crate::portable_service::portable_service_shmem_arg(&shmem_name)
|
||||
);
|
||||
{
|
||||
let _sender = SENDER.lock().unwrap();
|
||||
}
|
||||
StartPara::Logon(username, password) => {
|
||||
#[allow(unused_mut)]
|
||||
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
if let Some(dir) = Path::new(&exe).parent() {
|
||||
if set_path_permission(Path::new(dir), "RX").is_err() {
|
||||
*SHMEM.lock().unwrap() = None;
|
||||
bail!("Failed to set permission of {:?}", dir);
|
||||
match para {
|
||||
StartPara::Direct => {
|
||||
match crate::platform::run_background(
|
||||
&std::env::current_exe()?.to_string_lossy().to_string(),
|
||||
&portable_service_arg,
|
||||
) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
clear_runtime_shmem_state();
|
||||
bail!("Failed to run portable service process");
|
||||
}
|
||||
Err(e) => {
|
||||
clear_runtime_shmem_state();
|
||||
bail!("Failed to run portable service process: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
match hbb_common::directories_next::UserDirs::new() {
|
||||
Some(user_dir) => {
|
||||
let dir = user_dir
|
||||
.home_dir()
|
||||
.join("AppData")
|
||||
.join("Local")
|
||||
.join("rustdesk-sciter");
|
||||
if std::fs::create_dir_all(&dir).is_ok() {
|
||||
let dst = dir.join("rustdesk.exe");
|
||||
if std::fs::copy(&exe, &dst).is_ok() {
|
||||
if dst.exists() {
|
||||
if set_path_permission(&dir, "RX").is_ok() {
|
||||
exe = dst.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
StartPara::Logon(username, password) => {
|
||||
#[allow(unused_mut)]
|
||||
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
if let Some(dir) = Path::new(&exe).parent() {
|
||||
if let Err(err) = set_path_permission(
|
||||
Path::new(dir),
|
||||
FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0,
|
||||
) {
|
||||
clear_runtime_shmem_state();
|
||||
bail!("Failed to set permission of {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Err(e) = crate::platform::windows::create_process_with_logon(
|
||||
username.as_str(),
|
||||
password.as_str(),
|
||||
&exe,
|
||||
"--portable-service",
|
||||
) {
|
||||
*SHMEM.lock().unwrap() = None;
|
||||
bail!("Failed to run portable service process: {}", e);
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
if let Some((dir, dst)) =
|
||||
crate::platform::windows::portable_service_logon_helper_paths()
|
||||
{
|
||||
let cleanup_helper_artifacts = || {
|
||||
if Path::new(&exe) != dst {
|
||||
std::fs::remove_file(&dst).ok();
|
||||
}
|
||||
std::fs::remove_dir(&dir).ok();
|
||||
};
|
||||
let mut use_logon_helper_exe = false;
|
||||
if let Err(err) = std::fs::create_dir_all(&dir) {
|
||||
log::warn!(
|
||||
"Failed to create portable service logon helper dir {:?}: {}",
|
||||
dir,
|
||||
err
|
||||
);
|
||||
} else if let Err(err) = std::fs::copy(&exe, &dst) {
|
||||
log::warn!(
|
||||
"Failed to copy portable service logon helper binary from '{}' to {:?}: {}",
|
||||
exe,
|
||||
dst,
|
||||
err
|
||||
);
|
||||
cleanup_helper_artifacts();
|
||||
} else if !dst.exists() {
|
||||
log::warn!(
|
||||
"Portable service logon helper binary missing after copy: {:?}",
|
||||
dst
|
||||
);
|
||||
cleanup_helper_artifacts();
|
||||
} else if let Err(err) =
|
||||
set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0)
|
||||
{
|
||||
log::warn!(
|
||||
"Failed to set portable service logon helper path permission for {:?}: {}",
|
||||
dir,
|
||||
err
|
||||
);
|
||||
cleanup_helper_artifacts();
|
||||
} else {
|
||||
use_logon_helper_exe = true;
|
||||
}
|
||||
if use_logon_helper_exe {
|
||||
exe = dst.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
if let Err(e) = crate::platform::windows::create_process_with_logon(
|
||||
username.as_str(),
|
||||
password.as_str(),
|
||||
&exe,
|
||||
&portable_service_arg,
|
||||
) {
|
||||
clear_runtime_shmem_state();
|
||||
bail!("Failed to run portable service process: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
schedule_starting_timeout_reset(launch_token);
|
||||
Ok(())
|
||||
})();
|
||||
if start_result.is_err() {
|
||||
*STARTING.lock().unwrap() = false;
|
||||
}
|
||||
let _sender = SENDER.lock().unwrap();
|
||||
Ok(())
|
||||
start_result
|
||||
}
|
||||
|
||||
pub extern "C" fn drop_portable_service_shared_memory() {
|
||||
// https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout
|
||||
// Please make sure there is no print in the call stack
|
||||
let mut lock = SHMEM.lock().unwrap();
|
||||
if lock.is_some() {
|
||||
*lock = None;
|
||||
}
|
||||
clear_runtime_shmem_state();
|
||||
}
|
||||
|
||||
pub fn set_quick_support(v: bool) {
|
||||
@@ -655,7 +1197,11 @@ pub mod client {
|
||||
let mut option = SHMEM.lock().unwrap();
|
||||
if let Some(shmem) = option.as_mut() {
|
||||
unsafe {
|
||||
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
|
||||
libc::memset(
|
||||
shmem.as_ptr().add(ADDR_CURSOR_PARA) as _,
|
||||
0,
|
||||
shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _,
|
||||
);
|
||||
}
|
||||
utils::set_para(
|
||||
shmem,
|
||||
@@ -702,6 +1248,19 @@ pub mod client {
|
||||
if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) {
|
||||
let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO);
|
||||
let frame_info = frame_info_ptr as *const FrameInfo;
|
||||
let frame_len = (*frame_info).length;
|
||||
if !is_valid_capture_frame_length(shmem.len(), frame_len) {
|
||||
log::error!(
|
||||
"Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}",
|
||||
frame_len,
|
||||
shmem.len(),
|
||||
ADDR_CAPTURE_FRAME
|
||||
);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"invalid portable service frame length".to_string(),
|
||||
));
|
||||
}
|
||||
if (*frame_info).width != self.width || (*frame_info).height != self.height {
|
||||
log::info!(
|
||||
"skip frame, ({},{}) != ({},{})",
|
||||
@@ -716,7 +1275,7 @@ pub mod client {
|
||||
));
|
||||
}
|
||||
let frame_ptr = base.add(ADDR_CAPTURE_FRAME);
|
||||
let data = slice::from_raw_parts(frame_ptr, (*frame_info).length);
|
||||
let data = slice::from_raw_parts(frame_ptr, frame_len);
|
||||
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
|
||||
data,
|
||||
self.width,
|
||||
@@ -778,10 +1337,49 @@ pub mod client {
|
||||
Some(result) = incoming.next() => {
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let mut stream = Connection::new(stream);
|
||||
if !ipc::authorize_windows_portable_service_ipc_connection(
|
||||
&stream, postfix,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let mut consumed_token: Option<String> = None;
|
||||
let mut consumed_token_shmem_name: Option<String> = None;
|
||||
let handshake_result =
|
||||
ipc::portable_service_ipc_handshake_as_server(
|
||||
&mut stream,
|
||||
|token| {
|
||||
let (matched, matched_shmem_name) =
|
||||
consume_runtime_ipc_token_if_match(token);
|
||||
if matched {
|
||||
consumed_token = Some(token.to_owned());
|
||||
consumed_token_shmem_name = matched_shmem_name;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = handshake_result {
|
||||
if let Some(token) = consumed_token.as_deref() {
|
||||
restore_runtime_ipc_token_after_failed_handshake(
|
||||
token,
|
||||
consumed_token_shmem_name.as_deref(),
|
||||
);
|
||||
*STARTING.lock().unwrap() = false;
|
||||
}
|
||||
log::warn!(
|
||||
"Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}",
|
||||
postfix,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
log::info!("Got portable service ipc connection");
|
||||
let rx_clone = rx.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut stream = Connection::new(stream);
|
||||
let mut stream = stream;
|
||||
let postfix = postfix.to_owned();
|
||||
let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
|
||||
let mut nack = 0;
|
||||
@@ -805,6 +1403,7 @@ pub mod client {
|
||||
Pong => {
|
||||
nack = 0;
|
||||
*RUNNING.lock().unwrap() = true;
|
||||
*STARTING.lock().unwrap() = false;
|
||||
},
|
||||
ConnCount(None) => {
|
||||
if !quick_support {
|
||||
@@ -841,6 +1440,7 @@ pub mod client {
|
||||
}
|
||||
}
|
||||
*RUNNING.lock().unwrap() = false;
|
||||
*STARTING.lock().unwrap() = false;
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -990,3 +1590,23 @@ pub struct FrameInfo {
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME};
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_capture_frame_length_rejects_zero_length() {
|
||||
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() {
|
||||
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_capture_frame_length_accepts_in_bounds_length() {
|
||||
assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,9 +185,13 @@ pub mod client {
|
||||
pub mod service {
|
||||
use super::*;
|
||||
use hbb_common::lazy_static;
|
||||
#[cfg(target_os = "linux")]
|
||||
use parity_tokio_ipc::Connection as RawIpcConnection;
|
||||
use scrap::wayland::{
|
||||
pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -602,7 +606,10 @@ pub mod service {
|
||||
}
|
||||
DataKeyboard::KeyDown(enigo::Key::Raw(code)) => {
|
||||
if *code < 8 {
|
||||
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
|
||||
log::error!(
|
||||
"Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping",
|
||||
code
|
||||
);
|
||||
} else {
|
||||
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
|
||||
allow_err!(keyboard.emit(&[down_event]));
|
||||
@@ -610,7 +617,10 @@ pub mod service {
|
||||
}
|
||||
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
|
||||
if *code < 8 {
|
||||
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
|
||||
log::error!(
|
||||
"Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping",
|
||||
code
|
||||
);
|
||||
} else {
|
||||
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
|
||||
allow_err!(keyboard.emit(&[up_event]));
|
||||
@@ -909,6 +919,35 @@ pub mod service {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn authorize_uinput_peer(postfix: &str, stream: &RawIpcConnection) -> bool {
|
||||
if !hbb_common::config::is_service_ipc_postfix(postfix) {
|
||||
return true;
|
||||
}
|
||||
let peer_uid = ipc::peer_uid_from_fd(stream.as_raw_fd());
|
||||
let active_uid = crate::platform::linux::get_active_userid_fresh()
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.ok();
|
||||
let authorized =
|
||||
peer_uid.is_some_and(|uid| ipc::is_allowed_service_peer_uid(uid, active_uid));
|
||||
if !authorized {
|
||||
crate::ipc::log_rejected_uinput_connection(postfix, peer_uid, active_uid);
|
||||
return false;
|
||||
}
|
||||
if let Err(err) =
|
||||
ipc::ensure_peer_executable_matches_current_by_fd(stream.as_raw_fd(), postfix)
|
||||
{
|
||||
log::warn!(
|
||||
"Rejected connection on protected uinput ipc channel due to executable mismatch: postfix={}, err={}",
|
||||
postfix,
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Start uinput service.
|
||||
async fn start_service<F: FnOnce(ipc::Connection) + Copy>(postfix: &str, handler: F) {
|
||||
match new_listener(postfix).await {
|
||||
@@ -916,6 +955,10 @@ pub mod service {
|
||||
while let Some(result) = incoming.next().await {
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
if !authorize_uinput_peer(postfix, &stream) {
|
||||
continue;
|
||||
}
|
||||
log::debug!("Got new connection of uinput ipc {}", postfix);
|
||||
handler(Connection::new(stream));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user