mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-13 03:44:51 +03:00
* 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>
1076 lines
36 KiB
Rust
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, ¤t_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(¤t).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, ¤t_exe) {
|
|
return Ok(());
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
if macos_service_ipc_allows_gui_and_service_binaries(&peer_exe, ¤t_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(¤t_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, ¤t_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(¤t_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,
|
|
¤t_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);
|
|
}
|
|
}
|