Skip to content
andrewtheguyPublic

About

A VNC server for wlroots-based Wayland compositors, built for the zDesk gateway: density-aware capture of one or two outputs, VP9 streaming or exact PNG tiles with H.264 for moving regions, PipeWire sound, clipboard, camera and microphone over RFB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

wlshare

English | 简体中文 | 繁體中文

No backward compatibility. A release may change or remove a configuration key, a private RFB extension's wire format or a feature, with nothing that reads the old form. Check wlshare.example.toml against your config when you upgrade, and upgrade the zDesk gateway with it when the protocol version wlshare --version names has changed.

A VNC server for wlroots-based Wayland compositors, built for the zDesk gateway. It captures one output through wlr-screencopy, serves it over RFB 3.8 with Raw or ZRLE — or, to a client that asks, as zDesk does for its browsers, as one H.264 stream whose quality and frame rate follow the link, or for a link with room to spare as lossless PNG tiles with a video stream for each region that moves, each H.264 by the host's hardware or libx264, or, following the link, as RemoteFX Progressive tiles, still or moving (alpha), or for a LAN as lossless planar tiles, still or moving, as a Windows host draws under its Lossless policy (experimental) — injects input through the virtual keyboard and pointer protocols, shares the clipboard through wlr-data-control, carries the desktop's sound from PipeWire over the connection itself — and tells the client what pixel density the framebuffer is drawn at, which standard RFB cannot, so a scale 2 output is shown sharp at 2x and a client's own density becomes the output's. A desktop with more than one monitor sends the client the list, so the one being shared is the client's to choose, and a second connection from that client is shown another output beside it, so two are seen at once. The compositor pointer is excluded from captured frames, so wlshare captures the cursor image on its own — whatever shape the application under it chose, at the output's pixel density — and sends it through the RFB Cursor pseudo-encodings, with its alpha to a client that lists Cursor With Alpha. The client moves it without waiting for a framebuffer update.

See it in action on its page, and set it up with Setting up wlshare.

It is compositor-independent within that protocol surface: any wlroots-based compositor exposing the required protocols is the same kind of peer. wlr-screencopy version 2 or later is required, and so are ext-image-copy-capture with output capture sources, which the cursor image comes from, and xdg-output version 3, which says where each output lies in the layout, where the pointer is aimed. Output resizing and rescaling additionally need wlr-output-management; input and clipboard use the virtual keyboard, virtual pointer, and wlr-data-control protocols when the compositor offers them. Keeping the cursor separate on a headless output, and capturing it, requires wlroots 0.19 or newer.

At the framebuffer layer, any RFB 3.8 client that advertises the standard Cursor pseudo-encoding and can draw Raw rectangles can connect. ZRLE is used when listed unless the client also asks for wlshare's private video stream or its tiles. A configured login additionally requires RSA-AES. Cursor support is required because wlshare never puts the pointer in framebuffer pixels. The density and outputs extensions are asked for by the client and stay silent otherwise; zDesk asks for both, and for the video stream, or with the picker's Tiles with motion, Progressive tiles (alpha) or Lossless tiles (experimental) the tiles, on a vnc target with subtype = "wlshare", and reads wlshare as any VNC server on a target without it. Audio is a private extension that zDesk asks for in a session on such a target started with sound: while it listens wlshare's own sink is the default, so every stream that follows the default plays there rather than on the host's speakers, and that sink's monitor is sent encoded as lossless FLAC, or as Opus at the rate the client names where it asks for that instead. An application pinned to a sink of the host's stays there and is heard on the host. A client that does not list it hears nothing. The sound, the camera and the microphone are always on, with no configuration to turn them on: the package depends on the PipeWire, WirePlumber and pipewire-pulse they run through. A client that lists the camera extension — zDesk does, for a session started with the picker's Camera tick — can lend the desktop its camera: the H.264 it sends is decoded with the system's libavcodec into a PipeWire video source, "wlshare remote camera", which applications reaching cameras through PipeWire can open, and the client is asked for frames only while one has it open. A browser is one through xdg-desktop-portal's Camera interface, which the portal offers only beside a backend that implements Access, such as xdg-desktop-portal-gtk — xdg-desktop-portal-wlr alone does not. The package recommends both, so an install that takes its Recommends has the interface. Chrome also needs chrome://flags/#enable-webrtc-pipewire-camera enabled; without the flag or the portal it looks only at /dev/video* and lists no camera. A client that lists the microphone extension can lend the desktop its microphone the same way: a PipeWire audio source, "wlshare remote microphone", fed with the 16-bit PCM the client sends while an application records from it. One client is on the desktop at a time: a connection that finishes the handshake takes it from whoever holds it, the way Windows Remote Desktop does, and the RFB shared flag changes nothing. See docs/architecture.md for how it works and what it deliberately leaves out.

