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
+7 -6
View File
@@ -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 (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 paired fingerprints (`punktfunk1-paired.json`) and gates sessions with `--require-pairing` (the
default; `--allow-tofu`/`--open` accept unpaired clients). 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 mDNS (`_punktfunk._udp`, `crate::discovery`) with TXT `proto`/`fp`(cert fingerprint to
pin)/`pair`(required|optional)/`id`; `punktfunk-probe --discover` lists hosts, Apple clients pin)/`pair`(required|optional)/`id`; `punktfunk-probe --discover` lists hosts, Apple clients
browse the same service via NWBrowser (validated cross-LAN 2026-06-12). 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 slice threads) → `GtkGraphicsOffload`-wrapped picture, PipeWire playback (mic-player
jitter ring inverted), SDL3 gamepad capture + rumble/lightbar feedback, keyboard via 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 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 + ≈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, 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 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 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 (~24 ms NVENC SDK wrapper (libavcodec only emits whole AUs) — the next big latency lever (~24 ms
at high res). at high res).
3. **punktfunk/1 protocol growth.** **Done:** unified host (`serve --native` runs GameStream + the 3. **punktfunk/1 protocol growth.** **Done:** unified host (`serve --gamestream` runs GameStream + the
punktfunk/1 QUIC host in one process) with native pairing driven over the mgmt API / 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). 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 **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 `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). # launcher menu is EMPTY (no apps, no System Settings).
bash scripts/headless/run-headless-kde.sh 1920x1080 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 \ 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 # punktfunk/1 native loopback test (no Moonlight needed; same env as serve, listener persists
# across sessions — bound it with --max-sessions): # across sessions — bound it with --max-sessions):
+6 -3
View File
@@ -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 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 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. 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 Both run from **one process**: bare `punktfunk-host serve` is the **secure native-only default**
REST API and web console. Builds against FFmpeg 7 or 8. (`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)** · 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)**. 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) | | **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). `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)**. console. Full instructions: **[docs.punktfunk.unom.io/docs/install](https://docs.punktfunk.unom.io/docs/install)**.
## Connect a client ## Connect a client
+38 -22
View File
@@ -154,57 +154,73 @@ impl AppState {
/// QUIC server on `cfg.port` in the same process, sharing one [`crate::native_pairing`] handle with /// 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 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`). /// (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( pub fn serve(
mgmt: crate::mgmt::Options, mgmt: crate::mgmt::Options,
native: Option<crate::punktfunk1::NativeServe>, native: crate::punktfunk1::NativeServe,
gamestream: bool,
) -> Result<()> { ) -> Result<()> {
let host = Host::detect()?; let host = Host::detect()?;
let identity = cert::ServerIdentity::load_or_create().context("host certificate")?; let identity = cert::ServerIdentity::load_or_create().context("host certificate")?;
let state = Arc::new(AppState::new(host, identity)); 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 // The native plane always runs, so the shared native-pairing handle (linking the QUIC ceremony
// ceremony and the management API. // and the management API) always exists.
let np = match &native { let np = Arc::new(
Some(_) => Some(Arc::new(
crate::native_pairing::NativePairing::load_with(None, None, false) crate::native_pairing::NativePairing::load_with(None, None, false)
.context("native pairing store")?, .context("native pairing store")?,
)), );
None => None,
};
tracing::info!( tracing::info!(
hostname = %state.host.hostname, hostname = %state.host.hostname,
uniqueid = %state.host.uniqueid, uniqueid = %state.host.uniqueid,
ip = %state.host.local_ip, ip = %state.host.local_ip,
native = native.is_some(), native_port = native.port,
require_pairing = native.as_ref().map(|n| n.require_pairing), require_pairing = native.require_pairing,
"punktfunk host (GameStream P1.1: serverinfo + pairing + mDNS)" 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")?; let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?;
rt.block_on(async move { rt.block_on(async move {
// rustls needs a process-wide crypto provider before any TLS config is built. // rustls needs a process-wide crypto provider before any TLS config is built.
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
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")?; let _advert = mdns::advertise(&state.host).context("mDNS advertise")?;
rtsp::spawn(state.clone()).context("start RTSP server")?; rtsp::spawn(state.clone()).context("start RTSP server")?;
control::spawn(state.clone()).context("start ENet control server")?; control::spawn(state.clone()).context("start ENet control server")?;
match (native, np) {
(Some(cfg), Some(np)) => {
tracing::info!( tracing::info!(
port = cfg.port, port = native.port,
require_pairing = cfg.require_pairing, "unified host: GameStream/Moonlight compat + native punktfunk/1 (QUIC)"
"unified host: also serving native punktfunk/1 (QUIC)"
); );
tokio::try_join!( tokio::try_join!(
nvhttp::run(state.clone()), nvhttp::run(state.clone()),
crate::mgmt::run(state.clone(), mgmt, Some(np.clone())), crate::mgmt::run(state.clone(), mgmt, Some(np.clone())),
crate::punktfunk1::serve(crate::punktfunk1::native_serve_opts(&cfg), np), 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!( tokio::try_join!(
nvhttp::run(state.clone()), crate::mgmt::run(state.clone(), mgmt, Some(np.clone())),
crate::mgmt::run(state, mgmt, None) crate::punktfunk1::serve(native_opts, np),
)?; )?;
} }
}
Ok(()) Ok(())
}) })
} }
+36 -30
View File
@@ -6,10 +6,10 @@
//! `#[cfg(target_os = "linux")]`; the crate compiles everywhere so the workspace builds //! `#[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. //! 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 //! Subcommands: `serve` runs the native punktfunk/1 host + management REST API by default, and —
//! `--native`, the native punktfunk/1 host in-process); `punktfunk1-host` runs the native //! with `--gamestream` — the GameStream/Moonlight-compat planes too (opt-in, trusted-LAN only);
//! punktfunk/1 host standalone; `spike` is a capture→encode→file pipeline dev tool that also //! `punktfunk1-host` runs the native punktfunk/1 host standalone; `spike` is a capture→encode→file
//! round-trips the encoded AUs through a `punktfunk_core` loopback. //! 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. // Scaffold: trait methods and config paths are defined ahead of their backends.
#![allow(dead_code)] #![allow(dead_code)]
@@ -103,11 +103,11 @@ fn real_main() -> Result<()> {
crate::capture::dxgi::install_gpu_pref_hook(); crate::capture::dxgi::install_gpu_pref_hook();
match args.first().map(String::as_str) { match args.first().map(String::as_str) {
// GameStream host control plane (P1.1: mDNS + serverinfo) + management API, and (with // The host: the native punktfunk/1 plane + management API by default (secure), and with
// --native) the native punktfunk/1 host in the same process — the unified host. // --gamestream — the GameStream/Moonlight-compat planes too (opt-in; #5/#9 trusted-LAN caveat).
Some("serve") => { Some("serve") => {
let (mgmt_opts, native) = parse_serve(&args[1..])?; let (mgmt_opts, native, gamestream) = parse_serve(&args[1..])?;
gamestream::serve(mgmt_opts, native) gamestream::serve(mgmt_opts, native, gamestream)
} }
// Print the management API's OpenAPI document (for client codegen). // Print the management API's OpenAPI document (for client codegen).
Some("openapi") => { Some("openapi") => {
@@ -332,14 +332,16 @@ fn input_test() -> Result<()> {
bail!("input-test requires Linux") bail!("input-test requires Linux")
} }
/// `serve` options: the management API (GameStream ports are protocol-fixed) + whether to also run /// `serve` options. The **native punktfunk/1 plane + management API are the secure default and always
/// the native punktfunk/1 host in-process (`--native`, the unified host). Returns the mgmt options /// run**; `--gamestream` additionally enables the GameStream/Moonlight-compat planes (opt-in — they
/// and the native host config (`None` = GameStream only). Native pairing is **required by default** /// 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. /// (an open host any LAN device can stream from is insecure); `--open` turns it off.
fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<punktfunk1::NativeServe>)> { fn parse_serve(args: &[String]) -> Result<(mgmt::Options, punktfunk1::NativeServe, bool)> {
let mut opts = mgmt::Options::default(); let mut opts = mgmt::Options::default();
let mut native_port: Option<u16> = None; let mut native_port: u16 = 9777; // the native plane always runs now
let mut open = false; let mut open = false;
let mut gamestream = false;
let mut i = 0; let mut i = 0;
while i < args.len() { while i < args.len() {
let arg = args[i].as_str(); let arg = args[i].as_str();
@@ -365,16 +367,17 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<punktfunk1::Nat
} }
opts.token = Some(token); opts.token = Some(token);
} }
// Also run the native punktfunk/1 (QUIC) host in this process — the unified host. // The native plane is now the DEFAULT (always runs in `serve`); `--native` is kept as an
// Pairing is then armed on demand from the management API / web console. // accepted no-op for back-compat / explicitness.
"--native" => native_port = Some(native_port.unwrap_or(9777)), "--native" => {}
"--native-port" => { "--native-port" => {
native_port = Some( native_port = next()?
next()?
.parse() .parse()
.map_err(|_| anyhow::anyhow!("bad --native-port (want a port number)"))?, .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 // Disable mandatory native pairing — any device can connect (trusted single-user
// setups only). The default REQUIRES pairing. // setups only). The default REQUIRES pairing.
"--open" => open = true, "--open" => open = true,
@@ -393,11 +396,11 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<punktfunk1::Nat
if opts.token.is_none() { if opts.token.is_none() {
opts.token = Some(crate::mgmt_token::load_or_generate()?); opts.token = Some(crate::mgmt_token::load_or_generate()?);
} }
let native = native_port.map(|port| punktfunk1::NativeServe { let native = punktfunk1::NativeServe {
port, port: native_port,
require_pairing: !open, require_pairing: !open,
}); };
Ok((opts, native)) Ok((opts, native, gamestream))
} }
fn parse_spike(args: &[String]) -> Result<Options> { fn parse_spike(args: &[String]) -> Result<Options> {
@@ -502,8 +505,8 @@ fn print_usage() {
"punktfunk-host — Linux streaming host "punktfunk-host — Linux streaming host
USAGE: USAGE:
punktfunk-host serve [OPTIONS] GameStream host control plane (mDNS + serverinfo …) punktfunk-host serve [OPTIONS] native punktfunk/1 host + management REST API
+ the management REST API (secure default; add --gamestream for Moonlight compat)
punktfunk-host openapi print the management API's OpenAPI document (codegen) 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 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) punktfunk-host probe-compositor exit 0 iff the compositor is up + ready (bringup gate)
@@ -513,9 +516,12 @@ SERVE OPTIONS:
--mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990) --mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990)
--mgmt-token <TOKEN> bearer token for the management API (or PUNKTFUNK_MGMT_TOKEN); --mgmt-token <TOKEN> bearer token for the management API (or PUNKTFUNK_MGMT_TOKEN);
required when --mgmt-bind is not loopback required when --mgmt-bind is not loopback
--native also run the native punktfunk/1 (QUIC) host in this process — --gamestream (--moonlight) ALSO run the GameStream/Moonlight-compat planes (nvhttp pairing,
the unified host; pairing is armed from the management API/console RTSP, ENet control, _nvstream mDNS). OFF by default — they carry
--native-port <PORT> native QUIC port (default 9777; implies --native) 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 <PORT> native QUIC port (default 9777)
--open disable mandatory native pairing (default: pairing REQUIRED — --open disable mandatory native pairing (default: pairing REQUIRED —
an open host any LAN device can stream from is insecure) 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. (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 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. 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." (_punktfunk._udp) for client auto-discovery — 'punktfunk-probe --discover' lists them."
); );
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
+2 -2
View File
@@ -850,7 +850,7 @@ async fn get_native_pairing(State(st): State<Arc<MgmtState>>) -> Json<NativePair
request_body = ArmNativePairing, request_body = ArmNativePairing,
responses( responses(
(status = OK, description = "Pairing armed; the response carries the PIN to display", body = NativePairStatus), (status = OK, description = "Pairing armed; the response carries the PIN to display", body = NativePairStatus),
(status = SERVICE_UNAVAILABLE, description = "Native host not enabled (run `serve --native`)", body = ApiError), (status = SERVICE_UNAVAILABLE, description = "Native host not available in this process", body = ApiError),
(status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError), (status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError),
) )
)] )]
@@ -861,7 +861,7 @@ async fn arm_native_pairing(
let Some(np) = &st.native else { let Some(np) = &st.native else {
return api_error( return api_error(
StatusCode::SERVICE_UNAVAILABLE, StatusCode::SERVICE_UNAVAILABLE,
"native host not enabled (run `serve --native`)", "native host not available in this process",
); );
}; };
let ttl = req.ttl_secs.unwrap_or(120).clamp(15, 600); let ttl = req.ttl_secs.unwrap_or(120).clamp(15, 600);
+8 -5
View File
@@ -58,9 +58,11 @@ const SERVICE_DESCRIPTION: &str =
"Low-latency desktop/game streaming host. Launches the punktfunk host into the active session."; "Low-latency desktop/game streaming host. Launches the punktfunk host into the active session.";
/// The host subcommand the service launches, overridable via `PUNKTFUNK_HOST_CMD` in host.env. /// The host subcommand the service launches, overridable via `PUNKTFUNK_HOST_CMD` in host.env.
/// `serve --native` runs the GameStream (Moonlight) host + the native punktfunk/1 QUIC host in one /// `serve --gamestream` runs the native punktfunk/1 QUIC host (always on) PLUS the GameStream
/// process — the unified host an end user wants. /// (Moonlight) compat planes — the unified host a Windows end user typically wants (Moonlight is the
const DEFAULT_HOST_CMD: &str = "serve --native"; /// common Windows client). Drop `--gamestream` for a secure native-only host (no plain-HTTP pairing /
/// legacy GCM nonce reuse — security-review #5/#9; native clients only).
const DEFAULT_HOST_CMD: &str = "serve --gamestream";
/// Event handles shared between the SCM control handler (which signals them) and the supervision loop /// Event handles shared between the SCM control handler (which signals them) and the supervision loop
/// (which waits on them). Stored as raw `isize` so the `'static + Send` handler can reach them without /// (which waits on them). Stored as raw `isize` so the `'static + Send` handler can reach them without
@@ -619,8 +621,9 @@ fn ensure_default_host_env() -> Result<()> {
PUNKTFUNK_SECURE_DDA=1\n\ PUNKTFUNK_SECURE_DDA=1\n\
RUST_LOG=info\n\ RUST_LOG=info\n\
\n\ \n\
# The host subcommand the service launches (default: serve --native).\n\ # The host subcommand the service launches (default: serve --gamestream = native + Moonlight\n\
# PUNKTFUNK_HOST_CMD=serve --native\n\ # compat). Use `serve` for a SECURE native-only host (no GameStream #5/#9 surface).\n\
# PUNKTFUNK_HOST_CMD=serve --gamestream\n\
\n\ \n\
# Force a specific NVENC render GPU by name substring (multi-GPU boxes only):\n\ # Force a specific NVENC render GPU by name substring (multi-GPU boxes only):\n\
# PUNKTFUNK_RENDER_ADAPTER=4090\n"; # PUNKTFUNK_RENDER_ADAPTER=4090\n";
+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 ## 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 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. 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 (`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.) that flag.)
## Codec and FEC ## 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 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 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 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). [Clients](/docs/clients) and [Running as a Service](/docs/running-as-a-service).
## Appendix — build from source ## 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 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). 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 normal way to run a host. By default `serve` starts the **secure native host**: the native
the native `punktfunk/1` server, plus the management API/web console — all in one process. `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 ```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 | | 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`). | | `--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. | | `--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`). | | `--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 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`. 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 **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 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`. 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`). | | `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). |
`--max-concurrent`, `--allow-pairing`, and `--require-pairing` are **`punktfunk1-host`-only** — `serve` does not `--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. 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`. hosts from another machine with `punktfunk-probe --discover`.
## Environment ## 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) | | **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) | | **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) | | **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 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` 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: 2. Start the host inside your desktop session:
```sh ```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`: 3. Enable the web console and read its login password, then open `http://<host-ip>:3000`:
```sh ```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 > discovery/pairing — including **Windows** and **Android** (phone and Android TV). See
> [Clients](/docs/clients) before reaching for Moonlight. > [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. Moonlight needs the GameStream planes, which are **opt-in**. Run the host with `--gamestream`:
The host advertises itself on the network, so Moonlight usually finds it on its own.
```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 ## 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. 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 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 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 list of paired devices. This works on a headless host over the network — there is no command-line flag
to arm pairing on `serve`. to arm pairing on `serve`.
(The standalone headless test host, `punktfunk1-host`, takes `--allow-pairing`/`--require-pairing` on its (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: Then, on the client:
@@ -61,7 +61,7 @@ the right setting on a shared network: a device has to complete the PIN ceremony
connect. connect.
If you're on a fully trusted single-user network and want to skip pairing, run the host open with 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. `pair=optional` and accepts unpaired clients. Requiring pairing is strongly recommended.
## Trust-on-first-use (host opt-in) ## 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): From a terminal **inside your desktop session** (so the host can reach your compositor):
```sh ```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 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 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).) [Running as a Service](/docs/running-as-a-service).)
+4 -3
View File
@@ -29,9 +29,10 @@ see [Status & Progress](/docs/status).
## ✅ Shipped ## ✅ Shipped
- **The host, two ways.** A GameStream host any [Moonlight](/docs/moonlight) client can use, and the - **The host, two ways.** The lower-latency native [`punktfunk/1`](/docs/how-it-works) protocol (QUIC
lower-latency native [`punktfunk/1`](/docs/how-it-works) protocol (QUIC control + UDP data with control + UDP data with GF(2¹⁶) Leopard FEC + AES-GCM) — the secure default — and, opt-in via
GF(2¹⁶) Leopard FEC + AES-GCM). Both run from one process. `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 - **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). 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 - **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. 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. 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 ## 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 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. manually.
- Prefer a **wired** connection or 5 GHz Wi-Fi between host and client. - 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 - 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. usually bitrate-bound, so lower the bitrate first.
## Still stuck? ## 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: step 3, then run it inside your GNOME session:
```sh ```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: Write `~/.config/punktfunk/host.env` as in step 3, then run it inside your Plasma session:
```sh ```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.)
+1 -1
View File
@@ -621,7 +621,7 @@
} }
}, },
"503": { "503": {
"description": "Native host not enabled (run `serve --native`)", "description": "Native host not available in this process",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
+2 -2
View File
@@ -45,7 +45,7 @@ settings, in-app SPAKE2 PIN pairing) + the video on a **`SwapChainPanel`**, all
## What we're building ## 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 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`), 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 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`, `punktfunk-core { path, features=["quic"] }`, `windows`, the Reactor crate, `ffmpeg-next`, `opus`,
`sdl3`, `mdns-sd`, `anyhow`, `tracing`. Mirror `clients/linux/Cargo.toml`. `sdl3`, `mdns-sd`, `anyhow`, `tracing`. Mirror `clients/linux/Cargo.toml`.
3. **Connect + control plane.** Port `session.rs` + `trust.rs`; validate headless against the 4090 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 + 4. **Decode + present.** FFmpeg D3D11VA → `SwapChainPanel`. SDR (8-bit BGRA) first, then **P010 +
HDR colorspace** (see the HDR section). HDR colorspace** (see the HDR section).
5. **Audio.** WASAPI render + Opus decode (port `audio.rs`). 5. **Audio.** WASAPI render + Opus decode (port `audio.rs`).
+2 -2
View File
@@ -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 ## 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 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 (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 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 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 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. 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 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 `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. match). The SudoVDA monitor activates once a real GPU drives WDDM, so capture + NVENC then work.
+1 -1
View File
@@ -74,7 +74,7 @@ PUNKTFUNK_ENCODER=nvenc
PUNKTFUNK_VIDEO_SOURCE=virtual PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_SECURE_DDA=1 PUNKTFUNK_SECURE_DDA=1
RUST_LOG=info 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 The service loads these into its environment and carries `PUNKTFUNK_*` + `RUST_LOG` to the host child
+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: 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 - **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). 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 | | Port | Proto | Purpose |
|---|---|---| |---|---|---|
+20 -12
View File
@@ -233,19 +233,23 @@ systemctl --user status punktfunk-host
journalctl --user -u punktfunk-host -f journalctl --user -u punktfunk-host -f
``` ```
> **What `serve` actually starts.** The unit's `ExecStart` runs `punktfunk-host serve`, which is the > **What `serve` actually starts.** The bundled unit's `ExecStart` runs `punktfunk-host serve
> **GameStream / Moonlight-compatible** host (mDNS discovery, pairing, RTSP, the fixed GameStream > --gamestream`, so out of the box you get the **unified host**: the native `punktfunk/1` (QUIC) plane
> ports, **plus the management REST API on 47990**). The native `punktfunk/1` (QUIC) host is a > — always on in `serve` — **plus** the GameStream/Moonlight-compat planes (mDNS discovery, pairing,
> *separate* subcommand — `punktfunk-host punktfunk1-host` — and is **not** what the bundled systemd unit > RTSP, the fixed GameStream ports) and the management REST API on 47990. The `--gamestream` flag is
> launches. So out of the box on Bazzite you get the **Moonlight-compatible** host. > what adds the Moonlight surface; GameStream pairs over plain HTTP and its legacy encryption is weaker
> (Source: `crates/punktfunk-host/src/main.rs` — `serve` → `gamestream::serve`; `punktfunk1-host` is its own > than the native plane's (security-review #5/#9), so it's **opt-in and trusted-LAN only**. For a
> path.) > **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 > **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 > 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 > **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 > 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, > the GameStream-host port-map (`docs/gamestream-host-plan.md`). Treat the `firewall-cmd` lines as recommended-but-verified,
> not a checked-in script. > 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 | | Port | Proto | Purpose |
|---|---|---| |---|---|---|
@@ -382,7 +388,8 @@ desktop viewer.
- **Service `ExecStart` points at a missing path in `$HOME`.** The dev unit references - **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 `%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 - **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. (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. 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. 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 3. The bundled systemd unit runs `serve --gamestream` — the native `punktfunk/1` QUIC plane (always
`punktfunk/1` QUIC host (`punktfunk1-host` is separate and unmanaged by the unit). 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. 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: 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 - **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). 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 | | Port | Proto | Purpose |
|---|---|---| |---|---|---|
+4 -2
View File
@@ -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): # Install (against an already-running compositor session):
# mkdir -p ~/.config/systemd/user && cp scripts/punktfunk-host.service ~/.config/systemd/user/ # mkdir -p ~/.config/systemd/user && cp scripts/punktfunk-host.service ~/.config/systemd/user/
@@ -29,7 +31,7 @@ PartOf=punktfunk-kde-session.service
[Service] [Service]
EnvironmentFile=%h/.config/punktfunk/host.env 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 Restart=on-failure
RestartSec=2 RestartSec=2
+4 -3
View File
@@ -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 - **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 password + session secret). Trust material (`cert.pem`, `mgmt-token`, `punktfunk1-paired.json`) lives
here too and persists across updates. here too and persists across updates.
- **Services:** `~/.config/systemd/user/punktfunk-host.service` (runs `serve --native --mgmt-bind - **Services:** `~/.config/systemd/user/punktfunk-host.service` (runs `serve --gamestream --mgmt-bind
0.0.0.0:47990`, `+ --open` if chosen) and `punktfunk-web.service`. Linger is enabled so they run 0.0.0.0:47990`, `+ --open` if chosen — `--gamestream` adds the Moonlight-compat planes so the Deck's
without a login session. 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 - **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. high-bitrate lever), `/etc/udev/rules.d/60-punktfunk.rules`, and `$USER` in the `input` group.
+3 -1
View File
@@ -170,7 +170,9 @@ fi
# --- 5. systemd user services --------------------------------------------- # --- 5. systemd user services ---------------------------------------------
log "Installing systemd user services" log "Installing systemd user services"
mkdir -p "$UNITS" 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" [ "$OPEN" = 1 ] && SERVE_ARGS="$SERVE_ARGS --open"
cat > "$UNITS/punktfunk-host.service" <<EOF cat > "$UNITS/punktfunk-host.service" <<EOF
# Generated by scripts/steamdeck/install.sh — punktfunk Steam Deck host (native binary). # Generated by scripts/steamdeck/install.sh — punktfunk Steam Deck host (native binary).
+4 -3
View File
@@ -23,9 +23,10 @@ PUNKTFUNK_SECURE_DDA=1
# Log level (info | debug | trace). Logs land in %ProgramData%\punktfunk\logs\. # Log level (info | debug | trace). Logs land in %ProgramData%\punktfunk\logs\.
RUST_LOG=info RUST_LOG=info
# The host subcommand the service launches. Default: `serve --native` (GameStream/Moonlight + the # The host subcommand the service launches. Default: `serve --gamestream` (native punktfunk/1 host
# native punktfunk/1 QUIC host in one process). Uncomment to override. # ALWAYS on + the GameStream/Moonlight-compat planes). Use `serve` for a SECURE native-only host
#PUNKTFUNK_HOST_CMD=serve --native # (no plain-HTTP pairing / legacy GCM nonce reuse — security-review #5/#9). Uncomment to override.
#PUNKTFUNK_HOST_CMD=serve --gamestream
# Multi-GPU boxes only: force the NVENC/Desktop-Duplication GPU by Description substring. Leave # Multi-GPU boxes only: force the NVENC/Desktop-Duplication GPU by Description substring. Leave
# unset on single-GPU machines (the default auto-picks the discrete adapter). # unset on single-GPU machines (the default auto-picks the discrete adapter).
+2 -1
View File
@@ -23,7 +23,8 @@ bun run dev # http://localhost:3000
Start a host with the management API up: Start a host with the management API up:
```sh ```sh
# from the repo root — `serve` brings up the GameStream control plane + the mgmt API: # from the repo root — `serve` brings up the native punktfunk/1 plane + the mgmt API (the console
# only needs the mgmt API; add --gamestream too if you also want the Moonlight surface):
WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE \ WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE \
cargo run -rp punktfunk-host -- serve cargo run -rp punktfunk-host -- serve
# loopback :47990, no token (a token is mandatory for non-loopback binds). # loopback :47990, no token (a token is mandatory for non-loopback binds).