Files
rustdesk/src/ipc.rs
Mariano Abad 5baeb8feb5 drm: bound the _drm body read, stream-scope cursor teardown, refresh a stale verdict, drop dead clear (review 5)
- recv_msg_timeout2 only gated the wait for the first byte, so a peer that sent one
  byte then stalled pinned the task forever. The same budget now also bounds the body
  read; a body that overruns is a hard error that tears the stream down (recv_msg
  bodies are small JSON, so a healthy peer never trips it).
- The cursor cache is keyed by display index, which a rebuilt stream reuses, so a
  predecessor exiting after its replacement published a fresh cursor erased it. Stamp
  each entry with a monotonic per-stream epoch and compare-and-remove on teardown.
- ProbeState::Available had no TTL, so an idle hotplug left a phantom display in
  enumeration. Give it a timestamp and refresh the list off the hot path once it ages
  past POSITIVE_TTL. The verdict stays true across the refresh (never bounces a live
  session to the portal) and the probe runs on a background thread (never blocks the
  async enumeration).
- Remove the dead clear(): it is unreferenced, and wiring it into teardown would force
  the blocking re-probe on the next enumeration that swap_available_displays exists to
  avoid.
2026-07-24 08:50:17 -03:00

3513 lines
142 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#[path = "ipc/auth.rs"]
mod ipc_auth;
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[path = "ipc/fs.rs"]
mod ipc_fs;
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::plugin::ipc::Plugin;
use crate::{
common::{is_server, CheckTestNatType},
privacy_mode,
privacy_mode::PrivacyModeState,
rendezvous_mediator::RendezvousMediator,
ui_interface::{get_local_option, set_local_option},
};
use bytes::Bytes;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use clipboard::ClipboardFile;
#[cfg(target_os = "linux")]
use hbb_common::anyhow;
use hbb_common::{
allow_err, bail, bytes,
bytes_codec::BytesCodec,
config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2},
futures::StreamExt as _,
futures_util::sink::SinkExt,
log, password_security as password, timeout,
tokio::{
self,
io::{AsyncRead, AsyncWrite},
},
tokio_util::codec::Framed,
ResultType,
};
#[cfg(windows)]
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
#[cfg(windows)]
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection};
#[cfg(target_os = "macos")]
use ipc_auth::authorize_user_server_process;
#[cfg(windows)]
use ipc_auth::{
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
should_allow_everyone_create_on_windows,
};
#[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(target_os = "linux")]
use ipc_fs::terminal_count_candidate_uids;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_fs::{
check_pid, ensure_secure_ipc_parent_dir, scrub_secure_ipc_parent_dir,
should_scrub_parent_entries_after_check_pid, write_pid,
};
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
use serde_derive::{Deserialize, Serialize};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::cell::Cell;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::fs::PermissionsExt;
#[cfg(all(target_os = "linux", feature = "drm"))]
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
// IPC actions here.
pub const IPC_ACTION_CLOSE: &str = "close";
#[cfg(target_os = "windows")]
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
#[cfg(target_os = "windows")]
pub(crate) const IPC_TOKEN_LEN: usize = 64;
#[cfg(target_os = "windows")]
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
#[cfg(target_os = "windows")]
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
#[cfg(any(target_os = "linux", target_os = "macos"))]
thread_local! {
static USE_USER_MAIN_IPC: Cell<bool> = Cell::new(false);
}
#[must_use = "bind this guard to a local variable to keep the IPC scope active"]
/// Thread-local guard for routing root main IPC to the active user on Linux/macOS.
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) struct UserMainIpcScope {
previous: bool,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl UserMainIpcScope {
pub(crate) fn new() -> Self {
let previous = USE_USER_MAIN_IPC.with(|use_user_main| {
let previous = use_user_main.get();
use_user_main.set(true);
previous
});
Self { previous }
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Drop for UserMainIpcScope {
fn drop(&mut self) {
USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous));
}
}
#[inline]
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
connect(ms_timeout, crate::POSTFIX_SERVICE).await
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum FS {
ReadEmptyDirs {
dir: String,
include_hidden: bool,
},
ReadDir {
dir: String,
include_hidden: bool,
},
RemoveDir {
path: String,
id: i32,
recursive: bool,
},
RemoveFile {
path: String,
id: i32,
file_num: i32,
},
CreateDir {
path: String,
id: i32,
},
NewWrite {
path: String,
id: i32,
file_num: i32,
files: Vec<(String, u64)>,
overwrite_detection: bool,
total_size: u64,
conn_id: i32,
},
CancelWrite {
id: i32,
},
WriteBlock {
id: i32,
file_num: i32,
data: Bytes,
compressed: bool,
},
WriteDone {
id: i32,
file_num: i32,
},
WriteError {
id: i32,
file_num: i32,
err: String,
},
WriteOffset {
id: i32,
file_num: i32,
offset_blk: u32,
},
CheckDigest {
id: i32,
file_num: i32,
file_size: u64,
last_modified: u64,
is_upload: bool,
is_resume: bool,
},
SendConfirm(Vec<u8>),
Rename {
id: i32,
path: String,
new_name: String,
},
// CM-side file reading operations (Windows only)
// These enable Connection Manager to read files and stream them back to Connection
ReadFile {
path: String,
id: i32,
file_num: i32,
include_hidden: bool,
conn_id: i32,
overwrite_detection: bool,
},
CancelRead {
id: i32,
conn_id: i32,
},
SendConfirmForRead {
id: i32,
file_num: i32,
skip: bool,
offset_blk: u32,
conn_id: i32,
},
ReadAllFiles {
path: String,
id: i32,
include_hidden: bool,
conn_id: i32,
},
}
#[cfg(target_os = "windows")]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t")]
pub struct ClipboardNonFile {
pub compress: bool,
pub content: bytes::Bytes,
pub content_len: usize,
pub next_raw: bool,
pub width: i32,
pub height: i32,
// message.proto: ClipboardFormat
pub format: i32,
pub special_name: String,
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataKeyboard {
Sequence(String),
KeyDown(enigo::Key),
KeyUp(enigo::Key),
KeyClick(enigo::Key),
GetKeyState(enigo::Key),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataKeyboardResponse {
GetKeyState(bool),
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataMouse {
MoveTo(i32, i32),
MoveRelative(i32, i32),
Down(enigo::MouseButton),
Up(enigo::MouseButton),
Click(enigo::MouseButton),
ScrollX(i32),
ScrollY(i32),
Refresh,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataControl {
Resolution {
minx: i32,
maxx: i32,
miny: i32,
maxy: i32,
},
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum DataPortableService {
Ping,
Pong,
AuthToken(String),
AuthResult(bool),
ConnCount(Option<usize>),
Mouse((Vec<u8>, i32, String, u32, bool, bool)),
Pointer((Vec<u8>, i32)),
Key(Vec<u8>),
RequestStart,
WillClose,
CmShowElevation(bool),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum Data {
Login {
id: i32,
is_file_transfer: bool,
is_view_camera: bool,
is_terminal: bool,
peer_id: String,
name: String,
avatar: String,
authorized: bool,
port_forward: String,
keyboard: bool,
clipboard: bool,
audio: bool,
file: bool,
file_transfer_enabled: bool,
restart: bool,
recording: bool,
block_input: bool,
privacy_mode: bool,
from_switch: bool,
},
ChatMessage {
text: String,
},
SwitchPermission {
name: String,
enabled: bool,
},
SystemInfo(Option<String>),
ClickTime(i64),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
MouseMoveTime(i64),
Authorize,
Close,
#[cfg(windows)]
SAS,
UserSid(Option<u32>),
OnlineStatus(Option<(i64, bool)>),
Config((String, Option<String>)),
Options(Option<HashMap<String, String>>),
NatType(Option<i32>),
ConfirmedKey(Option<(Vec<u8>, Vec<u8>)>),
RawMessage(Vec<u8>),
Socks(Option<config::Socks5Server>),
FS(FS),
Test,
SyncConfig(Option<Box<(Config, Config2)>>),
#[cfg(target_os = "windows")]
ClipboardFile(ClipboardFile),
ClipboardFileEnabled(bool),
#[cfg(target_os = "windows")]
ClipboardNonFile(Option<(String, Vec<ClipboardNonFile>)>),
PrivacyModeState((i32, PrivacyModeState, String)),
TestRendezvousServer,
Deployed,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Keyboard(DataKeyboard),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
KeyboardResponse(DataKeyboardResponse),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Mouse(DataMouse),
Control(DataControl),
Theme(String),
Language(String),
Empty,
Disconnected,
DataPortableService(DataPortableService),
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
SwitchSidesRequest(String),
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
SwitchSidesUuid(String, String, Option<bool>),
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
SwitchSidesBack,
UrlLink(String),
VoiceCallIncoming,
StartVoiceCall,
VoiceCallResponse(bool),
CloseVoiceCall(String),
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Plugin(Plugin),
#[cfg(windows)]
SyncWinCpuUsage(Option<f64>),
FileTransferLog((String, String)),
#[cfg(windows)]
ControlledSessionCount(usize),
CmErr(String),
// CM-side file reading responses (Windows only)
// These are sent from CM back to Connection when CM handles file reading
/// Response to ReadFile: contains initial file list or error
ReadJobInitResult {
id: i32,
file_num: i32,
include_hidden: bool,
conn_id: i32,
/// Serialized protobuf bytes of FileDirectory, or error string
result: Result<Vec<u8>, String>,
},
/// File data block read by CM.
///
/// The actual data is sent separately via `send_raw()` after this message to avoid
/// JSON encoding overhead for large binary data. This mirrors the `WriteBlock` pattern.
///
/// **Protocol:**
/// - Sender: `send(FileBlockFromCM{...})` then `send_raw(data)`
/// - Receiver: `next()` returns `FileBlockFromCM`, then `next_raw()` returns data bytes
///
/// **Note on empty data (e.g., empty files):**
/// Empty data is supported. The IPC connection uses `BytesCodec` with `raw=false` (default),
/// which prefixes each frame with a length header. So `send_raw(Bytes::new())` sends a
/// 1-byte frame (length=0), and `next_raw()` correctly returns an empty `BytesMut`.
/// See `libs/hbb_common/src/bytes_codec.rs` test `test_codec2` for verification.
FileBlockFromCM {
id: i32,
file_num: i32,
/// Data is sent separately via `send_raw()` to avoid JSON encoding overhead.
/// This field is skipped during serialization; sender must call `send_raw()` after sending.
/// Receiver must call `next_raw()` and populate this field manually.
#[serde(skip)]
data: bytes::Bytes,
compressed: bool,
conn_id: i32,
},
/// File read completed successfully
FileReadDone {
id: i32,
file_num: i32,
conn_id: i32,
},
/// File read failed with error
FileReadError {
id: i32,
file_num: i32,
err: String,
conn_id: i32,
},
/// Digest info from CM for overwrite detection
FileDigestFromCM {
id: i32,
file_num: i32,
last_modified: u64,
file_size: u64,
is_resume: bool,
conn_id: i32,
},
/// Response to ReadAllFiles: recursive directory listing
AllFilesResult {
id: i32,
conn_id: i32,
path: String,
/// Serialized protobuf bytes of FileDirectory, or error string
result: Result<Vec<u8>, String>,
},
CheckHwcodec,
#[cfg(feature = "flutter")]
VideoConnCount(Option<usize>),
// Although the key is not necessary, it is used to avoid hardcoding the key.
WaylandScreencastRestoreToken((String, String)),
HwCodecConfig(Option<String>),
RemoveTrustedDevices(Vec<Bytes>),
ClearTrustedDevices,
#[cfg(all(target_os = "windows", feature = "flutter"))]
PrinterData(Vec<u8>),
InstallOption(Option<(String, String)>),
#[cfg(all(
feature = "flutter",
not(any(target_os = "android", target_os = "ios"))
))]
ControllingSessionCount(usize),
#[cfg(target_os = "linux")]
TerminalSessionCount(usize),
#[cfg(target_os = "windows")]
PortForwardSessionCount(Option<usize>),
SocksWs(Option<Box<(Option<config::Socks5Server>, String)>>),
#[cfg(target_os = "macos")]
HasNoActiveConns(Option<bool>),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Whiteboard((String, crate::whiteboard::CustomEvent)),
ControlPermissionsRemoteModify(Option<bool>),
#[cfg(target_os = "windows")]
FileTransferEnabledState(Option<bool>),
// --- DRM/KMS capture (opt-in `drm` feature) over the `_drm` service-scoped channel ---
// All of the following are `cfg(all(linux, drm))`, so the drm-off IPC wire is byte-identical
// to upstream. Protocol on `_drm`: on connect the root service sends `DrmDisplayList`, the
// client replies `DrmStart{display}`, then the service streams `DrmFrame` + send_raw(BGRA) and
// `DrmCursor` + send_raw(RGBA). A frame/cursor header is ALWAYS immediately followed by exactly
// one `send_raw()` payload (the same header-then-raw pairing as `FileBlockFromCM`). This keeps
// the header extensible. The zero-copy `DrmFrameDmabuf(DmabufDesc)` sibling below carries only a
// small JSON metadata descriptor; the scanout dma-buf fd rides an SCM_RIGHTS ancillary message on
// the same `DrmConn` send (see `DrmConn::send_msg`), so it has NO trailing `send_raw()` body.
/// Client -> service: begin streaming the chosen display.
#[cfg(all(target_os = "linux", feature = "drm"))]
// `need_cpu` is set by an unprivileged consumer that could not open a render-node convert context
// (drmtap_open_render failed, or an old .so lacks the split symbols). The service then streams the
// CPU-converted `DrmFrame` path for this connection instead of a dma-buf fd the consumer cannot
// detile, so a render-node-less seat still captures instead of losing the stream.
DrmStart { display: i32, need_cpu: bool },
/// Service -> client: the enumerated DRM displays (sent once, before frames).
#[cfg(all(target_os = "linux", feature = "drm"))]
DrmDisplayList(Vec<DrmDisplayInfo>),
/// Service -> client: the connector topology changed mid-stream (a monitor hotplug/unplug/modeset,
/// observed by the service's udev DRM-uevent listener). Carries the freshly-enumerated list so the
/// consumer can swap its sticky positive availability cache off the hot path, WITHOUT re-probing
/// `_drm` (which would trip the enumeration restart loop). Interleaved with frames on the same
/// stream; carries no `send_raw()` body and no fd.
#[cfg(all(target_os = "linux", feature = "drm"))]
DrmDisplaysChanged(Vec<DrmDisplayInfo>),
/// Service -> client: a frame header; the packed BGRA pixels follow via `send_raw()`.
/// CPU-fallback path (old .so, no render node): pixels cross the wire.
#[cfg(all(target_os = "linux", feature = "drm"))]
DrmFrame { width: u32, height: u32 },
/// Service -> client: a zero-copy dma-buf frame descriptor. The scanout fd is NOT a field; when
/// `desc.has_fd` it rides an SCM_RIGHTS ancillary message on the same `DrmConn::send_msg`, and
/// there is NO trailing `send_raw()` body. The unprivileged `--server` imports the fd and does
/// the EGL detile/convert itself (see `DmabufDesc`).
#[cfg(all(target_os = "linux", feature = "drm"))]
DrmFrameDmabuf(DmabufDesc),
/// Service -> client: a hardware-cursor header; the RGBA pixels follow via `send_raw()`.
#[cfg(all(target_os = "linux", feature = "drm"))]
DrmCursor {
id: u64,
width: u32,
height: u32,
hotx: i32,
hoty: i32,
},
}
/// One enumerated DRM display shipped over `_drm` (physical geometry). The serializable IPC
/// form of `scrap::drm_reader::DisplaySnapshot`; the server augments it with the Wayland
/// logical geometry/scale, which needs the user session.
#[cfg(all(target_os = "linux", feature = "drm"))]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct DrmDisplayInfo {
pub name: String,
pub crtc_id: u32,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub active: bool,
}
/// Serializable metadata descriptor of a scanout dma-buf, shipped over `_drm` as the JSON payload of
/// `Data::DrmFrameDmabuf`. It mirrors `scrap::drm_reader::drmtap_dmabuf_desc` field-for-field EXCEPT
/// the process-local `dma_buf_fd` (which never serializes — it rides SCM_RIGHTS ancillary), and adds
/// `buffer_id` (the producer's stable pool key) and `has_fd` (whether this message's `send_msg`
/// carries the fd, vs an import-once cache hit that omits it). The converter rebuilds a
/// `drmtap_dmabuf_desc` from these fields and overwrites its `dma_buf_fd` with the received fd.
#[cfg(all(target_os = "linux", feature = "drm"))]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DmabufDesc {
/// Producer-side stable pool key (e.g. fb_id + a connection epoch). Distinct from `fb_id`, which
/// is libdrmtap's import-once cache key.
pub buffer_id: u64,
pub width: u32,
pub height: u32,
/// DRM fourcc of the scanout.
pub format: u32,
/// DRM format modifier (tiling/compression).
pub modifier: u64,
/// KMS framebuffer id — libdrmtap's import-once cache key. 0 disables caching for this frame.
pub fb_id: u32,
/// Used entries in `offsets`/`pitches` (1..4); 0 is treated as 1.
pub num_planes: u32,
/// Per-plane byte offsets into the dma-buf (CCS main + aux + clear-color).
pub offsets: [u32; 4],
/// Per-plane strides in bytes; `pitches[0]` is the main-surface stride.
pub pitches: [u32; 4],
/// DRMTAP_EOTF_* (SDR=0, PQ=2, HLG=3). PQ triggers the HDR->SDR tone-map on convert.
pub hdr_eotf: u32,
/// Content/mastering peak luminance (cd/m2); 0 = unknown.
pub hdr_max_nits: u32,
/// True: this message's `send_msg` attaches the dma-buf fd in an SCM_RIGHTS cmsg. False: an
/// import-once cache hit for `fb_id` — no fd attached, converter reuses its cached EGLImage.
pub has_fd: bool,
}
#[tokio::main(flavor = "current_thread")]
pub async fn start(postfix: &str) -> ResultType<()> {
let mut incoming = new_listener(postfix).await?;
loop {
if let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
let postfix = postfix.to_owned();
#[cfg(any(target_os = "linux", target_os = "macos"))]
if config::is_service_ipc_postfix(&postfix) {
if !authorize_service_scoped_ipc_connection(&stream, &postfix) {
continue;
}
}
#[cfg(windows)]
if postfix.is_empty() {
// Windows main IPC (`postfix == ""`) is authorized here.
// Other security-sensitive channels use dedicated authorization paths:
// - `_portable_service`: portable-service listener + handshake policy
// - service-scoped postfixes: service-specific listener/authorization
if !authorize_windows_main_ipc_connection(&stream, &postfix) {
continue;
}
}
tokio::spawn(async move {
loop {
match stream.next().await {
Err(err) => {
log::trace!("ipc '{}' connection closed: {}", postfix, err);
break;
}
Ok(Some(data)) => {
// On Linux/macOS, the protected `_service` channel is used only for
// syncing config between root service and the active user process.
//
// NOTE: `is_service_ipc_postfix()` also includes `_uinput_*`, but those
// channels are handled by the dedicated uinput listener/protocol in
// `src/server/uinput.rs` and therefore do not share this Data enum
// allowlist. The SyncConfig allowlist here is intentionally scoped to the
// `_service` channel only.
//
// Keep this explicit branch to avoid policy drift between `_service` and
// uinput IPC paths while still minimizing exposed message surface here.
#[cfg(any(target_os = "linux", target_os = "macos"))]
if postfix == crate::POSTFIX_SERVICE {
if matches!(&data, Data::SyncConfig(_)) {
handle(data, &mut stream).await;
} else {
log::warn!(
"Rejected non-sync data on protected _service IPC channel: postfix={}, data_kind={:?}, peer_uid={:?}",
postfix,
std::mem::discriminant(&data),
stream.peer_uid()
);
// Close the connection to avoid keeping a protected channel
// alive while repeatedly receiving invalid traffic.
break;
}
continue;
}
handle(data, &mut stream).await;
}
Ok(None) => {
// `Ok(None)` means a complete frame arrived but did not
// deserialize into `Data`. Peer close/reset is returned as
// `Err` by `ConnectionTmpl::next()`. Keep the historical
// ignore behavior except on the protected `_service` channel.
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
if postfix == crate::POSTFIX_SERVICE {
break;
}
}
}
}
}
});
}
Err(err) => {
log::error!("Couldn't get client: {:?}", err);
}
}
}
}
}
pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
let path = Config::ipc_path(postfix);
#[cfg(any(target_os = "linux", target_os = "macos"))]
let should_scrub_parent_entries = ensure_secure_ipc_parent_dir(&path, postfix)?;
#[cfg(any(target_os = "linux", target_os = "macos"))]
let existing_listener_alive = check_pid(postfix).await;
#[cfg(any(target_os = "linux", target_os = "macos"))]
if should_scrub_parent_entries_after_check_pid(
should_scrub_parent_entries,
existing_listener_alive,
) {
scrub_secure_ipc_parent_dir(&path, postfix)?;
}
let mut endpoint = Endpoint::new(path.clone());
let security_attrs = {
#[cfg(windows)]
{
if postfix == "_portable_service" {
portable_service_listener_security_attributes()
} else if should_allow_everyone_create_on_windows(postfix) {
SecurityAttributes::allow_everyone_create()
} else {
Ok(SecurityAttributes::empty())
}
}
#[cfg(not(windows))]
{
SecurityAttributes::allow_everyone_create()
}
};
match security_attrs {
Ok(attr) => endpoint.set_security_attributes(attr),
Err(err) => {
log::error!("Failed to set ipc{} security: {}", postfix, err);
#[cfg(windows)]
if postfix == "_portable_service" {
// Fail closed for `_portable_service` when SDDL construction fails.
// This endpoint is security-critical and must not start with default ACLs.
return Err(err.into());
}
}
};
match endpoint.incoming() {
Ok(incoming) => {
if postfix == crate::POSTFIX_SERVICE {
log::info!("Started protected ipc service server: postfix={}", postfix);
} else {
log::info!("Started ipc{} server at path: {}", postfix, &path);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
// NOTE: On Linux/macOS, some IPC sockets are intentionally world-connectable
// (0666) so the active (non-root) user process can connect. Authorization is
// enforced at accept-time for these channels, and the protected `_service`
// channel is further restricted by an explicit message allowlist (SyncConfig
// only).
let socket_mode = if config::is_service_ipc_postfix(postfix) {
0o0666
} else {
0o0600
};
if let Err(err) =
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(socket_mode))
{
log::error!(
"Failed to set permissions on ipc{} socket at path {}: {}",
postfix,
&path,
err
);
std::fs::remove_file(&path).ok();
return Err(err.into());
}
write_pid(postfix);
}
Ok(incoming)
}
Err(err) => {
log::error!(
"Failed to start ipc{} server at path {}: {}",
postfix,
path,
err
);
Err(err.into())
}
}
}
pub struct CheckIfRestart {
stop_service: String,
rendezvous_servers: Vec<String>,
audio_input: String,
voice_call_input: String,
ws: String,
disable_udp: String,
allow_insecure_tls_fallback: String,
api_server: String,
}
impl CheckIfRestart {
pub fn new() -> CheckIfRestart {
CheckIfRestart {
stop_service: Config::get_option("stop-service"),
rendezvous_servers: Config::get_rendezvous_servers(),
audio_input: Config::get_option("audio-input"),
voice_call_input: Config::get_option("voice-call-input"),
ws: Config::get_option(OPTION_ALLOW_WEBSOCKET),
disable_udp: Config::get_option(config::keys::OPTION_DISABLE_UDP),
allow_insecure_tls_fallback: Config::get_option(
config::keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK,
),
api_server: Config::get_option("api-server"),
}
}
}
impl Drop for CheckIfRestart {
fn drop(&mut self) {
// If https proxy is used, we need to restart rendezvous mediator.
// No need to check if https proxy is used, because this option does not change frequently
// and restarting mediator is safe even https proxy is not used.
let allow_insecure_tls_fallback_changed = self.allow_insecure_tls_fallback
!= Config::get_option(config::keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK);
if allow_insecure_tls_fallback_changed
|| self.stop_service != Config::get_option("stop-service")
|| self.rendezvous_servers != Config::get_rendezvous_servers()
|| self.ws != Config::get_option(OPTION_ALLOW_WEBSOCKET)
|| self.disable_udp != Config::get_option(config::keys::OPTION_DISABLE_UDP)
|| self.api_server != Config::get_option("api-server")
{
if allow_insecure_tls_fallback_changed {
hbb_common::tls::reset_tls_cache();
}
RendezvousMediator::restart();
}
if self.audio_input != Config::get_option("audio-input") {
crate::audio_service::restart();
}
if self.voice_call_input != Config::get_option("voice-call-input") {
crate::audio_service::set_voice_call_input_device(
Some(Config::get_option("voice-call-input")),
true,
)
}
}
}
async fn handle(data: Data, stream: &mut Connection) {
match data {
Data::SystemInfo(_) => {
let info = format!(
"log_path: {}, config: {}, username: {}",
Config::log_path().to_str().unwrap_or(""),
Config::file().to_str().unwrap_or(""),
crate::username(),
);
allow_err!(stream.send(&Data::SystemInfo(Some(info))).await);
}
Data::ClickTime(_) => {
let t = crate::server::CLICK_TIME.load(Ordering::SeqCst);
allow_err!(stream.send(&Data::ClickTime(t)).await);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::MouseMoveTime(_) => {
let t = crate::server::MOUSE_MOVE_TIME.load(Ordering::SeqCst);
allow_err!(stream.send(&Data::MouseMoveTime(t)).await);
}
Data::Close => {
log::info!("Receive close message");
if EXIT_RECV_CLOSE.load(Ordering::SeqCst) {
#[cfg(not(target_os = "android"))]
crate::server::input_service::fix_key_down_timeout_at_exit();
if is_server() {
let _ = privacy_mode::turn_off_privacy(0, Some(PrivacyModeState::OffByPeer));
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
if crate::is_main() {
// below part is for main windows can be reopen during rustdesk installation and installing service from UI
// this make new ipc server (domain socket) can be created.
std::fs::remove_file(&Config::ipc_path("")).ok();
#[cfg(target_os = "linux")]
{
hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0)
.await;
// https://github.com/rustdesk/rustdesk/discussions/9254
crate::run_me::<&str>(vec!["--no-server"]).ok();
}
#[cfg(target_os = "macos")]
{
// our launchagent interval is 1 second
hbb_common::sleep(1.5).await;
std::process::Command::new("open")
.arg("-n")
.arg(&format!("/Applications/{}.app", crate::get_app_name()))
.spawn()
.ok();
}
// leave above open a little time
hbb_common::sleep(0.3).await;
// in case below exit failed
crate::platform::quit_gui();
}
std::process::exit(-1); // to make sure --server luauchagent process can restart because SuccessfulExit used
}
}
Data::OnlineStatus(_) => {
let x = config::get_online_state();
let confirmed = Config::get_key_confirmed();
allow_err!(stream.send(&Data::OnlineStatus(Some((x, confirmed)))).await);
}
Data::ConfirmedKey(None) => {
let out = if Config::get_key_confirmed() {
Some(Config::get_key_pair())
} else {
None
};
allow_err!(stream.send(&Data::ConfirmedKey(out)).await);
}
Data::Socks(s) => match s {
None => {
allow_err!(stream.send(&Data::Socks(Config::get_socks())).await);
}
Some(data) => {
let _nat = CheckTestNatType::new();
if data.proxy.is_empty() {
Config::set_socks(None);
} else {
Config::set_socks(Some(data));
}
RendezvousMediator::restart();
log::info!("socks updated");
}
},
Data::SocksWs(s) => match s {
None => {
allow_err!(
stream
.send(&Data::SocksWs(Some(Box::new((
Config::get_socks(),
Config::get_option(OPTION_ALLOW_WEBSOCKET)
)))))
.await
);
}
_ => {}
},
#[cfg(feature = "flutter")]
Data::VideoConnCount(None) => {
let n = crate::server::AUTHED_CONNS
.lock()
.unwrap()
.iter()
.filter(|x| x.conn_type == crate::server::AuthConnType::Remote)
.count();
allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await);
}
Data::Config((name, value)) => match value {
None => {
let value;
if name == "id" {
value = Some(Config::get_id());
} else if name == "temporary-password" {
value = Some(password::temporary_password());
} else if name == "permanent-password-storage-and-salt" {
let (storage, salt) = Config::get_local_permanent_password_storage_and_salt();
value = Some(storage + "\n" + &salt);
} else if name == "permanent-password-set" {
value = Some(if Config::has_permanent_password() {
"Y".to_owned()
} else {
"N".to_owned()
});
} else if name == "permanent-password-is-preset" {
value = Some(if Config::is_using_preset_password() {
"Y".to_owned()
} else {
"N".to_owned()
});
} else if name == "salt" {
value = Some(Config::get_salt());
} else if name == "rendezvous_server" {
value = Some(format!(
"{},{}",
Config::get_rendezvous_server(),
Config::get_rendezvous_servers().join(",")
));
} else if name == "rendezvous_servers" {
value = Some(Config::get_rendezvous_servers().join(","));
} else if name == "fingerprint" {
value = if Config::get_key_confirmed() {
Some(crate::common::pk_to_fingerprint(Config::get_key_pair().1))
} else {
None
};
} else if name == "hide_cm" {
value = if crate::hbbs_http::sync::is_pro() || crate::common::is_custom_client()
{
Some(hbb_common::password_security::hide_cm().to_string())
} else {
None
};
} else if name == "voice-call-input" {
value = crate::audio_service::get_voice_call_input_device();
} else if name == "unlock-pin" {
value = Some(Config::get_unlock_pin());
} else if name == "trusted-devices" {
value = Some(Config::get_trusted_devices_json());
} else {
value = None;
}
allow_err!(stream.send(&Data::Config((name, value))).await);
}
Some(value) => {
let mut updated = true;
if name == "id" {
// An empty id would wipe the local id and unconfirm the key (cf. #15626).
if value.is_empty() {
log::warn!("Ignoring empty id write over IPC");
updated = false;
} else {
Config::set_key_confirmed(false);
Config::set_id(&value);
}
} else if name == "temporary-password" {
password::update_temporary_password();
} else if name == "permanent-password" {
if Config::is_disable_change_permanent_password() {
log::warn!("Changing permanent password is disabled");
updated = false;
} else {
updated = Config::set_permanent_password(&value);
}
// Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to
// distinguish "accepted by daemon" vs "IPC send succeeded" without
// reading back any secret.
let ack = if updated { "Y" } else { "N" }.to_owned();
allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await);
} else if name == "salt" {
Config::set_salt(&value);
} else if name == "voice-call-input" {
crate::audio_service::set_voice_call_input_device(Some(value), true);
} else if name == "unlock-pin" {
Config::set_unlock_pin(&value);
} else {
return;
}
if updated {
log::info!("{} updated", name);
}
}
},
Data::Options(value) => match value {
None => {
let v = Config::get_options();
allow_err!(stream.send(&Data::Options(Some(v))).await);
}
Some(value) => {
let _chk = CheckIfRestart::new();
let _nat = CheckTestNatType::new();
if let Some(v) = value.get("privacy-mode-impl-key") {
crate::privacy_mode::switch(v);
}
Config::set_options(value);
allow_err!(stream.send(&Data::Options(None)).await);
}
},
Data::NatType(_) => {
let t = Config::get_nat_type();
allow_err!(stream.send(&Data::NatType(Some(t))).await);
}
Data::SyncConfig(Some(configs)) => {
let (config, config2) = *configs;
let _chk = CheckIfRestart::new();
Config::set(config);
Config2::set(config2);
allow_err!(stream.send(&Data::SyncConfig(None)).await);
}
Data::SyncConfig(None) => {
allow_err!(
stream
.send(&Data::SyncConfig(Some(
(Config::get(), Config2::get()).into()
)))
.await
);
}
#[cfg(windows)]
Data::SyncWinCpuUsage(None) => {
allow_err!(
stream
.send(&Data::SyncWinCpuUsage(
hbb_common::platform::windows::cpu_uage_one_minute()
))
.await
);
}
Data::TestRendezvousServer => {
crate::test_rendezvous_server();
}
Data::Deployed => {
crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst);
crate::rendezvous_mediator::RendezvousMediator::restart();
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::SwitchSidesRequest(id) => {
let uuid = uuid::Uuid::new_v4();
crate::server::insert_switch_sides_uuid(id, uuid.clone());
allow_err!(
stream
.send(&Data::SwitchSidesRequest(uuid.to_string()))
.await
);
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::SwitchSidesUuid(uuid, id, None) => {
let allowed = uuid
.parse::<uuid::Uuid>()
.map(|uuid| crate::server::remove_pending_switch_sides_uuid(&id, &uuid))
.unwrap_or(false);
allow_err!(
stream
.send(&Data::SwitchSidesUuid(uuid, id, Some(allowed)))
.await
);
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::Plugin(plugin) => crate::plugin::ipc::handle_plugin(plugin, stream).await,
#[cfg(windows)]
Data::ControlledSessionCount(_) => {
allow_err!(
stream
.send(&Data::ControlledSessionCount(
crate::Connection::alive_conns().len()
))
.await
);
}
#[cfg(target_os = "macos")]
Data::HasNoActiveConns(None) => {
allow_err!(
stream
.send(&Data::HasNoActiveConns(Some(
crate::updater::has_no_active_conns()
)))
.await
);
}
#[cfg(all(
feature = "flutter",
not(any(target_os = "android", target_os = "ios"))
))]
Data::ControllingSessionCount(count) => {
crate::updater::update_controlling_session_count(count);
}
#[cfg(target_os = "linux")]
Data::TerminalSessionCount(_) => {
let count = crate::terminal_service::get_terminal_session_count(true);
allow_err!(stream.send(&Data::TerminalSessionCount(count)).await);
}
#[cfg(feature = "hwcodec")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::CheckHwcodec => {
scrap::hwcodec::start_check_process();
}
#[cfg(feature = "hwcodec")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::HwCodecConfig(c) => {
match c {
None => {
let v = match scrap::hwcodec::HwCodecConfig::get_set_value() {
Some(v) => Some(serde_json::to_string(&v).unwrap_or_default()),
None => None,
};
allow_err!(stream.send(&Data::HwCodecConfig(v)).await);
}
Some(v) => {
// --server and portable
scrap::hwcodec::HwCodecConfig::set(v);
}
}
}
Data::WaylandScreencastRestoreToken((key, value)) => {
let v = if value == "get" {
let opt = get_local_option(key.clone());
#[cfg(not(target_os = "linux"))]
{
Some(opt)
}
#[cfg(target_os = "linux")]
{
let v = if opt.is_empty() {
if scrap::wayland::pipewire::is_rdp_session_hold() {
"fake token".to_string()
} else {
"".to_owned()
}
} else {
opt
};
Some(v)
}
} else if value == "clear" {
set_local_option(key.clone(), "".to_owned());
#[cfg(target_os = "linux")]
scrap::wayland::pipewire::close_session();
Some("".to_owned())
} else {
None
};
if let Some(v) = v {
allow_err!(
stream
.send(&Data::WaylandScreencastRestoreToken((key, v)))
.await
);
}
}
Data::RemoveTrustedDevices(v) => {
Config::remove_trusted_devices(&v);
}
Data::ClearTrustedDevices => {
Config::clear_trusted_devices();
}
Data::InstallOption(opt) => match opt {
Some((_k, _v)) => {
#[cfg(target_os = "windows")]
if let Err(e) = crate::platform::windows::update_install_option(&_k, &_v) {
log::error!(
"Failed to update install option \"{}\" to \"{}\", error: {}",
&_k,
&_v,
e
);
}
}
None => {
// `None` is usually used to get values.
// This branch is left blank for unification and further use.
}
},
#[cfg(target_os = "windows")]
Data::PortForwardSessionCount(c) => match c {
None => {
let count = crate::server::AUTHED_CONNS
.lock()
.unwrap()
.iter()
.filter(|c| c.conn_type == crate::server::AuthConnType::PortForward)
.count();
allow_err!(
stream
.send(&Data::PortForwardSessionCount(Some(count)))
.await
);
}
_ => {
// Port forward session count is only a get value.
}
},
Data::ControlPermissionsRemoteModify(_) => {
use hbb_common::rendezvous_proto::control_permissions::Permission;
let state =
crate::server::get_control_permission_state(Permission::remote_modify, true);
allow_err!(
stream
.send(&Data::ControlPermissionsRemoteModify(state))
.await
);
}
#[cfg(target_os = "windows")]
Data::FileTransferEnabledState(_) => {
use hbb_common::rendezvous_proto::control_permissions::Permission;
let state = crate::server::get_control_permission_state(Permission::file, false);
let enabled = state.unwrap_or_else(|| {
crate::server::Connection::is_permission_enabled_locally(
config::keys::OPTION_ENABLE_FILE_TRANSFER,
)
});
allow_err!(
stream
.send(&Data::FileTransferEnabledState(Some(enabled)))
.await
);
}
_ => {}
};
}
#[cfg(target_os = "windows")]
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
use hbb_common::rand::{rngs::OsRng, RngCore as _};
use std::fmt::Write as _;
let mut random_bytes = [0u8; IPC_TOKEN_RANDOM_BYTES];
let mut rng = OsRng;
rng.try_fill_bytes(&mut random_bytes).map_err(|err| {
hbb_common::anyhow::anyhow!(
"failed to generate portable service ipc token from OsRng: {}",
err
)
})?;
let mut token = String::with_capacity(IPC_TOKEN_LEN);
for byte in random_bytes {
let _ = write!(token, "{:02x}", byte);
}
Ok(token)
}
#[cfg(target_os = "windows")]
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
return false;
}
expected
.as_bytes()
.iter()
.zip(candidate.as_bytes().iter())
.fold(0u8, |diff, (left, right)| diff | (*left ^ *right))
== 0
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>,
token: &str,
) -> ResultType<()>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
{
stream
.send(&Data::DataPortableService(DataPortableService::AuthToken(
token.to_owned(),
)))
.await?;
match stream
.next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS)
.await?
{
Some(Data::DataPortableService(DataPortableService::AuthResult(true))) => Ok(()),
Some(Data::DataPortableService(DataPortableService::AuthResult(false))) => {
bail!("portable service ipc handshake was rejected by server")
}
Some(_) | None => bail!("portable service ipc handshake returned an unexpected response"),
}
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>,
mut validate_token: F,
) -> ResultType<()>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
// Token validators must use `constant_time_ipc_token_eq` or an equivalent
// fixed-length comparison; this handshake is part of the privilege boundary.
F: FnMut(&str) -> bool,
{
let authorized = match stream
.next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS)
.await?
{
Some(Data::DataPortableService(DataPortableService::AuthToken(token))) => {
validate_token(&token)
}
Some(_) | None => false,
};
stream
.send(&Data::DataPortableService(DataPortableService::AuthResult(
authorized,
)))
.await?;
if !authorized {
bail!("portable service ipc handshake failed")
}
Ok(())
}
#[inline]
async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
let client = timeout(ms_timeout, Endpoint::connect(path)).await??;
Ok(ConnectionTmpl::new(client))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn select_server_uid_for_user_main_ipc(
server_uids: &[u32],
active_uid: Option<u32>,
prefer_root: bool,
) -> ResultType<u32> {
let mut server_uids = server_uids.to_vec();
server_uids.sort_unstable();
server_uids.dedup();
match server_uids.as_slice() {
[] => {
if let Some(uid) = active_uid {
// If no `--server` processes are found but the active user is identifiable,
// try the active user anyway because the main process may also listen on "" IPC.
return Ok(uid);
} else {
bail!("No --server process found for user main IPC")
}
}
[uid] => return Ok(*uid),
_ => {}
}
if prefer_root && server_uids.contains(&0) {
return Ok(0);
}
if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) {
return Ok(active_uid);
}
bail!("Multiple --server processes found for user main IPC");
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn running_server_uids_for_current_exe() -> ResultType<Vec<u32>> {
let current_exe = std::env::current_exe()?;
let current_exe_path = std::fs::canonicalize(&current_exe)?;
let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id());
let mut sys = hbb_common::sysinfo::System::new();
sys.refresh_processes();
let mut server_uids = Vec::new();
for process in sys.processes().values() {
if process.pid() == current_pid {
continue;
}
if process.cmd().get(1).map_or(true, |arg| arg != "--server") {
continue;
}
let Ok(process_path) = std::fs::canonicalize(process.exe()) else {
continue;
};
if process_path != current_exe_path {
continue;
}
let Some(uid) = process.user_id().map(|uid| **uid as u32) else {
// Root CLI management commands need a stable matching `--server` target.
// If this key process races during enumeration, failing the command is clearer
// than silently skipping it; `--server` is not expected to exit frequently.
bail!("Failed to read --server process uid");
};
server_uids.push(uid);
}
Ok(server_uids)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn user_main_ipc_server_uid() -> ResultType<u32> {
let server_uids = running_server_uids_for_current_exe()?;
#[cfg(target_os = "linux")]
let prefer_root = crate::platform::linux::is_login_screen_wayland();
#[cfg(target_os = "macos")]
let prefer_root = false;
select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root)
}
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get());
let is_root_main_ipc =
unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc;
if is_root_main_ipc {
let uid = user_main_ipc_server_uid()?;
let path = Config::ipc_path_for_uid(uid, postfix);
return connect_with_path(ms_timeout, &path).await;
}
let path = Config::ipc_path(postfix);
return connect_with_path(ms_timeout, &path).await;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let path = Config::ipc_path(postfix);
connect_with_path(ms_timeout, &path).await
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub async fn connect_for_uid(
ms_timeout: u64,
uid: u32,
postfix: &str,
) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path_for_uid(uid, postfix);
let conn = connect_with_path(ms_timeout, &path).await?;
#[cfg(target_os = "macos")]
if postfix.is_empty()
&& !authorize_user_server_process(conn.peer_uid(), conn.peer_pid(), uid)
{
bail!("Rejected user IPC peer for uid {}", uid);
}
Ok(conn)
}
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
pub async fn start_pa() {
use crate::audio_service::AUDIO_DATA_SIZE_U8;
match new_listener("_pa").await {
Ok(mut incoming) => {
loop {
if let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
let mut device: String = "".to_owned();
if let Some(Ok(Some(Data::Config((_, Some(x)))))) =
stream.next_timeout2(1000).await
{
device = x;
}
if !device.is_empty() {
device = crate::platform::linux::get_pa_source_name(&device);
}
if device.is_empty() {
device = crate::platform::linux::get_pa_monitor();
}
if device.is_empty() {
continue;
}
let spec = pulse::sample::Spec {
format: pulse::sample::Format::F32le,
channels: 2,
rate: crate::platform::PA_SAMPLE_RATE,
};
log::info!("pa monitor: {:?}", device);
// systemctl --user status pulseaudio.service
let mut buf: Vec<u8> = vec![0; AUDIO_DATA_SIZE_U8];
match psimple::Simple::new(
None, // Use the default server
&crate::get_app_name(), // Our applications name
pulse::stream::Direction::Record, // We want a record stream
Some(&device), // Use the default device
"record", // Description of our stream
&spec, // Our sample format
None, // Use default channel map
None, // Use default buffering attributes
) {
Ok(s) => loop {
if let Ok(_) = s.read(&mut buf) {
let out =
if buf.iter().filter(|x| **x != 0).next().is_none() {
vec![]
} else {
buf.clone()
};
if let Err(err) = stream.send_raw(out.into()).await {
log::error!("Failed to send audio data:{}", err);
break;
}
}
},
Err(err) => {
log::error!("Could not create simple pulse: {}", err);
}
}
}
Err(err) => {
log::error!("Couldn't get pa client: {:?}", err);
}
}
}
}
}
Err(err) => {
log::error!("Failed to start pa ipc server: {}", err);
}
}
}
/// Filesystem path of the `_drm` capture socket. It lives beside the hardened `_service` socket in
/// the shared `/tmp/<app>-service` directory (cross-uid, traversable) so the root `--service` and
/// the user `--server` share one uid-independent path. Derived from the real `_service` path so we
/// inherit hbb_common's directory convention WITHOUT teaching hbb_common about a drm-specific
/// postfix (keeps the isolation clean: no shared-lib change). Both ends call this.
#[cfg(all(target_os = "linux", feature = "drm"))]
pub(crate) fn drm_ipc_path() -> String {
let service_path = Config::ipc_path("_service");
let dir = std::path::Path::new(&service_path)
.parent()
.unwrap_or_else(|| std::path::Path::new("/tmp"));
dir.join("ipc_drm").to_string_lossy().into_owned()
}
/// Connect (from the user `--server`) to the root service's `_drm` capture channel. Uses the
/// derived `drm_ipc_path()` rather than `Config::ipc_path` since `_drm` is not a hbb_common
/// service postfix (Option 2 isolation — no shared-lib change). Returns a [`DrmConn`] (bespoke
/// SCM_RIGHTS framing) rather than the `Framed<_, BytesCodec>` `ConnectionTmpl`: the `_drm` channel
/// must carry the scanout dma-buf fd as ancillary data, which the codec cannot do (see `DrmConn`).
#[cfg(all(target_os = "linux", feature = "drm"))]
pub(crate) async fn connect_drm(ms_timeout: u64) -> ResultType<DrmConn> {
use std::os::fd::AsRawFd;
let path = drm_ipc_path();
let stream = timeout(ms_timeout, tokio::net::UnixStream::connect(&path)).await??;
// The producer MUST be root. DRM/KMS scanout export is a root-service capability, and the DRM
// path outranks PipeWire (an available DRM stream suppresses the portal consent prompt), so a
// non-root peer that won a socket-path race must not be trusted to supply the display list,
// frames and an arbitrary dma-buf fd (review 4.1). The producer direction is authorized in
// handle_drm_conn; this closes the same gap on the consumer direction.
if peer_uid_from_fd(stream.as_raw_fd()) != Some(0) {
bail!("drm: _drm producer is not root; refusing to consume");
}
Ok(DrmConn::new(stream))
}
/// Bind the `_drm` listener. Unlike `new_listener`, this does not route through hbb_common's
/// service-postfix machinery — it places the socket in the shared service dir directly, so the
/// drm-off build needs no hbb_common change. The socket is 0666 (world-connectable) so the
/// unprivileged `--server` can reach it; every accepted peer is still authorized in
/// `handle_drm_conn` (root or the active session uid + exe identity), so connectable != authorized.
#[cfg(all(target_os = "linux", feature = "drm"))]
async fn new_drm_listener() -> ResultType<Incoming> {
let path = drm_ipc_path();
// Ensure the shared service dir exists at its hardened (0711) mode. Passing the `_service`
// postfix reuses hbb_common's expected mode for that directory; it only creates/chmods the
// directory (no pid/socket side effects) and is idempotent with the real `_service` listener.
let _ = ensure_secure_ipc_parent_dir(&path, "_service")?;
// Clear any stale socket from a previous run before binding.
std::fs::remove_file(&path).ok();
let mut endpoint = Endpoint::new(path.clone());
endpoint.set_security_attributes(SecurityAttributes::allow_everyone_create()?);
let incoming = endpoint.incoming()?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o666)).map_err(|err| {
std::fs::remove_file(&path).ok();
err
})?;
log::info!("Started drm ipc server at path: {}", &path);
Ok(incoming)
}
/// Message from a per-connection DRM worker thread (which owns the `!Send` `DrmReader`) to its
/// async socket task. The worker does the blocking device I/O; the task only forwards to the wire.
#[cfg(all(target_os = "linux", feature = "drm"))]
enum DrmProducerMsg {
/// Enumerated displays, sent once before any frame so the task can answer the handshake.
Displays(Vec<DrmDisplayInfo>),
/// A captured frame (split/zero-copy path): the serializable dma-buf descriptor plus the (owned)
/// scanout fd to hand to the peer via SCM_RIGHTS. The worker always produces a real `fd` here; the
/// async task's `ExportLedger` decides whether to actually attach it (`desc.has_fd`) or elide it as
/// an import-once cache hit. The `OwnedFd` is closed once the send has dup'd it into the peer (or
/// immediately, when elided).
Frame {
desc: DmabufDesc,
fd: Option<OwnedFd>,
},
/// A captured frame (CPU-mapped fallback path): a full packed-BGRA frame body. Used when the
/// loaded libdrmtap predates the split API (no `drmtap_grab_desc`) or the seat has no transferable
/// dma-buf (ENOTSUP). Forwarded as `Data::DrmFrame{width,height}` + `send_raw(BGRA)`, exactly like
/// the pre-split protocol, so an unprivileged converter is never required.
FrameCpu {
width: u32,
height: u32,
data: Bytes,
},
/// A changed hardware-cursor shape + its packed RGBA pixels.
Cursor {
id: u64,
width: u32,
height: u32,
hotx: i32,
hoty: i32,
colors: Vec<u8>,
},
}
/// Sets the shared stop flag when the async task ends (any path), so the blocking worker thread
/// terminates promptly even while it is between channel sends (e.g. spinning on WouldBlock).
#[cfg(all(target_os = "linux", feature = "drm"))]
struct DrmStopGuard(std::sync::Arc<std::sync::atomic::AtomicBool>);
#[cfg(all(target_os = "linux", feature = "drm"))]
impl Drop for DrmStopGuard {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
/// Producer-side fd-elision ledger (root `--service`, one per `_drm` connection). Decides, per
/// exported frame, whether the scanout dma-buf fd must ride an SCM_RIGHTS cmsg (`has_fd = true`) or
/// can be elided as an import-once cache hit (`has_fd = false`) because the peer's converter already
/// imported that `fb_id`. Keyed by `fb_id -> (modifier, dims)`; a change in any of those (a resize,
/// a modifier/tiling change, or a recycled fb_id that also changed geometry) forces a real fd, and a
/// modeset/hotplug that invalidates the CRTC ends the connection (so a reconnect starts with a fresh,
/// empty ledger — matching the peer's fresh, empty converter cache).
///
/// SAFETY / CORRECTNESS: eliding relies solely on `(fb_id, modifier, dims)` uniquely identifying a
/// buffer, but the kernel can recycle an `fb_id` onto a *different* buffer with identical geometry
/// and modifier; eliding then would serve a stale EGLImage. libdrmtap's own import cache keys on
/// `fb_id + dma-buf inode` and can re-import ONLY when it is handed a real fd. Because always sending
/// the fd is cheap (the converter still imports once per `fb_id` and closes the surplus fd) and is
/// strictly safe, `DRM_FD_ELISION` defaults to `false` for v1 (always send). The ledger's `epoch`
/// tracks `DRM_DISPLAY_GENERATION` (bumped by the udev listener on a connector-topology change), so a
/// hotplug/modeset invalidates every cached buffer and forces a real fd; but the ledger still cannot
/// see the dma-buf inode, so a recycled fb_id within the SAME generation (identical geometry +
/// modifier) would elide onto a stale EGLImage. Enabling elision needs that inode case validated
/// first.
#[cfg(all(target_os = "linux", feature = "drm"))]
const DRM_FD_ELISION: bool = false;
#[cfg(all(target_os = "linux", feature = "drm"))]
struct SeenBuf {
modifier: u64,
dims: (u32, u32),
epoch: u64,
}
#[cfg(all(target_os = "linux", feature = "drm"))]
struct ExportLedger {
seen: HashMap<u32, SeenBuf>,
order: std::collections::VecDeque<u32>, // insertion order, for evict-oldest
epoch: u64,
}
#[cfg(all(target_os = "linux", feature = "drm"))]
impl ExportLedger {
// Grow-once, hard-capped (preallocated model): a hostile/buggy peer or a fb_id churn cannot grow
// this unbounded; oldest keys are evicted so a real fd is simply re-sent for them later.
const MAX_LEDGER: usize = 32;
fn new() -> Self {
Self {
seen: HashMap::new(),
order: std::collections::VecDeque::new(),
epoch: 0,
}
}
/// Returns true if this frame's fd must be attached (new/changed/recycled buffer, caching
/// disabled, or elision off), false if the converter already holds `fb_id` imported.
fn should_send_fd(&mut self, desc: &DmabufDesc) -> bool {
// fb_id == 0 disables caching for that frame; elision-off always sends.
if !DRM_FD_ELISION || desc.fb_id == 0 {
return true;
}
let ident = SeenBuf {
modifier: desc.modifier,
dims: (desc.width, desc.height),
epoch: self.epoch,
};
if let Some(prev) = self.seen.get(&desc.fb_id) {
if prev.modifier == ident.modifier
&& prev.dims == ident.dims
&& prev.epoch == ident.epoch
{
return false; // import-once cache hit: elide the fd
}
} else {
// New key: record insertion order and evict the oldest if at capacity.
if self.order.len() >= Self::MAX_LEDGER {
if let Some(old) = self.order.pop_front() {
self.seen.remove(&old);
}
}
self.order.push_back(desc.fb_id);
}
self.seen.insert(desc.fb_id, ident);
true
}
}
/// Build a [`DrmConn`] from an already-authorized `_drm` `Connection` (root `--service` side). The
/// parity `Connection` wraps a tokio `UnixStream` but exposes no way to move it out, so we `dup()`
/// its fd into a fresh, independently-owned tokio `UnixStream` for the bespoke SCM_RIGHTS framing.
/// A dup gives a NEW fd number, which registers as its own epoll entry in tokio's reactor (reusing
/// the same fd number would double-register); the caller drops the parity `Connection` afterwards,
/// closing ITS fd, while the dup keeps the socket alive via the shared open file description.
#[cfg(all(target_os = "linux", feature = "drm"))]
fn dup_to_drm_conn(stream: &Connection) -> ResultType<DrmConn> {
let raw = stream.inner.get_ref().as_raw_fd();
let dup = unsafe { hbb_common::libc::dup(raw) };
if dup < 0 {
return Err(std::io::Error::last_os_error().into());
}
// SAFETY: `dup` is a freshly dup'd, owned fd for a connected SOCK_STREAM unix socket.
let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(dup) };
std_stream.set_nonblocking(true)?;
let tokio_stream = tokio::net::UnixStream::from_std(std_stream)?;
Ok(DrmConn::new(tokio_stream))
}
/// Cached DRM display enumeration. The pre-warm populates it and each capture open refreshes it, so
/// a consumer's handshake can send the display list without first paying a DRM enumeration open.
#[cfg(all(target_os = "linux", feature = "drm"))]
static DRM_DISPLAY_CACHE: std::sync::Mutex<Vec<DrmDisplayInfo>> = std::sync::Mutex::new(Vec::new());
/// Monotonic generation bumped by the udev DRM-uevent listener ONLY when a connector-topology change
/// actually altered `DRM_DISPLAY_CACHE` (a monitor hotplug/unplug/modeset). Each live `handle_drm_conn`
/// forward loop watches this (one atomic load per frame) and, on a bump, pushes a `DrmDisplaysChanged`
/// with the fresh list to its consumer — the cheap live-refresh path that avoids a consumer re-probe.
/// `Release`/`Acquire` order it after the cache write so a reader that sees the new generation also sees
/// the new cache (the cache `Mutex` re-synchronizes the contents regardless).
#[cfg(all(target_os = "linux", feature = "drm"))]
static DRM_DISPLAY_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Snapshot a reader's enumerated displays as the IPC `DrmDisplayInfo` form. `displays()` lists all
/// device outputs regardless of the reader's target CRTC, so a capture reader can refresh the cache.
#[cfg(all(target_os = "linux", feature = "drm"))]
fn drm_displays_from_reader(reader: &mut scrap::drm_reader::DrmReader) -> Vec<DrmDisplayInfo> {
reader
.displays()
.into_iter()
// Only offer outputs actually bound to a CRTC (i.e. scanning out). A
// CONNECTED-but-unbound connector (e.g. a virtual/dummy HDMI plug the
// compositor is not driving) enumerates with `crtc_id == 0`. Such an
// entry has no scanout to capture, yet was still shipped to the client as
// a selectable monitor; picking it made libdrmtap's `open(crtc=0)`
// AUTO-SELECT the first active CRTC (the primary) and stream ITS frames at
// the wrong geometry (e.g. a 3840x2160 frame into a 1280x1024 encoder ->
// `src rect > dst rect`), which failed every frame and drove a ~1/sec
// capturer restart loop (the flap that leaked EGL contexts to OOM). Drop
// these here so they are never offered; the client keeps its real monitors.
.filter(|d| d.active && d.crtc_id != 0)
.map(|d| DrmDisplayInfo {
name: d.name,
crtc_id: d.crtc_id,
x: d.x,
y: d.y,
width: d.width,
height: d.height,
active: d.active,
})
.collect()
}
/// True if a kernel uevent datagram is a DRM-subsystem topology change (a connector hotplug/modeset).
/// A uevent is NUL-separated `KEY=value` records; we require `SUBSYSTEM=drm` plus a `change` action or
/// `HOTPLUG=1`, so an `add`/`remove` of an unrelated node (a render device, a fb) does not trigger a
/// re-enumeration. Byte-exact record matching avoids any allocation/UTF-8 handling on the hot recv path.
#[cfg(all(target_os = "linux", feature = "drm"))]
fn uevent_is_drm_change(msg: &[u8]) -> bool {
let mut is_drm = false;
let mut is_change = false;
for rec in msg.split(|&b| b == 0) {
if rec == b"SUBSYSTEM=drm" {
is_drm = true;
} else if rec == b"ACTION=change" || rec == b"HOTPLUG=1" {
is_change = true;
}
}
is_drm && is_change
}
/// Listen for DRM connector hotplug/modeset uevents and refresh the display cache when the topology
/// actually changes. Uses a raw `NETLINK_KOBJECT_UEVENT` socket (the same hotplug stream udev consumes)
/// so no libudev dependency is added; the root `--service` already runs privileged and joining the
/// kernel-uevent multicast group needs no extra cap. On a real change it re-enumerates (off any hot
/// path — this is a dedicated thread, so the blocking `open`/`displays` is fine), and only when the
/// enumerated set differs does it swap `DRM_DISPLAY_CACHE` and bump `DRM_DISPLAY_GENERATION`; live
/// `handle_drm_conn` loops then push the fresh list to their consumers. Best-effort: if the socket is
/// unavailable it logs and returns, and DRM capture still works (a consumer reconnect re-reads the
/// fresh list) — just without the mid-session live refresh.
#[cfg(all(target_os = "linux", feature = "drm"))]
fn drm_udev_listener() {
use hbb_common::libc;
use std::sync::atomic::Ordering;
let sock = unsafe {
libc::socket(
libc::AF_NETLINK,
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
libc::NETLINK_KOBJECT_UEVENT,
)
};
if sock < 0 {
log::info!(
"drm: udev uevent socket unavailable ({}); hotplug refresh disabled",
std::io::Error::last_os_error()
);
return;
}
// Own the fd so it is closed on every return / unwind path.
let _owned = unsafe { OwnedFd::from_raw_fd(sock) };
let mut addr: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
addr.nl_family = libc::AF_NETLINK as u16;
// Group 1 = kernel-originated uevents (udev re-broadcasts on group 2); pid 0 => kernel assigns.
addr.nl_groups = 1;
let rc = unsafe {
libc::bind(
sock,
&addr as *const libc::sockaddr_nl as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
)
};
if rc < 0 {
log::info!(
"drm: udev uevent bind failed ({}); hotplug refresh disabled",
std::io::Error::last_os_error()
);
return;
}
log::info!("drm: udev DRM-uevent listener started");
// Fixed-size receive buffer (preallocated model): a uevent is well under 8 KiB; a rare larger
// datagram is truncated by `recv` and simply re-enumerates on the next matching event.
let mut buf = [0u8; 8192];
loop {
// recvmsg (not recv) so the source address is available: bound to the kernel-uevent multicast
// group, a genuine uevent comes from the kernel (source nl_pid == 0) via a multicast group
// (nl_groups != 0). A local unprivileged process could otherwise UNICAST a spoofed
// "change@.../drm/..." datagram to this root listener and drive it to re-enumerate at will;
// dropping any non-kernel/non-multicast source closes that.
let mut src: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
let mut iov = libc::iovec {
iov_base: buf.as_mut_ptr() as *mut libc::c_void,
iov_len: buf.len(),
};
let mut mhdr: libc::msghdr = unsafe { std::mem::zeroed() };
mhdr.msg_name = &mut src as *mut libc::sockaddr_nl as *mut libc::c_void;
mhdr.msg_namelen = std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t;
mhdr.msg_iov = &mut iov;
mhdr.msg_iovlen = 1;
let n = unsafe { libc::recvmsg(sock, &mut mhdr, 0) };
if n <= 0 {
let err = std::io::Error::last_os_error();
if n < 0 && err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
log::info!("drm: udev uevent recv ended ({err}); hotplug refresh stopped");
break;
}
// Trust only a kernel-originated (nl_pid == 0), multicast-delivered (nl_groups != 0) datagram
// with a full source address; drop a unicast or user-spoofed message.
if (mhdr.msg_namelen as usize) < std::mem::size_of::<libc::sockaddr_nl>()
|| src.nl_pid != 0
|| src.nl_groups == 0
{
continue;
}
if !uevent_is_drm_change(&buf[..n as usize]) {
continue;
}
// Re-enumerate and diff. Only a real change swaps the cache + bumps the generation, so a
// uevent that does not alter the captured topology stays silent (no consumer churn).
if let Some(mut r) = scrap::drm_reader::DrmReader::open(None, 0) {
let fresh = drm_displays_from_reader(&mut r);
let changed = {
let mut cache = DRM_DISPLAY_CACHE.lock().unwrap();
if *cache != fresh {
*cache = fresh;
true
} else {
false
}
};
if changed {
DRM_DISPLAY_GENERATION.fetch_add(1, Ordering::Release);
log::info!("drm: connector topology changed (udev); display cache refreshed");
}
}
}
}
/// Best-effort warm-up at listener start: loads libdrmtap, initializes EGL, enumerates displays into
/// the cache, and maps the first framebuffer once. Moves that one-time cost (which otherwise lands
/// on the first consumer and can push the first frame past the client's initial-frame timeout) off
/// the critical path. Runs on its own thread since `DrmReader` is `!Send` and `open`/`grab` block.
#[cfg(all(target_os = "linux", feature = "drm"))]
fn drm_prewarm() {
let t = std::time::Instant::now();
match scrap::drm_reader::DrmReader::open(None, 0) {
Some(mut r) => {
let displays = drm_displays_from_reader(&mut r);
let n = displays.len();
// Warm the first framebuffer export. On the split path, grab_desc() exports a dma-buf fd
// WITHOUT loading libEGL/libGLESv2 into the root service (the convert now runs in the
// unprivileged --server); only an old .so (no grab_desc) still force-maps via grab().
if r.supports_grab_desc() {
if let Ok((fd, _desc)) = r.grab_desc() {
drop(fd); // close the warm-up fd; we only wanted to prime the device/import path
}
} else {
let _ = r.grab();
}
*DRM_DISPLAY_CACHE.lock().unwrap() = displays;
log::info!("drm: pre-warm ok ({n} displays) in {:?}", t.elapsed());
}
None => log::info!("drm: pre-warm skipped (reader unavailable)"),
}
}
/// DRM/KMS capture producer. Runs in the ROOT `--service` (which holds CAP_SYS_ADMIN, so libdrmtap
/// reads the scanout in-process — no helper, no setcap). One dedicated `current_thread` runtime
/// owns the `_drm` listener and `tokio::spawn`s a task per accepted consumer, so a multi-monitor
/// client (which opens one `_drm` connection per captured display) is served CONCURRENTLY instead
/// of serially. The `!Send` `DrmReader` never runs on this runtime: each connection offloads its
/// blocking `grab()` loop to a private std worker thread (see `handle_drm_conn`), which keeps the
/// connection future `Send` (thus spawnable) and lets the tasks multiplex on the one listener
/// thread while the workers capture in parallel.
#[cfg(all(target_os = "linux", feature = "drm"))]
#[tokio::main(flavor = "current_thread")]
pub async fn start_drm() {
match new_drm_listener().await {
Ok(mut incoming) => {
// Warm libdrmtap/EGL + enumeration off-thread so the first consumer does not pay that
// one-time cost on its critical path.
std::thread::spawn(drm_prewarm);
// Watch for connector hotplug/modeset uevents so a mid-session topology change refreshes
// the display cache and is pushed to live consumers (best-effort; own thread since it
// blocks on recv and re-enumeration is a blocking `!Send` open).
std::thread::spawn(drm_udev_listener);
loop {
match incoming.next().await {
Some(Ok(stream)) => {
tokio::spawn(async move {
if let Err(err) = handle_drm_conn(Connection::new(stream)).await {
log::info!("drm ipc connection ended: {}", err);
}
});
}
Some(Err(err)) => log::error!("Couldn't get drm client: {:?}", err),
// Stream exhausted: without this the `if let Some` form would re-poll the dead
// stream forever and busy-spin the root service. Stop the producer instead.
None => {
log::error!("drm ipc listener stream ended; stopping drm producer");
break;
}
}
}
}
Err(err) => {
log::error!("Failed to start drm ipc server: {}", err);
}
}
}
/// Handle one `_drm` consumer. `DrmReader` is `!Send` and `grab()` is a blocking C call, so it
/// cannot live on the shared listener runtime; this task spawns a private std worker thread that
/// owns the reader (`drm_capture_worker`) and streams `DrmProducerMsg`s back over a bounded channel
/// (capacity 2 = backpressure: a slow consumer throttles capture instead of growing memory). The
/// task itself stays fully async — hence `Send`, hence `tokio::spawn`able — and only forwards
/// messages to the wire. On any error / disconnect it returns; the `DrmStopGuard` plus dropping the
/// channels tears the worker down, and the client falls back to PipeWire/portal.
#[cfg(all(target_os = "linux", feature = "drm"))]
async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
// The `_drm` socket is world-connectable (0666) so the unprivileged `--server` can reach it,
// so we MUST authorize the peer here — this is a dedicated listener that does not go through
// the generic `start()` accept loop where service-scoped channels are checked. Same policy as
// `_service`: peer must be root or the active session uid, with a `/proc/pid/exe` identity
// match. Without this any local process could connect and receive the screen contents.
if !authorize_service_scoped_ipc_connection(&stream, "_drm") {
log::warn!("drm: rejected unauthorized connection to _drm");
return Ok(());
}
// Admission bound: each accepted _drm consumer spawns a worker thread that opens a DRM context.
// The peer is authorized (root/active-session), but we still cap concurrency so a buggy or
// compromised --server cannot exhaust root-service threads/memory by opening an unbounded number
// of streams. One connection per served display is plenty; MAX_DRM_CONNS covers multi-monitor
// plus a little slack for a reconnect overlapping an old worker still tearing down.
const MAX_DRM_CONNS: usize = 8;
static DRM_CONN_COUNT: AtomicUsize = AtomicUsize::new(0);
struct DrmConnGuard;
impl Drop for DrmConnGuard {
fn drop(&mut self) {
DRM_CONN_COUNT.fetch_sub(1, Ordering::SeqCst);
}
}
if DRM_CONN_COUNT.fetch_add(1, Ordering::SeqCst) >= MAX_DRM_CONNS {
DRM_CONN_COUNT.fetch_sub(1, Ordering::SeqCst);
log::warn!("drm: too many concurrent _drm connections (>= {MAX_DRM_CONNS}); rejecting");
return Ok(());
}
let _conn_guard = DrmConnGuard;
// Capture the peer uid now so the forward loop can RE-authorize every frame. The check above runs
// once at accept, but DRM/KMS capture is NOT session-scoped: `drm_capture_worker` grabs the
// physical scanout of a CRTC regardless of which session currently owns the display. So a stream
// authorized for one session must stop the moment the active session changes, or the outgoing
// user's --server keeps receiving the incoming user's screen (and the greeter in between) until
// the socket dies (review 3.3). `peer_uid` is the --server's fixed uid.
let peer_uid = stream.peer_uid();
// Move the authorized `_drm` stream onto the bespoke SCM_RIGHTS framing (see `DrmConn`). ALL
// further traffic — display list, `DrmStart`, frame descriptors + their ancillary fd, and the
// cursor / CPU-fallback bodies — goes through `conn` so no `Framed` read buffer ever competes with
// a `recvmsg` for the fd. The parity `Connection` (used only for the authorization above) is
// dropped here, closing its fd; the dup inside `conn` keeps the socket alive.
let mut conn = dup_to_drm_conn(&stream)?;
drop(stream);
// worker -> task: display list, frames, cursor (bounded = backpressure).
let (frame_tx, mut frame_rx) = tokio::sync::mpsc::channel::<DrmProducerMsg>(2);
// task -> worker: the chosen CRTC + whether the consumer needs the CPU path, sent once after the
// client's DrmStart.
let (crtc_tx, crtc_rx) = std::sync::mpsc::channel::<(u32, bool)>();
let stop = Arc::new(AtomicBool::new(false));
let _stop_guard = DrmStopGuard(stop.clone());
let worker_stop = stop.clone();
std::thread::spawn(move || drm_capture_worker(frame_tx, crtc_rx, worker_stop));
// Handshake: the worker sends the display list (from the pre-warmed cache, or a throwaway
// enumeration open if the cache is empty). A closed channel (no Displays) means the reader was
// unavailable, so let the client fall back.
let displays = match frame_rx.recv().await {
Some(DrmProducerMsg::Displays(d)) => d,
_ => {
log::info!("drm: reader unavailable; closing _drm connection (client falls back)");
return Ok(());
}
};
conn.send_msg(&Data::DrmDisplayList(displays.clone()), None).await?;
// Wait for the client to choose a display before streaming. `recv_msg_timeout2` gates only the
// wait for the first byte, so a timeout leaves the stream at a clean frame boundary.
let (display_idx, need_cpu) = loop {
match conn.recv_msg_timeout2(10_000).await {
Some(Ok((Data::DrmStart { display, need_cpu }, _fd))) => break (display, need_cpu),
Some(Ok((_, _fd))) => continue, // ignore unexpected messages; drop any stray fd
Some(Err(e)) => return Err(e),
None => return Ok(()), // timed out: client never chose a display
}
};
// Resolve the chosen display's CRTC. `displays` here is already filtered to
// CRTC-bound outputs (see drm_displays_from_reader), so a valid selection
// always yields a non-zero crtc_id. Reject a 0 (out-of-range index, or an
// unbound display that somehow slipped through) rather than passing it to
// `open(crtc=0)`, whose "auto-select the first/primary CRTC" sentinel would
// silently stream the WRONG monitor at a mismatched geometry and flap the
// capturer. Closing lets the consumer fall back (PipeWire) for that display.
let target_crtc = usize::try_from(display_idx)
.ok()
.and_then(|i| displays.get(i))
.map(|d| d.crtc_id)
.unwrap_or(0);
if target_crtc == 0 {
log::warn!(
"drm: client selected display {display_idx} with no bound CRTC; closing _drm (client falls back)"
);
return Ok(());
}
// Hand the CRTC + the consumer's CPU-path request to the worker; an error means it already gave up
// (reader vanished).
if crtc_tx.send((target_crtc, need_cpu)).is_err() {
return Ok(());
}
// Forward frames + cursor updates until the worker ends or the client disconnects (a wire send
// error on a dropped client propagates out and tears the worker down via the guard). The
// per-connection `ExportLedger` decides, for the zero-copy path, whether each frame's fd must ride
// an SCM_RIGHTS cmsg or can be elided as an import-once cache hit.
let mut ledger = ExportLedger::new();
// Live hotplug: the udev listener bumps DRM_DISPLAY_GENERATION when the connector topology changes.
// Seed from the value current at handshake (the list already sent reflects it) and, whenever it
// moves, push the fresh list to this consumer. Piggybacked on the frame cadence so it costs only one
// atomic load per frame; a genuinely idle stream tears down after MAX_STALLED and the consumer
// reconnects to a fresh list anyway.
let mut seen_gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
while let Some(first) = frame_rx.recv().await {
// Re-authorize per frame (review 3.3): root (0) is always allowed; any other peer must still
// be the active-session uid. On a session change the outgoing --server no longer matches, so
// we stop within one frame (~33ms) instead of streaming the new session's screen to it. Fail
// closed if the peer uid cannot be determined.
match peer_uid {
Some(0) => {}
Some(uid) if Some(uid) == active_uid() => {}
_ => {
log::warn!("drm: _drm peer no longer matches the active session; closing");
break;
}
}
let gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
// Keep the ledger's epoch at the live generation so a hotplug/modeset (which may recycle an
// fb_id onto a new buffer) invalidates every cached buffer and forces a real fd on the next
// frame. Cheap (one field write) and only observable when DRM_FD_ELISION is enabled.
ledger.epoch = gen;
if gen != seen_gen {
seen_gen = gen;
let fresh = DRM_DISPLAY_CACHE.lock().unwrap().clone();
if !fresh.is_empty() {
conn.send_msg(&Data::DrmDisplaysChanged(fresh), None).await?;
}
}
// Coalesce to latest-wins at the source (review 4.8). The `_drm` socket is a FIFO, so a
// consumer that drains slower than we produce (a 4K convert on a modest GPU) would fall
// seconds behind stale frames. Drain everything already queued without blocking and forward
// only the NEWEST frame; each replaced frame drops here, closing its OwnedFd (zero-copy path)
// and freeing its pixel buffer (CPU path). Cursor updates are latency-insensitive state
// (latest-wins by id downstream), so they are forwarded in order and never coalesced away.
let mut latest_frame: Option<DrmProducerMsg> = None;
let mut msg = Some(first);
while let Some(m) = msg.take() {
match m {
f @ (DrmProducerMsg::Frame { .. } | DrmProducerMsg::FrameCpu { .. }) => {
latest_frame = Some(f);
}
DrmProducerMsg::Cursor {
id,
width,
height,
hotx,
hoty,
colors,
} => {
conn.send_msg(
&Data::DrmCursor {
id,
width,
height,
hotx,
hoty,
},
None,
)
.await?;
conn.send_raw(Bytes::from(colors)).await?;
}
DrmProducerMsg::Displays(_) => {}
}
msg = frame_rx.try_recv().ok();
}
match latest_frame {
Some(DrmProducerMsg::Frame { mut desc, fd }) => {
// The worker always supplies a real fd; the ledger decides whether to attach it.
let send_fd = fd.is_some() && ledger.should_send_fd(&desc);
desc.has_fd = send_fd;
let borrowed = if send_fd { fd.as_ref().map(|f| f.as_fd()) } else { None };
conn.send_msg(&Data::DrmFrameDmabuf(desc), borrowed).await?;
// `fd` (OwnedFd) is closed here whether or not it was attached (the cmsg dup'd it into
// the peer). Closing immediately bounds our fd usage to ~1 in flight per frame.
}
Some(DrmProducerMsg::FrameCpu {
width,
height,
data,
}) => {
// CPU-mapped fallback: pixels cross the wire, exactly like the pre-split protocol.
conn.send_msg(&Data::DrmFrame { width, height }, None).await?;
conn.send_raw(data).await?;
}
_ => {}
}
}
Ok(())
}
/// The blocking half of a `_drm` connection: owns the `!Send` `DrmReader`(s) on its own thread and
/// streams messages to the async task. Ends (thread exits, reader closes) when the device is
/// unavailable, errors/stalls, or the task drops the channels / sets the stop flag.
#[cfg(all(target_os = "linux", feature = "drm"))]
fn drm_capture_worker(
frame_tx: tokio::sync::mpsc::Sender<DrmProducerMsg>,
crtc_rx: std::sync::mpsc::Receiver<(u32, bool)>,
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
) {
use std::sync::atomic::Ordering;
use std::time::Duration;
// ~30 fps producer ceiling; the consumer's encoder/QoS sets the effective rate and the bounded
// channel throttles us further if it is slower. Also avoids a busy-spin when `grab()` returns
// the same scanout repeatedly.
const FRAME_INTERVAL: Duration = Duration::from_millis(33);
// Bound continuous no-frame (WouldBlock) time so a wedged device ends the stream (~5s) instead
// of freezing forever; the client then falls back.
const MAX_STALLED: u32 = 150;
let t_conn = std::time::Instant::now();
// Send the display list. Prefer the pre-warmed cache (skips a per-connection enumeration open);
// fall back to a throwaway enumeration reader if the pre-warm has not populated it yet.
let displays = {
let cached = DRM_DISPLAY_CACHE.lock().unwrap().clone();
if !cached.is_empty() {
cached
} else {
let mut enum_reader = match scrap::drm_reader::DrmReader::open(None, 0) {
Some(r) => r,
None => return,
};
drm_displays_from_reader(&mut enum_reader)
}
};
if frame_tx
.blocking_send(DrmProducerMsg::Displays(displays))
.is_err()
{
return;
}
// Wait for the task to relay the client's chosen CRTC + CPU-path request (Err => the task gave up
// / disconnected).
let (target_crtc, need_cpu) = match crtc_rx.recv() {
Ok(c) => c,
Err(_) => return,
};
let t_open = std::time::Instant::now();
let mut reader = match scrap::drm_reader::DrmReader::open(None, target_crtc) {
Some(r) => r,
None => {
log::warn!("drm: failed to open crtc {target_crtc}; closing _drm connection");
// The cached display list handed out a CRTC that no longer opens (a hotplug/modeset
// likely invalidated it). Drop the cache so the next connection re-enumerates from the
// live device instead of serving the same stale, unopenable CRTC on every reconnect.
DRM_DISPLAY_CACHE.lock().unwrap().clear();
return;
}
};
// Refresh the cache from the live device so the next consumer's handshake uses fresh geometry.
*DRM_DISPLAY_CACHE.lock().unwrap() = drm_displays_from_reader(&mut reader);
log::debug!(
"drm: capture reader for crtc {target_crtc} opened in {:?}",
t_open.elapsed()
);
// A per-connection buffer-pool epoch so `buffer_id` is unique across connections even for the same
// fb_id (the consumer may key a pool by buffer_id).
static DRM_CONN_EPOCH: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let conn_epoch = DRM_CONN_EPOCH.fetch_add(1, Ordering::Relaxed);
// Prefer the zero-copy split export (root does NO EGL / convert / copy). Fall back to the
// CPU-mapped path for this connection (pixels cross the wire) when: the loaded libdrmtap predates
// the split API, grab_desc later reports ENOTSUP (no transferable dma-buf on this seat), OR the
// consumer asked for the CPU path because it has no render-node convert context (need_cpu) — in
// that last case the dma-buf fd would be useless to it and the stream would be lost.
let mut use_dmabuf = reader.supports_grab_desc() && !need_cpu;
let mut last_cursor_id: u64 = 0;
let mut stalled: u32 = 0;
let mut logged_first = false;
while !stop.load(Ordering::Relaxed) {
// Grab one frame in the current mode, producing an OWNED message (no borrow of `reader`
// outlives this, so `reader.cursor()` below is free to run). The dma-buf path ships only the
// descriptor + fd; the CPU path copies the packed BGRA once (Bytes::copy_from_slice).
let grabbed: std::io::Result<DrmProducerMsg> = if use_dmabuf {
match reader.grab_desc() {
Ok((fd, d)) => Ok(DrmProducerMsg::Frame {
desc: DmabufDesc {
buffer_id: (d.fb_id as u64) | ((conn_epoch as u64) << 32),
width: d.width,
height: d.height,
format: d.format,
modifier: d.modifier,
fb_id: d.fb_id,
num_planes: d.num_planes,
offsets: d.offsets,
pitches: d.pitches,
hdr_eotf: d.hdr_eotf,
hdr_max_nits: d.hdr_max_nits,
has_fd: true, // the async task's ExportLedger may downgrade this
},
fd: Some(fd),
}),
Err(err) => Err(err),
}
} else {
match reader.grab() {
Ok((buf, w, h)) => Ok(DrmProducerMsg::FrameCpu {
width: w as u32,
height: h as u32,
data: Bytes::copy_from_slice(buf),
}),
Err(err) => Err(err),
}
};
match grabbed {
Ok(msg) => {
stalled = 0;
if !logged_first {
logged_first = true;
log::debug!(
"drm: first frame for crtc {target_crtc} in {:?} ({} path)",
t_conn.elapsed(),
if use_dmabuf { "dma-buf" } else { "cpu" }
);
}
if frame_tx.blocking_send(msg).is_err() {
break;
}
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
stalled += 1;
if stalled > MAX_STALLED {
log::info!("drm: capture stalled (no frame); closing _drm connection");
break;
}
std::thread::sleep(FRAME_INTERVAL);
continue;
}
Err(err) if use_dmabuf && err.kind() == std::io::ErrorKind::Unsupported => {
// The split export cannot work on this seat/driver (ENOTSUP). Switch this connection
// to the CPU-mapped fallback (pixels over the wire) instead of tearing down or
// rebuild-looping; the reader is already open and usable via grab().
log::warn!(
"drm: grab_desc unsupported ({err}); switching to CPU-mapped fallback for this connection"
);
use_dmabuf = false;
logged_first = false;
continue;
}
Err(err) => {
log::warn!("drm: capture error: {err}; closing _drm connection");
break;
}
}
// Ship the cursor shape only when it changes (id is a content hash or the hidden sentinel).
if let Some(c) = reader.cursor() {
if c.id != last_cursor_id {
last_cursor_id = c.id;
if frame_tx
.blocking_send(DrmProducerMsg::Cursor {
id: c.id,
width: c.width,
height: c.height,
hotx: c.hotx,
hoty: c.hoty,
colors: c.colors,
})
.is_err()
{
break;
}
}
}
std::thread::sleep(FRAME_INTERVAL);
}
}
pub struct ConnectionTmpl<T> {
inner: Framed<T, BytesCodec>,
}
pub type Connection = ConnectionTmpl<Conn>;
impl<T> ConnectionTmpl<T>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
{
pub fn new(conn: T) -> Self {
Self {
inner: Framed::new(conn, BytesCodec::new()),
}
}
pub async fn send(&mut self, data: &Data) -> ResultType<()> {
let v = serde_json::to_vec(data)?;
self.inner.send(bytes::Bytes::from(v)).await?;
Ok(())
}
async fn send_config(&mut self, name: &str, value: String) -> ResultType<()> {
self.send(&Data::Config((name.to_owned(), Some(value))))
.await
}
pub async fn next_timeout(&mut self, ms_timeout: u64) -> ResultType<Option<Data>> {
Ok(timeout(ms_timeout, self.next()).await??)
}
pub async fn next_timeout2(&mut self, ms_timeout: u64) -> Option<ResultType<Option<Data>>> {
if let Ok(x) = timeout(ms_timeout, self.next()).await {
Some(x)
} else {
None
}
}
pub async fn next(&mut self) -> ResultType<Option<Data>> {
match self.inner.next().await {
Some(res) => {
let bytes = res?;
if let Ok(s) = std::str::from_utf8(&bytes) {
if let Ok(data) = serde_json::from_str::<Data>(s) {
return Ok(Some(data));
}
}
return Ok(None);
}
_ => {
bail!("reset by the peer");
}
}
}
pub async fn send_raw(&mut self, data: Bytes) -> ResultType<()> {
self.inner.send(data).await?;
Ok(())
}
pub async fn next_raw(&mut self) -> ResultType<bytes::BytesMut> {
match self.inner.next().await {
Some(Ok(res)) => Ok(res),
_ => {
bail!("reset by the peer");
}
}
}
}
/// Ancillary-fd transport for the `_drm` channel.
///
/// `ConnectionTmpl`'s `Framed<_, BytesCodec>` cannot carry (nor collect) an SCM_RIGHTS control
/// message: tokio's `AsyncRead` never does a `recvmsg` with a control buffer, so a fd sent alongside
/// a `Framed` byte-frame is silently dropped on receive, and interleaving a raw `sendmsg` with the
/// codec desyncs its internal read buffer. So the WHOLE `_drm` channel moves onto this bespoke
/// length-prefixed `sendmsg`/`recvmsg` framing, owning the raw `tokio::net::UnixStream` directly:
/// handshake (`DrmDisplayList`/`DrmStart`), frame descriptors, and the CPU-fallback/cursor bodies all
/// go through it so no `Framed` read buffer ever competes with a `recvmsg`.
///
/// Framing: each frame is a 4-byte big-endian length prefix + payload. `send_msg`/`recv_msg` carry a
/// JSON `Data`; `send_raw`/`next_raw` carry an opaque body. The dma-buf fd (when present) rides an
/// SCM_RIGHTS cmsg bound to the frame's first (prefix) byte, so reading the prefix with a control
/// buffer reliably collects it (`MSG_CTRUNC` is rejected). Reads use exact-length loops so they never
/// cross a frame boundary and thus never discard a following frame's ancillary fd.
#[cfg(all(target_os = "linux", feature = "drm"))]
pub struct DrmConn {
/// The raw stream. Obtained from `connect_drm` (client) or the accepted `_drm` listener stream
/// (service). All framing is done by hand on this fd; there is no `Framed` codec.
stream: tokio::net::UnixStream,
/// Grow-once accumulation buffer for `recv_msg`/`next_raw` length-prefixed reads (preallocated
/// model: it grows to the largest frame seen and is then reused, never per-frame reallocated).
read_buf: Vec<u8>,
}
/// Cap on a JSON `Data` message read by `recv_msg` (headers/handshake are tiny; this only bounds a
/// hostile/oversized length prefix). Distinct from the raw-body cap because a body can be a whole
/// CPU-fallback frame.
#[cfg(all(target_os = "linux", feature = "drm"))]
const MAX_DRM_JSON_BYTES: usize = 8 * 1024 * 1024;
/// Cap on a raw body read by `next_raw` (CPU-fallback BGRA / cursor RGBA). Covers a 256 MiB 8K
/// scanout (`DrmReader` bounds a frame to that) with margin.
#[cfg(all(target_os = "linux", feature = "drm"))]
const MAX_DRM_RAW_BYTES: usize = 512 * 1024 * 1024;
/// Control-buffer capacity for one SCM_RIGHTS cmsg carrying a single fd. `CMSG_SPACE(sizeof(int))` is
/// 24 bytes on our targets; 64 gives headroom and the `align(8)` matches `cmsghdr` alignment.
#[cfg(all(target_os = "linux", feature = "drm"))]
const DRM_CMSG_CAP: usize = 64;
/// Aligned storage for the SCM_RIGHTS control buffer (`msg_control` must be `cmsghdr`-aligned).
#[cfg(all(target_os = "linux", feature = "drm"))]
#[repr(align(8))]
struct DrmCmsgBuf([u8; DRM_CMSG_CAP]);
/// One non-blocking `sendmsg`: writes `buf` and, when `pass_fd` is `Some`, attaches exactly one
/// SCM_RIGHTS cmsg carrying that fd. The cmsg is attached ONLY when a fd is present (a -1 fd in an
/// SCM_RIGHTS cmsg fails the whole call). Returns bytes sent, or a `WouldBlock`/other io error.
///
/// SAFETY: `fd` must be a valid open socket fd; `buf` a valid readable slice; `pass_fd` (if any) a
/// valid open fd. The ancillary data is delivered by the kernel with the first byte of `buf`.
#[cfg(all(target_os = "linux", feature = "drm"))]
unsafe fn drm_sendmsg(fd: RawFd, buf: &[u8], pass_fd: Option<RawFd>) -> std::io::Result<usize> {
use hbb_common::libc;
let mut iov = libc::iovec {
iov_base: buf.as_ptr() as *mut libc::c_void,
iov_len: buf.len(),
};
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
let mut cbuf = DrmCmsgBuf([0u8; DRM_CMSG_CAP]);
if let Some(sfd) = pass_fd {
msg.msg_control = cbuf.0.as_mut_ptr() as *mut libc::c_void;
msg.msg_controllen = libc::CMSG_SPACE(std::mem::size_of::<libc::c_int>() as u32) as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
// Sized above so CMSG_FIRSTHDR is non-null; guard anyway to avoid UB on any platform quirk.
if cmsg.is_null() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"drm: CMSG_FIRSTHDR null",
));
}
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<libc::c_int>() as u32) as _;
let sfd_c: libc::c_int = sfd;
std::ptr::copy_nonoverlapping(
&sfd_c as *const libc::c_int as *const u8,
libc::CMSG_DATA(cmsg),
std::mem::size_of::<libc::c_int>(),
);
}
let n = libc::sendmsg(fd, &msg, libc::MSG_NOSIGNAL);
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
/// One non-blocking `recvmsg` into `buf` with a control buffer. Collects at most one SCM_RIGHTS fd
/// (any surplus fds are closed); rejects a truncated cmsg (`MSG_CTRUNC`) as a hard error after closing
/// whatever it parsed. Returns (bytes read, fd). Received fds are `O_CLOEXEC` (`MSG_CMSG_CLOEXEC`).
///
/// SAFETY: `fd` must be a valid open socket fd; `buf` a valid writable slice.
#[cfg(all(target_os = "linux", feature = "drm"))]
unsafe fn drm_recvmsg(fd: RawFd, buf: &mut [u8]) -> std::io::Result<(usize, Option<OwnedFd>)> {
use hbb_common::libc;
let mut iov = libc::iovec {
iov_base: buf.as_mut_ptr() as *mut libc::c_void,
iov_len: buf.len(),
};
let mut cbuf = DrmCmsgBuf([0u8; DRM_CMSG_CAP]);
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = cbuf.0.as_mut_ptr() as *mut libc::c_void;
msg.msg_controllen = cbuf.0.len() as _;
let n = libc::recvmsg(fd, &mut msg, libc::MSG_CMSG_CLOEXEC);
if n < 0 {
return Err(std::io::Error::last_os_error());
}
// Walk the cmsgs; keep the first SCM_RIGHTS fd, close any extras. Each parsed int is wrapped in an
// OwnedFd immediately so it is always closed on drop (no fd leak on any error path below).
let mut got: Option<OwnedFd> = None;
let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
while !cmsg.is_null() {
if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS {
let data = libc::CMSG_DATA(cmsg);
let hdr = libc::CMSG_LEN(0) as usize;
let payload = ((*cmsg).cmsg_len as usize).saturating_sub(hdr);
let count = payload / std::mem::size_of::<libc::c_int>();
for i in 0..count {
let mut rawfd: libc::c_int = -1;
std::ptr::copy_nonoverlapping(
data.add(i * std::mem::size_of::<libc::c_int>()),
&mut rawfd as *mut libc::c_int as *mut u8,
std::mem::size_of::<libc::c_int>(),
);
if rawfd >= 0 {
let owned = OwnedFd::from_raw_fd(rawfd);
if got.is_none() {
got = Some(owned);
} // else: surplus fd, dropped here -> closed
}
}
}
cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
}
// A truncated control message means the kernel dropped fd(s) that did not fit: fail rather than
// proceed with a missing/partial fd (drop `got` so anything parsed is closed first).
if msg.msg_flags & libc::MSG_CTRUNC != 0 {
drop(got);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"drm: truncated SCM_RIGHTS control message (MSG_CTRUNC)",
));
}
Ok((n as usize, got))
}
/// Write all of `buf` to `stream`, attaching `pass_fd` (if any) to the FIRST byte (the kernel binds
/// SCM_RIGHTS ancillary to the first data byte of the `sendmsg` that carried it). Loops on
/// `WouldBlock` via `writable()`; the fd is attached only until the first `sendmsg` sends >= 1 byte.
#[cfg(all(target_os = "linux", feature = "drm"))]
async fn drm_write_all(
stream: &tokio::net::UnixStream,
mut buf: &[u8],
mut pass_fd: Option<RawFd>,
) -> ResultType<()> {
while !buf.is_empty() {
stream.writable().await?;
let raw = stream.as_raw_fd();
let chunk = buf;
let fd_now = pass_fd;
match stream.try_io(tokio::io::Interest::WRITABLE, || unsafe {
drm_sendmsg(raw, chunk, fd_now)
}) {
Ok(0) => bail!("drm: socket write returned 0 (peer closed)"),
Ok(n) => {
pass_fd = None; // ancillary delivered with these bytes; do not re-send it
buf = &buf[n..];
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e.into()),
}
}
Ok(())
}
/// Write one length-prefixed frame: a 4-byte big-endian length + payload, with `pass_fd` (if any)
/// riding the prefix's first byte.
#[cfg(all(target_os = "linux", feature = "drm"))]
async fn drm_send_frame(
stream: &tokio::net::UnixStream,
payload: &[u8],
pass_fd: Option<RawFd>,
) -> ResultType<()> {
if payload.len() > u32::MAX as usize {
bail!("drm: frame too large ({} bytes)", payload.len());
}
let prefix = (payload.len() as u32).to_be_bytes();
// The fd rides the prefix (its first byte); the payload carries no ancillary.
drm_write_all(stream, &prefix, pass_fd).await?;
drm_write_all(stream, payload, None).await?;
Ok(())
}
/// Read exactly `buf.len()` bytes from `stream`. When `want_cmsg` is true, the FIRST read uses a
/// control buffer to collect an SCM_RIGHTS fd (which the sender bound to the frame's first byte);
/// subsequent reads within the same frame are plain. Returns the collected fd, if any.
#[cfg(all(target_os = "linux", feature = "drm"))]
async fn drm_read_full(
stream: &tokio::net::UnixStream,
buf: &mut [u8],
want_cmsg: bool,
) -> ResultType<Option<OwnedFd>> {
use hbb_common::libc;
let mut off = 0usize;
let mut got: Option<OwnedFd> = None;
while off < buf.len() {
stream.readable().await?;
let raw = stream.as_raw_fd();
// Only the first read of a frame carries the fd (bound to byte 0); after that, plain reads.
let use_cmsg = want_cmsg && got.is_none();
let n = {
let dst: &mut [u8] = &mut buf[off..];
match stream.try_io(tokio::io::Interest::READABLE, move || unsafe {
if use_cmsg {
drm_recvmsg(raw, dst)
} else {
let m = libc::read(raw, dst.as_mut_ptr() as *mut libc::c_void, dst.len());
if m < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok((m as usize, None))
}
}
}) {
Ok((0, _fd)) => bail!("drm: socket closed by peer"),
Ok((m, fd)) => {
if let Some(f) = fd {
if got.is_none() {
got = Some(f);
}
}
m
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e.into()),
}
};
off += n;
}
Ok(got)
}
#[cfg(all(target_os = "linux", feature = "drm"))]
impl DrmConn {
/// Take ownership of an already-connected/accepted raw `_drm` stream.
pub fn new(stream: tokio::net::UnixStream) -> Self {
Self {
stream,
read_buf: Vec::new(),
}
}
/// Send one `Data` message (JSON, length-prefixed). When `fd` is `Some`, attach exactly one
/// SCM_RIGHTS cmsg carrying that fd on the SAME frame as the payload (a -1 in an SCM_RIGHTS cmsg
/// fails the whole call, so the cmsg is attached ONLY when a fd is present). `fd` is borrowed so
/// the caller keeps ownership and closes it after the send has dup'd it into the peer.
pub async fn send_msg(&mut self, data: &Data, fd: Option<BorrowedFd<'_>>) -> ResultType<()> {
let payload = serde_json::to_vec(data)?;
let pass_fd = fd.map(|f| f.as_raw_fd());
drm_send_frame(&self.stream, &payload, pass_fd).await
}
/// Receive one `Data` message plus any dma-buf fd delivered via SCM_RIGHTS. Reads the 4-byte
/// length prefix (with a `CMSG_SPACE(size_of::<c_int>())` control buffer that collects the fd bound
/// to the frame's first byte, rejecting `MSG_CTRUNC`), then the payload into the reusable
/// `read_buf`. Returns the decoded `Data` and an `OwnedFd` iff one arrived.
pub async fn recv_msg(&mut self) -> ResultType<(Data, Option<OwnedFd>)> {
let mut prefix = [0u8; 4];
let fd = drm_read_full(&self.stream, &mut prefix, true).await?;
let len = u32::from_be_bytes(prefix) as usize;
if len > MAX_DRM_JSON_BYTES {
// `fd` (if any) is closed on drop.
bail!("drm: message length {len} exceeds cap {MAX_DRM_JSON_BYTES}");
}
if self.read_buf.len() < len {
self.read_buf.resize(len, 0);
}
// Disjoint field borrows: &self.stream (read) + &mut self.read_buf (dest). No fd on the body.
drm_read_full(&self.stream, &mut self.read_buf[..len], false).await?;
let data: Data = serde_json::from_slice(&self.read_buf[..len])?;
Ok((data, fd))
}
/// Cancel-safe timeout wrapper around `recv_msg`, mirroring `ConnectionTmpl::next_timeout2`, so a
/// dropped consumer re-checks its `stop` flag between frames. `None` when no frame started within
/// the window (a clean boundary: `readable()` consumes nothing, so re-polling is safe). Once a byte
/// is available the frame has started, so the SAME budget also bounds the body read: a peer that
/// sends one byte then stalls cannot pin this task forever (the `readable()` gate alone does not
/// cover the length prefix or payload). A body that overruns the budget is a hard error, not a
/// `None`, because the frame is partially consumed and cannot be safely resumed -- the caller tears
/// the stream down. `recv_msg` bodies are small length-prefixed JSON (<= MAX_DRM_JSON_BYTES), well
/// under any caller's budget over a local socket, so this never trips a healthy peer.
pub async fn recv_msg_timeout2(
&mut self,
ms_timeout: u64,
) -> Option<ResultType<(Data, Option<OwnedFd>)>> {
// Bind the readiness result to a `let` so the borrowed `readable()` future temporary is dropped
// at the `;` (releasing `&self.stream`) BEFORE `recv_msg()` takes `&mut self` in an arm.
let ready = timeout(ms_timeout, self.stream.readable()).await;
match ready {
Err(_) => None, // no frame started: clean boundary, caller re-checks `stop`
Ok(Err(e)) => Some(Err(e.into())),
Ok(Ok(())) => match timeout(ms_timeout, self.recv_msg()).await {
Ok(res) => Some(res),
Err(_) => Some(Err(anyhow::anyhow!(
"drm: frame body stalled past {ms_timeout}ms after first byte; closing"
))),
},
}
}
/// Send a raw length-prefixed body (cursor pixels, CPU-fallback BGRA). Parity with
/// `ConnectionTmpl::send_raw`, over the same manual framing (never carries an fd).
pub async fn send_raw(&mut self, data: Bytes) -> ResultType<()> {
drm_send_frame(&self.stream, &data, None).await
}
/// Receive a raw length-prefixed body. Parity with `ConnectionTmpl::next_raw`. A raw body never
/// carries an fd; a stray fd (protocol desync) is collected by `drm_read_full` and dropped/closed.
pub async fn next_raw(&mut self) -> ResultType<bytes::BytesMut> {
let mut prefix = [0u8; 4];
if drm_read_full(&self.stream, &mut prefix, true).await?.is_some() {
log::warn!("drm: unexpected fd on a raw-body frame; dropping");
}
let len = u32::from_be_bytes(prefix) as usize;
if len > MAX_DRM_RAW_BYTES {
bail!("drm: raw body length {len} exceeds cap {MAX_DRM_RAW_BYTES}");
}
let mut out = bytes::BytesMut::new();
out.resize(len, 0);
drm_read_full(&self.stream, &mut out[..], false).await?;
Ok(out)
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn get_config(name: &str) -> ResultType<Option<String>> {
get_config_async(name, 1_000).await
}
async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType<Option<String>> {
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::Config((name.to_owned(), None))).await?;
if let Some(Data::Config((name2, value))) = c.next_timeout(ms_timeout).await? {
if name == name2 {
return Ok(value);
}
}
return Ok(None);
}
pub async fn set_config_async(name: &str, value: String) -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send_config(name, value).await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_data(data: &Data) -> ResultType<()> {
set_data_async(data).await
}
async fn set_data_async(data: &Data) -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(data).await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_config(name: &str, value: String) -> ResultType<()> {
set_config_async(name, value).await
}
pub fn update_temporary_password() -> ResultType<()> {
set_config("temporary-password", "".to_owned())
}
fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> {
let Some(payload) = payload else {
return Ok(());
};
let Some((storage, salt)) = payload.split_once('\n') else {
bail!("Invalid permanent-password-storage-and-salt payload");
};
Config::set_permanent_password_storage_for_sync(storage, salt)?;
Ok(())
}
pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> {
let v = get_config("permanent-password-storage-and-salt")?;
apply_permanent_password_storage_and_salt_payload(v.as_deref())
}
async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> {
let ms_timeout = 1_000;
let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?;
apply_permanent_password_storage_and_salt_payload(v.as_deref())
}
pub fn is_permanent_password_set() -> bool {
match get_config("permanent-password-set") {
Ok(Some(v)) => {
let v = v.trim();
return v == "Y";
}
Ok(None) => {
// No response/value (timeout).
}
Err(_) => {
// Connection error.
}
}
log::warn!("Failed to query permanent password state from daemon");
false
}
pub fn is_permanent_password_preset() -> bool {
if let Ok(Some(v)) = get_config("permanent-password-is-preset") {
let v = v.trim();
return v == "Y";
}
false
}
pub fn get_fingerprint() -> String {
get_config("fingerprint")
.unwrap_or_default()
.unwrap_or_default()
}
pub fn set_permanent_password(v: String) -> ResultType<()> {
if Config::is_disable_change_permanent_password() {
bail!("Changing permanent password is disabled");
}
if set_permanent_password_with_ack(v)? {
Ok(())
} else {
bail!("Changing permanent password was rejected by daemon");
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_permanent_password_with_ack(v: String) -> ResultType<bool> {
set_permanent_password_with_ack_async(v).await
}
async fn set_permanent_password_with_ack_async(v: String) -> ResultType<bool> {
// The daemon ACK/NACK is expected quickly since it applies the config in-process.
let ms_timeout = 1_000;
let mut c = connect(ms_timeout, "").await?;
c.send_config("permanent-password", v).await?;
if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? {
if name2 == "permanent-password" {
let v = v.trim();
let ok = v == "Y";
if ok {
// Ensure the hashed permanent password storage is written to the user config file.
// This sync must not affect the daemon ACK outcome.
if let Err(err) = sync_permanent_password_storage_from_daemon_async().await {
log::warn!("Failed to sync permanent password storage from daemon: {err}");
}
}
return Ok(ok);
}
}
Ok(false)
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> {
let v = v.trim().to_owned();
let min_len = 4;
let max_len = crate::ui_interface::max_encrypt_len();
let len = v.chars().count();
if !v.is_empty() {
if len < min_len {
let err = if translate {
crate::lang::translate(
"Requires at least {".to_string() + &format!("{min_len}") + "} characters",
)
} else {
// Sometimes, translated can't show normally in command line
format!("Requires at least {} characters", min_len)
};
bail!(err);
}
if len > max_len {
bail!("No more than {max_len} characters");
}
}
Config::set_unlock_pin(&v);
set_config("unlock-pin", v)
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_unlock_pin() -> String {
if let Ok(Some(v)) = get_config("unlock-pin") {
Config::set_unlock_pin(&v);
v
} else {
Config::get_unlock_pin()
}
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_trusted_devices() -> String {
if let Ok(Some(v)) = get_config("trusted-devices") {
v
} else {
Config::get_trusted_devices_json()
}
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn remove_trusted_devices(hwids: Vec<Bytes>) {
Config::remove_trusted_devices(&hwids);
allow_err!(set_data(&Data::RemoveTrustedDevices(hwids)));
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn clear_trusted_devices() {
Config::clear_trusted_devices();
allow_err!(set_data(&Data::ClearTrustedDevices));
}
pub fn get_id() -> String {
// An empty id may come from a process that took over the main IPC with a
// config scope that has no id yet (e.g. a user GUI that became the server
// while the installed service was restarting). Treat it as no answer,
// otherwise the empty id is adopted below and wipes the local one.
if let Ok(Some(v)) = get_config("id") {
if !v.is_empty() {
// update salt also, so that next time reinstallation not causing first-time auto-login failure
if let Ok(Some(v2)) = get_config("salt") {
Config::set_salt(&v2);
}
if v != Config::get_id() {
Config::set_key_confirmed(false);
Config::set_id(&v);
}
return v;
}
}
Config::get_id()
}
pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<String>) {
if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await {
let mut urls = v.split(",");
let a = urls.next().unwrap_or_default().to_owned();
let b: Vec<String> = urls.map(|x| x.to_owned()).collect();
(a, b)
} else {
(
Config::get_rendezvous_server(),
Config::get_rendezvous_servers(),
)
}
}
async fn get_options_(ms_timeout: u64) -> ResultType<HashMap<String, String>> {
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::Options(None)).await?;
if let Some(Data::Options(Some(value))) = c.next_timeout(ms_timeout).await? {
Config::set_options(value.clone());
Ok(value)
} else {
Ok(Config::get_options())
}
}
pub async fn get_options_async() -> HashMap<String, String> {
get_options_(1000).await.unwrap_or(Config::get_options())
}
#[tokio::main(flavor = "current_thread")]
pub async fn get_options() -> HashMap<String, String> {
get_options_async().await
}
pub async fn get_option_async(key: &str) -> String {
if let Some(v) = get_options_async().await.get(key) {
v.clone()
} else {
"".to_owned()
}
}
pub fn set_option(key: &str, value: &str) {
let mut options = get_options();
if value.is_empty() {
options.remove(key);
} else {
options.insert(key.to_owned(), value.to_owned());
}
set_options(options).ok();
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_options(value: HashMap<String, String>) -> ResultType<()> {
let _nat = CheckTestNatType::new();
if let Ok(mut c) = connect(1000, "").await {
c.send(&Data::Options(Some(value.clone()))).await?;
// do not put below before connect, because we need to check should_exit
c.next_timeout(1000).await.ok();
}
Config::set_options(value);
Ok(())
}
#[inline]
async fn get_nat_type_(ms_timeout: u64) -> ResultType<i32> {
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::NatType(None)).await?;
if let Some(Data::NatType(Some(value))) = c.next_timeout(ms_timeout).await? {
Config::set_nat_type(value);
Ok(value)
} else {
Ok(Config::get_nat_type())
}
}
pub async fn get_nat_type(ms_timeout: u64) -> i32 {
get_nat_type_(ms_timeout)
.await
.unwrap_or(Config::get_nat_type())
}
pub async fn get_rendezvous_servers(ms_timeout: u64) -> Vec<String> {
if let Ok(Some(v)) = get_config_async("rendezvous_servers", ms_timeout).await {
return v.split(',').map(|x| x.to_owned()).collect();
}
return Config::get_rendezvous_servers();
}
#[inline]
async fn get_socks_(ms_timeout: u64) -> ResultType<Option<config::Socks5Server>> {
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::Socks(None)).await?;
if let Some(Data::Socks(value)) = c.next_timeout(ms_timeout).await? {
Config::set_socks(value.clone());
Ok(value)
} else {
Ok(Config::get_socks())
}
}
pub async fn get_socks_async(ms_timeout: u64) -> Option<config::Socks5Server> {
get_socks_(ms_timeout).await.unwrap_or(Config::get_socks())
}
#[tokio::main(flavor = "current_thread")]
pub async fn get_socks() -> Option<config::Socks5Server> {
get_socks_async(1_000).await
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> {
let _nat = CheckTestNatType::new();
Config::set_socks(if value.proxy.is_empty() {
None
} else {
Some(value.clone())
});
connect(1_000, "")
.await?
.send(&Data::Socks(Some(value)))
.await?;
Ok(())
}
async fn get_socks_ws_(ms_timeout: u64) -> ResultType<(Option<config::Socks5Server>, String)> {
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::SocksWs(None)).await?;
if let Some(Data::SocksWs(Some(value))) = c.next_timeout(ms_timeout).await? {
Config::set_socks(value.0.clone());
Config::set_option(OPTION_ALLOW_WEBSOCKET.to_string(), value.1.clone());
Ok(*value)
} else {
Ok((
Config::get_socks(),
Config::get_option(OPTION_ALLOW_WEBSOCKET),
))
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn get_socks_ws() -> (Option<config::Socks5Server>, String) {
get_socks_ws_(1_000).await.unwrap_or((
Config::get_socks(),
Config::get_option(OPTION_ALLOW_WEBSOCKET),
))
}
pub fn get_proxy_status() -> bool {
Config::get_socks().is_some()
}
#[tokio::main(flavor = "current_thread")]
pub async fn test_rendezvous_server() -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::TestRendezvousServer).await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn notify_deployed() -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::Deployed).await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn send_url_scheme(url: String) -> ResultType<()> {
connect(1_000, "_url")
.await?
.send(&Data::UrlLink(url))
.await?;
Ok(())
}
// Emit `close` events to ipc.
pub fn close_all_instances() -> ResultType<bool> {
match crate::ipc::send_url_scheme(IPC_ACTION_CLOSE.to_owned()) {
Ok(_) => Ok(true),
Err(err) => Err(err),
}
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
pub async fn connect_to_user_session(usid: Option<u32>) -> ResultType<()> {
let mut stream = crate::ipc::connect_service(1000).await?;
timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn notify_server_to_check_hwcodec() -> ResultType<()> {
connect(1_000, "").await?.send(&&Data::CheckHwcodec).await?;
Ok(())
}
#[cfg(target_os = "windows")]
pub async fn get_port_forward_session_count(ms_timeout: u64) -> ResultType<usize> {
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::PortForwardSessionCount(None)).await?;
if let Some(Data::PortForwardSessionCount(Some(count))) = c.next_timeout(ms_timeout).await? {
return Ok(count);
}
bail!("Failed to get port forward session count");
}
#[cfg(feature = "hwcodec")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tokio::main(flavor = "current_thread")]
pub async fn get_hwcodec_config_from_server() -> ResultType<()> {
if !scrap::codec::enable_hwcodec_option() || scrap::hwcodec::HwCodecConfig::already_set() {
return Ok(());
}
let mut c = connect(50, "").await?;
c.send(&Data::HwCodecConfig(None)).await?;
if let Some(Data::HwCodecConfig(v)) = c.next_timeout(50).await? {
match v {
Some(v) => {
scrap::hwcodec::HwCodecConfig::set(v);
return Ok(());
}
None => {
bail!("hwcodec config is none");
}
}
}
bail!("failed to get hwcodec config");
}
#[cfg(feature = "hwcodec")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn client_get_hwcodec_config_thread(wait_sec: u64) {
static ONCE: std::sync::Once = std::sync::Once::new();
if !crate::platform::is_installed()
|| !scrap::codec::enable_hwcodec_option()
|| scrap::hwcodec::HwCodecConfig::already_set()
{
return;
}
ONCE.call_once(move || {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(1));
let mut intervals: Vec<u64> = vec![wait_sec, 3, 3, 6, 9];
for i in intervals.drain(..) {
if i > 0 {
std::thread::sleep(std::time::Duration::from_secs(i));
}
if get_hwcodec_config_from_server().is_ok() {
break;
}
}
});
});
}
#[cfg(feature = "hwcodec")]
#[tokio::main(flavor = "current_thread")]
pub async fn hwcodec_process() {
let s = scrap::hwcodec::check_available_hwcodec();
for _ in 0..5 {
match crate::ipc::connect(1000, "").await {
Ok(mut conn) => {
match conn
.send(&crate::ipc::Data::HwCodecConfig(Some(s.clone())))
.await
{
Ok(()) => {
log::info!("send ok");
break;
}
Err(e) => {
log::error!("send failed: {e:?}");
}
}
}
Err(e) => {
log::error!("connect failed: {e:?}");
}
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
#[tokio::main(flavor = "current_thread")]
pub async fn get_wayland_screencast_restore_token(key: String) -> ResultType<String> {
let v = handle_wayland_screencast_restore_token(key, "get".to_owned()).await?;
Ok(v.unwrap_or_default())
}
#[tokio::main(flavor = "current_thread")]
pub async fn clear_wayland_screencast_restore_token(key: String) -> ResultType<bool> {
if let Some(v) = handle_wayland_screencast_restore_token(key, "clear".to_owned()).await? {
return Ok(v.is_empty());
}
return Ok(false);
}
#[cfg(all(
feature = "flutter",
not(any(target_os = "android", target_os = "ios"))
))]
#[tokio::main(flavor = "current_thread")]
pub async fn update_controlling_session_count(count: usize) -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::ControllingSessionCount(count)).await?;
Ok(())
}
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
pub async fn get_terminal_session_count() -> ResultType<usize> {
let timeout_ms = 1_000;
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let candidate_uids = terminal_count_candidate_uids(effective_uid);
let mut last_err: Option<anyhow::Error> = None;
for candidate_uid in candidate_uids {
let socket_path = Config::ipc_path_for_uid(candidate_uid, "");
let connect_result = timeout(timeout_ms, Endpoint::connect(&socket_path))
.await
.map_err(|err| {
anyhow::anyhow!(
"Timeout connecting to terminal ipc at {}: {}",
socket_path,
err
)
});
let connection = match connect_result {
Ok(Ok(connection)) => connection,
Ok(Err(err)) => {
last_err = Some(anyhow::anyhow!(
"Failed to connect to terminal ipc at {}: {}",
socket_path,
err
));
continue;
}
Err(err) => {
last_err = Some(err);
continue;
}
};
let mut ipc_conn = ConnectionTmpl::new(connection);
if let Err(err) = ipc_conn.send(&Data::TerminalSessionCount(0)).await {
last_err = Some(anyhow::anyhow!(
"Failed to request terminal session count via ipc at {}: {}",
socket_path,
err
));
continue;
}
match ipc_conn.next_timeout(timeout_ms).await {
Ok(Some(Data::TerminalSessionCount(session_count))) => {
return Ok(session_count);
}
Ok(None) => {
last_err = Some(anyhow::anyhow!(
"Invalid response when requesting terminal session count via ipc at {}",
socket_path
));
}
Ok(other) => {
last_err = Some(anyhow::anyhow!(
"Unexpected response when requesting terminal session count via ipc at {}: {:?}",
socket_path,
other.map(|v| std::mem::discriminant(&v))
));
}
Err(err) => {
last_err = Some(anyhow::anyhow!(
"Failed to read terminal session count via ipc at {}: {}",
socket_path,
err
));
}
}
}
if let Some(err) = last_err {
Err(err.into())
} else {
Ok(0)
}
}
async fn handle_wayland_screencast_restore_token(
key: String,
value: String,
) -> ResultType<Option<String>> {
let ms_timeout = 1_000;
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::WaylandScreencastRestoreToken((key, value)))
.await?;
if let Some(Data::WaylandScreencastRestoreToken((_key, v))) = c.next_timeout(ms_timeout).await?
{
return Ok(Some(v));
}
return Ok(None);
}
#[tokio::main(flavor = "current_thread")]
pub async fn set_install_option(k: String, v: String) -> ResultType<()> {
if let Ok(mut c) = connect(1000, "").await {
c.send(&&Data::InstallOption(Some((k, v)))).await?;
// do not put below before connect, because we need to check should_exit
c.next_timeout(1000).await.ok();
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() <= 120);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_service_ipc_path_is_shared_across_uids() {
assert_eq!(
Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE),
Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE)
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_ipc_path_differs_by_uid_for_cm() {
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let other_uid = effective_uid.saturating_add(1);
let postfix = "_cm";
// Default connect path targets the current effective uid.
assert_eq!(
Config::ipc_path(postfix),
Config::ipc_path_for_uid(effective_uid, postfix)
);
// A different uid yields a different socket path - this is the root cause of the
// cross-user regression when root spawns a user process but still connects as uid 0.
assert_ne!(
Config::ipc_path(postfix),
Config::ipc_path_for_uid(other_uid, postfix)
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_uses_active_uid_when_no_server_found() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_uses_single_server_uid() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_prefers_active_uid_with_multiple_servers() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_prefers_root_on_wayland_login_screen() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(),
0
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() {
assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err());
}
}