RustDesk 在 Wayland(niri / sway)上的配置与排障总结

机器:Ubuntu 24.04.5 + nix home-manager,合成器 niri 26.04 / sway 1.11


一、背景与现象

RustDesk 1.4.5 在这台机器上抓屏完全不可用。客户端连接后立刻报:

ERROR [src/server/wayland.rs:76] Failed scrap Failed to obtain screen capture.

上一行是关键的 portal 响应:

WARN [libs/scrap/src/wayland/pipewire.rs:488] DBus response: Unknown error, code: 2.

两个特征值得注意:

  • response: 2 —— portal 明确拒绝了请求
  • 从请求到失败只有 ~160ms —— 快到不可能弹出过任何授权对话框

二、根因

两个互相独立的坑,缺一不可。

坑 1:运行模式决定用哪个 portal 接口(决定性)

RustDesk 的 Wayland 抓屏在 on_create_session_response 中按 is_server_running() 分叉:

运行方式 使用的 portal 接口 输入方式 结果
用户态(GUI 直接跑) org.freedesktop.portal.RemoteDesktop.CreateSession RemoteDesktop portal ❌ response: 2
服务态(root 服务在跑) org.freedesktop.portal.ScreenCast.CreateSession uinput(内核级) ✅ response: 0

而 RemoteDesktop 这个接口在 niri 和 sway 上没有任何后端能提供:

  • niri 只导出 org.gnome.Mutter.ScreenCast / DisplayConfig / ServiceChannel / Shell.Introspect / Shell.Screenshot,没有 org.gnome.Mutter.RemoteDesktop
  • xdg-desktop-portal-wlr 只实现 ScreenCast + Screenshot
  • xdg-desktop-portal-gnome 只在 mutter 提供 RemoteDesktop 时才注册该接口,所以在 niri 上它也没有

决定性证据在 portal 前端的日志里:

A backend call failed: No such interface "org.freedesktop.impl.portal.RemoteDesktop"

结论:用户态在 niri/sway 上是死路,改任何配置都无效。

坑 2:wayland-restore-token 在显示器布局变化后变毒

RustDesk 把 portal 返回的 restore token 存进 ~/.config/rustdesk/RustDesk_local.toml,下次连接时带 persist_mode 直接恢复上次会话、跳过授权框。

但恢复出来的是保存那一刻的显示器坐标。 一旦布局变化(拔插显示器、改分辨率、改位置),坐标就对不上:

portal 返回的流:  position: [3840, 0]     ← 旧布局的坐标
实际枚举到的: (0, 0) 3840x2160
结果: #displays=0, rects: [] ← 匹配失败,连接被重置

规律:流的 position 是 (0,0) 时成功,是 (3840,0) 时失败。


三、解决方案

3.1 安装 RustDesk 特权服务

注意:rustdesk-server 是错的包。 它提供 hbbs / hbbr / rustdesk-utils,是自建中继/信令服务器,与客户端服务无关。

需要的是客户端自带的特权服务,单元文件在 ~/rustdesk.service:

[Unit]
Description=RustDesk Service
Documentation=https://rustdesk.com
After=network.target

[Service]
Type=simple
# 必须指向 nix wrapper(它会设置 GStreamer 插件路径 / LD_LIBRARY_PATH 等),
# 不能指向 /nix/store/.../bin/.rustdesk-wrapped 裸二进制,否则 PipeWire 抓屏管线起不来
ExecStart=/home/papillon/.nix-profile/bin/rustdesk --service
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

安装:

sudo cp ~/rustdesk.service /etc/systemd/system/rustdesk.service
sudo systemctl daemon-reload
sudo systemctl enable --now rustdesk

两个容易踩的坑:

  1. 单元文件必须手写。 rustdesk --install-service 只会调 systemctl enable/start,它不生成单元文件(deb/rpm 能用是因为发行版包里自带了)。直接跑会得到 Unit file rustdesk.service does not exist。
  2. ExecStart 必须指向 nix wrapper,不能指向 store 里的裸二进制。wrapper 会设置 GST_PLUGIN_SYSTEM_PATH_1_0(GStreamer/PipeWire 抓屏管线必需)、LD_LIBRARY_PATH、XDG_DATA_DIRS 等。

