feat(host): unified host + native pairing over the management API

`serve --native` now runs the GameStream host AND the native punktfunk/1 (QUIC)
host in ONE process, sharing a single NativePairing handle with the management API
— so native pairing is operable from the web console instead of journalctl.

- gamestream::serve gains a native_port: spawns crate::m3::serve in the same
  runtime and passes the shared NativePairing to mgmt::run. Validated live: one
  process binds both RTSP 48010 and QUIC 9777.
- mgmt API: new `native` endpoints — GET /native/pair (status), POST
  /native/pair/arm (mint a fresh, time-limited PIN to DISPLAY), DELETE /native/pair
  (disarm), GET/DELETE /native/clients (list/unpair). GameStream-only hosts report
  enabled:false. OpenAPI regenerated (checked-in doc + drift test).
- main.rs: serve --native / --native-port flags.

The native host arms pairing on demand (the operator reads the PIN from the
console; the SPAKE2 ceremony is host-shows-PIN). New mgmt + native_pairing tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:50:33 +00:00
parent 5ca860533e
commit 19666ba57e
5 changed files with 680 additions and 17 deletions
+25 -5
View File
@@ -56,8 +56,12 @@ fn real_main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
// M2 GameStream host control plane (P1.1: mDNS + serverinfo) + management API.
Some("serve") => gamestream::serve(parse_serve(&args[1..])?),
// 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)
}
// Print the management API's OpenAPI document (for client codegen).
Some("openapi") => {
print!("{}", mgmt::openapi_json());
@@ -220,9 +224,12 @@ fn input_test() -> Result<()> {
bail!("input-test requires Linux")
}
/// `serve` options — all about the management API; the GameStream ports are protocol-fixed.
fn parse_serve(args: &[String]) -> Result<mgmt::Options> {
/// `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<u16>)> {
let mut opts = mgmt::Options::default();
let mut native_port: Option<u16> = None;
let mut i = 0;
while i < args.len() {
let arg = args[i].as_str();
@@ -248,6 +255,16 @@ fn parse_serve(args: &[String]) -> Result<mgmt::Options> {
}
opts.token = Some(token);
}
// Also run the native punktfunk/1 (QUIC) host in this process — the unified host.
// Pairing is then armed on demand from the management API / web console.
"--native" => native_port = Some(native_port.unwrap_or(9777)),
"--native-port" => {
native_port = Some(
next()?
.parse()
.map_err(|_| anyhow::anyhow!("bad --native-port (want a port number)"))?,
)
}
"-h" | "--help" => {
print_usage();
std::process::exit(0);
@@ -262,7 +279,7 @@ fn parse_serve(args: &[String]) -> Result<mgmt::Options> {
.ok()
.filter(|t| !t.is_empty());
}
Ok(opts)
Ok((opts, native_port))
}
fn parse_m0(args: &[String]) -> Result<Options> {
@@ -378,6 +395,9 @@ SERVE OPTIONS:
--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);
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 <PORT> native QUIC port (default 9777; implies --native)
M3-HOST OPTIONS:
--port <N> QUIC listen port (default: 9777)