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:
fufesou
2026-05-09 18:15:00 +08:00
committed by GitHub
parent 72d27c3c47
commit 9df486a689
12 changed files with 4500 additions and 249 deletions

View File

@@ -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"))?;