服务做三件事:

  • 以 root 接收连接 —— 无人值守的前提(GUI 没开也能连)
  • 访问 /dev/uinput —— 创建虚拟键鼠做输入注入
  • 降权到用户跑 --server —— sudo -E -u papillon .../rustdesk --server,实际会话由它处理

3.2 修 portal 后端选择

~/.config/xdg-desktop-portal/niri-portals.conf:

[preferred]
default=gnome;gtk;
org.freedesktop.impl.portal.ScreenCast=wlr;
org.freedesktop.impl.portal.Screenshot=wlr;

~/.config/xdg-desktop-portal/sway-portals.conf(新建):

[preferred]
default=gtk;wlr;
org.freedesktop.impl.portal.ScreenCast=wlr;
org.freedesktop.impl.portal.Screenshot=wlr;

为什么必须显式指定:

  • niri 那份原本指向 hyprland 后端 —— compositor 对不上(/run/user/1000/hypr/ 不存在),必然失败
  • sway 上 xdg-desktop-portal-hyprland 的 UseIn 包含 sway,且按字母序排在 wlr 前面,会劫持 sway 会话;必须用显式配置压过去
  • gnome 后端在 niri 上虽然接口存在,但其 ScreenCast 授权对话框可能依赖 gnome-shell,从未跑通过实际会话。用 wlr

3.3 修复 portal 的 dbus 激活

nix 装的 portal 在会话总线上不可激活,导致任何需要 portal 的程序都会失败。

根因:会话总线是 dbus-broker,它在启动时就把服务目录列表固定了,而它启动得比 niri 的 ~/scripts/systemctl-env.sh 早。虽然那个脚本后来用 import-environment 把含 nix profile 的 XDG_DATA_DIRS 导入了 user manager,但总线已经不会重扫。

易误判点:systemctl --user show-environment 里 XDG_DATA_DIRS 是含 nix profile 的,看着一切正常。必须去看 /proc/<dbus-broker-launch pid>/environ 才能发现差异。

修法(绕开 XDG_DATA_DIRS,走总线一定会扫的 XDG_DATA_HOME):

