From 9a6058cd20445972f2ffb10872fb8d21713c043c Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 11 Jun 2026 10:23:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(host):=20=C2=A78a=20=E2=80=94=20require=20?= =?UTF-8?q?native=20pairing=20by=20default=20(serve=20--open=20to=20disabl?= =?UTF-8?q?e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An open punktfunk/1 host any LAN device can trust-on-first-use and stream from is insecure. The unified host now gates native sessions on pairing by DEFAULT: a client must complete the SPAKE2 PIN ceremony (armed from the web console) before it's admitted; paired devices persist. `serve --open` keeps the old TOFU behavior for trusted single-user setups. native_serve_opts now takes a NativeServe { port, require_pairing }; parse_serve builds it with require_pairing = !--open. GameStream pairing (separate) is unchanged. The require_pairing gate + ceremony are already covered by m3::pairing_ceremony_and_gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/gamestream/mod.rs | 25 ++++++++++++--------- crates/punktfunk-host/src/m3.rs | 21 ++++++++++++----- crates/punktfunk-host/src/main.rs | 21 ++++++++++++----- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/crates/punktfunk-host/src/gamestream/mod.rs b/crates/punktfunk-host/src/gamestream/mod.rs index 93aeb77..3041116 100644 --- a/crates/punktfunk-host/src/gamestream/mod.rs +++ b/crates/punktfunk-host/src/gamestream/mod.rs @@ -145,17 +145,17 @@ impl AppState { } /// Run the host (blocks): mDNS, the nvhttp servers, and the management REST API. -/// `native_port = Some(p)` makes this the **unified** host — it also runs the native punktfunk/1 -/// QUIC server on `p` 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 +/// `native = Some(cfg)` makes this the **unified** host — it also runs the native punktfunk/1 +/// 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`). -pub fn serve(mgmt: crate::mgmt::Options, native_port: Option) -> Result<()> { +pub fn serve(mgmt: crate::mgmt::Options, native: Option) -> 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 native = match native_port { + let np = match &native { Some(_) => Some(Arc::new( crate::native_pairing::NativePairing::load_with(None, None, false) .context("native pairing store")?, @@ -166,7 +166,8 @@ pub fn serve(mgmt: crate::mgmt::Options, native_port: Option) -> Result<()> hostname = %state.host.hostname, uniqueid = %state.host.uniqueid, ip = %state.host.local_ip, - native = native_port.is_some(), + native = native.is_some(), + require_pairing = native.as_ref().map(|n| n.require_pairing), "punktfunk host (GameStream P1.1: serverinfo + pairing + mDNS)" ); let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?; @@ -176,13 +177,17 @@ pub fn serve(mgmt: crate::mgmt::Options, native_port: Option) -> Result<()> 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_port, native) { - (Some(port), Some(np)) => { - tracing::info!(port, "unified host: also serving native punktfunk/1 (QUIC)"); + 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::m3::serve(crate::m3::native_serve_opts(port), np), + crate::m3::serve(crate::m3::native_serve_opts(&cfg), np), )?; } _ => { diff --git a/crates/punktfunk-host/src/m3.rs b/crates/punktfunk-host/src/m3.rs index f33bf9d..97b4590 100644 --- a/crates/punktfunk-host/src/m3.rs +++ b/crates/punktfunk-host/src/m3.rs @@ -115,17 +115,26 @@ fn fingerprint_hex(fp: &[u8; 32]) -> String { /// served one at a time (the virtual output + NVENC are single-tenant); a client that /// connects mid-session waits in the accept queue. A failed session logs and the loop /// keeps serving — only endpoint-level failures are fatal. -/// Default options for the native host when the unified `serve --native` runs it in-process: -/// real virtual capture, persistent (no session/duration cut), pairing armed on demand via the -/// management API (the shared [`NativePairing`] starts disarmed). -pub(crate) fn native_serve_opts(port: u16) -> M3Options { +/// Config for the native (punktfunk/1) host when the unified `serve` runs it in-process. +pub(crate) struct NativeServe { + pub port: u16, + /// Gate sessions on pairing. **Default on** — an open host any LAN device can stream from is + /// insecure; `serve --open` turns it off (trusted single-user setups). Pairing is armed on + /// demand from the web console (arm → PIN); paired devices persist. + pub require_pairing: bool, +} + +/// Options for the native host when the unified `serve --native` runs it: real virtual capture, +/// persistent (no session/duration cut), pairing armed on demand via the management API (the +/// shared [`NativePairing`] starts disarmed). +pub(crate) fn native_serve_opts(cfg: &NativeServe) -> M3Options { M3Options { - port, + port: cfg.port, source: M3Source::Virtual, seconds: 7 * 24 * 3600, // per-session cap; large enough not to cut a live stream frames: 0, max_sessions: 0, - require_pairing: false, + require_pairing: cfg.require_pairing, allow_pairing: false, pairing_pin: None, paired_store: None, diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 76fbce2..0432603 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -59,8 +59,8 @@ fn real_main() -> Result<()> { // 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. Some("serve") => { - let (mgmt_opts, native_port) = parse_serve(&args[1..])?; - gamestream::serve(mgmt_opts, native_port) + let (mgmt_opts, native) = parse_serve(&args[1..])?; + gamestream::serve(mgmt_opts, native) } // Print the management API's OpenAPI document (for client codegen). Some("openapi") => { @@ -226,10 +226,12 @@ fn input_test() -> Result<()> { /// `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 QUIC port (`None` = GameStream only). -fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option)> { +/// and the native host config (`None` = GameStream only). 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)> { let mut opts = mgmt::Options::default(); let mut native_port: Option = None; + let mut open = false; let mut i = 0; while i < args.len() { let arg = args[i].as_str(); @@ -265,6 +267,9 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option)> { .map_err(|_| anyhow::anyhow!("bad --native-port (want a port number)"))?, ) } + // Disable mandatory native pairing — any device can connect (trusted single-user + // setups only). The default REQUIRES pairing. + "--open" => open = true, "-h" | "--help" => { print_usage(); std::process::exit(0); @@ -279,7 +284,11 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option)> { .ok() .filter(|t| !t.is_empty()); } - Ok((opts, native_port)) + let native = native_port.map(|port| m3::NativeServe { + port, + require_pairing: !open, + }); + Ok((opts, native)) } fn parse_m0(args: &[String]) -> Result { @@ -398,6 +407,8 @@ SERVE OPTIONS: --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) + --open disable mandatory native pairing (default: pairing REQUIRED — + an open host any LAN device can stream from is insecure) M3-HOST OPTIONS: --port QUIC listen port (default: 9777)