feat(host): GameStream/Moonlight compat is now opt-in (--gamestream) — secure native-only by default
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

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>
This commit is contained in:
2026-06-21 10:19:40 +00:00
parent 3c55ec37fa
commit 54b75c9be4
30 changed files with 226 additions and 141 deletions
+3 -2
View File
@@ -102,11 +102,12 @@ so it's a much lighter sysext than the host.
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 --native-port N` to change).
- **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):
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 |
|---|---|---|
+20 -12
View File
@@ -233,19 +233,23 @@ systemctl --user status punktfunk-host
journalctl --user -u punktfunk-host -f
```
> **What `serve` actually starts.** The unit's `ExecStart` runs `punktfunk-host serve`, which is the
> **GameStream / Moonlight-compatible** host (mDNS discovery, pairing, RTSP, the fixed GameStream
> ports, **plus the management REST API on 47990**). The native `punktfunk/1` (QUIC) host is a
> *separate* subcommand — `punktfunk-host punktfunk1-host` — and is **not** what the bundled systemd unit
> launches. So out of the box on Bazzite you get the **Moonlight-compatible** host.
> (Source: `crates/punktfunk-host/src/main.rs` — `serve` → `gamestream::serve`; `punktfunk1-host` is its own
> path.)
> **What `serve` actually starts.** The bundled unit's `ExecStart` runs `punktfunk-host serve
> --gamestream`, so out of the box you get the **unified host**: the native `punktfunk/1` (QUIC) plane
> — always on in `serve` — **plus** the GameStream/Moonlight-compat planes (mDNS discovery, pairing,
> RTSP, the fixed GameStream ports) and the management REST API on 47990. The `--gamestream` flag is
> what adds the Moonlight surface; GameStream pairs over plain HTTP and its legacy encryption is weaker
> than the native plane's (security-review #5/#9), so it's **opt-in and trusted-LAN only**. For a
> **secure native-only host**, drop `--gamestream` from the unit's `ExecStart` (bare `serve`) — native
> clients still work; only stock Moonlight stops.
> (Source: `crates/punktfunk-host/src/main.rs` — `serve` runs the native plane + mgmt; `--gamestream`
> adds `gamestream::serve`.)
> **Unit caveat:** `scripts/punktfunk-host.service` declares only `After=pipewire.service` and (in
> the upstream/dev layout) assumes the binary at `%h/punktfunk/target/release/punktfunk-host`. The
> **RPM-installed** binary lives at `/usr/bin/punktfunk-host`. If `systemctl --user cat
> punktfunk-host` shows `ExecStart` pointing at a missing path in your home dir, drop an override
> (`systemctl --user edit punktfunk-host`) setting `ExecStart=/usr/bin/punktfunk-host serve`.
> (`systemctl --user edit punktfunk-host`) setting `ExecStart=/usr/bin/punktfunk-host serve
> --gamestream` (or bare `serve` for a secure native-only host).
---
@@ -256,7 +260,9 @@ journalctl --user -u punktfunk-host -f
> the GameStream-host port-map (`docs/gamestream-host-plan.md`). Treat the `firewall-cmd` lines as recommended-but-verified,
> not a checked-in script.
**GameStream / Moonlight ports** (fixed; Moonlight derives them from the HTTP base):
**GameStream / Moonlight ports** (fixed; Moonlight derives them from the HTTP base). These only apply
when the host runs `serve --gamestream` (the bundled unit's default); on a bare-`serve` native-only
host you don't open them:
| Port | Proto | Purpose |
|---|---|---|
@@ -382,7 +388,8 @@ desktop viewer.
- **Service `ExecStart` points at a missing path in `$HOME`.** The dev unit references
`%h/punktfunk/target/release/...`. The RPM binary is `/usr/bin/punktfunk-host`. Override
`ExecStart=/usr/bin/punktfunk-host serve` if needed (section 5).
`ExecStart=/usr/bin/punktfunk-host serve --gamestream` (or bare `serve` for native-only) if needed
(section 5).
- **Moonlight can't see the host.** Ensure UDP 5353 (mDNS) and the GameStream ports are open
(section 6) and client + host are on the same L2 LAN segment.
@@ -413,6 +420,7 @@ matching your Bazzite Fedora base (`rpm -E %fedora`).
1. The COPR is **operator-run / not assumed published** — both install paths depend on it.
2. There is **no firewall script/doc in the repo** — the ports above are derived from the code.
3. The bundled systemd unit runs the **GameStream/Moonlight** `serve` host, **not** the native
`punktfunk/1` QUIC host (`punktfunk1-host` is separate and unmanaged by the unit).
3. The bundled systemd unit runs `serve --gamestream` — the native `punktfunk/1` QUIC plane (always
on) **plus** the GameStream/Moonlight planes. Drop `--gamestream` for a secure native-only host;
`punktfunk1-host` is a separate standalone native host, unmanaged by the unit.
4. The mgmt port (47990) is **loopback-only by default** — don't open it.
+3 -2
View File
@@ -52,11 +52,12 @@ journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
Open the ports the host listens on. The **native `punktfunk/1`** plane:
- **QUIC control plane: UDP 9777** (`serve --native --native-port N` to change).
- **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):
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 |
|---|---|---|