Compare commits

...
Author SHA1 Message Date
enricobuehler 647e2b8891 fix(pf-vdisplay): bind our wrapper over the hardcoded gamescope path, where GAMESCOPE_BIN cannot reach
ci / bun-nix (pull_request) Successful in 29s
ci / web (pull_request) Successful in 1m28s
ci / rust-arm64 (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m44s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m45s
android / android (pull_request) Successful in 3m49s
ci / rust (pull_request) Successful in 11m12s
Nobara's gamescope-session-plus HARDCODES an absolute gamescope path and never reads GAMESCOPE_BIN —
verified on the box: `grep GAMESCOPE_BIN` over the script returns nothing, and line 244 opens
`GAMESCOPECMD="/usr/bin/gamescope \`. So neither the wrapper nor a PATH shim can reach it, and the
managed session comes up as stock gamescope with none of our flags. `verify_managed_spawn_flags`
catches that and refuses HDR and the in-node cursor rather than streaming a session planned around
flags that never arrived — correct, but it leaves every Nobara-family box with no composited cursor
and no HDR, and the operator's only recourse is overwriting a distro-owned binary.

Bind it instead, inside the session unit's own mount namespace: the session gets our gamescope, and
nothing outside the unit changes — the distro still owns the file on disk.

⚠ The bind source is the WRAPPER, not the patched binary. Binding the binary would have been a
vacuous fix: the flags this whole mechanism exists to deliver are injected BY the wrapper, so a bound
binary arrives with no flags and `verify_managed_spawn_flags` refuses exactly as before. Binding the
wrapper reproduces what GAMESCOPE_BIN would have done had the script consulted it.

Applied only where the escape hatch is structurally absent, decided by READING the script rather than
by distro name: if `GAMESCOPE_BIN` appears anywhere in it, we keep the mechanism that already works
and take no mount namespace we don't need. Parsing is pure and unit-tested — the `GAMESCOPECMD+=`
appends and the `[ -z "$GAMESCOPECMD" ]` test must not be mistaken for the opening assignment, or the
bind would land over `$socket`. Three further refusals: the target must be a file; the resolved
source must be absolute and NOT the target (a fork-bomb guard — the wrapper execs the real binary, so
shadowing that same path would exec itself forever); and the preflight must pass.

Availability is PROVEN, not inferred, with a throwaway `systemd-run --user --wait --collect
--property=BindReadOnlyPaths=<the real value> -- /bin/true`. That matters: on .25 the bind fails with
`status=226/NAMESPACE` while `kernel.unprivileged_userns_clone=1` and `max_user_namespaces=29006` are
both permissive — AppArmor's `apparmor_restrict_unprivileged_userns` blocks it. A sysctl check would
have said "available" and armed a bind that cannot work. The payload is /bin/true, so the preflight
can never accidentally start a compositor.

Runtime backstop for anything the preflight cannot see: `ExecMainStatus == 226` disarms the bind and
relaunches plain, latching one-way per process.

The transient host-owned unit takes the setting via `systemd-run --property=`, which is atomic with
the start and leaves nothing behind if the host dies. The box's own unit needs a drop-in, so it gets
`zz-punktfunk-bind.conf` on the TEMPLATE, sorted last so nothing can take the setting back out;
removal targets that one filename only — never the directory, never a glob — because the operator's
`10-headless.conf` next to it is what lets game mode start on that box at all.
2026-08-09 23:11:00 +02:00
enricobuehler 3c096835c2 fix(pf-vdisplay): the takeover blamed polkit for a group it never named, and offered two remedies that cannot work
Field triage on Nobara, 2026-08-09. Every connect degraded to ATTACH — which on that box mirrors a
game-mode session the host never configured, and looked like a black screen on every connect. The
host said:

    the packaged pf-dm-helper polkit action is missing or was denied (reinstall the punktfunk
    package, or install the display-manager polkit rule from the docs)

Every clause of that was wrong. The action was installed, `allow_any`, and its exec.path annotation
matched the installed helper; pkexec authorized it and RAN the helper. The helper refused, and said
exactly why:

    pf-dm-helper: user 'nobara-user' is not in the 'punktfunk' group — refusing.
      Grant it with: sudo usermod -aG punktfunk nobara-user   (then re-login)

That text never reached the log, because `dm_helper` ran the helper with `.status()` — which
discards stderr and collapses the exit code to a bool. The one thing that would have ended the
investigation in seconds was thrown away at the call site, and the caller then guessed. Neither
suggested remedy adds anyone to a group, so a reader who followed both stayed broken and learned the
docs were useless. It fails soft, with no error and no failed unit, so nobody finds it on purpose.

Now: `.output()`, and four failure modes that stay distinguishable because they need different
fixes — helper not installed, pkexec could not run it, polkit denied it (pkexec's own 126/127), and
the helper ran and refused, whose stderr rides through VERBATIM rather than being re-described. Null
stdin too, so a pkexec that decides to prompt gets EOF instead of parking a stream thread on a tty
read.

The same gate gates the `linger` verb, so on a sessionless host an unjoined user fails there first —
carrying the reason there as well, or the misdiagnosis just moves one message earlier.

A new startup preflight says it before a stream is being built rather than during one, gated so it
cannot nag a box that would never attempt a takeover: not root, a display-manager alias exists, a
managed session launcher exists, a packaged helper exists, and the user is not in the group. It reads
membership from the user database rather than this process's groups, deliberately: that is what the
helper reads (it runs as root and resolves the caller from the database), so `usermod -aG` satisfies
the DM gate immediately and the warning stops. Using `getgroups()` would keep warning on a box where
the takeover already works.

Packaging said the group was for "the virtual Steam Deck pad (usbip)" — so anyone without a Deck pad
correctly skipped it and landed here by following instructions properly. All three scriptlets now
lead with Game Mode, name both grants, and record that creating the group is necessary and NOT
sufficient. Docs get the same treatment: the group is an admonition above the DM-flavor list in
gamescope.md, a black-screen entry in troubleshooting.md that tells the reader to read the quoted
reason FIRST, and the per-distro install pages no longer frame it as pad-only.
2026-08-09 22:35:36 +02:00
15 changed files with 1060 additions and 124 deletions
+4 -3
View File
@@ -87,9 +87,10 @@ pub use session::{session_epoch, try_recover_session};
#[path = "vdisplay/routing.rs"]
pub(crate) mod routing;
pub use routing::{
apply_input_env, managed_session_available, resolve_gamescope_route, restore_managed_session,
restore_takeover_now, restore_takeover_on_startup, start_restore_worker,
wants_dedicated_game_session, GamescopeRoute,
apply_input_env, managed_session_available, preflight_takeover_privilege,
resolve_gamescope_route, restore_managed_session, restore_takeover_now,
restore_takeover_on_startup, start_restore_worker, wants_dedicated_game_session,
GamescopeRoute,
};
#[cfg(target_os = "linux")]
pub use routing::{
File diff suppressed because it is too large Load Diff
@@ -371,6 +371,19 @@ pub fn restore_takeover_on_startup() {
#[cfg(not(target_os = "linux"))]
pub fn restore_takeover_on_startup() {}
/// Warn ONCE, at startup, when this box will need the managed gamescope takeover but its user is
/// not in the `punktfunk` group the packaged privilege helper gates on — the one takeover
/// prerequisite that fails silently mid-stream instead of at setup time. Gated so a box that will
/// never attempt a takeover stays quiet; see [`gamescope::preflight_takeover_privilege`] for the
/// exact conditions. Call once at `serve` startup, alongside [`restore_takeover_on_startup`].
#[cfg(target_os = "linux")]
pub fn preflight_takeover_privilege() {
gamescope::preflight_takeover_privilege();
}
#[cfg(not(target_os = "linux"))]
pub fn preflight_takeover_privilege() {}
/// Give the box its own session back **now**, synchronously, because the host is exiting. Blocks
/// (it shells out to `systemctl`), so call it off the async runtime. Call from the host's shutdown
/// path — a takeover that outlives the host leaves the box with no display manager and nobody left
+6
View File
@@ -374,6 +374,12 @@ pub(crate) async fn serve(
// A3: recover a TV takeover stranded by a crashed previous host instance (persisted to
// $XDG_RUNTIME_DIR) — schedule a restore after a reconnect grace. No-op on a clean start.
crate::vdisplay::restore_takeover_on_startup();
// …and check the takeover's one un-automatable prerequisite BEFORE a stream needs it: on a box
// that will use the takeover, the host's user must be in the `punktfunk` group the packaged
// privilege helper gates on. Missing membership fails nothing — the takeover degrades to
// mirroring the box's own session — so without this it surfaces only as a black screen on
// every connect. No-op off Linux and on any box the takeover can't apply to.
crate::vdisplay::preflight_takeover_privilege();
// …and the other end of that: give the box its session back when WE are the ones going away.
install_shutdown_restore();
// Host-lifetime cover-art warmer: fetches + caches GOG/Xbox cover art (no-auth api.gog.com /
+9 -5
View File
@@ -57,17 +57,21 @@ sudo pacman -Syu punktfunk-scripting # optional: the plugin/script runner (see b
sudo usermod -aG input "$USER" # /dev/uinput access for virtual gamepads (re-login to apply)
```
Only if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro — it reaches games
as a real USB pad, which is why Steam Input adopts it), also join `punktfunk`:
Also join `punktfunk` if **either** applies — you want the **virtual Steam Deck controller**
(paddles, trackpads, gyro — it reaches games as a real USB pad, which is why Steam Input adopts
it), or this box autologins into Steam **Gaming Mode** and you want the host to take that session
over at the client's resolution:
```sh
sudo usermod -aG punktfunk "$USER" # usbip/vhci access (re-login to apply)
sudo usermod -aG punktfunk "$USER" # usbip/vhci + display-manager takeover (re-login to apply)
```
That is a second group on purpose. It grants write access to the usbip `attach` file, which
materialises an arbitrary emulated USB device — so it stays off the `input` group everyone is
routinely told to join. Join it only on a machine you trust. Without it, everything else still
works and the pad simply arrives as an ordinary Xbox 360 controller.
routinely told to join. Join it only on a machine you trust. On a plain desktop host, everything
else still works without it and the pad simply arrives as an ordinary Xbox 360 controller; on a
Gaming Mode box the takeover silently degrades to mirroring the box's own screen — see
[gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
Each install is a **full** `-Syu`, on purpose: our packages are built against current Arch
sonames, and `pacman -Sy <pkg>` would drop one onto a system whose other packages are still old —
+10 -6
View File
@@ -126,17 +126,21 @@ ujust add-user-to-input-group
Then **log out and back in**. (A controller that's "detected but does nothing" is almost always this
permission, not a client problem.)
Only if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro), also join
`punktfunk``usermod` is fine here, because unlike `input` this group is ours and the sysext
creates it on merge:
Then join `punktfunk``usermod` is fine here, because unlike `input` this group is ours and the
sysext creates it on merge:
```sh
sudo usermod -aG punktfunk "$USER" # then log out and back in
```
It is a separate group on purpose: it gates the usbip `attach` file, which can materialise
arbitrary emulated USB hardware, so it is not folded into the group everyone is told to join for
gamepads. Skip it and the pad arrives as an ordinary Xbox 360 controller instead.
This box **is** a Gaming Mode box, so that group is not optional in practice: it authorizes the
helper the host uses to stop the display manager when it takes the Gaming Mode session over at your
client's resolution, and it gates the usbip `attach` file the **virtual Steam Deck controller**
(paddles, trackpads, gyro) attaches through. It is a separate group on purpose — writing that file
can materialise arbitrary emulated USB hardware, so it is not folded into the group everyone is
told to join for gamepads. Without it the pad arrives as an ordinary Xbox 360 controller, and the
takeover degrades to mirroring the box's own screen — see
[gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
## Configure
+9 -5
View File
@@ -93,17 +93,21 @@ sudo dnf install punktfunk
sudo usermod -aG input "$USER" # /dev/uinput access for virtual gamepads (re-login to apply)
```
Only if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro — it reaches games
as a real USB pad, which is why Steam Input adopts it), also join `punktfunk`:
Also join `punktfunk` if **either** applies — you want the **virtual Steam Deck controller**
(paddles, trackpads, gyro — it reaches games as a real USB pad, which is why Steam Input adopts
it), or this box autologins into Steam **Gaming Mode** (Nobara and friends) and you want the host
to take that session over at the client's resolution:
```sh
sudo usermod -aG punktfunk "$USER" # usbip/vhci access (re-login to apply)
sudo usermod -aG punktfunk "$USER" # usbip/vhci + display-manager takeover (re-login to apply)
```
That is a second group on purpose: it grants write access to the usbip `attach` file, which
materialises an arbitrary emulated USB device, so it stays off the `input` group everyone is
routinely told to join. Join it only on a machine you trust. Skip it and the pad simply arrives as
an ordinary Xbox 360 controller.
routinely told to join. Join it only on a machine you trust. Skip it on a plain desktop host and
the pad simply arrives as an ordinary Xbox 360 controller; skip it on a Gaming Mode box and the
takeover silently degrades to mirroring the box's own screen — see
[gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
Updates later are just `sudo dnf upgrade punktfunk`, followed by
`systemctl --user restart punktfunk-host` so the running host picks up the new binary. The package
+50 -12
View File
@@ -40,16 +40,37 @@ the [Bazzite template](/docs/bazzite) ships with **attach** chosen instead.
### Nobara and other autologin display managers
The managed takeover has to stop the box's Gaming Mode session to free Steam. How it does that
depends on the display manager driving the autologin:
The managed takeover has to stop the box's Gaming Mode session to free Steam — and when that
session is a display-manager autologin, it has to stop the **display manager** too, for the length
of the stream. That is a privileged operation, and the privilege is granted to one group.
- **SDDM** (Bazzite, SteamOS): handled automatically — no setup.
- **plasmalogin** (Nobara) and other display managers: the host must stop the display manager
itself for the length of the stream and restart it afterwards, which needs privilege. The
packages ship that privilege: a root helper (`/usr/libexec/punktfunk/pf-dm-helper`, or
`/usr/lib/punktfunk/pf-dm-helper` from the Arch package) behind its own polkit action
(`io.unom.punktfunk.dm-helper`), invoked automatically when the plain
`systemctl` verbs are denied — no setup. The helper only stops/restores the unit the
> **Join the `punktfunk` group on any box you stream Game Mode from.** The takeover's root helper
> runs for members of that group and for nobody else, so this one command is what authorizes it:
>
> ```sh
> sudo usermod -aG punktfunk "$USER" # then log out and back in
> ```
>
> Your package created the group at install time and put **nobody** in it, on purpose: it also
> gates the usbip nodes the virtual Steam Deck pad attaches through, and writing those can present
> arbitrary emulated USB hardware — so joining stays a deliberate act, on a machine you trust.
> Skip it and nothing fails loudly. Every takeover degrades to mirroring the box's own session
> (below), which on a box whose panel is off reads as a black screen on every connect. The host
> checks this at startup on any box that will need the takeover and says so in its log; the
> symptom side is [Game Mode: black screen on
> connect](/docs/troubleshooting#game-mode-black-screen-on-connect-or-the-stream-is-stuck-at-the-boxs-resolution).
How the takeover gets that privilege depends on the display manager driving the autologin:
- **SDDM** (Bazzite, SteamOS): SDDM survives having the session unit masked, so a box without the
grant still streams — at the cost of SDDM relogin-looping against the takeover for the whole
stream, which churns logind sessions and can starve the game.
- **plasmalogin** (Nobara) and other display managers: masking is fatal there (the autologin
start-limit-kills the display manager), so the host stops the display manager itself and
restarts it afterwards. The packages ship that privilege: a root helper
(`/usr/libexec/punktfunk/pf-dm-helper`, or `/usr/lib/punktfunk/pf-dm-helper` from the Arch
package) behind its own polkit action (`io.unom.punktfunk.dm-helper`), invoked automatically
when the plain `systemctl` verbs are denied. The helper only stops/restores the unit the
`display-manager.service` symlink points at, the same class of local-seat operation these
distros already authorize for their own session switcher (Nobara's `os-session-select`).
@@ -71,8 +92,11 @@ depends on the display manager driving the autologin:
With no privilege path at all the host degrades safely: it **attaches** to the live Gaming Mode
session instead (Game Mode stays on the box's display at the box's own resolution, mirrored to
the client — if your monitor stays on and the stream runs at the desktop's resolution, this is
what happened; check the host log for "managed takeover unavailable"). If the display-manager
restart ever loses its privilege mid-restore, `PUNKTFUNK_RECOVER_SESSION_CMD` (see
what happened; check the host log for "managed takeover unavailable"). That log line now quotes
the privileged path's own reason for refusing, so read it before changing anything: by far the
most common one is `not in the 'punktfunk' group`, which the group command above fixes and
neither a reinstall nor a polkit rule does. If the display-manager restart ever loses its
privilege mid-restore, `PUNKTFUNK_RECOVER_SESSION_CMD` (see
[Configuration](/docs/configuration)) is fired as the fallback.
**Lingering is required here**, and the host turns it on for you the first time it takes the box
@@ -81,7 +105,9 @@ depends on the display manager driving the autologin:
taking the host with it, mid-stream, with the display manager down and nothing left to bring it
back. If lingering can't be enabled the host refuses the takeover and degrades to attach instead
(above) rather than risk that. Run `sudo loginctl enable-linger "$USER"` once, as the setup guides
ask; `loginctl disable-linger "$USER"` reverts it.
ask; `loginctl disable-linger "$USER"` reverts it. (A host with no login session of its own turns
lingering on through the *same* helper, so a missing group grant surfaces here first — the log
says "enabling lingering failed" and then quotes the same reason.)
With the takeover authorized the **in-stream session switch round-trips** in managed mode:
Steam's "Switch to Desktop" inside the streamed Game Mode returns the box to its desktop session
@@ -214,6 +240,18 @@ These apply to the **Gaming Mode (gamescope)** path only; the desktop path is un
mode and rate, and `PUNKTFUNK_GAMESCOPE_REFRESH_RATES=60,90,120` puts more than one entry in that
menu. If the host log says *"the session did not start at the mode we asked for"*, a file in
`/etc/gamescope-session-plus/sessions.d/` is overriding `GAMESCOPE_BIN` or setting `GAMESCOPECMD`.
- **Some distros' session script ignores `GAMESCOPE_BIN` entirely — the host works around it with a
bind mount.** Nobara's `gamescope-session-plus` never reads that variable and hardcodes
`/usr/bin/gamescope` outright, so neither the variable nor a `PATH` entry can point the session at
`punktfunk-gamescope`; every session ran the stock binary, which cost HDR *and* the in-stream
cursor. The host now detects that shape by reading the script, and binds its own wrapper over the
hardcoded path **inside the session unit's own mount namespace** — the session gets the patched
gamescope and the flags, and nothing outside that unit changes (the distro still owns
`/usr/bin/gamescope`; you do not need to overwrite it). You will see *"binding the punktfunk
wrapper over it"* in the log when this engages. It needs unprivileged user namespaces; on a box
without them the host logs that it cannot take a mount namespace and streams SDR with a
host-composited cursor, exactly as before. Boxes whose script does honour `GAMESCOPE_BIN`
(Bazzite, SteamOS) are untouched by any of this.
- **The performance overlay (fps / frametime / stats) needs the patched build.** It is mangoapp,
which gamescope draws as an *external overlay* — a layer upstream's capture composite has never
included on any version, so on a stock gamescope you can turn the overlay on and it simply will
+10 -5
View File
@@ -156,11 +156,16 @@ you; on NixOS the module does steps 1 and 2, and [NixOS](#nixos) above has the u
command differs per distro — see your guide (`usermod -aG input "$USER"`, or `ujust
add-user-to-input-group` on Bazzite).
Only if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro), also join
`punktfunk`: `sudo usermod -aG punktfunk "$USER"`. Your package created that group at install
time; it gates the usbip nodes that pad attaches through, and it is separate from `input` on
purpose, because writing them can present arbitrary emulated USB hardware. Join it only on a
machine you trust — skipping it costs you nothing but that one pad type.
Also join `punktfunk``sudo usermod -aG punktfunk "$USER"`, then log out and back in — if
**either** of these is true: you want the **virtual Steam Deck controller** (paddles,
trackpads, gyro), or this box autologins into Steam **Gaming Mode** and you want the host to
take that session over at your client's resolution
([gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers)). Your package created
that group at install time and left it empty. It gates the usbip nodes the pad attaches through
*and* the helper that stops the display manager for a takeover, and it is separate from `input`
on purpose, because writing those nodes can present arbitrary emulated USB hardware — so join it
only on a machine you trust. On a plain desktop host that streams no Gaming Mode, skipping it
costs you nothing but that one pad type.
2. Put your `host.env` in place, then start the host. Every Linux package ships a systemd **user**
unit, so you don't run the host by hand — but that unit reads `~/.config/punktfunk/host.env` and
won't start until the file exists. Each package ships a template to copy; your distro and desktop
@@ -133,7 +133,11 @@ disable, and the session unit differ per compositor, so each is documented on it
- GNOME: [GNOME → Headless session](/docs/gnome#headless-session).
- KDE Plasma: [KDE → Headless session](/docs/kde#headless-session).
- Steam / gamescope: [gamescope](/docs/gamescope) — the host launches its own session per client, so
there's no separate session unit.
there's no separate session unit. A headless box that autologins into **Gaming Mode** needs one
more thing: your user in the `punktfunk` group (`sudo usermod -aG punktfunk "$USER"`, then log
out and back in). Without it the host cannot stop the display manager to take that session over,
so every connect quietly mirrors the box's own screen — which, headless, is a black one. See
[gamescope → autologin display managers](/docs/gamescope#nobara-and-other-autologin-display-managers).
Once a session comes up at boot, enable the host user service (section A) and reboot. The host comes up
on that session.
+42
View File
@@ -198,6 +198,44 @@ Current hosts detect the display-manager flavor and never mask the session unit
[gamescope → autologin display managers](/docs/gamescope) for the polkit rule that enables the full
managed takeover on these boxes (without it the host mirrors Game Mode instead).
## Game Mode: black screen on connect, or the stream is stuck at the box's resolution
You connect to a box that autologins into Steam **Gaming Mode** and get a black picture every time
— or a picture at the box's own resolution instead of the one your client asked for, with the box's
monitor still lit. Nothing errors: the client connects, the host logs no failure, no unit is failed.
The managed takeover is being refused and the host is falling back to mirroring the box's own
session. On a box whose panel is off (a headless appliance, a TV that's been switched away) there
is nothing to mirror, so the fallback is a black screen. Almost always the cause is **group
membership**: the takeover stops the display manager through a root helper, and that helper serves
members of the `punktfunk` group only.
```sh
id -nG | tr ' ' '\n' | grep -x punktfunk # are you in it?
journalctl --user -u punktfunk-host | grep -iE "punktfunk. group|takeover unavailable"
```
The host also checks at startup on any box that will need the takeover, so a fresh
`systemctl --user restart punktfunk-host` puts the answer at the top of the log. The fix is one
command and a fresh login:
```sh
sudo usermod -aG punktfunk "$USER" # then log out and back in
```
> **Read the reason the log quotes before doing anything else.** The takeover has three other ways
> to be refused — no packaged helper (a tarball or source install), no polkit on the box, and
> polkit denying the action — and the host now prints which one it hit, verbatim from the
> privileged path. Hosts up to 0.27.0 printed a fixed guess instead ("reinstall the punktfunk
> package, or install the display-manager polkit rule from the docs"), and on the group case both
> of those suggestions were dead ends: neither adds anyone to a group.
Two things this is *not*: it isn't the [pad group problem](#the-pad-works-but-arrives-as-an-xbox-360-controller-instead-of-a-steam-deck)
(same group, different symptom), and it isn't lingering — though a host with no login session of
its own enables lingering through the same helper, so an unjoined user often sees "enabling
lingering failed" first. Both are covered in
[gamescope → autologin display managers](/docs/gamescope#nobara-and-other-autologin-display-managers).
## Session fails right after editing host.env
- Keys are **case-sensitive**: `punktfunk_gamescope_attach=1` sets nothing — use the exact
@@ -275,6 +313,10 @@ the reliable way to get one.
Joining the group is optional, and there is a real reason it is not automatic: writing that
`attach` file materialises an arbitrary emulated USB device. Skip it on a machine you share.
It is not only the pad, though: the same group authorizes the helper that stops the display manager
for a managed **Gaming Mode** takeover, so on a box that autologins into Game Mode, skipping it also
costs you [the takeover](#game-mode-black-screen-on-connect-or-the-stream-is-stuck-at-the-boxs-resolution).
## Copy and paste between host and client does nothing
The shared clipboard needs **two** separate switches on, and turning on only one looks exactly like
+8 -5
View File
@@ -111,17 +111,20 @@ re-login so the new group membership takes effect:
sudo usermod -aG input "$USER" # re-login to apply
```
Only if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro), also join
`punktfunk`. That pad reaches games as a real USB device over usbip — which is what makes Steam
Input adopt it — and the group gating those nodes is deliberately separate from `input`, because
Also join `punktfunk` if **either** applies — you want the **virtual Steam Deck controller**
(paddles, trackpads, gyro), or this box autologins into Steam **Gaming Mode** and you want the host
to take that session over at the client's resolution. That pad reaches games as a real USB device
over usbip — which is what makes Steam Input adopt it — and the same group authorizes the helper
that stops the display manager for a takeover. It is deliberately separate from `input`, because
writing the usbip `attach` file can materialise arbitrary emulated USB hardware:
```sh
sudo usermod -aG punktfunk "$USER" # re-login to apply
```
Join it only on a machine you trust. Skip it and everything else still works; the pad just arrives
as an ordinary Xbox 360 controller.
Join it only on a machine you trust. On a plain desktop host, skipping it costs you nothing but
that one pad type; on a Gaming Mode box the takeover silently degrades to mirroring the box's own
screen — see [gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
## 4. Check it installed
+9 -4
View File
@@ -8,7 +8,10 @@ _ensure_punktfunk_group() {
# Owns the usbip vhci attach/detach nodes (60-punktfunk.rules). Separate from 'input' on
# purpose: writing 'attach' materialises an arbitrary emulated USB device, which is a root-only
# kernel primitive and must not ride on the group users are told to join for gamepads
# (security-review 2026-08-05 M-4).
# (security-review 2026-08-05 M-4). It is ALSO the group pf-dm-helper authorizes on (its polkit
# action must stay allow_any, so membership is the real gate), i.e. what a managed gamescope
# takeover needs to stop the display manager. Creating the group is necessary and NOT sufficient
# for either use: membership is.
getent group punktfunk >/dev/null 2>&1 || groupadd --system punktfunk 2>/dev/null || true
}
@@ -62,9 +65,11 @@ post_install() {
punktfunk-host installed.
1. Add yourself to the 'input' group for virtual gamepads:
sudo usermod -aG input "$USER" # then re-login
Only if you want the virtual Steam Deck pad (usbip), ALSO join 'punktfunk':
sudo usermod -aG punktfunk "$USER"
That group can emulate arbitrary USB devices — join it only on a machine you trust.
ALSO join 'punktfunk' if this box streams Steam Gaming Mode (gamescope), or you want the
virtual Steam Deck pad (usbip):
sudo usermod -aG punktfunk "$USER" # then log out and back in
It authorizes stopping the display manager for a managed gamescope session, and the pad's
usbip nodes. It can emulate arbitrary USB devices — join it only on a machine you trust.
2. Pick a backend config (gamescope is the no-desktop default on SteamOS/Deck):
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
+11 -3
View File
@@ -292,7 +292,10 @@ if [ "$1" = "configure" ]; then
# Owns the usbip vhci attach/detach nodes (60-punktfunk.rules). Deliberately NOT 'input':
# writing 'attach' materialises an arbitrary emulated USB device — a root-only kernel
# primitive that must not ride on the group users are told to join for gamepads
# (security-review 2026-08-05 M-4).
# (security-review 2026-08-05 M-4). It is ALSO the group pf-dm-helper authorizes on (its
# polkit action must stay allow_any, so membership is the real gate), i.e. what a managed
# gamescope takeover needs to stop the display manager. Creating the group is necessary and
# NOT sufficient for either use: membership is.
getent group punktfunk >/dev/null 2>&1 || addgroup --system punktfunk 2>/dev/null || true
# NO capability on the host binary — and an active removal of the one 0.26.0-1 granted here.
#
@@ -318,8 +321,13 @@ if [ "$1" = "configure" ]; then
sysctl -p /usr/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
echo "punktfunk-host installed. Add yourself to the 'input' group for virtual gamepads:"
echo " sudo usermod -aG input \"\$USER\" # then re-login"
echo "For the virtual Steam Deck pad (usbip) ALSO: sudo usermod -aG punktfunk \"\$USER\""
echo " — that group can emulate arbitrary USB devices; join it only on a machine you trust."
# Naming only the usbip pad here is how a Nobara host shipped broken: its owner had no Deck
# pad, so they correctly skipped this group — and then every managed gamescope takeover
# degraded silently, because pf-dm-helper (which stops the display manager) gates on membership.
echo "ALSO join 'punktfunk' if this box streams Steam Gaming Mode (gamescope) or you want the"
echo "virtual Steam Deck pad: sudo usermod -aG punktfunk \"\$USER\" # then log out and back in"
echo " — it authorizes stopping the display manager for a managed gamescope session, and the"
echo " pad's usbip nodes; it can emulate arbitrary USB devices, so join it only on a box you trust."
echo "Config: mkdir -p ~/.config/punktfunk && cp /usr/share/punktfunk-host/host.env.example ~/.config/punktfunk/host.env"
echo "Enable: systemctl --user enable --now punktfunk-host"
# Debian ships no active firewall and Ubuntu's ufw is inactive by default; hint whichever is present.
+10 -2
View File
@@ -595,6 +595,9 @@ getent group punktfunk-update >/dev/null 2>&1 || groupadd --system punktfunk-upd
# Owns the usbip vhci attach/detach nodes (60-punktfunk.rules). Deliberately NOT 'input': writing
# 'attach' materialises an arbitrary emulated USB device — a root-only kernel primitive that must
# not ride on the group users are told to join for gamepads (security-review 2026-08-05 M-4).
# It is ALSO the group `pf-dm-helper` authorizes on (the polkit action must stay `allow_any`, so
# membership is the real gate) — so it is what a managed gamescope takeover needs to stop the
# display manager. Creating it is necessary and NOT sufficient for either use: membership is.
getent group punktfunk >/dev/null 2>&1 || groupadd --system punktfunk 2>/dev/null || :
# Reload udev so /dev/uinput picks up the new rule without a reboot (best-effort).
udevadm control --reload-rules 2>/dev/null || :
@@ -603,8 +606,13 @@ udevadm trigger --subsystem-match=misc 2>/dev/null || :
# it takes effect on the next boot into the layered deployment).
sysctl -p %{_prefix}/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || :
echo "punktfunk installed. Add yourself to the 'input' group (sudo usermod -aG input \$USER)"
echo "For the virtual Steam Deck pad (usbip) ALSO: sudo usermod -aG punktfunk \$USER"
echo " that group can emulate arbitrary USB devices; join it only on a machine you trust."
# Naming only the usbip pad here is how a Nobara host shipped broken: its owner had no Deck pad, so
# they correctly skipped this group — and then every managed gamescope takeover degraded silently,
# because pf-dm-helper (which stops the display manager for the stream) gates on THIS membership.
echo "ALSO join 'punktfunk' if this box streams Steam Gaming Mode (gamescope) or you want the"
echo "virtual Steam Deck pad: sudo usermod -aG punktfunk \$USER # then log out and back in"
echo " it authorizes stopping the display manager for a managed gamescope session, and the"
echo " pad's usbip nodes; it can emulate arbitrary USB devices, so join it only on a box you trust."
echo "then enable the host: systemctl --user enable --now punktfunk-host"
echo "Config: cp %{_datadir}/%{name}/host.env.bazzite ~/.config/punktfunk/host.env"
# Fedora/RHEL run firewalld by default — point the way to the installed service definitions.