zDesk
简体中文

指南

设置 wlshare

如何在 sway 或 labwc 等 wlroots 合成器上设置 wlshare,即 zDesk 连接 Linux 桌面所经由的 VNC 服务器:它需要什么、谁可以连接、无人登录时也在开机后就绪的桌面,以及它承载的内容。所有设置项都在带注释的 wlshare.example.toml中,工作原理见架构。

本页内容

它需要什么

wlshare 运行在它所共享的 Wayland 会话之内,以拥有该会话的用户身份运行,并通过 wlroots 的公开协议与合成器通信。任何提供这些协议的 wlroots 合成器都是同一类对等方:

协议用途
wlr-screencopy画面,版本 2 或更高;必需
ext-image-copy-capture指针的图像,与画面分开捕获;必需
wlr-output-management为客户端调整无头输出的尺寸和缩放
虚拟键盘和虚拟指针客户端的键盘和鼠标
wlr-data-control双向剪贴板

指针从不出现在捕获的像素中:wlshare 按输出的密度单独发送它的图像,客户端移动指针时无需等待新的一帧。在无头输出上,这需要 wlroots 0.19,它是第一个让无头后端把光标放在独立平面上的版本;0.18 会把光标画进捕获的画面。声音、摄像头和麦克风都经由 PipeWire,软件包依赖它。

在 Chrome 中以 Mac 的 Retina 密度通过 wlshare 显示 Sway:wlr-randr 报告无头输出的缩放为 2,wpctl 显示浏览器收听时 wlshare 自己的音频输出设备。

安装

不保证向后兼容。wlshare 的某个发布可能会更改或移除某个设置项、某个私有扩展的传输格式或某项功能,并且不再读取旧的形式。升级时,请对照 /usr/share/doc/wlshare/wlshare.example.toml 检查你的配置。

Debian trixie 提供基于 wlroots 0.18 的 sway 1.10 和 labwc 0.8.3。wlshare 自己的签名 APT 仓库提供依据 Debian 的打包为 trixie 重新构建的 sway 1.11、labwc 0.9.7 和 wlroots 0.19,支持 amd64 和 arm64,各自的用途如下:

软件包用途
wlroots 0.19wlshare 缺少就无法启动的指针图像捕获(ext-image-copy-capture),以及在无头输出上让指针不进入画面,0.18 会把指针画进每一次捕获。wlshare 的软件包依赖它。
wlroots 的补丁在有两个输出的 labwc 上:labwc 重新加载配置(labwc --reconfigure)之后,让客户端的指针仍落在它所指的位置。labwc 届时会把每个指针重新映射到它所指明的输出,而 wlshare 的指针没有指明任何输出,于是它的位置被拉伸到整个布局上,两个输出都受影响,直到下一次重新排列输出为止;wlshare 在任何 wlroots 上都会在布局每次变化时重新创建它的指针,但重新加载配置并不改变布局。只有一个输出的桌面以及 sway 都不需要这个补丁:原版的 wlroots 0.19 或更高版本就够了。
sway 1.11、labwc 0.9.7基于 wlroots 0.19 构建的版本,原样重新构建:trixie 的 sway 1.10 和 labwc 0.8.3 链接的是 0.18,wlshare 在其上无法启动。

它们的版本号排在 trixie 的版本之上,因此无需设置 pin:

sudo mkdir -p /etc/apt/keyrings
sudo curl -fsSL -o /etc/apt/keyrings/wlshare.gpg https://andrewtheguy.github.io/wlshare/wlshare.gpg
sudo tee /etc/apt/sources.list.d/wlshare.sources <<'EOF'
Types: deb
URIs: https://andrewtheguy.github.io/wlshare
Suites: trixie
Components: main
Signed-By: /etc/apt/keyrings/wlshare.gpg
EOF
sudo apt update && sudo apt install sway  # or labwc

wlshare 本身是其发布页面上的软件包,不在该仓库中。把 0.0.x 换成实际版本;在 arm64 主机上,把 amd64 换成 arm64:

curl -fsSLO https://github.com/andrewtheguy/wlshare/releases/download/v0.0.x/wlshare-trixie-amd64.deb
sudo apt install ./wlshare-trixie-amd64.deb

