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
+2 -2
View File
@@ -43,12 +43,12 @@ The client requests a bitrate; the host encodes to it. To find a good value for
## Multiple devices at once
Today the native `punktfunk/1` host (`serve --native`) streams **one session at a time** — additional
Today the native `punktfunk/1` host (`serve`) streams **one session at a time** — additional
clients wait in the accept queue until the active session ends. Each session gets its own virtual
display at the client's exact resolution; concurrent native sessions are on the roadmap.
(`punktfunk1-host`, the standalone test host, has a `--max-concurrent N` knob, default 4, bounded by your
GPU's encoder — see the [Host CLI](/docs/host-cli) reference — but `serve --native` does **not** take
GPU's encoder — see the [Host CLI](/docs/host-cli) reference — but `serve` does **not** take
that flag.)
## Codec and FEC
+1 -1
View File
@@ -116,7 +116,7 @@ mDNS. It requires **PIN pairing** by default (secure on a LAN); pair once from y
From any [client](/docs/clients) — `punktfunk-client --discover` finds the host on the LAN. On
first connect, complete the PIN pairing — **arm it from the host's web console / mgmt API**, which
makes the host display a 4-digit PIN to type into the client. (Pairing is required by default; pass
`serve --native --open` only if you deliberately want to disable the requirement.) See
`serve --open` only if you deliberately want to disable the requirement.) See
[Clients](/docs/clients) and [Running as a Service](/docs/running-as-a-service).
## Appendix — build from source
+20 -8
View File
@@ -6,18 +6,30 @@ description: The punktfunk-host commands and the flags you'll actually use.
The host is one binary, `punktfunk-host`. Most of the time you'll run a single command; the rest reads
its settings from [`host.env`](/docs/configuration).
## `serve --native`
## `serve`
The normal way to run a host. Starts the unified host: the GameStream server (for Moonlight) **and**
the native `punktfunk/1` server, plus the management API/web console — all in one process.
The normal way to run a host. By default `serve` starts the **secure native host**: the native
`punktfunk/1` server (QUIC, SPAKE2 PIN pairing, per-direction AEAD) plus the management API/web
console — all in one process. The native plane is **always on**; there is no flag to turn it off.
```sh
punktfunk-host serve --native
punktfunk-host serve
```
Add `--gamestream` (alias `--moonlight`) to **also** run the GameStream/Moonlight-compatible planes
(nvhttp pairing, RTSP, ENet control, `_nvstream` mDNS) — required for stock [Moonlight](/docs/moonlight)
clients. This is **opt-in** because GameStream carries inherent on-path weaknesses (pairing over plain
HTTP; its legacy control encryption can reuse GCM nonces — security-review #5/#9), so enable it **only
on a trusted LAN**. The native plane is immune to those issues.
```sh
punktfunk-host serve --gamestream
```
| Flag | Meaning |
|---|---|
| `--native` | Also run the native `punktfunk/1` server (recommended; enables the native clients and discovery). |
| `--gamestream` / `--moonlight` | Also run the GameStream/Moonlight-compat planes (for stock Moonlight clients). Opt-in, trusted-LAN only — see above. |
| `--native` | No-op. The native `punktfunk/1` server always runs in `serve`; kept only for backward compatibility. |
| `--native-port <PORT>` | Native QUIC port (default `9777`). |
| `--open` | Don't require pairing — serve any device on the network. Off by default; only for trusted single-user setups. |
| `--mgmt-bind <IP:PORT>` | Management API address (default loopback `127.0.0.1:47990`). |
@@ -29,7 +41,7 @@ The management API is **always HTTPS with bearer-token auth**. If you don't pass
is auto-generated and persisted to `~/.config/punktfunk/mgmt-token`; `--mgmt-token` only overrides it. A
token is **required** when you bind the API off loopback with `--mgmt-bind`.
By default the host **requires pairing** — see [Pairing & Trust](/docs/pairing). On `serve --native` you
By default the host **requires pairing** — see [Pairing & Trust](/docs/pairing). On `serve` you
**arm pairing from the web console** (or mgmt API); the host then displays a 4-digit PIN. Pass `--open` to
turn off the mandatory-pairing default and serve any device on the network (trusted single-user setups
only). The pairing flags below are `punktfunk1-host`-only and do **not** apply to `serve`.
@@ -54,10 +66,10 @@ punktfunk-host punktfunk1-host --source virtual
| `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). |
`--max-concurrent`, `--allow-pairing`, and `--require-pairing` are **`punktfunk1-host`-only** — `serve` does not
accept them. On `serve --native` you arm pairing from the web console instead, and concurrency is not
accept them. On `serve` you arm pairing from the web console instead, and concurrency is not
yet capped from the command line.
Both `serve --native` and `punktfunk1-host` advertise the host on the network so clients can discover it. List
Both `serve` and `punktfunk1-host` advertise the host on the network so clients can discover it. List
hosts from another machine with `punktfunk-probe --discover`.
## Environment
+5 -2
View File
@@ -15,7 +15,7 @@ On **Windows** (NVIDIA), the host ships as a signed installer instead — see [W
| **Ubuntu / Debian** | apt | `sudo apt install punktfunk-host` | [Ubuntu — GNOME](/docs/ubuntu-gnome) · [Ubuntu — KDE](/docs/ubuntu-kde) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Fedora / Bazzite** | rpm-ostree | `rpm-ostree install punktfunk punktfunk-web` | [Fedora — KDE](/docs/fedora-kde) · [Bazzite](/docs/bazzite) · [packaging/rpm](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/rpm/README.md) |
| **Arch** | PKGBUILD | `makepkg -si` | [packaging/arch](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md) |
| **Steam Deck (host)** | on-device script | `bash scripts/steamdeck/install.sh` | [Steam Deck (Host)](/docs/steam-deck-host) |
| **SteamOS (host)** | on-device script | `bash scripts/steamdeck/install.sh` | [SteamOS (Host)](/docs/steamos-host) |
Each registry is public — no auth, you just trust the repo's signing key. Adding the repo is a
one-time step covered in the linked guide; after that, normal `apt upgrade` / `rpm-ostree upgrade`
@@ -62,9 +62,12 @@ You need an NVIDIA GPU + driver (the host is NVENC-only on Windows). More detail
2. Start the host inside your desktop session:
```sh
punktfunk-host serve --native
punktfunk-host serve
```
Bare `serve` is the secure native-only default (native `punktfunk/1` + the web console). On a
trusted LAN, add `--gamestream` to also serve stock [Moonlight](/docs/moonlight) clients.
3. Enable the web console and read its login password, then open `http://<host-ip>:3000`:
```sh
+13 -3
View File
@@ -11,10 +11,20 @@ a browser, a smart TV, or any device without a native client.
> discovery/pairing — including **Windows** and **Android** (phone and Android TV). See
> [Clients](/docs/clients) before reaching for Moonlight.
## 1. Make sure the host is running
## 1. Make sure the host is running with GameStream enabled
On the host machine, `serve --native` (or your [service](/docs/running-as-a-service)) should be up.
The host advertises itself on the network, so Moonlight usually finds it on its own.
Moonlight needs the GameStream planes, which are **opt-in**. Run the host with `--gamestream`:
```sh
punktfunk-host serve --gamestream
```
(Bare `serve` is the secure native-only default and stock Moonlight clients can't connect to it; the
native plane is always on, and `--gamestream` adds the Moonlight-compat surface.) GameStream pairs over
plain HTTP and its legacy control encryption is weaker than the native plane's, so only enable it on a
**trusted LAN**. If you run the host as a [service](/docs/running-as-a-service), make sure its
`ExecStart` includes `--gamestream`. The host advertises itself on the network, so Moonlight usually
finds it on its own.
## 2. Add the host in Moonlight
+3 -3
View File
@@ -40,13 +40,13 @@ PIN ceremony before it can stream. It's the right path for the *first* device (b
admitted anything) or when you're at the client and the console isn't handy.
Pairing has to be **armed** on the host before a client can pair (so a random device can't pair
itself). On the production host (`serve --native`), this is done from the **web console**: open the
itself). On the production host (`serve`), this is done from the **web console**: open the
host's management console, click to arm pairing, and the host displays a 4-digit PIN along with the
list of paired devices. This works on a headless host over the network — there is no command-line flag
to arm pairing on `serve`.
(The standalone headless test host, `punktfunk1-host`, takes `--allow-pairing`/`--require-pairing` on its
command line instead; the production `serve --native` host arms pairing from the console.)
command line instead; the production `serve` host arms pairing from the console.)
Then, on the client:
@@ -61,7 +61,7 @@ the right setting on a shared network: a device has to complete the PIN ceremony
connect.
If you're on a fully trusted single-user network and want to skip pairing, run the host open with
`serve --native --open` (or `punktfunk1-host --allow-tofu` for the standalone host) — it then advertises
`serve --open` (or `punktfunk1-host --allow-tofu` for the standalone host) — it then advertises
`pair=optional` and accepts unpaired clients. Requiring pairing is strongly recommended.
## Trust-on-first-use (host opt-in)
+3 -1
View File
@@ -22,9 +22,11 @@ Each one covers the NVIDIA driver, the dependencies, and how to build and run th
From a terminal **inside your desktop session** (so the host can reach your compositor):
```sh
punktfunk-host serve --native
punktfunk-host serve
```
This is the secure native-only default — the native `punktfunk/1` plane plus the web console. To also
serve stock Moonlight clients, add `--gamestream` (trusted-LAN only; see [Moonlight](/docs/moonlight)).
The host starts listening and prints its identity fingerprint. It advertises itself on your local
network, so clients can find it by name. Leave it running. (To start it automatically at boot, see
[Running as a Service](/docs/running-as-a-service).)
+4 -3
View File
@@ -29,9 +29,10 @@ see [Status & Progress](/docs/status).
## ✅ Shipped
- **The host, two ways.** A GameStream host any [Moonlight](/docs/moonlight) client can use, and the
lower-latency native [`punktfunk/1`](/docs/how-it-works) protocol (QUIC control + UDP data with
GF(2¹⁶) Leopard FEC + AES-GCM). Both run from one process.
- **The host, two ways.** The lower-latency native [`punktfunk/1`](/docs/how-it-works) protocol (QUIC
control + UDP data with GF(2¹⁶) Leopard FEC + AES-GCM) — the secure default — and, opt-in via
`serve --gamestream`, a GameStream host any [Moonlight](/docs/moonlight) client can use. Both run
from one process.
- **Native-resolution virtual displays** on Linux across KWin, GNOME/Mutter, gamescope, and
Sway/wlroots, with a fully zero-copy GPU path to NVENC (stable 240 fps at 5120×1440).
- **A native Windows host** (NVIDIA, x64) — a signed installer with secure-desktop capture and a
@@ -3,9 +3,14 @@ title: Running as a Service
description: Start the host at boot — for a desktop you log into, or a fully headless always-on machine.
---
Running `serve --native` in a terminal is fine for trying punktfunk out. To make a machine an
Running `serve` in a terminal is fine for trying punktfunk out. To make a machine an
always-available host, run it as a service. There are two cases.
> The bundled unit `scripts/punktfunk-host.service` runs `serve --gamestream`, so it serves both the
> native `punktfunk/1` plane and stock [Moonlight](/docs/moonlight) clients. For a **secure native-only
> host** (no GameStream — its pairing runs over plain HTTP and its legacy encryption is weaker;
> security-review #5/#9), drop `--gamestream` from the unit's `ExecStart` and use bare `serve`.
## A. A desktop you log into
If you sit at the machine (or it auto-logs-in to a desktop), run the host as a **systemd user
+1 -1
View File
@@ -70,7 +70,7 @@ Then log out and back in. On other distros this is `sudo usermod -aG input $USER
manually.
- Prefer a **wired** connection or 5 GHz Wi-Fi between host and client.
- Streaming to **many devices at once** shares the GPU encoder. The production host
(`serve --native`) handles one native session at a time, with extra clients queued; heavy load is
(`serve`) handles one native session at a time, with extra clients queued; heavy load is
usually bitrate-bound, so lower the bitrate first.
## Still stuck?
+4 -1
View File
@@ -148,5 +148,8 @@ The host binary lands at `target/release/punktfunk-host`. Write `~/.config/punkt
step 3, then run it inside your GNOME session:
```sh
cargo run --release -p punktfunk-host -- serve --native
cargo run --release -p punktfunk-host -- serve --gamestream
```
(The native plane is always on; `--gamestream` adds the Moonlight-compat surface this guide's
GameStream ports refer to — trusted LAN only. Drop it for a secure native-only host.)
+4 -1
View File
@@ -104,5 +104,8 @@ cargo build --release -p punktfunk-host
Write `~/.config/punktfunk/host.env` as in step 3, then run it inside your Plasma session:
```sh
cargo run --release -p punktfunk-host -- serve --native
cargo run --release -p punktfunk-host -- serve --gamestream
```
(The native plane is always on; `--gamestream` adds the Moonlight-compat surface this guide's
GameStream ports refer to — trusted LAN only. Drop it for a secure native-only host.)