Sway, labwc and wlroots for Debian trixie

Trixie ships Sway 1.10 and labwc 0.8.3 on wlroots 0.18, whose headless backend paints the cursor into captures. GitHub Pages serves a signed APT repository with Sway 1.11, labwc 0.9.7 and wlroots 0.19 rebuilt for trixie against its own libraries. The repository does not carry wlshare itself.

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

The packages are Debian's own source packages, pinned by their .dsc on snapshot.debian.org in packaging/apt/sources.env, with the series in packaging/apt/patches/<source>/ applied after Debian's patches. wlroots stays on 0.19, the newest series trixie's libdrm and wayland-protocols can build, and labwc on 0.9, its last series built against wlroots 0.19. They are versioned <upstream>+<YYYYMMDD>-<N>~trixie, above both trixie's packages and Debian's builds of the same release. scripts/build-sway-debs.sh builds them in Docker into dist/sway/<arch>/.

The Release Sway packages workflow builds both architectures and publishes them as the prerelease sway-<YYYYMMDD>-<N>, which only stores them. Publish APT repository runs after it: it indexes the three most recent sway-* releases, signs the index with the key in packaging/apt/pubkey.asc, installs Sway and labwc from the result in a trixie container, and deploys it as the whole Pages site. It needs the private key as the GPG_PRIVATE_KEY secret. That key is the one podman-package's repository uses; its private half stays in the gitignored keys/. To run the assembly locally, import that key first:

gpg --import keys/apt-signing-key.private.asc
./scripts/apt-repo-build.sh dist/sway site https://andrewtheguy.github.io/wlshare
./scripts/apt-repo-smoke.sh site

Ubuntu 26.04

Ubuntu 26.04 (resolute) ships Sway 1.11 and labwc 0.9.3 on wlroots 0.19.2, which wlshare runs on as they are, so the APT repository has no suite for it.

wlshare's pointer is bound to no output and aimed over the whole layout, so a reload of the compositor's configuration (swaymsg reload, labwc --reconfigure), which unbinds every pointer from its output, changes nothing for it, on trixie and Ubuntu 26.04 alike (docs/architecture.md, "Input and clipboard").

Running

The daemon runs inside the Wayland session it captures, as the user who owns it:

WAYLAND_DISPLAY=wayland-1 wlshare --config ~/.config/wlshare/config.toml

or as the systemd user unit the package installs, started with the graphical session after its environment contains WAYLAND_DISPLAY:

systemctl --user enable --now wlshare.service

Configuration is one TOML file, $XDG_CONFIG_HOME/wlshare/config.toml by default. An empty file uses defaults, and wlshare.example.toml lists every setting.

Who may connect is one setting, and there are three answers. Nothing set: anyone who reaches the port is in, and the session is in the clear, so listen stays on loopback or behind a VPN or an SSH tunnel. [pam]: RSA-AES, RealVNC's security type that TigerVNC and zDesk speak — the client sends the username and password of the account wlshare runs as, PAM checks them under the service wlshare (the package installs /etc/pam.d/wlshare), and everything after the key exchange is encrypted. No other account is accepted, since the desktop behind the port is that one user's. [password]: the same RSA-AES, asking for a password alone, checked against an Argon2 hash in the configuration file — for a host where no system account's password should be the way in. Print the hash with

wlshare hash-password

and paste it into the table; the password itself is never written down.

Classic VncAuth is deliberately not offered in any of the three: it names nobody, truncates the password to eight characters, proves only knowledge of a machine's secret, and leaves the session in the clear. The server's RSA key is generated on first start into rsa_key_file and its fingerprint logged, so it can be compared with the one the client shows. Under [pam], because the password PAM verifies is the account's, the stack can pass it on — a pam_exec ... expose_authtok line there is how a headless session gets its keyring unlocked at VNC login.