它依赖上面的仓库所提供的 libwlroots-0.19,以及 pipewire、wireplumber 和 pipewire-pulse。它推荐安装 xdg-desktop-portal-gtk,让桌面上的浏览器能找到客户端的摄像头,以及 va-driver-all,用于主机的硬件 H.264 编码器。

zDesk 网关和 wlshare 必须使用同一个版本的 wlshare 协议,wlshare --version 会给出该版本。连接到另一个版本的 wlshare 时,会话会在连接时失败,并指明两个版本;把两者中较旧的那一个升级即可。不改动协议的发布会保持其版本不变。

运行

配置是一个 TOML 文件,默认为 $XDG_CONFIG_HOME/wlshare/config.toml,空文件即采用所有默认值。软件包安装了一个 systemd 用户单元,在图形会话把 WAYLAND_DISPLAY 交给用户管理器之后随会话启动:

mkdir -p ~/.config/wlshare
cp /usr/share/doc/wlshare/wlshare.example.toml ~/.config/wlshare/config.toml
systemctl --user enable --now wlshare.service
journalctl --user -u wlshare -f

手动运行时,需指明会话的套接字:WAYLAND_DISPLAY=wayland-1 wlshare。它监听 listen,未设置时为 127.0.0.1:5900,并共享合成器的第一个输出,或 output 指定的那个输出。

同一时间只有一个客户端在桌面上。完成握手(包括登录)的连接会从当前持有者手中接管桌面,就像 Windows 远程桌面那样,被接管的客户端会被告知是谁接管了它。RFB 的 shared 标志不起任何作用。因此崩溃的客户端不会留下任何残留:再次连接即可取回桌面,旧连接按住的按键和鼠标按钮都会被释放。唯一的例外是持有者自己的第二个连接,它会显示另一个输出(见输出)。

谁可以连接

一个设置,三种选择。任何一种都不提供经典 VncAuth。

不要求登录

两个表都不设置。

登录
无
加密
否
适用于
listen 位于回环地址或 VPN 地址上,或在 SSH 隧道之后

系统登录

[pam]

登录
wlshare 运行所用账户的用户名和密码,由 PAM 校验
加密
RSA-AES
适用于
以账户密码作为入口、并能用它解锁密钥环的桌面

独立的密码

[password]

登录
一个密码,与配置中的 Argon2 哈希比对
加密
RSA-AES
适用于
不应以任何系统账户的密码作为入口的主机
listen = "[::]:5900"

[pam]
# service = "wlshare"   # /etc/pam.d/wlshare, from the package

在 [pam] 下,wlshare 运行所用账户之外的任何用户名都会在询问 PAM 之前被拒绝,因为端口背后的桌面属于这一个用户。对于 [password],wlshare hash-password 会询问密码,并输出要作为 hash 粘贴的哈希;密码本身从不写下来。无论哪种方式,服务器的 RSA 密钥都会在首次启动时生成在配置旁边,其指纹会写入日志,供与客户端显示的指纹比对:zDesk 网关会在日志中记录它看到的指纹。

由登录解锁的密钥环

由 systemd 启动的无头桌面没有显示管理器把登录密码交给密钥环,因此登录密钥环会一直锁定,直到有程序请求它并有人在提示框中输入密码。在 [pam] 下,PAM 刚校验过的密码可以完成这件事:在 /etc/pam.d/wlshare 的 auth 栈中、common-auth 之后加一行 pam_exec,把密码交给一个脚本,由它先后启动 gnome-keyring-daemon --login 和 --start,即 pam_gnome_keyring 会做的两部分工作,因为 wlshare 不打开 PAM 会话,其 session 钩子无处运行:

@include common-auth
auth    optional    pam_exec.so expose_authtok quiet /usr/local/lib/wlshare/unlock-keyring
@include common-account

wlshare 及其软件包都不提供这个脚本,内容如下:

#!/bin/sh
# /usr/local/lib/wlshare/unlock-keyring: run by pam_exec from /etc/pam.d/wlshare
# with the password PAM has just checked on stdin. It starts the keyring daemon
# with it, both halves of what pam_gnome_keyring does, since wlshare opens no
# PAM session.
set -eu
XDG_RUNTIME_DIR="/run/user/$(id -u)"
DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
export XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS

