Files
rustdesk/src/ipc/auth.rs
fufesou bc2c36215d fix(ipc): scope active-user IPC routing to root CLI main requests (#15058)
* fix(ipc): scope active-user IPC routing to root CLI main requests

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): cmdline, comments fails close

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): cmdline, better check

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): cmdline, try active uid when no --server processes

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): cmdline, select active uid

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): remove unused import

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-18 16:32:46 +08:00

1076 lines
36 KiB
Rust

use crate::ipc::{Connection, ConnectionTmpl};
#[cfg(all(windows, not(feature = "flutter")))]
use hbb_common::sha2::{Digest, Sha256};
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
use hbb_common::{anyhow, bail, log, ResultType};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use hbb_common::{
libc,
tokio::io::{AsyncRead, AsyncWrite},
};
#[cfg(windows)]
use parity_tokio_ipc::SecurityAttributes;
#[cfg(windows)]
use std::io;
#[cfg(all(windows, not(feature = "flutter")))]
use std::io::Read;
#[cfg(target_os = "macos")]
use std::os::unix::fs::MetadataExt;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::io::RawFd;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
use std::{
fs,
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
};
#[cfg(windows)]
use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId};
#[cfg(windows)]
#[inline]
pub(crate) fn should_allow_everyone_create_on_windows(postfix: &str) -> bool {
postfix.is_empty() || hbb_common::config::is_service_ipc_postfix(postfix)
}
#[cfg(windows)]
#[inline]
pub(crate) fn portable_service_listener_security_attributes() -> io::Result<SecurityAttributes> {
let user_sid = crate::platform::windows::current_process_user_sid_string().map_err(|err| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to resolve current process SID: {}", err),
)
})?;
debug_assert!(
user_sid.starts_with("S-1-")
&& user_sid
.bytes()
.all(|byte| byte.is_ascii_digit() || byte == b'-'),
"current_process_user_sid_string returned a non-SDDL SID: {}",
user_sid
);
// SDDL:
// - `D:P` => protected DACL (no inherited ACEs)
// - `(A;;GA;;;SY)` => allow GENERIC_ALL to LocalSystem
// - `(A;;GA;;;{user_sid})` => allow GENERIC_ALL to current process user SID
// References:
// - Security Descriptor String Format: https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format
// - ACE strings in SDDL: https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings
let sddl = format!("D:P(A;;GA;;;SY)(A;;GA;;;{user_sid})");
SecurityAttributes::from_sddl(&sddl).map_err(|err| {
io::Error::new(
io::ErrorKind::Other,
format!(
"failed to build portable service listener security attributes from SDDL '{}': {}",
sddl, err
),
)
})
}
#[cfg(target_os = "macos")]
#[inline]
fn macos_service_ipc_allows_gui_and_service_binaries(
peer_exe: &Path,
current_exe: &Path,
postfix: &str,
) -> bool {
if postfix != crate::POSTFIX_SERVICE {
return false;
}
let Some(peer_dir) = peer_exe.parent() else {
return false;
};
let Some(current_dir) = current_exe.parent() else {
return false;
};
if !executable_paths_match(peer_dir, current_dir) {
return false;
}
// On installed macOS builds, `_service` is listened by the `service` binary while the GUI
// process connects from the app executable within the same app bundle.
let gui_exe_name = std::ffi::OsString::from(crate::get_app_name());
let gui_exe = gui_exe_name.as_os_str();
let service_exe = std::ffi::OsStr::new("service");
let allowed_exe = [Some(gui_exe), Some(service_exe)];
let peer_name = peer_exe.file_name();
let current_name = current_exe.file_name();
allowed_exe
.iter()
.any(|name| os_str_eq_ignore_ascii_case(peer_name, *name))
&& allowed_exe
.iter()
.any(|name| os_str_eq_ignore_ascii_case(current_name, *name))
}
#[cfg(target_os = "windows")]
#[inline]
fn windows_portable_service_ipc_allows_logon_helper_executable(
_peer_exe: &Path,
postfix: &str,
) -> bool {
if postfix != "_portable_service" {
return false;
}
#[cfg(feature = "flutter")]
{
false
}
#[cfg(not(feature = "flutter"))]
{
let Some((_, expected)) = crate::platform::windows::portable_service_logon_helper_paths()
else {
return false;
};
let Ok(expected) = fs::canonicalize(expected) else {
return false;
};
let Ok(current_exe) = current_exe_canonical_path() else {
return false;
};
portable_service_helper_is_trusted(_peer_exe, &expected, &current_exe)
}
}
#[cfg(windows)]
#[inline]
pub(crate) fn is_allowed_windows_session_scoped_peer(
client_is_system: bool,
client_session_id: Option<u32>,
expected_session_id: Option<u32>,
) -> bool {
client_is_system
|| matches!(
(client_session_id, expected_session_id),
(Some(client), Some(expected)) if client == expected
)
}
#[cfg(windows)]
#[inline]
fn is_allowed_windows_portable_service_peer(
client_is_system: Option<bool>,
_client_session_id: Option<u32>,
_expected_session_id: Option<u32>,
) -> bool {
// Portable-service listener DACL includes SYSTEM and current-process SID.
// In the portable-service path, current process is expected to run as SYSTEM,
// and the higher-layer peer policy stays SYSTEM-only.
matches!(client_is_system, Some(true))
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[inline]
pub(crate) fn is_allowed_service_peer_uid(peer_uid: u32, active_uid: Option<u32>) -> bool {
// Root is allowed at the UID gate because the service side may run as root.
// Callers still enforce executable matching before accepting service-scoped peers.
peer_uid == 0 || active_uid.is_some_and(|uid| uid == peer_uid)
}
#[cfg(target_os = "macos")]
#[inline]
fn console_owner_uid() -> Option<u32> {
fs::metadata("/dev/console")
.ok()
.map(|metadata| metadata.uid())
}
#[cfg(target_os = "macos")]
#[inline]
fn active_uid_strict() -> Option<u32> {
// Prefer the filesystem metadata over parsing external command output.
console_owner_uid()
}
#[cfg(target_os = "linux")]
#[inline]
fn active_uid_strict() -> Option<u32> {
let reported_uid_raw = crate::platform::linux::get_active_userid();
let trimmed = reported_uid_raw.trim();
if let Ok(uid) = trimmed.parse::<u32>() {
return Some(uid);
}
if trimmed.is_empty() {
log::debug!("Failed to resolve active user uid on linux: active uid is empty");
} else {
log::warn!("Failed to parse active user uid on linux: '{}'", trimmed);
}
None
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
pub(crate) fn active_uid() -> Option<u32> {
active_uid_strict()
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
pub(crate) fn peer_uid_from_fd(fd: RawFd) -> Option<u32> {
#[cfg(target_os = "linux")]
{
return peer_cred_from_fd(fd).map(|cred| cred.uid as u32);
}
#[cfg(target_os = "macos")]
{
let mut uid = 0;
let mut gid = 0;
if unsafe { libc::getpeereid(fd, &mut uid, &mut gid) } == 0 {
Some(uid as u32)
} else {
None
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn peer_pid_from_fd(fd: RawFd) -> Option<u32> {
#[cfg(target_os = "linux")]
{
return peer_cred_from_fd(fd).and_then(|cred| (cred.pid > 0).then_some(cred.pid as u32));
}
#[cfg(target_os = "macos")]
{
let mut pid = 0;
let mut len = std::mem::size_of::<libc::pid_t>() as _;
let rc = unsafe {
libc::getsockopt(
fd,
libc::SOL_LOCAL,
libc::LOCAL_PEERPID,
&mut pid as *mut _ as *mut libc::c_void,
&mut len,
)
};
if rc == 0 && pid > 0 {
Some(pid as _)
} else {
None
}
}
}
#[cfg(target_os = "linux")]
#[inline]
fn peer_cred_from_fd(fd: RawFd) -> Option<libc::ucred> {
let mut cred: libc::ucred = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::ucred>() as _;
let rc = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut cred as *mut _ as *mut libc::c_void,
&mut len,
)
};
if rc == 0 {
Some(cred)
} else {
None
}
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[inline]
fn current_exe_canonical_path() -> ResultType<PathBuf> {
let current = std::env::current_exe()
.map_err(|err| anyhow::anyhow!("Failed to resolve current executable path: {}", err))?;
fs::canonicalize(&current).map_err(|err| {
anyhow::anyhow!(
"Failed to canonicalize current executable path '{}': {}",
current.display(),
err
)
.into()
})
}
#[cfg(target_os = "linux")]
#[inline]
fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType<PathBuf> {
let proc_exe = PathBuf::from(format!("/proc/{peer_pid}/exe"));
let peer_exe = fs::read_link(&proc_exe).map_err(|err| {
anyhow::anyhow!(
"Failed to read peer executable link '{}': {}",
proc_exe.display(),
err
)
})?;
fs::canonicalize(&peer_exe).map_err(|err| {
anyhow::anyhow!(
"Failed to canonicalize peer executable path '{}': {}",
peer_exe.display(),
err
)
.into()
})
}
#[cfg(target_os = "macos")]
#[inline]
fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType<PathBuf> {
const PROC_PIDPATH_BUF_SIZE: usize = libc::PROC_PIDPATHINFO_MAXSIZE as _;
let mut buffer = vec![0u8; PROC_PIDPATH_BUF_SIZE];
let length = unsafe {
libc::proc_pidpath(
peer_pid as _,
buffer.as_mut_ptr() as _,
PROC_PIDPATH_BUF_SIZE as _,
)
};
if length <= 0 {
bail!("Failed to query peer process path from pid {}", peer_pid);
}
buffer.truncate(length as _);
let path = PathBuf::from(String::from_utf8_lossy(&buffer).to_string());
fs::canonicalize(&path).map_err(|err| {
anyhow::anyhow!(
"Failed to canonicalize peer executable path '{}': {}",
path.display(),
err
)
.into()
})
}
#[cfg(target_os = "windows")]
#[inline]
fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType<PathBuf> {
let path = crate::platform::windows::get_process_executable_path(peer_pid)?;
fs::canonicalize(&path).map_err(|err| {
anyhow::anyhow!(
"Failed to canonicalize peer executable path '{}': {}",
path.display(),
err
)
.into()
})
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[inline]
pub(crate) fn executable_paths_match(left: &Path, right: &Path) -> bool {
#[cfg(target_os = "windows")]
{
// Callers pass paths resolved through fs::canonicalize() first, so NT
// namespace paths and 8.3 short names are expected to be resolved before
// this check. Keep this normalization limited to remaining Win32 spelling
// differences.
fn normalize(path: &Path) -> String {
let mut normalized = path.to_string_lossy().replace('/', "\\");
if let Some(stripped) = normalized.strip_prefix(r"\\?\") {
normalized = stripped.to_owned();
}
normalized.to_ascii_lowercase()
}
return normalize(left) == normalize(right);
}
#[cfg(target_os = "macos")]
{
return paths_refer_to_same_file(left, right);
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
left == right
}
}
#[cfg(target_os = "macos")]
#[inline]
fn paths_refer_to_same_file(left: &Path, right: &Path) -> bool {
if left == right {
return true;
}
let (Ok(left), Ok(right)) = (fs::metadata(left), fs::metadata(right)) else {
return false;
};
left.dev() == right.dev() && left.ino() == right.ino()
}
#[cfg(target_os = "macos")]
#[inline]
fn os_str_eq_ignore_ascii_case(
left: Option<&std::ffi::OsStr>,
right: Option<&std::ffi::OsStr>,
) -> bool {
let (Some(left), Some(right)) = (left, right) else {
return false;
};
left.to_string_lossy()
.eq_ignore_ascii_case(&right.to_string_lossy())
}
#[cfg(all(windows, not(feature = "flutter")))]
#[inline]
fn file_sha256(path: &Path) -> ResultType<[u8; 32]> {
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8 * 1024];
loop {
let read_bytes = file.read(&mut buffer)?;
if read_bytes == 0 {
break;
}
hasher.update(&buffer[..read_bytes]);
}
Ok(hasher.finalize().into())
}
#[cfg(all(windows, not(feature = "flutter")))]
#[inline]
fn portable_service_helper_is_trusted(
peer_exe: &Path,
expected_exe: &Path,
current_exe: &Path,
) -> bool {
if !executable_paths_match(peer_exe, expected_exe) {
return false;
}
let peer_hash = match file_sha256(peer_exe) {
Ok(hash) => hash,
Err(err) => {
log::warn!(
"Failed to hash peer portable helper executable '{}': {}",
peer_exe.display(),
err
);
return false;
}
};
let current_hash = match file_sha256(current_exe) {
Ok(hash) => hash,
Err(err) => {
log::warn!(
"Failed to hash current executable '{}' for portable helper trust check: {}",
current_exe.display(),
err
);
return false;
}
};
peer_hash == current_hash
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[inline]
fn ensure_peer_executable_matches_current_by_pid(peer_pid: u32, postfix: &str) -> ResultType<()> {
let peer_exe = peer_exe_canonical_path_by_pid(peer_pid)?;
let current_exe = current_exe_canonical_path()?;
if executable_paths_match(&peer_exe, &current_exe) {
return Ok(());
}
#[cfg(target_os = "macos")]
if macos_service_ipc_allows_gui_and_service_binaries(&peer_exe, &current_exe, postfix) {
return Ok(());
}
#[cfg(target_os = "windows")]
if windows_portable_service_ipc_allows_logon_helper_executable(&peer_exe, postfix) {
return Ok(());
}
bail!(
"Peer executable path mismatch on ipc channel '{}': peer_pid={}, peer_exe='{}', current_exe='{}'",
postfix,
peer_pid,
peer_exe.display(),
current_exe.display()
);
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[inline]
pub(crate) fn ensure_peer_executable_matches_current_by_pid_opt(
peer_pid: Option<u32>,
postfix: &str,
) -> ResultType<()> {
let peer_pid = peer_pid.ok_or_else(|| {
anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix)
})?;
ensure_peer_executable_matches_current_by_pid(peer_pid, postfix)
}
#[cfg(target_os = "linux")]
#[inline]
pub(crate) fn ensure_peer_executable_matches_current_by_fd(
fd: RawFd,
postfix: &str,
) -> ResultType<()> {
let peer_pid = peer_pid_from_fd(fd).ok_or_else(|| {
anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix)
})?;
ensure_peer_executable_matches_current_by_pid(peer_pid, postfix)
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
const UNAUTHORIZED_IPC_LOG_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
#[derive(Default)]
struct UnauthorizedIpcLogThrottle {
last_log_at: Option<std::time::Instant>,
suppressed: u64,
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
impl UnauthorizedIpcLogThrottle {
#[inline]
fn on_reject(&mut self, now: std::time::Instant) -> Option<u64> {
if let Some(last) = self.last_log_at {
if now.saturating_duration_since(last) < UNAUTHORIZED_IPC_LOG_INTERVAL {
self.suppressed += 1;
return None;
}
}
self.last_log_at = Some(now);
Some(std::mem::take(&mut self.suppressed))
}
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
#[inline]
fn throttled_unauthorized_ipc_log(
throttle_cell: &OnceLock<Mutex<UnauthorizedIpcLogThrottle>>,
emit: impl FnOnce(u64),
) {
let throttle = throttle_cell.get_or_init(|| Mutex::new(UnauthorizedIpcLogThrottle::default()));
let should_log = match throttle.lock() {
Ok(mut throttle) => throttle.on_reject(std::time::Instant::now()),
Err(_) => Some(0),
};
if let Some(suppressed) = should_log {
emit(suppressed);
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn log_rejected_service_connection(postfix: &str, peer_uid: Option<u32>, active_uid: Option<u32>) {
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
if suppressed > 0 {
log::warn!(
"Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)",
postfix,
peer_uid,
active_uid,
suppressed
);
} else {
log::warn!(
"Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?}",
postfix,
peer_uid,
active_uid
);
}
});
}
#[cfg(target_os = "linux")]
#[inline]
pub(crate) fn log_rejected_uinput_connection(
postfix: &str,
peer_uid: Option<u32>,
active_uid: Option<u32>,
) {
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
if suppressed > 0 {
log::warn!(
"Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)",
postfix,
peer_uid,
active_uid,
suppressed
);
} else {
log::warn!(
"Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?}",
postfix,
peer_uid,
active_uid
);
}
});
}
#[cfg(windows)]
#[inline]
pub(crate) fn log_rejected_windows_ipc_connection(
postfix: &str,
peer_pid: Option<u32>,
peer_session_id: Option<u32>,
expected_session_id: Option<u32>,
peer_is_system: Option<bool>,
peer_is_elevated: Option<bool>,
) {
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
if suppressed > 0 {
log::warn!(
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)",
postfix,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system,
peer_is_elevated,
suppressed
);
} else {
log::warn!(
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}",
postfix,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system,
peer_is_elevated
);
}
});
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postfix: &str) -> bool {
let peer_pid = stream.peer_pid();
let (authorized, peer_uid, active_uid) = stream.service_authorization_status();
if !authorized {
log_rejected_service_connection(postfix, peer_uid, active_uid);
return false;
}
if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) {
log::warn!(
"Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}",
postfix,
peer_pid,
err
);
return false;
}
true
}
#[cfg(windows)]
pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool {
let (
authorized,
peer_pid,
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
) = stream.server_authorization_status();
if !authorized {
log_rejected_windows_ipc_connection(
postfix,
peer_pid,
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
);
return false;
}
if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) {
log::warn!(
"Rejected unauthorized connection on ipc channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}",
postfix,
peer_pid,
err
);
return false;
}
true
}
#[cfg(windows)]
pub(crate) fn authorize_windows_portable_service_ipc_connection(
stream: &Connection,
postfix: &str,
) -> bool {
// Portable service IPC policy:
// - only SYSTEM peers are authorized by is_allowed_windows_portable_service_peer()
// - expected_session_id is still collected for diagnostics and identity checks
// - final privilege boundary is enforced by named-pipe ACL + one-time token handshake
// - when peer identity is unavailable on some hosts, executable verification remains
// best-effort telemetry (not fail-closed) to avoid breaking valid SYSTEM bootstrap
// flows that cannot be fully introspected
let expected_session_id = crate::platform::windows::get_current_process_session_id();
let (authorized, peer_pid, peer_session_id, peer_is_system) =
stream.portable_service_authorization_status_for_session(expected_session_id);
if !authorized {
// Session lookup may succeed while SYSTEM identity lookup fails, so only the
// SYSTEM identity result determines whether peer identity is unavailable here.
// Don't use `peer_pid.is_some() && peer_session_id.is_none() && peer_is_system.is_none();` here.
let identity_unavailable = peer_pid.is_some() && peer_is_system.is_none();
if identity_unavailable {
// In portable-service startup, resolving SYSTEM peer identity may fail on some hosts.
// `ProcessIdToSessionId` can still succeed while `OpenProcessToken(TOKEN_QUERY)` is
// denied by the peer token DACL or missing privileges. Treat that partial identity
// failure as unavailable and defer final authorization to pipe ACL + token handshake.
if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) {
log::warn!(
"Portable service ipc peer identity unavailable and executable verification failed; continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, err={}",
postfix,
peer_pid,
err
);
} else {
log::warn!(
"Portable service ipc peer identity unavailable; executable verification matched, continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, expected_session_id={:?}",
postfix,
peer_pid,
expected_session_id
);
}
return true;
}
log::warn!(
"Rejected unauthorized connection on portable service ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}",
postfix,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system
);
return false;
}
true
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl<T> ConnectionTmpl<T>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin + std::os::unix::io::AsRawFd,
{
pub(super) fn peer_uid(&self) -> Option<u32> {
peer_uid_from_fd(self.inner.get_ref().as_raw_fd())
}
fn service_authorization_status(&self) -> (bool, Option<u32>, Option<u32>) {
let peer_uid = self.peer_uid();
// On Linux, `_service` can use the cached active UID from the service loop for
// stable config sync. Uinput does a fresh active-UID lookup in its own authorizer.
let active_uid = active_uid();
let authorized = peer_uid.is_some_and(|uid| is_allowed_service_peer_uid(uid, active_uid));
(authorized, peer_uid, active_uid)
}
pub(super) fn peer_pid(&self) -> Option<u32> {
peer_pid_from_fd(self.inner.get_ref().as_raw_fd())
}
}
#[cfg(windows)]
impl ConnectionTmpl<parity_tokio_ipc::Connection> {
fn peer_pid(&self) -> Option<u32> {
let pipe_handle = self.inner.get_ref().as_raw_handle();
if pipe_handle.is_null() {
return None;
}
let mut pid = 0u32;
let ok = unsafe { GetNamedPipeClientProcessId(HANDLE(pipe_handle), &mut pid as *mut u32) }
.is_ok();
if ok && pid != 0 {
Some(pid)
} else {
None
}
}
fn server_authorization_status(
&self,
) -> (
bool,
Option<u32>,
Option<u32>,
Option<u32>,
Option<bool>,
Option<bool>,
) {
let peer_pid = self.peer_pid();
let server_session_id = crate::platform::windows::get_current_process_session_id();
let peer_session_id =
peer_pid.and_then(crate::platform::windows::get_session_id_of_process);
let peer_is_system_result =
peer_pid.map(crate::platform::windows::is_process_running_as_system);
let peer_is_system = peer_is_system_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
let session_authorized = is_allowed_windows_session_scoped_peer(
peer_is_system.unwrap_or(false),
peer_session_id,
server_session_id,
);
let peer_is_elevated_result = if session_authorized {
None
} else {
peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid)))
};
let peer_is_elevated = peer_is_elevated_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
if server_session_id.is_none()
&& !peer_is_system.unwrap_or(false)
&& !peer_is_elevated.unwrap_or(false)
{
// When the server session id cannot be determined, the session-id allow-path is
// disabled and only privileged peers can be authorized.
log::debug!(
"IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}",
peer_pid,
peer_session_id
);
}
// Main IPC trusts same-session peers, LocalSystem, and elevated administrators.
// Service-scoped IPC channels keep their own stricter authorization paths.
let authorized = session_authorized || peer_is_elevated.unwrap_or(false);
if !authorized {
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
log::debug!(
"Failed to determine whether peer process is SYSTEM, pid={}, err={}",
pid,
err
);
}
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) {
log::debug!(
"Failed to determine whether peer process is elevated, pid={}, err={}",
pid,
err
);
}
}
(
authorized,
peer_pid,
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
)
}
pub(crate) fn service_authorization_status_for_session(
&self,
expected_active_session_id: Option<u32>,
) -> (bool, Option<u32>, Option<u32>, Option<bool>) {
let peer_pid = self.peer_pid();
let peer_session_id =
peer_pid.and_then(crate::platform::windows::get_session_id_of_process);
let peer_is_system_result =
peer_pid.map(crate::platform::windows::is_process_running_as_system);
let peer_is_system = peer_is_system_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
let authorized = is_allowed_windows_session_scoped_peer(
peer_is_system.unwrap_or(false),
peer_session_id,
expected_active_session_id,
);
if !authorized {
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
log::debug!(
"Failed to determine whether peer process is SYSTEM, pid={}, err={}",
pid,
err
);
}
}
(authorized, peer_pid, peer_session_id, peer_is_system)
}
pub(crate) fn portable_service_authorization_status_for_session(
&self,
expected_active_session_id: Option<u32>,
) -> (bool, Option<u32>, Option<u32>, Option<bool>) {
// Portable-service policy:
// only SYSTEM peers are allowed.
let (_service_authorized, peer_pid, peer_session_id, peer_is_system) =
self.service_authorization_status_for_session(expected_active_session_id);
(
is_allowed_windows_portable_service_peer(
peer_is_system,
peer_session_id,
expected_active_session_id,
),
peer_pid,
peer_session_id,
peer_is_system,
)
}
}
#[cfg(test)]
mod tests {
#[test]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn test_service_peer_uid_policy() {
assert!(super::is_allowed_service_peer_uid(0, None));
assert!(super::is_allowed_service_peer_uid(501, Some(501)));
assert!(!super::is_allowed_service_peer_uid(502, Some(501)));
assert!(!super::is_allowed_service_peer_uid(501, None));
}
#[test]
#[cfg(windows)]
fn test_windows_server_peer_policy() {
assert!(super::is_allowed_windows_session_scoped_peer(
true, None, None
));
assert!(super::is_allowed_windows_session_scoped_peer(
false,
Some(1),
Some(1)
));
assert!(!super::is_allowed_windows_session_scoped_peer(
false,
Some(1),
Some(2)
));
assert!(!super::is_allowed_windows_session_scoped_peer(
false,
None,
Some(1)
));
}
#[test]
#[cfg(windows)]
fn test_windows_portable_service_peer_policy() {
assert!(super::is_allowed_windows_portable_service_peer(
Some(true),
None,
None
));
assert!(!super::is_allowed_windows_portable_service_peer(
Some(false),
Some(1),
Some(1)
));
assert!(!super::is_allowed_windows_portable_service_peer(
Some(false),
Some(1),
Some(2)
));
assert!(!super::is_allowed_windows_portable_service_peer(
None,
Some(1),
Some(1)
));
}
#[test]
#[cfg(windows)]
fn test_should_allow_everyone_create_on_windows_policy() {
assert!(super::should_allow_everyone_create_on_windows(""));
assert!(super::should_allow_everyone_create_on_windows("_service"));
assert!(!super::should_allow_everyone_create_on_windows(
"_portable_service"
));
}
#[test]
#[cfg(windows)]
fn test_executable_paths_match_windows_normalization() {
let left = std::path::PathBuf::from(r"\\?\C:\Program Files\RustDesk\RustDesk.exe");
let right = std::path::PathBuf::from(r"c:\program files\rustdesk\rustdesk.exe");
assert!(super::executable_paths_match(&left, &right));
}
#[test]
#[cfg(target_os = "macos")]
fn test_os_str_eq_ignore_ascii_case_for_process_names() {
assert!(super::os_str_eq_ignore_ascii_case(
Some(std::ffi::OsStr::new("RustDesk")),
Some(std::ffi::OsStr::new("rustdesk"))
));
assert!(!super::os_str_eq_ignore_ascii_case(
Some(std::ffi::OsStr::new("RustDesk")),
Some(std::ffi::OsStr::new("service"))
));
}
#[cfg(all(windows, not(feature = "flutter")))]
struct TempDirGuard(std::path::PathBuf);
#[cfg(all(windows, not(feature = "flutter")))]
impl Drop for TempDirGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
#[cfg(all(windows, not(feature = "flutter")))]
fn test_portable_service_helper_trust_requires_content_match() {
let unique = format!(
"rustdesk-portable-helper-trust-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let _cleanup = TempDirGuard(base.clone());
let current_exe = base.join("current.exe");
let helper_exe = base.join("helper.exe");
std::fs::write(&current_exe, b"trusted-binary").unwrap();
std::fs::write(&helper_exe, b"tampered-binary").unwrap();
assert!(
!super::portable_service_helper_is_trusted(&helper_exe, &helper_exe, &current_exe),
"helper trust check must reject path-match-only binaries with mismatched content"
);
}
#[test]
#[cfg(all(windows, not(feature = "flutter")))]
fn test_portable_service_helper_trust_accepts_matching_content() {
let unique = format!(
"rustdesk-portable-helper-trust-match-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let _cleanup = TempDirGuard(base.clone());
let current_exe = base.join("current.exe");
let helper_exe = base.join("helper.exe");
std::fs::write(&current_exe, b"trusted-binary").unwrap();
std::fs::write(&helper_exe, b"trusted-binary").unwrap();
assert!(super::portable_service_helper_is_trusted(
&helper_exe,
&helper_exe,
&current_exe
));
}
#[cfg(target_os = "macos")]
#[test]
fn test_console_owner_uid_matches_get_active_userid() {
let console_uid =
super::console_owner_uid().expect("/dev/console must have a resolvable uid");
let raw_uid = crate::platform::macos::get_active_userid();
let parsed_uid: u32 = raw_uid
.trim()
.parse()
.unwrap_or_else(|_| panic!("failed to parse get_active_userid() output: '{raw_uid}'"));
assert_eq!(parsed_uid, console_uid);
}
}