From 8e18d01af572b01d3a50b359b1a73fa6786a1d6c Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 27 Jun 2026 11:15:39 +0000 Subject: [PATCH] fix(host/kwin): authorize Desktop-mode streaming via a shipped .desktop Streaming the KDE *Desktop* (KWin) session failed on a real interactive Plasma session with "KWin does not expose zkde_screencast_unstable_v1": KWin treats the screencast/virtual-output and fake_input globals as restricted and advertises them only to a client whose installed .desktop lists them under X-KDE-Wayland-Interfaces (matched by /proc//exe -> Exec, and cached per-executable on first connect). The host shipped no .desktop, so it was permanently denied; it only ever worked on the headless dev box via KWIN_WAYLAND_NO_PERMISSION_CHECKS=1. Ship packaging/linux/io.unom.Punktfunk.Host.desktop (least-privilege: only the host, only zkde_screencast_unstable_v1 + org_kde_kwin_fake_input) and install it from the RPM/.deb/Arch host packaging so it is present before the host first connects. Drop the blunt session-wide NO_PERMISSION_CHECKS hack from kde-desktop-setup.sh (it now only seeds the RemoteDesktop input grant) and fix the now-misleading kwin.rs docs/errors. Validated live on a Bazzite Kinoite box (KWin 6.6.4): probe-compositor + spike --source kwin-virtual succeed against a KWin running WITHOUT the permission bypass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../punktfunk-host/src/vdisplay/linux/kwin.rs | 23 ++++++--- packaging/arch/PKGBUILD | 6 +++ packaging/bazzite/kde-desktop-setup.sh | 47 ++++++++++--------- packaging/debian/build-deb.sh | 7 +++ .../linux/io.unom.Punktfunk.Host.desktop | 19 ++++++++ packaging/rpm/punktfunk.spec | 12 ++++- 6 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 packaging/linux/io.unom.Punktfunk.Host.desktop diff --git a/crates/punktfunk-host/src/vdisplay/linux/kwin.rs b/crates/punktfunk-host/src/vdisplay/linux/kwin.rs index 8ee651d..a234d6a 100644 --- a/crates/punktfunk-host/src/vdisplay/linux/kwin.rs +++ b/crates/punktfunk-host/src/vdisplay/linux/kwin.rs @@ -6,8 +6,14 @@ //! node for it. The node lives on the user's default PipeWire daemon, so [`VirtualOutput::remote_fd`] //! is `None` and capture connects to that daemon directly. //! -//! Requirements: KWin must expose the privileged `zkde_screencast` global — a real Plasma session -//! authorizes it for its own clients; the headless test exposes it to bare clients via +//! Requirements: KWin must expose the privileged `zkde_screencast` global. It is a *restricted* +//! protocol — KWin advertises it only to a client whose installed `.desktop` lists it under +//! `X-KDE-Wayland-Interfaces` (KWin maps the connecting client to a `.desktop` by resolving +//! `/proc//exe` against `Exec=`, then caches the grant per-executable for the session's life). +//! So an interactive Plasma session does NOT hand it to a bare client — the host packages ship +//! `io.unom.Punktfunk.Host.desktop` (`Exec=/usr/bin/punktfunk-host`, +//! `X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1,…`) so it is present before the host first +//! connects. The headless test path instead exposes it to bare clients via //! `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`. The compositor backend must implement //! `createVirtualOutput`: the **DRM backend** (any version) or the **VirtualBackend since KWin //! 6.5.6** (`kwin_wayland --virtual`); on `--virtual` < 6.5.6 the request fails with @@ -406,9 +412,11 @@ pub fn probe() -> Result<()> { queue.roundtrip(&mut state).context("registry roundtrip")?; if state.screencast.is_none() { bail!( - "KWin is up but does not (yet) expose zkde_screencast_unstable_v1 — needs a real \ - KDE session (or KWIN_WAYLAND_NO_PERMISSION_CHECKS=1), and KWin ≥ 6.5.6 for the \ - headless virtual output" + "KWin is up but does not expose zkde_screencast_unstable_v1 to this client — KWin gates \ + it on the host's .desktop X-KDE-Wayland-Interfaces (install \ + io.unom.Punktfunk.Host.desktop with Exec=/usr/bin/punktfunk-host, then re-login so KWin \ + re-reads it — the grant is cached per-exe on first connect), or set \ + KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 for the headless test; needs KWin ≥ 6.5.6" ); } Ok(()) @@ -437,8 +445,9 @@ fn run( let screencast = state.screencast.clone().ok_or_else(|| { anyhow!( - "KWin does not expose zkde_screencast_unstable_v1 (need a real KDE session, or run \ - KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 for the headless test)" + "KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \ + .desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \ + KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless test)" ) })?; diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index c8838bd..57ab7a0 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -86,6 +86,12 @@ package_punktfunk-host() { install -Dm0644 "$R/scripts/punktfunk-kde-session.service" "$pkgdir/usr/lib/systemd/user/punktfunk-kde-session.service" sed -i 's#%h/punktfunk/scripts/headless/run-headless-kde.sh#/usr/share/punktfunk/headless/run-headless-kde.sh#' \ "$pkgdir/usr/lib/systemd/user/punktfunk-kde-session.service" + # KWin Desktop-mode authorization: non-launcher .desktop whose X-KDE-Wayland-Interfaces lets the + # host bind KWin's restricted zkde_screencast (virtual output) + fake_input globals on an + # interactive Plasma session. Must ship with the host (KWin caches the per-exe grant on first + # connect). See the file's header comment. + install -Dm0644 "$R/packaging/linux/io.unom.Punktfunk.Host.desktop" \ + "$pkgdir/usr/share/applications/io.unom.Punktfunk.Host.desktop" # headless session helpers + env templates + OpenAPI doc install -Dm0755 "$R/scripts/headless/run-headless-kde.sh" "$pkgdir/usr/share/punktfunk/headless/run-headless-kde.sh" install -Dm0755 "$R/scripts/headless/run-headless-sway.sh" "$pkgdir/usr/share/punktfunk/headless/run-headless-sway.sh" diff --git a/packaging/bazzite/kde-desktop-setup.sh b/packaging/bazzite/kde-desktop-setup.sh index ff50da7..ee2bb8f 100644 --- a/packaging/bazzite/kde-desktop-setup.sh +++ b/packaging/bazzite/kde-desktop-setup.sh @@ -1,35 +1,36 @@ #!/usr/bin/env bash -# One-shot setup so the punktfunk host can stream the Bazzite KDE *Desktop* session (KWin virtual -# output at the client's resolution). Run ONCE as the streaming user (no root needed). Gaming Mode -# (gamescope) needs none of this — it auto-attaches. Idempotent: safe to re-run. +# One-shot setup so the punktfunk host can INJECT INPUT while streaming the Bazzite KDE *Desktop* +# session. Run ONCE as the streaming user (no root needed). Gaming Mode (gamescope) needs none of +# this — it auto-attaches. Idempotent: safe to re-run. # # bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh # -# Two things a normal KDE login lacks that the headless host needs: -# 1. KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 — so KWin exposes the privileged `zkde_screencast` -# virtual-output protocol to the host (an external client) at all. -# 2. The `kde-authorized` RemoteDesktop grant — so libei input setup auto-approves instead of -# popping an "Allow remote control?" dialog the headless host can't answer. -# After running, log out + back into the KDE Desktop session once (or reboot) so KWin restarts -# with the flag. Gaming Mode is unaffected. +# The VIRTUAL OUTPUT (video) needs no setup: the host package ships io.unom.Punktfunk.Host.desktop, +# whose X-KDE-Wayland-Interfaces grants the host KWin's restricted zkde_screencast protocol on a +# normal interactive Plasma session — least-privilege (only the host, only that interface), the same +# mechanism krfb/krdp use. No session-wide KWIN_WAYLAND_NO_PERMISSION_CHECKS hack is needed. KWin +# caches the grant per-executable on first connect, so after a FRESH host install log out + back into +# the Desktop session once so KWin re-reads the file. +# +# The one thing a normal KDE login still lacks is the `kde-authorized` RemoteDesktop grant — so the +# host's libei input setup auto-approves instead of popping an "Allow remote control?" dialog the +# headless host can't answer. That's what this script seeds. set -euo pipefail GRANT_SRC="${PUNKTFUNK_GRANT_SRC:-/usr/share/punktfunk/headless/kde-authorized}" -ENVD="$HOME/.config/environment.d/10-punktfunk-kwin.conf" GRANT_DST="$HOME/.local/share/flatpak/db/kde-authorized" +# Older versions of this script wrote a session-wide KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 env file to +# unlock screencast. The shipped .desktop replaces it; remove the stale, over-broad override. +STALE_ENVD="$HOME/.config/environment.d/10-punktfunk-kwin.conf" -echo "punktfunk: KDE Desktop-mode setup" +echo "punktfunk: KDE Desktop-mode input setup" -# 1. KWin permission-check bypass (persistent, picked up by the next KDE session via systemd). -mkdir -p "$(dirname "$ENVD")" -cat > "$ENVD" <<'EOF' -# punktfunk: let the streaming host bind KWin's privileged zkde_screencast (virtual output). -# A dedicated streaming box; this relaxes KWin's Wayland permission checks for the desktop path. -KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 -EOF -echo " wrote $ENVD" +if [[ -f "$STALE_ENVD" ]] && grep -q KWIN_WAYLAND_NO_PERMISSION_CHECKS "$STALE_ENVD" 2>/dev/null; then + rm -f "$STALE_ENVD" + echo " removed stale $STALE_ENVD (screencast is now granted via the shipped .desktop)" +fi -# 2. RemoteDesktop portal grant for headless libei input (never clobber an existing one). +# RemoteDesktop portal grant for headless libei input (never clobber an existing one). if [[ -s "$GRANT_DST" ]]; then echo " grant DB already present ($GRANT_DST) — leaving it" elif [[ -s "$GRANT_SRC" ]]; then @@ -44,5 +45,5 @@ else echo " WARN: grant source not found at $GRANT_SRC — input will need a manual portal approval" >&2 fi -echo "punktfunk: done. Log out + back into the KDE Desktop session (or reboot) so KWin restarts" -echo " with the flag, then connect a client while in Desktop Mode." +echo "punktfunk: done. On a fresh host install, log out + back into the KDE Desktop session once" +echo " (so KWin authorizes the host's virtual output), then connect a client in Desktop Mode." diff --git a/packaging/debian/build-deb.sh b/packaging/debian/build-deb.sh index b8942e7..b945826 100755 --- a/packaging/debian/build-deb.sh +++ b/packaging/debian/build-deb.sh @@ -50,6 +50,13 @@ sed -i 's#%h/punktfunk/target/release/punktfunk-host#/usr/bin/punktfunk-host#' \ install -Dm0644 scripts/punktfunk-kde-session.service "$STAGE/usr/lib/systemd/user/punktfunk-kde-session.service" sed -i 's#%h/punktfunk/scripts/headless/run-headless-kde.sh#/usr/share/punktfunk-host/headless/run-headless-kde.sh#' \ "$STAGE/usr/lib/systemd/user/punktfunk-kde-session.service" + +# KWin Desktop-mode authorization: non-launcher .desktop whose X-KDE-Wayland-Interfaces lets the +# host bind KWin's restricted zkde_screencast (virtual output) + fake_input globals on an +# interactive Plasma session. Must ship with the host — KWin caches the per-exe grant on first +# connect, so it has to be present before the host ever connects. See the file's header comment. +install -Dm0644 packaging/linux/io.unom.Punktfunk.Host.desktop \ + "$STAGE/usr/share/applications/io.unom.Punktfunk.Host.desktop" install -Dm0755 scripts/headless/run-headless-kde.sh "$SHAREDIR/headless/run-headless-kde.sh" install -Dm0755 scripts/headless/run-headless-sway.sh "$SHAREDIR/headless/run-headless-sway.sh" install -Dm0644 scripts/headless/kde-authorized "$SHAREDIR/headless/kde-authorized" diff --git a/packaging/linux/io.unom.Punktfunk.Host.desktop b/packaging/linux/io.unom.Punktfunk.Host.desktop new file mode 100644 index 0000000..d2fb168 --- /dev/null +++ b/packaging/linux/io.unom.Punktfunk.Host.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Type=Application +Name=Punktfunk Host +Comment=punktfunk streaming host — KWin virtual-output / input authorization +Exec=/usr/bin/punktfunk-host +Terminal=false +NoDisplay=true +# This file is NOT a launcher — it exists so KWin authorizes the host to bind its restricted +# Wayland globals when streaming the *Desktop* (KWin) session. KWin maps a connecting client to a +# .desktop by resolving /proc//exe against `Exec` (hence the absolute /usr/bin path), then +# grants only the interfaces listed here (the same mechanism krfb-virtualmonitor / krdpserver use): +# * zkde_screencast_unstable_v1 — create the per-session virtual output at the client's mode. +# * org_kde_kwin_fake_input — inject input directly (no RemoteDesktop portal dialog). +# Comma-separated, per KWin's parser. Without this file KWin never advertises these to the host and +# desktop-mode streaming fails with "KWin does not expose zkde_screencast_unstable_v1". Gaming Mode +# (gamescope) does not use this path. NOTE: KWin caches the per-executable grant on first connect, +# so this must be installed *before* the host first connects (a package install satisfies that; an +# already-running KWin session needs a re-login to pick it up). +X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1,org_kde_kwin_fake_input diff --git a/packaging/rpm/punktfunk.spec b/packaging/rpm/punktfunk.spec index fc29a8c..6c00edc 100644 --- a/packaging/rpm/punktfunk.spec +++ b/packaging/rpm/punktfunk.spec @@ -196,6 +196,14 @@ sed -i 's#%h/punktfunk/target/release/punktfunk-host#%{_bindir}/punktfunk-host#' install -Dm0644 scripts/punktfunk-kde-session.service %{buildroot}%{_userunitdir}/punktfunk-kde-session.service sed -i 's#%h/punktfunk/scripts/headless/run-headless-kde.sh#%{_datadir}/%{name}/headless/run-headless-kde.sh#' %{buildroot}%{_userunitdir}/punktfunk-kde-session.service +# KWin authorization for Desktop-mode (KWin) streaming: a non-launcher .desktop whose +# X-KDE-Wayland-Interfaces grants the host the restricted zkde_screencast (virtual output) + +# fake_input globals on an interactive Plasma session. Must ship with the host so it is present +# before the host first connects (KWin caches the per-exe grant). Replaces the old manual +# KWIN_WAYLAND_NO_PERMISSION_CHECKS hack for the screencast permission. +install -Dm0644 packaging/linux/io.unom.Punktfunk.Host.desktop \ + %{buildroot}%{_datadir}/applications/io.unom.Punktfunk.Host.desktop + # --- client subpackage --- install -Dm0755 target/release/punktfunk-client %{buildroot}%{_bindir}/punktfunk-client install -Dm0644 packaging/linux/io.unom.Punktfunk.desktop \ @@ -221,7 +229,8 @@ install -Dm0644 scripts/headless/punktfunk-sink.conf %{buildroot}%{_datadir}/% install -Dm0644 scripts/host.env.example %{buildroot}%{_datadir}/%{name}/host.env.example install -Dm0644 packaging/bazzite/host.env %{buildroot}%{_datadir}/%{name}/host.env.bazzite install -Dm0644 packaging/kde/host.env %{buildroot}%{_datadir}/%{name}/host.env.kde -# Bazzite KDE Desktop-mode one-shot setup (KWIN_WAYLAND_NO_PERMISSION_CHECKS + RemoteDesktop grant). +# Bazzite KDE Desktop-mode one-shot setup (seeds the RemoteDesktop grant for libei input; the +# screencast/virtual-output grant ships as io.unom.Punktfunk.Host.desktop, installed above). install -d %{buildroot}%{_datadir}/%{name}/bazzite install -Dm0755 packaging/bazzite/kde-desktop-setup.sh %{buildroot}%{_datadir}/%{name}/bazzite/kde-desktop-setup.sh install -Dm0644 api/openapi.json %{buildroot}%{_datadir}/%{name}/openapi.json @@ -252,6 +261,7 @@ install -Dm0644 web/web.env.example %{buildroot}%{_datadir}/punkt %{_prefix}/lib/sysctl.d/99-punktfunk-net.conf %{_userunitdir}/punktfunk-host.service %{_userunitdir}/punktfunk-kde-session.service +%{_datadir}/applications/io.unom.Punktfunk.Host.desktop %dir %{_datadir}/%{name} %{_datadir}/%{name}/*