# A daemon already holding the Secret Service: an earlier login unlocked it.
owner=$(busctl --user list --no-legend 2>/dev/null |
    awk '$1 == "org.freedesktop.secrets" && $2 != "-" { print $2 }')
[ -z "$owner" ] || exit 0

# In a scope of its own, so that restarting wlshare does not take it down.
systemd-run --user --scope --quiet --collect --unit=gnome-keyring-login \
    /usr/bin/gnome-keyring-daemon --daemonize --login --components=pkcs11,secrets

# --start only once that daemon listens, or it starts a second one.
i=0
while [ ! -S "$XDG_RUNTIME_DIR/keyring/control" ] && [ "$i" -lt 50 ]; do
    i=$((i + 1)); sleep 0.1
done
[ -S "$XDG_RUNTIME_DIR/keyring/control" ] || exit 0
exec /usr/bin/gnome-keyring-daemon --start >/dev/null 2>&1

把它安装到上面那一行指定的位置,同时安装密钥环的软件包,并屏蔽守护进程的按需激活:

sudo apt install gnome-keyring libpam-gnome-keyring
sudo install -D -m 755 unlock-keyring /usr/local/lib/wlshare/unlock-keyring
systemctl --user mask gnome-keyring-daemon.socket gnome-keyring-daemon.service

设为 optional,这样打不开的密钥环只会让会话内多一个提示框,而不会让 VNC 登录失败。屏蔽(mask)gnome-keyring-daemon.socket 和 .service,以免某个由激活启动的守护进程在持有密码的那个守护进程之前占用 Secret Service。用 passwd 更改账户密码,它会通过 PAM 重新加密密钥环;以其他方式写入 /etc/shadow 的哈希会让密钥环仍停留在旧密码之下。

开机即启动的无头桌面

wlroots 的无头后端不驱动任何显示器,也不打开任何输入设备,因此合成器不需要 seat,也不需要 logind 会话:它可以作为 systemd 用户服务运行,开启 lingering 后,无需任何人登录即可在开机时启动。它创建一个输出 HEADLESS-1,由 wlshare 共享,并为每个客户端调整尺寸。演示中的 sway 和 LXQt 桌面就是这样运行的。

  1. 用户管理器

    由 loginctl enable-linger 在开机时启动,并启动合成器的单元

  2. 开机时,经由 lingering
  3. 合成器

    设置了 WLR_BACKENDS=headless 的 sway 或 labwc;套接字创建后,它启动自己的会话 target

  4. 随会话启动
  5. wlshare

    wlshare.service,由 graphical-session.target 拉起,会话 target 绑定到后者

# ~/.config/systemd/user/sway-headless.service
[Unit]
Description=sway (headless)
After=dbus.socket

[Service]
Environment=WLR_BACKENDS=headless
Environment=WLR_LIBINPUT_NO_DEVICES=1
ExecStart=/usr/bin/sway
# graphical-session.target is what wlshare is reached through: stop it with the compositor
ExecStopPost=-/usr/bin/systemctl --user stop graphical-session.target
Restart=on-failure

[Install]
WantedBy=default.target
# ~/.config/systemd/user/sway-session.target
[Unit]
Description=sway session
BindsTo=graphical-session.target

合成器在 sway 配置的末尾把自己的环境交给用户管理器,并在套接字创建后启动该 target(labwc 在 ~/.config/labwc/autostart 中运行同样的两行):

exec systemctl --user import-environment WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP \
    && systemctl --user start sway-session.target
loginctl enable-linger "$USER"
systemctl --user daemon-reload
systemctl --user enable --now sway-headless.service wlshare.service

退出时如果只停止会话 target,wlshare 会对着已经消失的合成器不断重启,所以合成器的单元自己停止 graphical-session.target。在合成器的配置中把 HEADLESS-1 固定在原点,这样为第二个显示器创建的输出会落在它的右边。

labwc(一个堆叠式合成器)上的 LXQt,在用户管理器下无头运行,通过同一个 wlshare,以缩放 2 显示。

输出

每个连接显示一个输出;输出从不合并成一个帧缓冲区。像 zDesk 这样提出请求的客户端会收到会话的输出列表,并在其间切换;同一客户端的第二个连接会在第一个旁边显示另一个输出,有自己的尺寸和密度:最多同时两个。普通 VNC 客户端只能看到一个,即 output 指定的那个,或上一个客户端选择的那个。

只有 HEADLESS-* 输出会为客户端调整尺寸或缩放;显示器保持其自身的模式。output 表示偏好而非要求:合成器没有该名称的输出时,共享的是另一个输出,等它出现后桌面再移到它上面。

两个虚拟显示器 beta

客户端可以请求一个或两个虚拟显示器,就像 RDP 客户端向 Windows 主机请求其显示器那样。在 sway 上,wlshare 通过 SWAYSOCK 中给出的 sway IPC 创建和拔除无头输出,直到数量符合请求,无需任何配置:设置了 virtual_displays = 2 的 zDesk 目标会得到第二个输出,下一个只请求一个的会话会让它被拔除。在其他合成器上,输出就是合成器已有的那些。

无头 sway 的两个输出显示在两个 Chromium 窗口中,每个窗口与各自输出的尺寸相同:一段视频用 $mod+Shift+→ 移到第二个输出再移回来。

也在控制台使用的桌面

接在真实显示器上的桌面可以作为一个与客户端尺寸和密度相同的无头输出交给客户端:在别处观看时显示器关闭,离开后显示器恢复。wlshare 在 state_socket 上报告是否有客户端在桌面上,wlshare watch 在单独的进程中跟踪这一状态,并为每种状态运行一条命令:

# config.toml
output = "HEADLESS-1"
state_socket = "wlshare/state.sock"
output_wait_secs = 5
wlshare watch \
    --held "swaymsg 'output HEADLESS-1 enable, output DP-1 disable'" \
    --free "swaymsg 'output DP-1 enable, output HEADLESS-1 disable'"
  • --held 在客户端接管桌面时运行,--free 在 --free-after-secs(5)秒内无人持有桌面时运行,因此在等待时间内重新连接的客户端不会触发任何一条。接管不算状态变化。
  • sway 会把被禁用输出上的工作区移到已启用的输出上,因此窗口会跟着移到无头输出上。设置 output_wait_secs 后,接管桌面的客户端会等待 HEADLESS-1 启用,并从它而不是显示器开始。
  • 守护进程退出时,无论以何种方式退出,套接字都会关闭,监视进程会将其视为桌面已被离开:即使 wlshare 在有客户端在桌面上时被杀死,显示器也会恢复。监视进程每次启动时都会按桌面当前的状态运行对应的命令,因此两条命令都必须能安全地重复运行。
  • 在会话开始时创建 HEADLESS-1(swaymsg create_output),放在不与任何显示器相邻的位置,远在它们的左侧,这样指针不会从控制台游移到它上面。

保持关闭的桌面可以在控制台或通过 SSH 恢复,需设置 SWAYSOCK:swaymsg 'output * enable'。把监视进程作为用户单元与 wlshare 的单元并列运行,作为 graphical-session.target 的一部分,让它随会话启动和停止。

声音、摄像头和麦克风

始终开启,无需任何配置:客户端逐项请求,zDesk 在勾选了它们启动的会话中会请求。

在桌面上何时存在
声音一个 PipeWire 音频输出设备 wlshare remote audio,设为默认;其监听流按客户端的请求以 FLAC 或 Opus 发送有客户端收听时
摄像头一个 PipeWire 视频源 wlshare remote camera,来自客户端的 H.264,由系统的 libavcodec 解码有程序打开它时
麦克风一个 PipeWire 音频源 wlshare remote microphone,来自客户端的 16 位 PCM有程序录音时

有客户端收听时,每个跟随默认设备的音频流都播放到 wlshare 的输出设备,在主机上静音;固定到主机某个输出设备的程序仍在主机上播放。最后一个收听者离开后,该输出设备随之消失,主机自己的设备重新成为默认。无头桌面不需要声卡。