state_socket names a Unix socket on which the daemon says whether a client is on the desktop, and wlshare watch --held COMMAND --free COMMAND follows it from a process of its own, so a sway session can move its windows onto the headless output the client is shown and turn its monitors off while they are watched from elsewhere, then put them back — also when the daemon was killed with a client on the desktop, which the watcher reads as the desktop left. The headless output can itself be off while nobody is watching: output names a preference, not a requirement, and output_wait_secs keeps a client taking the desktop waiting for it while the watcher enables it. The example configuration shows the two swaymsg lines, and docs/architecture.md says when each runs.

A client may ask for the number of virtual displays it wants, as an RDP client asks a Windows host for its monitors and a Mac's viewer asks for its virtual displays: one or two. On sway the daemon makes and unplugs headless outputs through sway's IPC, found in SWAYSOCK, until there are that many, with nothing to configure: a session started with one headless output is given a second when the zDesk gateway's target asks for two (virtual_displays = 2). On any other compositor the outputs are what the compositor has, and the list says so (docs/architecture.md).

A custom keyboard layout — for a modifier remap the session's only keyboard has to carry — goes under [xkb], with XKB_CONFIG_EXTRA_PATH in the unit's environment naming the directory that holds it. Each key must keep its keysym: the server resolves the client's keysyms through the same keymap it uploads.

Building

The workspace has two crates: wlshare, the daemon, with the RFB protocol it speaks inside it, and wlshare-proto, the bytes of wlshare's private extensions, which the zDesk gateway depends on too, pinned by its proto-v<N> tag, the protocol version the release that brought it tagged. The second has no OS APIs and builds and tests anywhere. The daemon needs libxkbcommon, libpam, libpipewire, libavcodec, libx264 and libva — the Wayland protocol is spoken in Rust, with no libwayland — and only runs under a wlroots-based Wayland compositor; its audio tests additionally need ffmpeg, the independent decoder that reads the Opus packets back. The FLAC coding is sound-flac and the Opus coding sound-opus, both shared with the zDesk gateway, each pinned by its release tag. Nothing asks of the processor more than its target's baseline. The daemon is the package at the repository's root, the protocol crate under crates/. A bare cargo test covers both; elsewhere cargo test -p wlshare-proto covers the second. Its H.264 tests decode with libavcodec, libx264's frames on every machine and the hardware's where there is a VA-API encoder. Build the daemon with cargo build --release on a Linux host with libxkbcommon-dev, libpam0g-dev, libpipewire-0.3-dev, libspa-0.2-dev, libavcodec-dev, libx264-dev, libva-dev, libclang-dev and pkg-config. libclang links nothing: PipeWire's, FFmpeg's and x264's -sys crates generate their bindings with bindgen, which loads it at build time; libva's are the daemon's own, written out by hand.

scripts/capture-frames.sh builds and runs the capture container (docker/capture/), with docker or with podman: a headless sway and the daemon started with --capture-frames, playing a scripted desktop at each size, so that what the video encoder was handed — every frame's pixels and damage — lands losslessly in dist/captures/; the video scenarios play full screen the file --video names. examples/wlcap.rs says what a capture holds and plays it out or cuts it. scripts/capture-samples.sh cuts each capture to 120 frames under its own name, into dist/capture-samples/, for an encoder measured on a part of each. --capture-frames is for a daemon run by hand, and one systemd started refuses it (docs/architecture.md, "Capturing what the encoder is handed").

Packages for Debian trixie and Ubuntu 26.04 (resolute), each on amd64 and arm64, are built in Docker by scripts/build-debs.sh, into dist/<suite>/<arch>/wlshare-<suite>-<arch>.deb. The Release wlshare workflow builds the same and publishes them as the GitHub release v<version>, the version being the workspace's in Cargo.toml; bump it before running the workflow. The package version is the crate's, and the distribution is in the file name only: each package names its own suite's libraries, so it installs there alone. The package depends on libwlroots-0.19 (>= 0.19.0), the first wlroots that keeps the cursor out of a headless capture, and on pipewire, wireplumber and pipewire-pulse: the PipeWire server the speaker, the camera and the microphone live in, the session manager that makes the speaker the default and links streams to it, and the PulseAudio server most applications play through. Without them the desktop's sound never reaches the speaker and the client hears nothing.

About

A VNC server for wlroots-based Wayland compositors, built for the zDesk gateway: density-aware capture of one or two outputs, VP9 streaming or exact PNG tiles with H.264 for moving regions, PipeWire sound, clipboard, camera and microphone over RFB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages