diff --git a/crates/punktfunk-host/src/windows/service.rs b/crates/punktfunk-host/src/windows/service.rs index f756ca63..d9d02695 100644 --- a/crates/punktfunk-host/src/windows/service.rs +++ b/crates/punktfunk-host/src/windows/service.rs @@ -890,11 +890,16 @@ pub(crate) fn allow_public_network(args: &[String]) -> bool { args.iter().any(|a| a == "--allow-public-network") } -/// Inbound firewall rules for the streaming ports (best-effort; logs but never fails the install). -/// Scoped by [`firewall_profile_arg`]: Domain + Private by default, all profiles when `allow_public`. +/// Inbound firewall rules for the streaming + mgmt ports (best-effort; logs but never fails the +/// install). Scoped by [`firewall_profile_arg`]: Domain + Private by default, all profiles when +/// `allow_public`. TCP 47990 is deliberate: `serve` binds the mgmt/library REST API to all interfaces +/// so paired clients can browse the game library over mTLS, and off-loopback `mgmt::require_auth` +/// exposes only the read-only status/library allowlist to a paired client cert — the bearer-token +/// admin surface stays loopback-only regardless of the bind — so opening it adds no admin exposure. fn add_firewall_rules(allow_public: bool) { let profile = firewall_profile_arg(allow_public); - // (name suffix, protocol, ports) + // (name suffix, protocol, ports). 47990 = mgmt/library (LAN = read-only, paired-cert only); the + // rest are the GameStream (47984/47989/48010, 47998-48010) + native (9777) + mDNS (5353) ports. let rules = [ ("TCP", "TCP", "47984,47989,48010,47990"), ("UDP", "UDP", "47998-48010,9777,5353"), diff --git a/docs-site/content/docs/arch.md b/docs-site/content/docs/arch.md index 193859b3..afec61ae 100644 --- a/docs-site/content/docs/arch.md +++ b/docs-site/content/docs/arch.md @@ -128,8 +128,9 @@ sudo ufw allow punktfunk-web sudo firewall-cmd --permanent --add-service=punktfunk-web && sudo firewall-cmd --reload # firewalld ``` -That opens **TCP 47992** (HTTPS, login-gated). The mgmt API (47990) stays loopback-only and is never -opened. Full port lists (`nftables`, explicit ports) are in +That opens **TCP 47992** (HTTPS, login-gated). The mgmt API (47990) is opened for paired clients by the +`punktfunk-native` profile (game-library browsing over mTLS); off-loopback it serves only read-only +status/library, and every admin action stays loopback-only. Full port lists (`nftables`, explicit ports) are in [`packaging/arch/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md#firewall). ## 6. Connect a client diff --git a/docs-site/content/docs/host-cli.md b/docs-site/content/docs/host-cli.md index 0b8b0a1c..a7151a3a 100644 --- a/docs-site/content/docs/host-cli.md +++ b/docs-site/content/docs/host-cli.md @@ -32,15 +32,19 @@ punktfunk-host serve --gamestream | `--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`). | +| `--mgmt-bind ` | Management API address (default `0.0.0.0:47990` — all interfaces, so paired clients can browse the game library over mTLS; pass `127.0.0.1:47990` to keep it loopback-only). | | `--mgmt-token ` | Override the bearer token for the management API. | These are the only flags `serve` accepts. -The management API is **always HTTPS with bearer-token auth**. If you don't pass `--mgmt-token`, a token -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`. Every endpoint is documented -in the interactive [**API Reference**](/api). +The management API is **always HTTPS**. It binds all interfaces by default so a **paired client** can +fetch the game library over its mTLS certificate — but off loopback that certificate reaches only the +read-only status + library endpoints. The **admin surface** (arming pairing, removing devices, session +control, library edits) authenticates with a **bearer token** and is honored **from loopback only**, so +it is never LAN-exposed even under the default wide bind. If you don't pass `--mgmt-token`, a token is +auto-generated and persisted to `~/.config/punktfunk/mgmt-token` (the bundled web console reads the same +file); `--mgmt-token` only overrides it. Pass `--mgmt-bind 127.0.0.1:47990` to keep 47990 loopback-only. +Every endpoint is documented in the interactive [**API Reference**](/api). 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 diff --git a/docs-site/content/docs/web-console.md b/docs-site/content/docs/web-console.md index a4664441..ed85da83 100644 --- a/docs-site/content/docs/web-console.md +++ b/docs-site/content/docs/web-console.md @@ -5,8 +5,9 @@ description: Enable the punktfunk browser console, read or change its login pass The web console is the browser UI for a punktfunk host — live status, paired devices, and the PIN pairing flow. It ships as the **`punktfunk-web`** systemd user unit on Linux and the **`PunktfunkWeb`** -task on Windows, and serves on **`http://:47992`**. The host's own management API stays -loopback-only behind it, so the console is the one surface you expose on the LAN. +task on Windows, and serves on **`http://:47992`**. It's the surface you expose on the LAN to +administer the host; the host's own management API (47990) keeps every admin action loopback-only and +off-loopback serves only read-only status + game-library browsing to paired clients. > New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of > the machine, so keep it on a trusted LAN or VPN and require pairing. diff --git a/docs-site/content/docs/windows-host.md b/docs-site/content/docs/windows-host.md index 1bd1f381..0006e3a3 100644 --- a/docs-site/content/docs/windows-host.md +++ b/docs-site/content/docs/windows-host.md @@ -76,7 +76,7 @@ only by Administrators and SYSTEM. To read or change it (with the `schtasks` res The host **requires PIN pairing** by default (secure on a LAN). To connect the first time, open the console from any browser on the LAN, log in, go to **Devices → [arm pairing](/docs/web-console#arm-pairing)**, -and enter the PIN on your [client](/docs/clients). The host's own management API stays loopback-only behind the console. +and enter the PIN on your [client](/docs/clients). The host's own management API keeps every admin action loopback-only; off-loopback it serves only read-only status and game-library browsing to paired clients. ### Configure diff --git a/packaging/arch/README.md b/packaging/arch/README.md index bc86ced8..a8eb70e2 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -160,17 +160,20 @@ sudo firewall-cmd --reload ``` `punktfunk-gamestream` opens the fixed Moonlight ports + mDNS; `punktfunk-native` opens the QUIC -control port (UDP 9777) + mDNS. Enable both if the host runs `serve --gamestream` (which serves -both planes). The **data plane is an *ephemeral* UDP port** the client opens with a hole-punch, so +control port (UDP 9777) + mDNS + the mgmt/library API (TCP 47990, HTTPS + mTLS). Enable both if the +host runs `serve --gamestream` (which serves both planes). The **data plane is an *ephemeral* UDP port** the client opens with a hole-punch, so there is no fixed data port in either service — the host streams back out through the path the client opened, which any firewall that allows outbound UDP (the default) passes. The mgmt REST API -(TCP 47990) binds to loopback by default — leave it closed unless you move it off loopback with -`--mgmt-bind IP:PORT` (which then requires `--mgmt-token`). +(TCP 47990, HTTPS + mTLS) binds all interfaces by default so paired clients can browse the game library +— the `punktfunk-native` profile opens it. Off-loopback it serves only read-only status/library to a +paired client cert and keeps the admin surface loopback-only (`--mgmt-bind 127.0.0.1:47990` to opt out). If you installed the **web console** (`punktfunk-web`) and want it reachable from another device, open its port with the matching one-liner — `sudo ufw allow punktfunk-web` or `sudo firewall-cmd --permanent --add-service=punktfunk-web && sudo firewall-cmd --reload` — which opens **TCP 47992** -(HTTPS, login-gated). The mgmt API (47990) stays loopback-only. +(HTTPS, login-gated). The mgmt API (47990) is opened for paired clients by the `punktfunk-native` +profile (game-library browsing over mTLS); off-loopback it serves only read-only status/library and +keeps admin loopback-only. Prefer explicit rules (or a firewall the shipped profiles don't cover)? Open the ports directly. The **native `punktfunk/1`** plane: @@ -198,13 +201,16 @@ And the **GameStream / Moonlight** ports (fixed) — only needed if you run the | 47998–48010 | UDP | Video RTP (+ FEC), ENet control (47999), audio (48000) | | 5353 | UDP | mDNS auto-discovery | -The mgmt API (TCP 47990) binds to loopback by default — leave it closed unless you move it off -loopback with `--mgmt-bind IP:PORT` (which then requires `--mgmt-token`). +The mgmt API (TCP 47990, HTTPS + mTLS) binds all interfaces by default so paired clients can browse the +game library — the `punktfunk-native` profile opens it. Off-loopback it serves only read-only +status/library to a paired client cert; the admin surface stays loopback-only. Pass +`--mgmt-bind 127.0.0.1:47990` to keep it loopback-only (then leave 47990 closed). With `ufw` (explicit ports, instead of the shipped `punktfunk-native`/`punktfunk-gamestream` profile): ```sh sudo ufw allow 9777/udp # punktfunk/1 control plane +sudo ufw allow 47990/tcp # mgmt/library API (HTTPS + mTLS; LAN = read-only, paired) sudo ufw allow 47984/tcp && sudo ufw allow 47989/tcp && sudo ufw allow 48010/tcp sudo ufw allow 47998,47999,48000/udp # GameStream video/control/audio sudo ufw allow 5353/udp # mDNS discovery @@ -217,6 +223,7 @@ With raw `nftables` (add to your `inet filter input` chain): ``` udp dport 9777 accept # punktfunk/1 control plane +tcp dport 47990 accept # mgmt/library API (HTTPS + mTLS; LAN = read-only, paired) tcp dport { 47984, 47989, 48010 } accept udp dport { 47998-48000, 5353 } accept # GameStream video/control/audio + mDNS # The punktfunk/1 data plane is a random UDP port — normally left closed (hole-punch + ~2.5s diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md index cbebd424..3a6c9550 100644 --- a/packaging/bazzite/README.md +++ b/packaging/bazzite/README.md @@ -335,7 +335,8 @@ sudo firewall-cmd --reload ``` `punktfunk-gamestream` opens the fixed Moonlight ports + mDNS; `punktfunk-native` opens the QUIC -control port (UDP 9777) + mDNS. Enable both if the host runs `serve --gamestream` (both planes). The +control port (UDP 9777) + mDNS + the mgmt/library API (TCP 47990, HTTPS + mTLS). Enable both if the +host runs `serve --gamestream` (both planes). The per-port breakdown below is for reference (or for opening ports by hand); the ports are the code constants (`crates/punktfunk-host/src/gamestream/mod.rs`, `mgmt.rs`) and the GameStream-host port-map (`design/gamestream-host-plan.md`). @@ -354,9 +355,11 @@ host you don't open them: | 48000 | UDP | Audio (Opus) | | 5353 | UDP | mDNS — so Moonlight auto-discovers the host (`_nvstream._tcp.local.`) | -**Management REST API:** **TCP 47990** — but `serve` **binds it to `127.0.0.1` (loopback) by -default**, so you do **not** open it in the firewall unless you deliberately move it off loopback -with `--mgmt-bind IP:PORT` (which also requires `--mgmt-token`). Leave it closed for a normal setup. +**Management REST API:** **TCP 47990** (HTTPS + mTLS) — `serve` **binds it to all interfaces by +default** so paired clients can browse the game library over the LAN; the `punktfunk-native` profile +(above) opens it, or open it by hand with `firewall-cmd --add-port=47990/tcp`. Off-loopback it serves +only read-only status/library to a paired client cert; every admin action stays loopback-only. Pass +`--mgmt-bind 127.0.0.1:47990` to keep it loopback-only instead. To open the GameStream ports by hand instead of the service (equivalent): @@ -508,4 +511,6 @@ matching your Bazzite Fedora base (`rpm -E %fedora`). 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. +4. The mgmt port (47990) **binds all interfaces by default** and is opened for paired clients by the + `punktfunk-native` profile (game-library browsing over mTLS); off-loopback it serves only read-only + status/library and keeps admin loopback-only. `--mgmt-bind 127.0.0.1:47990` restores loopback-only. diff --git a/packaging/debian/README.md b/packaging/debian/README.md index 74be8e39..a7f2ae89 100644 --- a/packaging/debian/README.md +++ b/packaging/debian/README.md @@ -71,7 +71,9 @@ sudo firewall-cmd --reload If you installed the **web console** (`punktfunk-web`) and want it reachable from another device, open its port with the matching one-liner — `sudo ufw allow punktfunk-web` or `sudo firewall-cmd --permanent --add-service=punktfunk-web && sudo firewall-cmd --reload` — which opens **TCP 47992** -(HTTPS, login-gated). The mgmt API (47990) stays loopback-only. +(HTTPS, login-gated). The mgmt API (47990) is opened for paired clients by the `punktfunk-native` +profile (game-library browsing over mTLS); off-loopback it serves only read-only status/library and +keeps admin loopback-only. Prefer explicit rules? Open the ports directly. The **native `punktfunk/1`** plane: @@ -98,13 +100,16 @@ And the **GameStream / Moonlight** ports (fixed) — only needed if you run the | 47998–48010 | UDP | Video RTP (+ FEC), ENet control (47999), audio (48000) | | 5353 | UDP | mDNS auto-discovery | -The mgmt API (TCP 47990) binds to loopback by default — leave it closed unless you move it off -loopback with `--mgmt-bind IP:PORT` (which then requires `--mgmt-token`). +The mgmt API (TCP 47990, HTTPS + mTLS) binds all interfaces by default so paired clients can browse the +game library — the `punktfunk-native` profile opens it. Off-loopback it serves only read-only +status/library to a paired client cert; the admin surface stays loopback-only. Pass +`--mgmt-bind 127.0.0.1:47990` to keep it loopback-only (then leave 47990 closed). With `ufw` (explicit ports, instead of the shipped profile): ```sh sudo ufw allow 9777/udp # punktfunk/1 control plane +sudo ufw allow 47990/tcp # mgmt/library API (HTTPS + mTLS; LAN = read-only, paired) sudo ufw allow 47984/tcp && sudo ufw allow 47989/tcp && sudo ufw allow 48010/tcp sudo ufw allow 47998,47999,48000/udp # GameStream video/control/audio sudo ufw allow 5353/udp # mDNS discovery @@ -117,6 +122,7 @@ With raw `nftables` (add to your `inet filter input` chain): ``` udp dport 9777 accept # punktfunk/1 control plane +tcp dport 47990 accept # mgmt/library API (HTTPS + mTLS; LAN = read-only, paired) tcp dport { 47984, 47989, 48010 } accept udp dport { 47998-48010, 5353 } accept # The punktfunk/1 data plane is a random UDP port — normally left closed (hole-punch + ~2.5s diff --git a/packaging/linux/punktfunk-gamestream.xml b/packaging/linux/punktfunk-gamestream.xml index 0c82d9cd..bab73540 100644 --- a/packaging/linux/punktfunk-gamestream.xml +++ b/packaging/linux/punktfunk-gamestream.xml @@ -10,7 +10,8 @@ (punktfunk.ufw). Exact commands: your distro's install guide, or the per-distro packaging README. Needed only when the host runs GameStream/Moonlight compat (serve with the gamestream flag). The - mgmt REST API (TCP 47990) stays on loopback by default and is deliberately not opened here. + mgmt REST API (TCP 47990) is opened by the punktfunk-native profile (not here) so paired clients can + browse the game library over mTLS; off-loopback it exposes only read-only status/library endpoints. Port map: design/gamestream-host-plan.md. --> diff --git a/packaging/linux/punktfunk-native.xml b/packaging/linux/punktfunk-native.xml index 5709fd6c..e2accc07 100644 --- a/packaging/linux/punktfunk-native.xml +++ b/packaging/linux/punktfunk-native.xml @@ -13,10 +13,18 @@ The media DATA plane binds an EPHEMERAL UDP port (0.0.0.0:0) chosen per session and reported to the client, so there is no fixed data port to open. On a restrictive firewall you must also allow the ephemeral UDP range (the project does not pin one). + + The mgmt REST API (TCP 47990, HTTPS + mTLS) is opened here because `serve` binds it to all interfaces + by default so paired clients can browse the game library over the LAN. Off-loopback it exposes ONLY + the read-only status + library endpoints, and only to a paired client certificate; every admin / + state-changing action (arming pairing, removing devices, session control, library edits) is honored + over loopback only. To keep 47990 loopback-only, bind the mgmt API to 127.0.0.1:47990 (the host's + `mgmt-bind` option). --> Punktfunk (native punktfunk/1) - Low-latency game-streaming host over the native punktfunk/1 protocol (QUIC control plane). Opens the default QUIC control port plus mDNS for auto-discovery. The media data plane uses an ephemeral UDP port negotiated per session, not opened here. + Low-latency game-streaming host over the native punktfunk/1 protocol (QUIC control plane). Opens the default QUIC control port plus mDNS for auto-discovery, and the mgmt/library REST API (HTTPS + mTLS; off-loopback it serves only read-only status/library to paired clients). The media data plane uses an ephemeral UDP port negotiated per session, not opened here. + diff --git a/packaging/linux/punktfunk-web.xml b/packaging/linux/punktfunk-web.xml index 6e9cc085..730d06f0 100644 --- a/packaging/linux/punktfunk-web.xml +++ b/packaging/linux/punktfunk-web.xml @@ -10,8 +10,9 @@ firewall-cmd (add-service=punktfunk-web, then reload). CachyOS/Ubuntu: use the ufw punktfunk-web profile instead. - The mgmt REST API (TCP 47990) is a different, loopback-only surface (the console proxies to it - locally) and is deliberately NOT opened here. + The mgmt REST API (TCP 47990) is a different surface — the console proxies to it over loopback, and + it is opened for paired clients by the punktfunk-native profile (not here). Off-loopback the mgmt API + serves only read-only status/library to a paired client cert; its admin surface stays loopback-only. --> Punktfunk web console diff --git a/packaging/linux/punktfunk.ufw b/packaging/linux/punktfunk.ufw index 73804cc8..b5cdbeb0 100644 --- a/packaging/linux/punktfunk.ufw +++ b/packaging/linux/punktfunk.ufw @@ -21,17 +21,22 @@ [punktfunk-native] title=punktfunk host (native punktfunk/1) -description=punktfunk/1 native streaming: QUIC control plane + mDNS auto-discovery -ports=9777/udp|5353/udp +description=punktfunk/1 native streaming: QUIC control plane + mDNS auto-discovery + mgmt/library API (HTTPS + mTLS) +ports=9777/udp|5353/udp|47990/tcp [punktfunk-gamestream] title=punktfunk host (GameStream/Moonlight) description=GameStream/Moonlight compatibility ports (opt-in, trusted LAN only) ports=47984,47989,48010/tcp|47998:48010/udp|5353/udp +# The mgmt REST API (TCP 47990, in the punktfunk-native profile above) binds all interfaces by +# default so paired clients can browse the game library over mTLS. Off-loopback it exposes ONLY the +# read-only status + library endpoints, and only to a paired client certificate; every admin action +# (arming pairing, removing devices, session control, library edits) is honored over loopback only. +# Run the host with `--mgmt-bind 127.0.0.1:47990` to keep 47990 loopback-only (then don't open it). +# # The optional web console (the separate punktfunk-web package). Open only if you installed it and # want to reach it from another device — it binds all interfaces on TCP 47992 (HTTPS, login-gated). -# The mgmt API (47990) is loopback-only and is deliberately not covered here. [punktfunk-web] title=punktfunk web console description=The optional punktfunk management web console (HTTPS, login-gated) reachable from the LAN