桌面上的浏览器通过 xdg-desktop-portal 的 Camera 接口访问摄像头,该接口只在有实现 Access 的后端(例如 xdg-desktop-portal-gtk)时提供; 只有 xdg-desktop-portal-wlr 是不够的。Chrome 还需要打开 chrome://flags/#enable-webrtc-pipewire-camera。直接打开 /dev/video* 的程序看不到它。麦克风两者都不需要:它是一个普通的 PipeWire 音频源。

键盘

客户端发送 keysym,wlshare 通过它作为虚拟键盘的 keymap 上传给合成器的那个 keymap,把它们转换为 keycode,因此会话的输入也按这个 keymap 进行。自定义布局放在 [xkb] 下,并在单元的环境中用 XKB_CONFIG_EXTRA_PATH 指定存放它的目录:

[xkb]
layout = "swayvnc"
systemctl --user edit wlshare.service
# [Service]
# Environment=XKB_CONFIG_EXTRA_PATH=/etc/xkb

每个键都必须保留自己的 keysym,只能增加动作,因为传入的 keysym 是在同一个 keymap 中查找的。远程桌面上把 sway 的修饰键移到右 Alt 就是这样做的:用一个布局,而不是一行 xkb_options,因为 sway 对虚拟键盘会忽略它。锁定键不能作为修饰键:Caps Lock 在任何内容发送之前就会锁定客户端自己的大写状态。

画面

wlshare 会告诉提出请求的客户端帧缓冲区按什么密度绘制,这是标准 RFB 做不到的,并把客户端的密度作为输出的缩放:2x 的浏览器得到缩放为 2 的输出,按真实尺寸清晰显示。普通 VNC 客户端收到同样的像素但没有密度信息,按 1x 显示。

zDesk 会话:缩放为 2 的输出按 2x 显示,完整显示在窗口中。
同一桌面作为普通 VNC,没有 subtype = "wlshare":其像素按 1x 显示,尺寸变为两倍,窗口中只能显示四分之一。

除了标准的 Raw 和 ZRLE,提出请求的客户端会收到 wlshare 自己的某种画面,zDesk 把它们作为选择器的 Picture 组提供(见zDesk 指南中的 wlshare):按链路调整的整个桌面的 H.264;RemoteFX Progressive 图块;每个运动区域各有一路视频流的无损 PNG 图块;或者无论静止还是运动都无损的 planar 图块。

H.264,无论是整个桌面的还是运动区域的,在 VA-API 找到硬件编码器时由主机的硬件编码器编码,否则由 libx264 以最快的预设编码;hardware_h264 = false 跳过硬件。启动时的探测会先尝试 Intel 的低功耗编码器,它把 GPU 的执行单元留给桌面,并把找到的结果写入日志:

journalctl --user -b -u wlshare | grep h264
# hardware H.264: /dev/dri/renderD128 through VA-API, its low-power encoder

没有视频编码器的虚拟机(例如使用 VirGL 的虚拟机)以软件编码每一帧:输出越大,占用的主机处理器越多。max_fps(默认 30)限制捕获的帧数;zDesk 页面最多绘制 30 帧。

客户端

zDesk 以带 subtype = "wlshare" 的 vnc 目标连接 wlshare,这让网关请求 wlshare 的密度、输出、画面、声音、摄像头和麦克风:

[[targets]]
name = "wayland"
protocol = "vnc"
subtype = "wlshare"
host = "192.0.2.12"
username = "me"          # leave out under [password]
password_ref = "wayland"
# virtual_displays = 2   # on sway, a second headless output
wlshare 目标上的选择器:跟随窗口的尺寸、Opus 或 FLAC 的声音、摄像头和麦克风,以及 wlshare 的四种画面。

任何其他列出 Cursor 伪编码并能绘制 Raw 矩形的 RFB 3.8 客户端都可以连接,按输出自身的像素显示,没有声音:例如支持 RSA-AES 的 TigerVNC。之所以需要 Cursor,是因为指针从不出现在帧缓冲区中。不列出 wlshare 扩展的客户端不会收到这些扩展。会话以哪些选项启动,见 zDesk 指南中的 wlshare。