feat(drm): opt-in DRM/KMS screen capture for Linux/Wayland

adds an opt-in `drm` feature for unattended remote access on Wayland: it
captures below the compositor via libdrmtap, so there is no
xdg-desktop-portal consent dialog and it works at the login screen.

off by default. when the feature is off the build is byte-identical.
everything is gated behind feature = "drm" or lives only in the separate
rustdesk-unattended-wayland deb, whose package name is the informed consent.

architecture (agreed with the maintainer): the capture runs inside the root
--service, which already holds the privilege it needs, and streams frames to
the user --server over a service-scoped _drm ipc channel. libdrmtap is loaded
with dlopen at runtime (no link-time dependency, so the base build is
unchanged and it still runs on ubuntu 18), and the .so is built in ci from the
rustdesk-org/libdrmtap fork and shipped only in the drm deb. no setcap helper.

- service: DrmReader reads scanout directly via the dlopen loader; an
  IpcDrmCapturer serves _drm consumers with a per-connection capture worker;
  durable availability cache + pre-warm to avoid enumerate/re-probe restarts
- capture: multi-display (targets the selected crtc), hardware cursor over
  _drm, transient-errno retry with a bounded stall, rejects non-32bpp scanouts
  before the frame copy
- robustness: only active, crtc-bound outputs are offered (an unbound
  crtc_id=0 connector is filtered and a client-selected 0 is refused, both
  fall back to pipewire); a per-display rapid-rebuild guard demotes a flapping
  display to pipewire; per-display (not global) zero-frame failure tracking
- root-service hardening: bounded frame allocation and a concurrent-connection
  cap so a malformed scanout or a buggy consumer cannot OOM or thread-exhaust
  the service; a negative availability verdict expires so displays that appear
  after startup recover without a --server restart; exactly-one .so selection
  in the packaging so a stale object is never silently shipped
- build: libdrmtap.so cloned at build time from rustdesk-org/libdrmtap main
  and bundled only for the --drm deb; ci builds a separate
  rustdesk-unattended-wayland deb (incl. an ubuntu 18.04 container)
- DRM_CAPTURE_SECURITY.md: threat model and hardening notes
This commit is contained in:
Mariano Abad
2026-07-17 20:55:26 -03:00
parent b4af82157b
commit 77a14339ad
16 changed files with 2128 additions and 26 deletions

View File

@@ -361,6 +361,13 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
}
pub fn get_cursor() -> ResultType<Option<u64>> {
// DRM/KMS capture: the hardware cursor arrives over the `_drm` stream, not from XFixes.
#[cfg(feature = "drm")]
if !is_x11() {
if let Some(id) = crate::server::drm_capturer::drm_cursor_id() {
return Ok(Some(id));
}
}
let mut res = None;
DISPLAY.with(|conn| {
if let Ok(d) = conn.try_borrow_mut() {
@@ -379,6 +386,22 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
}
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
// DRM/KMS capture: return the latest hardware-cursor snapshot from the `_drm` stream. Its id may
// have advanced past `hcursor` between get_cursor() and here, so return the latest rather than
// bailing (which would trigger a MouseCursorService backoff).
#[cfg(feature = "drm")]
if !is_x11() {
if let Some(c) = crate::server::drm_capturer::drm_cursor() {
let mut cd: CursorData = Default::default();
cd.id = c.id;
cd.width = c.width;
cd.height = c.height;
cd.hotx = c.hotx;
cd.hoty = c.hoty;
cd.colors = c.colors.into();
return Ok(cd);
}
}
let mut res = None;
DISPLAY.with(|conn| {
if let Ok(ref mut d) = conn.try_borrow_mut() {
@@ -810,6 +833,15 @@ pub fn start_os_service() {
allow_err!(crate::ipc::start(crate::POSTFIX_SERVICE));
});
// DRM/KMS capture producer (opt-in `drm` feature): a dedicated thread + runtime that streams
// scanout frames to the user `--server` over the `_drm` service-scoped channel. Runs here
// because this process is the root service that already holds CAP_SYS_ADMIN for the in-process
// (direct-mode) libdrmtap read.
#[cfg(feature = "drm")]
std::thread::spawn(|| {
crate::ipc::start_drm();
});
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let (mut display, mut xauth): (String, String) = ("".to_owned(), "".to_owned());