diff --git a/CLAUDE.md b/CLAUDE.md index 1a95227..c07672b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc (no re-TOFU shortcut). Clients present persistent identities via QUIC client auth, the host stores paired fingerprints (`punktfunk1-paired.json`) and gates sessions with `--require-pairing` (the default; `--allow-tofu`/`--open` accept unpaired clients). - **LAN auto-discovery**: both `serve --native` and `punktfunk1-host` advertise the native service over + **LAN auto-discovery**: both `serve` and `punktfunk1-host` advertise the native service over mDNS (`_punktfunk._udp`, `crate::discovery`) with TXT `proto`/`fp`(cert fingerprint to pin)/`pair`(required|optional)/`id`; `punktfunk-probe --discover` lists hosts, Apple clients browse the same service via NWBrowser (validated cross-LAN 2026-06-12). @@ -111,7 +111,7 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc slice threads) → `GtkGraphicsOffload`-wrapped picture, PipeWire playback (mic-player jitter ring inverted), SDL3 gamepad capture + rumble/lightbar feedback, keyboard via exact inverse of the host VK table, absolute mouse + 120-unit scroll. Validated live - against `serve --native` on this box: 1080p60, steady 60 fps, capture→decoded p50 + against `serve` on this box: 1080p60, steady 60 fps, capture→decoded p50 ≈6.4 ms (debug build). `--connect host[:port]` for scripting. **Swift-parity batch + stage 1.5 (2026-06-12 evening)**: capture state machine (click-to-capture, Ctrl+Alt+Shift+Q / focus-loss release, held-state flush), app-lifetime SDL gamepad @@ -176,8 +176,9 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc 2. **Sub-frame pipelining**: overlap encode and transmit within a frame. Requires a direct NVENC SDK wrapper (libavcodec only emits whole AUs) — the next big latency lever (~2–4 ms at high res). -3. **punktfunk/1 protocol growth.** **Done:** unified host (`serve --native` runs GameStream + the - punktfunk/1 QUIC host in one process) with native pairing driven over the mgmt API / +3. **punktfunk/1 protocol growth.** **Done:** unified host (`serve --gamestream` runs GameStream + the + punktfunk/1 QUIC host in one process; bare `serve` is the secure native-only default — GameStream is + opt-in, trusted-LAN only, security-review #5/#9) with native pairing driven over the mgmt API / web console (`mod native_pairing`: arm-on-demand → display PIN, paired-device list). **Done:** PIN pairing is the default, host-gated — the host requires pairing and advertises `pair=required` unless opted out with `--allow-tofu`/`--open` (then `pair=optional`, accepts @@ -280,9 +281,9 @@ scanout → KWin `--drm` impossible; everything renders offscreen via `renderD12 # launcher menu is EMPTY (no apps, no System Settings). bash scripts/headless/run-headless-kde.sh 1920x1080 -# host (shell 2): +# host (shell 2): bare `serve` is native-only (secure default); add --gamestream for Moonlight compat. WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE PUNKTFUNK_VIDEO_SOURCE=virtual \ -PUNKTFUNK_ZEROCOPY=1 cargo run -rp punktfunk-host -- serve +PUNKTFUNK_ZEROCOPY=1 cargo run -rp punktfunk-host -- serve --gamestream # punktfunk/1 native loopback test (no Moonlight needed; same env as serve, listener persists # across sessions — bound it with --max-sessions): diff --git a/README.md b/README.md index 6138592..e5ef8f1 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,10 @@ gamescope, Mutter, and Sway/wlroots backends), encoded with GPU **zero-copy** (d NVENC) up to 5120×1440@240. The native **`punktfunk/1`** protocol adds a QUIC control plane and a GF(2¹⁶) Leopard-FEC + AES-GCM data plane (p50 ~0.8 ms capture→reassembled at 720p120), with mid-stream mode renegotiation and a wall-clock skew handshake so latency stays valid across machines. -Both protocols run from **one process** (`punktfunk-host serve --native`) and are managed through a -REST API and web console. Builds against FFmpeg 7 or 8. +Both run from **one process**: bare `punktfunk-host serve` is the **secure native-only default** +(`punktfunk/1` + the management API/web console), and `serve --gamestream` additionally enables the +GameStream/Moonlight-compat planes (opt-in, trusted-LAN only — GameStream has inherent on-path +weaknesses). The host is managed through a REST API and web console. Builds against FFmpeg 7 or 8. Full milestone status: **[docs.punktfunk.unom.io/docs/status](https://docs.punktfunk.unom.io/docs/status)** · roadmap: **[/docs/roadmap](https://docs.punktfunk.unom.io/docs/roadmap)**. @@ -69,7 +71,8 @@ Windows host (NVIDIA-only) also ships as a signed installer. | **Windows** (NVIDIA, x64) | signed `setup.exe` from the package registry | [Windows Host](https://docs.punktfunk.unom.io/docs/windows-host) | `punktfunk-host` is the streaming host; `punktfunk-web` is the browser console (pairing + status). -After install, run `punktfunk-host serve --native` inside your desktop session, then pair from the web +After install, run `punktfunk-host serve` inside your desktop session (the secure native default; +add `--gamestream` on a trusted LAN if you also want stock Moonlight clients), then pair from the web console. Full instructions: **[docs.punktfunk.unom.io/docs/install](https://docs.punktfunk.unom.io/docs/install)**. ## Connect a client diff --git a/crates/punktfunk-host/src/gamestream/mod.rs b/crates/punktfunk-host/src/gamestream/mod.rs index d4d1983..83111ad 100644 --- a/crates/punktfunk-host/src/gamestream/mod.rs +++ b/crates/punktfunk-host/src/gamestream/mod.rs @@ -154,56 +154,72 @@ impl AppState { /// QUIC server on `cfg.port` in the same process, sharing one [`crate::native_pairing`] handle with /// the management API so the web console can arm pairing and show the PIN. `None` = GameStream only /// (the mgmt API's native endpoints report `enabled: false`). +/// Run the host. The **native punktfunk/1 plane + management API always run** (the secure default — +/// SPAKE2 pairing, per-direction AEAD nonces); `gamestream` additionally brings up the +/// GameStream/Moonlight-compat planes (nvhttp pairing, RTSP, ENet control, `_nvstream` mDNS), which +/// carry inherent on-path weaknesses (plain-HTTP pairing + legacy GCM nonce reuse, security-review +/// #5/#9) — so it is **opt-in** (`serve --gamestream`) and gated on a trusted LAN. pub fn serve( mgmt: crate::mgmt::Options, - native: Option, + native: crate::punktfunk1::NativeServe, + gamestream: bool, ) -> Result<()> { let host = Host::detect()?; let identity = cert::ServerIdentity::load_or_create().context("host certificate")?; let state = Arc::new(AppState::new(host, identity)); - // The shared native-pairing handle exists only when we run the native host; it links the QUIC - // ceremony and the management API. - let np = match &native { - Some(_) => Some(Arc::new( - crate::native_pairing::NativePairing::load_with(None, None, false) - .context("native pairing store")?, - )), - None => None, - }; + // The native plane always runs, so the shared native-pairing handle (linking the QUIC ceremony + // and the management API) always exists. + let np = Arc::new( + crate::native_pairing::NativePairing::load_with(None, None, false) + .context("native pairing store")?, + ); tracing::info!( hostname = %state.host.hostname, uniqueid = %state.host.uniqueid, ip = %state.host.local_ip, - native = native.is_some(), - require_pairing = native.as_ref().map(|n| n.require_pairing), - "punktfunk host (GameStream P1.1: serverinfo + pairing + mDNS)" + native_port = native.port, + require_pairing = native.require_pairing, + gamestream, + "punktfunk host" ); + if gamestream { + tracing::warn!( + "GameStream/Moonlight compat ENABLED (--gamestream): its pairing runs over plain HTTP and \ + its legacy control encryption can reuse GCM nonces (security-review #5/#9) — an on-path \ + LAN attacker could MITM pairing or recover input. Enable only on a TRUSTED network; prefer \ + the native punktfunk/1 plane + clients for untrusted/WAN use." + ); + } let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?; rt.block_on(async move { // rustls needs a process-wide crypto provider before any TLS config is built. let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - let _advert = mdns::advertise(&state.host).context("mDNS advertise")?; - rtsp::spawn(state.clone()).context("start RTSP server")?; - control::spawn(state.clone()).context("start ENet control server")?; - match (native, np) { - (Some(cfg), Some(np)) => { - tracing::info!( - port = cfg.port, - require_pairing = cfg.require_pairing, - "unified host: also serving native punktfunk/1 (QUIC)" - ); - tokio::try_join!( - nvhttp::run(state.clone()), - crate::mgmt::run(state.clone(), mgmt, Some(np.clone())), - crate::punktfunk1::serve(crate::punktfunk1::native_serve_opts(&cfg), np), - )?; - } - _ => { - tokio::try_join!( - nvhttp::run(state.clone()), - crate::mgmt::run(state, mgmt, None) - )?; - } + let native_opts = crate::punktfunk1::native_serve_opts(&native); + if gamestream { + // Unified host: GameStream compat planes + native + mgmt. + let _advert = mdns::advertise(&state.host).context("mDNS advertise")?; + rtsp::spawn(state.clone()).context("start RTSP server")?; + control::spawn(state.clone()).context("start ENet control server")?; + tracing::info!( + port = native.port, + "unified host: GameStream/Moonlight compat + native punktfunk/1 (QUIC)" + ); + tokio::try_join!( + nvhttp::run(state.clone()), + crate::mgmt::run(state.clone(), mgmt, Some(np.clone())), + crate::punktfunk1::serve(native_opts, np), + )?; + } else { + // Secure default: native punktfunk/1 + management API only (no GameStream surface). + tracing::info!( + port = native.port, + "secure host: native punktfunk/1 (QUIC) + management API \ + (GameStream OFF — pass --gamestream for stock-Moonlight compat)" + ); + tokio::try_join!( + crate::mgmt::run(state.clone(), mgmt, Some(np.clone())), + crate::punktfunk1::serve(native_opts, np), + )?; } Ok(()) }) diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 23a485d..702487e 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -6,10 +6,10 @@ //! `#[cfg(target_os = "linux")]`; the crate compiles everywhere so the workspace builds //! on non-Linux dev machines — it just can't run the pipeline there. //! -//! Subcommands: `serve` runs the GameStream-compatible host + management REST API (and, with -//! `--native`, the native punktfunk/1 host in-process); `punktfunk1-host` runs the native -//! punktfunk/1 host standalone; `spike` is a capture→encode→file pipeline dev tool that also -//! round-trips the encoded AUs through a `punktfunk_core` loopback. +//! Subcommands: `serve` runs the native punktfunk/1 host + management REST API by default, and — +//! with `--gamestream` — the GameStream/Moonlight-compat planes too (opt-in, trusted-LAN only); +//! `punktfunk1-host` runs the native punktfunk/1 host standalone; `spike` is a capture→encode→file +//! pipeline dev tool that also round-trips the encoded AUs through a `punktfunk_core` loopback. // Scaffold: trait methods and config paths are defined ahead of their backends. #![allow(dead_code)] @@ -103,11 +103,11 @@ fn real_main() -> Result<()> { crate::capture::dxgi::install_gpu_pref_hook(); match args.first().map(String::as_str) { - // GameStream host control plane (P1.1: mDNS + serverinfo) + management API, and (with - // --native) the native punktfunk/1 host in the same process — the unified host. + // The host: the native punktfunk/1 plane + management API by default (secure), and — with + // --gamestream — the GameStream/Moonlight-compat planes too (opt-in; #5/#9 trusted-LAN caveat). Some("serve") => { - let (mgmt_opts, native) = parse_serve(&args[1..])?; - gamestream::serve(mgmt_opts, native) + let (mgmt_opts, native, gamestream) = parse_serve(&args[1..])?; + gamestream::serve(mgmt_opts, native, gamestream) } // Print the management API's OpenAPI document (for client codegen). Some("openapi") => { @@ -332,14 +332,16 @@ fn input_test() -> Result<()> { bail!("input-test requires Linux") } -/// `serve` options: the management API (GameStream ports are protocol-fixed) + whether to also run -/// the native punktfunk/1 host in-process (`--native`, the unified host). Returns the mgmt options -/// and the native host config (`None` = GameStream only). Native pairing is **required by default** +/// `serve` options. The **native punktfunk/1 plane + management API are the secure default and always +/// run**; `--gamestream` additionally enables the GameStream/Moonlight-compat planes (opt-in — they +/// carry the inherent on-path #5/#9 weaknesses, so only on a trusted LAN). Returns the mgmt options, +/// the native host config, and whether GameStream is enabled. Native pairing is **required by default** /// (an open host any LAN device can stream from is insecure); `--open` turns it off. -fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option)> { +fn parse_serve(args: &[String]) -> Result<(mgmt::Options, punktfunk1::NativeServe, bool)> { let mut opts = mgmt::Options::default(); - let mut native_port: Option = None; + let mut native_port: u16 = 9777; // the native plane always runs now let mut open = false; + let mut gamestream = false; let mut i = 0; while i < args.len() { let arg = args[i].as_str(); @@ -365,16 +367,17 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option native_port = Some(native_port.unwrap_or(9777)), + // The native plane is now the DEFAULT (always runs in `serve`); `--native` is kept as an + // accepted no-op for back-compat / explicitness. + "--native" => {} "--native-port" => { - native_port = Some( - next()? - .parse() - .map_err(|_| anyhow::anyhow!("bad --native-port (want a port number)"))?, - ) + native_port = next()? + .parse() + .map_err(|_| anyhow::anyhow!("bad --native-port (want a port number)"))? } + // Opt into the GameStream/Moonlight-compat planes (off by default — they carry the + // inherent on-path #5/#9 weaknesses; only for a trusted LAN). + "--gamestream" | "--moonlight" => gamestream = true, // Disable mandatory native pairing — any device can connect (trusted single-user // setups only). The default REQUIRES pairing. "--open" => open = true, @@ -393,11 +396,11 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option Result { @@ -502,8 +505,8 @@ fn print_usage() { "punktfunk-host — Linux streaming host USAGE: - punktfunk-host serve [OPTIONS] GameStream host control plane (mDNS + serverinfo …) - + the management REST API + punktfunk-host serve [OPTIONS] native punktfunk/1 host + management REST API + (secure default; add --gamestream for Moonlight compat) punktfunk-host openapi print the management API's OpenAPI document (codegen) punktfunk-host punktfunk1-host [OPTIONS] native punktfunk/1 host (QUIC control + UDP data plane) punktfunk-host probe-compositor exit 0 iff the compositor is up + ready (bringup gate) @@ -513,9 +516,12 @@ SERVE OPTIONS: --mgmt-bind management API address (default: 127.0.0.1:47990) --mgmt-token bearer token for the management API (or PUNKTFUNK_MGMT_TOKEN); required when --mgmt-bind is not loopback - --native also run the native punktfunk/1 (QUIC) host in this process — - the unified host; pairing is armed from the management API/console - --native-port native QUIC port (default 9777; implies --native) + --gamestream (--moonlight) ALSO run the GameStream/Moonlight-compat planes (nvhttp pairing, + RTSP, ENet control, _nvstream mDNS). OFF by default — they carry + inherent on-path weaknesses (plain-HTTP pairing + legacy GCM nonce + reuse, security-review #5/#9); enable only on a TRUSTED LAN + --native no-op (the native punktfunk/1 plane always runs in `serve` now) + --native-port native QUIC port (default 9777) --open disable mandatory native pairing (default: pairing REQUIRED — an open host any LAN device can stream from is insecure) @@ -550,7 +556,7 @@ NOTES: (see docs/linux-setup.md). 'synthetic' needs no capture session and always runs. Encoded AUs are written to a playable file AND (unless --no-loopback) fed through a punktfunk_core host→client loopback that reassembles and byte-verifies each one. - Both 'serve --native' and 'punktfunk1-host' advertise the native service over mDNS + Both 'serve' and 'punktfunk1-host' advertise the native service over mDNS (_punktfunk._udp) for client auto-discovery — 'punktfunk-probe --discover' lists them." ); #[cfg(target_os = "windows")] diff --git a/crates/punktfunk-host/src/mgmt.rs b/crates/punktfunk-host/src/mgmt.rs index b50c7eb..0c480ef 100644 --- a/crates/punktfunk-host/src/mgmt.rs +++ b/crates/punktfunk-host/src/mgmt.rs @@ -850,7 +850,7 @@ async fn get_native_pairing(State(st): State>) -> Json Result<()> { PUNKTFUNK_SECURE_DDA=1\n\ RUST_LOG=info\n\ \n\ - # The host subcommand the service launches (default: serve --native).\n\ - # PUNKTFUNK_HOST_CMD=serve --native\n\ + # The host subcommand the service launches (default: serve --gamestream = native + Moonlight\n\ + # compat). Use `serve` for a SECURE native-only host (no GameStream #5/#9 surface).\n\ + # PUNKTFUNK_HOST_CMD=serve --gamestream\n\ \n\ # Force a specific NVENC render GPU by name substring (multi-GPU boxes only):\n\ # PUNKTFUNK_RENDER_ADAPTER=4090\n"; diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index e9c1823..c3dc186 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -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 diff --git a/docs-site/content/docs/fedora-kde.md b/docs-site/content/docs/fedora-kde.md index c8af769..bce1f99 100644 --- a/docs-site/content/docs/fedora-kde.md +++ b/docs-site/content/docs/fedora-kde.md @@ -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 diff --git a/docs-site/content/docs/host-cli.md b/docs-site/content/docs/host-cli.md index c5c3f65..29904c9 100644 --- a/docs-site/content/docs/host-cli.md +++ b/docs-site/content/docs/host-cli.md @@ -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 ` | 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 ` | 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 diff --git a/docs-site/content/docs/install.md b/docs-site/content/docs/install.md index 398f0a7..b164601 100644 --- a/docs-site/content/docs/install.md +++ b/docs-site/content/docs/install.md @@ -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://:3000`: ```sh diff --git a/docs-site/content/docs/moonlight.md b/docs-site/content/docs/moonlight.md index 81b40ee..5b68523 100644 --- a/docs-site/content/docs/moonlight.md +++ b/docs-site/content/docs/moonlight.md @@ -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 diff --git a/docs-site/content/docs/pairing.md b/docs-site/content/docs/pairing.md index 850f81a..1eb826b 100644 --- a/docs-site/content/docs/pairing.md +++ b/docs-site/content/docs/pairing.md @@ -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) diff --git a/docs-site/content/docs/quickstart.md b/docs-site/content/docs/quickstart.md index 62d9792..359adbc 100644 --- a/docs-site/content/docs/quickstart.md +++ b/docs-site/content/docs/quickstart.md @@ -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).) diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md index 54a7b3c..9eb044b 100644 --- a/docs-site/content/docs/roadmap.md +++ b/docs-site/content/docs/roadmap.md @@ -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 diff --git a/docs-site/content/docs/running-as-a-service.md b/docs-site/content/docs/running-as-a-service.md index b9b19b4..cf232ea 100644 --- a/docs-site/content/docs/running-as-a-service.md +++ b/docs-site/content/docs/running-as-a-service.md @@ -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 diff --git a/docs-site/content/docs/troubleshooting.md b/docs-site/content/docs/troubleshooting.md index 70425bf..a051b52 100644 --- a/docs-site/content/docs/troubleshooting.md +++ b/docs-site/content/docs/troubleshooting.md @@ -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? diff --git a/docs-site/content/docs/ubuntu-gnome.md b/docs-site/content/docs/ubuntu-gnome.md index 36b7417..8d09459 100644 --- a/docs-site/content/docs/ubuntu-gnome.md +++ b/docs-site/content/docs/ubuntu-gnome.md @@ -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.) diff --git a/docs-site/content/docs/ubuntu-kde.md b/docs-site/content/docs/ubuntu-kde.md index 4eae49d..31eff04 100644 --- a/docs-site/content/docs/ubuntu-kde.md +++ b/docs-site/content/docs/ubuntu-kde.md @@ -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.) diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 33ddc45..fed50b7 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -621,7 +621,7 @@ } }, "503": { - "description": "Native host not enabled (run `serve --native`)", + "description": "Native host not available in this process", "content": { "application/json": { "schema": { diff --git a/docs/windows-client-bootstrap.md b/docs/windows-client-bootstrap.md index 42bc1ce..11bcfe3 100644 --- a/docs/windows-client-bootstrap.md +++ b/docs/windows-client-bootstrap.md @@ -45,7 +45,7 @@ settings, in-app SPAKE2 PIN pairing) + the video on a **`SwapChainPanel`**, all ## What we're building -A native Windows client that connects to a punktfunk/1 host (`serve --native` / `punktfunk1-host`), decodes +A native Windows client that connects to a punktfunk/1 host (`serve` / `punktfunk1-host`), decodes HEVC, presents it low-latency, plays Opus audio, and captures local mouse/keyboard/gamepad to send back — i.e. the Windows analogue of the **GTK4 Linux client** (`clients/linux`), which is the architectural template. The Windows client is close to a 1:1 port of the Linux client @@ -149,7 +149,7 @@ Windows client should mirror it: `punktfunk-core { path, features=["quic"] }`, `windows`, the Reactor crate, `ffmpeg-next`, `opus`, `sdl3`, `mdns-sd`, `anyhow`, `tracing`. Mirror `clients/linux/Cargo.toml`. 3. **Connect + control plane.** Port `session.rs` + `trust.rs`; validate headless against the 4090 - box (`punktfunk1-host`/`serve --native`) — handshake, PIN/TOFU, plane counters — before any UI/decode. + box (`punktfunk1-host`/`serve`) — handshake, PIN/TOFU, plane counters — before any UI/decode. 4. **Decode + present.** FFmpeg D3D11VA → `SwapChainPanel`. SDR (8-bit BGRA) first, then **P010 + HDR colorspace** (see the HDR section). 5. **Audio.** WASAPI render + Opus decode (port `audio.rs`). diff --git a/docs/windows-host.md b/docs/windows-host.md index 71bcc8e..8dec174 100644 --- a/docs/windows-host.md +++ b/docs/windows-host.md @@ -15,7 +15,7 @@ plan + dev box + SudoVDA protocol + no-GPU strategy added 2026-06-14 (12-agent r ## Status (2026-06-15) — full pipeline live-validated on an RTX 4090 Every OS-touching backend is implemented behind the existing traits and **builds clean on -`x86_64-pc-windows-msvc`** (and Linux unaffected). `serve --native` / `punktfunk1-host` **run on Windows** +`x86_64-pc-windows-msvc`** (and Linux unaffected). `serve` / `punktfunk1-host` **run on Windows** (identity in `%APPDATA%`, QUIC bound, mDNS advertising, accepting sessions). The **full native pipeline is validated live on a real RTX 4090** (Windows 11): SudoVDA virtual display → DXGI Desktop Duplication (D3D11 zero-copy) → **NVENC HEVC** → punktfunk/1 → Rust reference client, at @@ -146,7 +146,7 @@ rustc 1.96 clippy is stricter than the Linux CI image on shared code, e.g. `need 3. `cargo build -p punktfunk-host --features nvenc` (needs NASM + CMake for aws-lc-rs; libclang for any ffmpeg-using client). Default build (no feature) uses the openh264 software encoder. 4. Run in the **interactive session** (not a Session-0 service / not over SSH — SendInput + DXGI - Desktop Duplication need a desktop): `serve --native` or `punktfunk1-host --source virtual`. Set + Desktop Duplication need a desktop): `serve` or `punktfunk1-host --source virtual`. Set `PUNKTFUNK_ENCODER=nvenc` to select NVENC (the DXGI capturer switches to zero-copy D3D11 output to match). The SudoVDA monitor activates once a real GPU drives WDDM, so capture + NVENC then work. diff --git a/docs/windows-service.md b/docs/windows-service.md index cbcdbad..1724d65 100644 --- a/docs/windows-service.md +++ b/docs/windows-service.md @@ -74,7 +74,7 @@ PUNKTFUNK_ENCODER=nvenc PUNKTFUNK_VIDEO_SOURCE=virtual PUNKTFUNK_SECURE_DDA=1 RUST_LOG=info -# PUNKTFUNK_HOST_CMD=serve --native # the host subcommand the service launches (default) +# PUNKTFUNK_HOST_CMD=serve --gamestream # the host subcommand the service launches (default: native + Moonlight) ``` The service loads these into its environment and carries `PUNKTFUNK_*` + `RUST_LOG` to the host child diff --git a/packaging/arch/README.md b/packaging/arch/README.md index 15920e4..18a7462 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -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 | |---|---|---| diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md index 1627582..2e6e6a0 100644 --- a/packaging/bazzite/README.md +++ b/packaging/bazzite/README.md @@ -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. diff --git a/packaging/debian/README.md b/packaging/debian/README.md index 21c064a..05468cc 100644 --- a/packaging/debian/README.md +++ b/packaging/debian/README.md @@ -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 | |---|---|---| diff --git a/scripts/punktfunk-host.service b/scripts/punktfunk-host.service index 411fd63..edcb5a7 100644 --- a/scripts/punktfunk-host.service +++ b/scripts/punktfunk-host.service @@ -1,4 +1,6 @@ -# punktfunk streaming host — systemd USER unit (`serve --native` = GameStream + punktfunk/1). +# punktfunk streaming host — systemd USER unit (`serve --gamestream` = native punktfunk/1 + the +# GameStream/Moonlight-compat planes). For a SECURE native-only host (no plain-HTTP pairing / legacy +# GCM nonce reuse — security-review #5/#9; native clients only), drop `--gamestream` from ExecStart. # # Install (against an already-running compositor session): # mkdir -p ~/.config/systemd/user && cp scripts/punktfunk-host.service ~/.config/systemd/user/ @@ -29,7 +31,7 @@ PartOf=punktfunk-kde-session.service [Service] EnvironmentFile=%h/.config/punktfunk/host.env -ExecStart=%h/punktfunk/target/release/punktfunk-host serve --native +ExecStart=%h/punktfunk/target/release/punktfunk-host serve --gamestream Restart=on-failure RestartSec=2 diff --git a/scripts/steamdeck/README.md b/scripts/steamdeck/README.md index 618f7c0..cd985d3 100644 --- a/scripts/steamdeck/README.md +++ b/scripts/steamdeck/README.md @@ -51,9 +51,10 @@ default `pf2`), `PUNKTFUNK_MGMT_PORT` (47990), `PUNKTFUNK_WEB_PORT` (3000). - **Config:** `~/.config/punktfunk/host.env` (encoder/compositor) and `web.env` (generated web login password + session secret). Trust material (`cert.pem`, `mgmt-token`, `punktfunk1-paired.json`) lives here too and persists across updates. -- **Services:** `~/.config/systemd/user/punktfunk-host.service` (runs `serve --native --mgmt-bind - 0.0.0.0:47990`, `+ --open` if chosen) and `punktfunk-web.service`. Linger is enabled so they run - without a login session. +- **Services:** `~/.config/systemd/user/punktfunk-host.service` (runs `serve --gamestream --mgmt-bind + 0.0.0.0:47990`, `+ --open` if chosen — `--gamestream` adds the Moonlight-compat planes so the Deck's + Game Mode also streams to stock Moonlight; the native `punktfunk/1` plane is always on) and + `punktfunk-web.service`. Linger is enabled so they run without a login session. - **System tuning (sudo):** `/etc/sysctl.d/99-punktfunk-net.conf` (32 MB UDP buffers — the #1 high-bitrate lever), `/etc/udev/rules.d/60-punktfunk.rules`, and `$USER` in the `input` group. diff --git a/scripts/steamdeck/install.sh b/scripts/steamdeck/install.sh index 07dbc70..1174346 100755 --- a/scripts/steamdeck/install.sh +++ b/scripts/steamdeck/install.sh @@ -170,7 +170,9 @@ fi # --- 5. systemd user services --------------------------------------------- log "Installing systemd user services" mkdir -p "$UNITS" -SERVE_ARGS="serve --native --mgmt-bind 0.0.0.0:$MGMT_PORT" +# --gamestream keeps the Moonlight-compat planes (the Deck commonly streams to Moonlight too); drop +# it for a secure native-only host (no #5/#9 surface — native clients only). +SERVE_ARGS="serve --gamestream --mgmt-bind 0.0.0.0:$MGMT_PORT" [ "$OPEN" = 1 ] && SERVE_ARGS="$SERVE_ARGS --open" cat > "$UNITS/punktfunk-host.service" <