mkdir -p ~/.local/share/dbus-1/services
for f in ~/.nix-profile/share/dbus-1/services/*.service; do
ln -sfn "$f" ~/.local/share/dbus-1/services/"$(basename "$f")"
done
dbus-send --session --dest=org.freedesktop.DBus --type=method_call \
/org/freedesktop/DBus org.freedesktop.DBus.ReloadConfig

软链指向 ~/.nix-profile/...(稳定符号链接),nix 更新后自动跟随,无需维护。ReloadConfig 立即生效,不需要重新登录。

3.4 免点击(跳过屏幕选择器)

wlr portal 的默认选择器是 slurp:

slurp -f 'Monitor: %o' -or

slurp 是图形化区域选择器,必须本地用户用鼠标点一下显示器。这就是「被连接方需要用鼠标点一下屏幕」的来源。

新建 ~/.config/xdg-desktop-portal-wlr/config:

[screencast]
chooser_type=none

# 如需固定共享某一块显示器,取消注释并填实际输出名
# output_name=DP-4

chooser_type 可选 dmenu / simple / none,none 会无条件授权,不再弹任何东西。

⚠️ 安全代价:任何能通过 RustDesk 连到本机的人都能直接拿到屏幕,本地不再有确认这道防线。需要保留确认就改回 simple。


四、验证方法

正常连接时,服务日志应出现:

response: 0, results: {"streams": [{position:[0,0], size:[3840,2160]}]}
[gstreamer] Pipeline N state confirmed as PLAYING.
#displays=1, primary=0, rects: [((0, 0), 3840, 2160)]
UInput keyboard created
UInput mouse created

日志位置(关键):

组件 路径
GUI ~/.local/share/logs/RustDesk/rustdesk_rCURRENT.log
服务端(连接事件真正写在这里) ~/.local/share/logs/RustDesk/server/rustdesk_rCURRENT.log

⚠️ 服务态下连接事件不会写进 GUI 日志。 不知道这点会误判成「连接根本没到达」,白白排查网络。


五、日常维护

每次改过显示器布局后(必做)

sed -i '/wayland-restore-token/d' ~/.config/rustdesk/RustDesk_local.toml
sudo systemctl restart rustdesk

之后首次连接会重新弹一次授权框(或按 3.4 的配置直接静默授权),成功后会写入一个匹配新布局的新 token,后续就正常了。

切换合成器(niri ↔ sway)后

需要把环境切过去,并让 --server 重新发现 display:

# 以 niri 为例
dbus-update-activation-environment --systemd \
XDG_CURRENT_DESKTOP=niri WAYLAND_DISPLAY=wayland-2 DISPLAY=:1 XDG_SESSION_TYPE=wayland
pkill -f xdg-desktop-portal # 让 portal 用新环境重启
sudo systemctl restart rustdesk

RustDesk 的 --server 子进程不直接继承 session 环境,它会去读其他 rustdesk / portal 进程的 environ 来「发现」WAYLAND_DISPLAY 和 DISPLAY。所以必须先有一个该会话下的 rustdesk GUI 进程在跑,它才找得对;否则可能拿到另一个会话的值。


六、关键配置文件清单

路径 作用
/etc/systemd/system/rustdesk.service RustDesk 特权服务(源本 ~/rustdesk.service)
~/.config/xdg-desktop-portal/niri-portals.conf niri 的 portal 后端选择(ScreenCast→wlr)
~/.config/xdg-desktop-portal/sway-portals.conf sway 的 portal 后端选择
~/.config/xdg-desktop-portal-wlr/config 跳过 chooser(chooser_type=none)
~/.local/share/dbus-1/services/*.service 7 个软链,让 nix portal 可被 dbus 激活
~/.config/rustdesk/RustDesk_local.toml 含 wayland-restore-token(布局变化时要清)

备份文件:

  • ~/.config/xdg-desktop-portal/niri-portals.conf.bak
  • ~/.config/rustdesk/RustDesk_local.toml.bak

七、已排除的方案(别重复走)

方案 为什么不行
RUSTDESK_FORCED_DISPLAY_SERVER=x11 niri/sway 都是 rootless XWayland,实测 ffmpeg -f x11grab 抓到的是全黑(没有合成后的根窗口)
换 gnome portal 后端 它的 ScreenCast 授权对话框可能依赖 gnome-shell;且没解决 RemoteDesktop 缺失的问题
只关掉一个显示器 无效。真凶是 restore token 里的过期坐标,不是显示器数量
装 rustdesk-server 那是自建中继服务器(hbbs/hbbr),与客户端抓屏毫无关系

八、已知遗留问题

  • 音频捕获未接上:服务日志每秒刷一次
    ERROR [src/server/service.rs:308] Error of audio service: Connection refused (os error 111)
    视频不受影响,但噪音较大,且远程听不到声音。
  • rustdesk-flutter(1.4.6)未构建成功:已写进 home.nix,但 store 里只有 .drv 没有构建产物,profile 仍是 1.4.5。NixOS Wiki 记录过某些情况下 rustdesk-flutter 能工作而 rustdesk 不行 —— 如再遇到抓屏问题可以试试它。
  • 多显示器场景未完整验证:目前只在单屏下验证通过。双屏时要留意(先清 token,若失败则在授权框里改选 x=0 的那块显示器)。

九、一句话总结

装 root 服务让 RustDesk 从 RemoteDesktop 分支切到 ScreenCast + uinput 分支,从而绕开了 niri/sway 都缺失的 RemoteDesktop 接口;再修好 portal 的后端选择和 dbus 激活,最后用 chooser_type=none 免掉本地点击。