Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Linux setup

Verified on Arch Linux with Hyprland (Wayland). Other Wayland compositors and X11 compile and should work but are untested; please report what you find.

Wayland

How rdc talks to the compositor

NeedMechanismWorks on
Screen captureorg.freedesktop.portal.Screenshot via xdg-desktop-portal, falling back to wlr-screencopyGNOME, KDE, Hyprland, sway, river…
Mouse and keyboardwlr-virtual-pointer + zwp-virtual-keyboard protocolswlroots compositors (Hyprland, sway, river, labwc…)
Window list and focushyprctl when HYPRLAND_INSTANCE_SIGNATURE is setHyprland only
Clipboardwlr-data-controlwlroots compositors and KDE

GNOME and KDE do not implement the wlr virtual input protocols, so on those desktops rdc can take screenshots but cannot move the mouse or type yet. The input library rdc uses (enigo) has paths through the RemoteDesktop portal and libei, but rdc does not enable them; wiring and testing them is tracked as an issue. rdc doctor reports the session type and whether input initialised.

rdc pins exactly one input backend per session (Wayland when WAYLAND_DISPLAY is set, X11 otherwise) so events are not delivered twice to Xwayland applications.

Absolute pointer positioning under Wayland is expressed as a fraction of the first output’s mode, which rdc maps from logical desktop coordinates, so clicks land correctly on scaled displays (verified at 2× on Hyprland).

Packages

Runtime: a portal backend for your compositor (xdg-desktop-portal-hyprland, xdg-desktop-portal-gnome, xdg-desktop-portal-kde, or xdg-desktop-portal-wlr), PipeWire, and tailscaled running. Build dependencies are listed in Install.

X11

Capture and input go through xcb/x11rb. Window focus uses the generic xcap window list (no _NET_ACTIVE_WINDOW focus yet, so focus is unsupported on X11 for now).

Running

Put the allowlist in ~/.config/rdc/config.toml first (see Configuration); the service reads it from there.

rdc doctor
rdc serve                                  # foreground test
rdc service install                        # systemd --user unit dev.rdc.daemon.service
journalctl --user -u dev.rdc.daemon -f

The unit is wanted by graphical-session.target, so it starts with your desktop session and restarts on failure. It runs the binary from the path where you invoked service install; put rdc somewhere permanent first (~/.local/bin or /usr/local/bin).

Config lives at ~/.config/rdc/config.toml; see Configuration.

What the unit is allowed to do

rdc service install writes a sandboxed unit. The daemon runs as your user inside your session (it has to, to see the screen and inject input), but systemd fences off everything it does not need. The directives are split across two files because in a --user service they are not all equally available:

Always applied, in dev.rdc.daemon.service — these are pure seccomp and prctl settings and need no namespace:

RestrictionEffect
NoNewPrivileges, RestrictSUIDSGIDneither rdc nor anything it runs (hyprctl, tailscale) can gain privileges
SystemCallFilter=@system-service minus @privileged @resources, native ABI onlyprivileged and resource-limit syscalls return EPERM
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6unix sockets (Wayland, D-Bus, portal, tailscaled) and the HTTP listener; nothing else
RestrictNamespaces, RestrictRealtime, LockPersonalityno new namespaces, no realtime scheduling, no personality changes
UMask=0077audit log and rotated copies are private to your user

Applied when unprivileged user namespaces work, in dev.rdc.daemon.service.d/10-sandbox.conf:

RestrictionEffect
PrivateUsers=yesevery uid but yours maps to nobody inside the service
empty CapabilityBoundingSet/AmbientCapabilitiesrdc can never hold a capability
ProtectKernelTunables, ProtectKernelModules, ProtectKernelLogs, ProtectControlGroups, ProtectClock, ProtectHostnameno kernel tunables, modules, logs, cgroups, clock or hostname
ProtectSystem=strict, ProtectHome=read-only, PrivateTmpthe filesystem is read-only except $XDG_RUNTIME_DIR, ~/.local/state/rdc and the audit log directory
BindReadOnlyPaths="-/tmp/.X11-unix"puts the X11 socket back, which PrivateTmp would otherwise hide

Every directive in the second table implies PrivateUsers= in a --user service (see systemd.exec(5)), so where unprivileged user namespaces are unavailable the unit would refuse to start at all. rdc service install therefore asks systemd itself rather than guessing: it runs a throwaway transient unit,

systemd-run --user --wait --quiet -p PrivateUsers=yes -p ProtectSystem=strict -p PrivateTmp=yes /bin/true

and writes the drop-in only if that starts. Reading /proc/sys is not enough (Ubuntu 23.10+ restricts namespaces through an AppArmor profile the old knobs do not reflect) and neither is unshare -Ur, which can succeed on hosts where systemd’s own mount setup still fails. Install says so when it skips the drop-in, and removes a stale one if an upgrade takes namespaces away.

Paths in the generated files are quoted and any % is written %%, so a binary or audit directory containing a space is one path rather than two, and a % is not read as a systemd specifier. A path containing a quote, a backslash or a newline is refused at install time rather than written out ambiguously.

Installing over a running daemon restarts it, so the new settings take effect immediately instead of at the next login. The unit is Type=exec, which means systemd calls it active only once the binary has actually been reached; install then waits past RestartSec and checks that NRestarts is still 0, so a daemon that starts and dies in a loop is reported rather than mistaken for a healthy one.

RemoveIPC is deliberately not set: in a --user unit it would delete every shared-memory object owned by your account when the unit stops, which can take Xwayland (MIT-SHM) and PipeWire down with it.

Check the result with systemd-analyze security --user dev.rdc.daemon. If your desktop needs something the sandbox blocks (the journal will show EPERM or a mount error), override it with systemctl --user edit dev.rdc.daemon rather than editing the generated files, so the change survives the next rdc service install.

Persistence is opt-in

Nothing rdc does installs itself anywhere. rdc serve runs until you stop it and leaves no unit, LaunchAgent, scheduled task or login item behind. Only rdc service install, run explicitly, creates the per-user service, and rdc service uninstall removes it completely.

Tailscale

rdc uses the LocalAPI socket at /var/run/tailscale/tailscaled.sock. If your user can’t read it, rdc doctor shows the failure and rdc falls back to the tailscale CLI. On most distributions the socket is world-connectable for read-only calls like whois and status.