feat(host): §8a — require native pairing by default (serve --open to disable)
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
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) <noreply@anthropic.com>
This commit is contained in:
@@ -145,17 +145,17 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run the host (blocks): mDNS, the nvhttp servers, and the management REST API.
|
/// 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
|
/// `native = Some(cfg)` 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
|
/// QUIC server on `cfg.port` in the same process, sharing one [`crate::native_pairing`] handle with
|
||||||
/// 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`).
|
||||||
pub fn serve(mgmt: crate::mgmt::Options, native_port: Option<u16>) -> Result<()> {
|
pub fn serve(mgmt: crate::mgmt::Options, native: Option<crate::m3::NativeServe>) -> 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 shared native-pairing handle exists only when we run the native host; it links the QUIC
|
||||||
// ceremony and the management API.
|
// ceremony and the management API.
|
||||||
let native = match native_port {
|
let np = match &native {
|
||||||
Some(_) => Some(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")?,
|
||||||
@@ -166,7 +166,8 @@ pub fn serve(mgmt: crate::mgmt::Options, native_port: Option<u16>) -> Result<()>
|
|||||||
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_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)"
|
"punktfunk host (GameStream P1.1: serverinfo + pairing + mDNS)"
|
||||||
);
|
);
|
||||||
let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?;
|
let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?;
|
||||||
@@ -176,13 +177,17 @@ pub fn serve(mgmt: crate::mgmt::Options, native_port: Option<u16>) -> Result<()>
|
|||||||
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_port, native) {
|
match (native, np) {
|
||||||
(Some(port), Some(np)) => {
|
(Some(cfg), Some(np)) => {
|
||||||
tracing::info!(port, "unified host: also serving native punktfunk/1 (QUIC)");
|
tracing::info!(
|
||||||
|
port = cfg.port,
|
||||||
|
require_pairing = cfg.require_pairing,
|
||||||
|
"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::m3::serve(crate::m3::native_serve_opts(port), np),
|
crate::m3::serve(crate::m3::native_serve_opts(&cfg), np),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -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
|
/// 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
|
/// connects mid-session waits in the accept queue. A failed session logs and the loop
|
||||||
/// keeps serving — only endpoint-level failures are fatal.
|
/// keeps serving — only endpoint-level failures are fatal.
|
||||||
/// Default options for the native host when the unified `serve --native` runs it in-process:
|
/// Config for the native (punktfunk/1) host when the unified `serve` runs it in-process.
|
||||||
/// real virtual capture, persistent (no session/duration cut), pairing armed on demand via the
|
pub(crate) struct NativeServe {
|
||||||
/// management API (the shared [`NativePairing`] starts disarmed).
|
pub port: u16,
|
||||||
pub(crate) fn native_serve_opts(port: u16) -> M3Options {
|
/// 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 {
|
M3Options {
|
||||||
port,
|
port: cfg.port,
|
||||||
source: M3Source::Virtual,
|
source: M3Source::Virtual,
|
||||||
seconds: 7 * 24 * 3600, // per-session cap; large enough not to cut a live stream
|
seconds: 7 * 24 * 3600, // per-session cap; large enough not to cut a live stream
|
||||||
frames: 0,
|
frames: 0,
|
||||||
max_sessions: 0,
|
max_sessions: 0,
|
||||||
require_pairing: false,
|
require_pairing: cfg.require_pairing,
|
||||||
allow_pairing: false,
|
allow_pairing: false,
|
||||||
pairing_pin: None,
|
pairing_pin: None,
|
||||||
paired_store: None,
|
paired_store: None,
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ fn real_main() -> Result<()> {
|
|||||||
// GameStream host control plane (P1.1: mDNS + serverinfo) + management API, and (with
|
// 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.
|
// --native) the native punktfunk/1 host in the same process — the unified host.
|
||||||
Some("serve") => {
|
Some("serve") => {
|
||||||
let (mgmt_opts, native_port) = parse_serve(&args[1..])?;
|
let (mgmt_opts, native) = parse_serve(&args[1..])?;
|
||||||
gamestream::serve(mgmt_opts, native_port)
|
gamestream::serve(mgmt_opts, native)
|
||||||
}
|
}
|
||||||
// Print the management API's OpenAPI document (for client codegen).
|
// Print the management API's OpenAPI document (for client codegen).
|
||||||
Some("openapi") => {
|
Some("openapi") => {
|
||||||
@@ -226,10 +226,12 @@ fn input_test() -> Result<()> {
|
|||||||
|
|
||||||
/// `serve` options: the management API (GameStream ports are protocol-fixed) + whether to also run
|
/// `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
|
/// the native punktfunk/1 host in-process (`--native`, the unified host). Returns the mgmt options
|
||||||
/// and the native QUIC port (`None` = GameStream only).
|
/// and the native host config (`None` = GameStream only). Native pairing is **required by default**
|
||||||
fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<u16>)> {
|
/// (an open host any LAN device can stream from is insecure); `--open` turns it off.
|
||||||
|
fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<m3::NativeServe>)> {
|
||||||
let mut opts = mgmt::Options::default();
|
let mut opts = mgmt::Options::default();
|
||||||
let mut native_port: Option<u16> = None;
|
let mut native_port: Option<u16> = None;
|
||||||
|
let mut open = 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();
|
||||||
@@ -265,6 +267,9 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<u16>)> {
|
|||||||
.map_err(|_| anyhow::anyhow!("bad --native-port (want a port number)"))?,
|
.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" => {
|
"-h" | "--help" => {
|
||||||
print_usage();
|
print_usage();
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
@@ -279,7 +284,11 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<u16>)> {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|t| !t.is_empty());
|
.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<Options> {
|
fn parse_m0(args: &[String]) -> Result<Options> {
|
||||||
@@ -398,6 +407,8 @@ SERVE OPTIONS:
|
|||||||
--native also run the native punktfunk/1 (QUIC) host in this process —
|
--native also run the native punktfunk/1 (QUIC) host in this process —
|
||||||
the unified host; pairing is armed from the management API/console
|
the unified host; pairing is armed from the management API/console
|
||||||
--native-port <PORT> native QUIC port (default 9777; implies --native)
|
--native-port <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:
|
M3-HOST OPTIONS:
|
||||||
--port <N> QUIC listen port (default: 9777)
|
--port <N> QUIC listen port (default: 9777)
|
||||||
|
|||||||
Reference in New Issue
Block a user