Files
enricobuehler 54b75c9be4
apple / swift (push) Successful in 55s
windows-host / package (push) Successful in 2m31s
android / android (push) Successful in 4m40s
ci / rust (push) Successful in 4m43s
ci / web (push) Successful in 30s
ci / docs-site (push) Successful in 34s
deb / build-publish (push) Successful in 2m9s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 21s
ci / bench (push) Successful in 4m44s
docker / deploy-docs (push) Successful in 19s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m6s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m19s
feat(host): GameStream/Moonlight compat is now opt-in (--gamestream) — secure native-only by default
Follows the security audit (#5/#9): the GameStream-compat plane carries inherent on-path weaknesses
that can't be fixed on the wire without breaking stock Moonlight — its pairing runs over plain HTTP
(#9, MITM-able during the pairing window) and its legacy control encryption can reuse GCM nonces (#5,
a passive eavesdropper can recover/forge input). The native punktfunk/1 plane (SPAKE2 PIN pairing +
per-direction AEAD nonces) has neither. So flip the default to secure-by-default:

- `serve`              → native punktfunk/1 plane + management API ONLY (no GameStream surface).
- `serve --gamestream` → ALSO the GameStream/Moonlight-compat planes (nvhttp pairing, RTSP, ENet
  control, _nvstream mDNS). Opt-in, logged with a trusted-LAN caveat. `--moonlight` is an alias.
- The native plane is now ALWAYS on in `serve` (`--native` is a kept-for-compat no-op); the unified
  GameStream+native host is `serve --gamestream`.

`gamestream::serve` gates the GameStream spawns (nvhttp/rtsp/control/mdns) on the flag; the native
plane + mgmt + native-pairing handle always run.

To avoid silently regressing validated Moonlight deployments, the explicit deployment configs PRESERVE
Moonlight via `--gamestream` (each documents dropping it for a secure native-only host): the Linux
systemd unit, the Steam Deck installer, and the Windows service default (DEFAULT_HOST_CMD). The bare
`serve` default (new/manual use) is secure.

Docs swept to match (host-cli, moonlight, quickstart, install, packaging READMEs, CLAUDE.md, README,
…): Moonlight setup now instructs `--gamestream`; native/console refs use bare `serve`. OpenAPI
regenerated (a stale "run `serve --native`" string). fmt + clippy clean; 94 host tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:19:40 +00:00

7.7 KiB
Raw Permalink Blame History

punktfunk on Arch Linux / SteamOS

Packaging for punktfunk on Arch and Arch-derived immutable distros. The PKGBUILD is a split package producing punktfunk-host (the gaming-rig host) and punktfunk-client (the GTK4 couch/Deck client) — mirrors the rpm subpackages (packaging/rpm/punktfunk.spec) and the deb build scripts. On a Steam Deck used as a client you want punktfunk-client (it's what the Decky plugin launches); on a gaming rig, punktfunk-host.

Steam Deck as a HOST: don't use this PKGBUILD — SteamOS's read-only root makes makepkg/sysext awkward, and a prebuilt binary breaks on OS library bumps. Use the on-device build script instead: scripts/steamdeck/install.sh (it builds in a Debian-trixie distrobox ABI-matched to SteamOS and uses VAAPI on the Deck's AMD GPU). The Deck host path is the one exception to "host encode is NVENC-only" below.

A third member, punktfunk-web (the browser management console — pairing + status), is opt-in: build it by setting PF_WITH_WEB=1, which requires bun at build time (bun-bin from the AUR if it isn't in your repos; the console then runs on plain nodejs). A default makepkg builds only host+client with no JS tooling — mirroring the RPM spec's %bcond_with web.

Host encode: NVENC on NVIDIA, VAAPI on AMD/Intel (PUNKTFUNK_ENCODER=auto picks one). The host now has a VAAPI encoder + zero-copy dmabuf path alongside NVENC/CUDA, so punktfunk-host works on Arch + NVIDIA and AMD/Intel (incl. the Steam Deck — see the on-device path above). The client decodes via VAAPI on AMD/Intel with a software fallback.

Arch Linux (mutable)

cd packaging/arch
# Build the working tree (CI / dev) — no git fetch:
PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver
# …or build the tagged release the AUR way:
makepkg -si
# …add the web console too (needs bun / bun-bin):
PF_WITH_WEB=1 PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver

Then the standard first-run (printed by the install scriptlet):

sudo usermod -aG input "$USER"          # virtual gamepads; re-login after
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env   # gamescope backend
systemctl --user enable --now punktfunk-host
# Web console (if you installed the punktfunk-web package): enable it + read the login password.
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'   # open http://<host-ip>:3000

NVENC/EGL come from the NVIDIA driver: sudo pacman -S --needed nvidia-utils. Arch's stock ffmpeg already has NVENC built in — no RPM-Fusion-style swap needed (unlike Fedora).

Runtime dependency map (Fedora/Debian → Arch)

Need Arch package
FFmpeg + NVENC ffmpeg (NVENC built in)
PipeWire + Pulse + session mgr pipewire pipewire-pulse wireplumber
Opus / input injection opus libei
GL/EGL + gbm + xkb + wayland libglvnd mesa libxkbcommon wayland
NVIDIA driver (NVENC/EGL/CUDA) nvidia-utils (optdepend — never a hard dep)
Compositor backends gamescope (≥3.16.22) / kwin / mutter / sway (optdepends)

SteamOS 3 (immutable) — use a systemd-sysext

SteamOS has a read-only /usr on A/B partitions, and every OS update reimages the rootfs — so steamos-readonly disable + pacman (and flatpak/distrobox) are fragile or unusable for a host that needs /dev/uinput, /dev/uhid, the host PipeWire socket, the GPU render node, and the right to spawn a compositor. The update-survivable, SteamOS-blessed mechanism is a systemd-sysext: an overlay image merged read-only over /usr at boot, living in the writable /var/lib/extensions/ (so it persists across A/B updates, no readonly-disable).

Build the package, then wrap its /usr payload into a sysext image:

# 1. build the pacman package (needs an Arch environment / container)
cd packaging/arch && PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver
# 2. turn it into a sysext .raw (extracts the package's /usr into an image + extension-release)
bash build-sysext.sh punktfunk-host-*.pkg.tar.zst
# 3. on the SteamOS box:
sudo cp punktfunk-host.raw /var/lib/extensions/
sudo systemctl enable --now systemd-sysext      # merges it; survives OS updates
systemctl --user enable --now punktfunk-host     # the user unit is now under /usr/lib

The udev rule, sysctl, and systemd user unit all live under /usr/lib, so the merged sysext exposes them. systemd-sysext refresh re-merges after a reboot.

Steam Deck — the client (what the Decky plugin launches)

To stream to a Deck, you install punktfunk-client there — same sysext mechanism, but wrapping the client package instead. The split makepkg produces both .pkg.tar.zst files; on the Deck use the client one:

cd packaging/arch && PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver
bash build-sysext.sh punktfunk-client-*.pkg.tar.zst        # → punktfunk-client.raw
# on the Deck:
sudo cp punktfunk-client.raw /var/lib/extensions/
sudo systemctl enable --now systemd-sysext
sudo pacman -S --needed libva-mesa-driver                  # VAAPI hw decode on the Deck's AMD APU

Now punktfunk-client is on PATH, so the Decky plugin finds and launches it (punktfunk-client --connect host:port) — gamescope composites its video like a game. The client needs no /dev/uinput or compositor-spawning rights (it captures input and decodes), so it's a much lighter sysext than the host.

Firewall

If the host box runs a firewall, open the ports it listens on. The native punktfunk/1 plane:

  • QUIC control plane: UDP 9777 (serve --native-port N to change).
  • Data plane: an ephemeral UDP port — negotiated per session, so there is no fixed port to open. For a restrictive firewall you'd need to allow a UDP range (the repo does not pin one).

And the GameStream / Moonlight ports (fixed) — only needed if you run the host with serve --gamestream (opt-in, trusted LAN only); bare serve is native-only and doesn't open these:

Port Proto Purpose
47984 TCP HTTPS nvhttp (paired, mutual-TLS)
47989 TCP HTTP nvhttp (/serverinfo, /pair PIN flow)
48010 TCP RTSP handshake
4799848010 UDP Video RTP (+ FEC), ENet control (47999), audio (48000)
5353 UDP mDNS auto-discovery

The mgmt API (TCP 47990) binds to loopback by default — leave it closed unless you move it off loopback with --mgmt-bind IP:PORT (which then requires --mgmt-token).

With ufw:

sudo ufw allow 9777/udp                                 # punktfunk/1 control plane
sudo ufw allow 47984/tcp && sudo ufw allow 47989/tcp && sudo ufw allow 48010/tcp
sudo ufw allow 47998:48010/udp
sudo ufw allow 5353/udp
# plus the ephemeral punktfunk/1 data port — open a UDP range you reserve for it.

With raw nftables (add to your inet filter input chain):

udp dport 9777 accept                  # punktfunk/1 control plane
tcp dport { 47984, 47989, 48010 } accept
udp dport { 47998-48010, 5353 } accept
# plus the ephemeral punktfunk/1 data port (a reserved UDP range).

Files

  • PKGBUILD — split package: punktfunk-host + punktfunk-client (builds the working tree via PF_SRCDIR, or a git tag for AUR).
  • punktfunk-host.install / punktfunk-client.install — pacman scriptlets (udev reload + sysctl + first-run hint), mirror the RPM %post / deb postinst.
  • build-sysext.sh — wraps either built .pkg.tar.zst into a systemd-sysext .raw for SteamOS (derives the name from the package, so it works for host or client).