From b9fde03f1e5a85c21a3856e0710b8914420c19be Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 3 Jul 2026 14:08:17 +0000 Subject: [PATCH] feat(security): finish Windows firewall Public opt-in wiring + vuln-disclosure + doc cleanup Firewall (the service.rs core landed in efb1ba2): scope the web-console rule (TCP 47992) to Domain+Private by default with a `--allow-public-network` opt-in that deletes-then-re-adds the rule, and add the installer "Allow connections on Public networks" task (unchecked) forwarding the flag to `service install` and `web setup`. Default is now trusted-networks-only; Public is explicit. Vulnerability disclosure: SECURITY.md (report to security@punktfunk.com, scope, SLAs, safe harbor), a Gitea issue-template contact link, a README security line, and a Reporting section on the docs Security page. Docs: the Security page now documents the Private/Domain firewall default (and how to fix a misclassified-Public network / opt in); removed internal design-doc and CLAUDE.md links from the user-facing docs. Co-Authored-By: Claude Opus 4.8 --- .gitea/ISSUE_TEMPLATE/config.yaml | 9 +++ README.md | 4 +- SECURITY.md | 69 +++++++++++++++++++ crates/punktfunk-host/src/windows/install.rs | 16 +++++ docs-site/content/docs/host-cli.md | 4 +- .../content/docs/running-as-a-service.md | 16 +++-- docs-site/content/docs/security.md | 45 ++++++++---- docs-site/content/docs/status.md | 3 +- packaging/windows/punktfunk-host.iss | 20 +++++- 9 files changed, 159 insertions(+), 27 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/config.yaml create mode 100644 SECURITY.md diff --git a/.gitea/ISSUE_TEMPLATE/config.yaml b/.gitea/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..de6db08 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,9 @@ +# Shown on the "new issue" chooser so security reports go to the private channel, not a public issue. +blank_issues_enabled: true +contact_links: + - name: ๐Ÿ”’ Report a security vulnerability + url: https://git.unom.io/unom/punktfunk/src/branch/main/SECURITY.md + about: >- + Found a security issue? Please report it privately by email to security@punktfunk.com โ€” do not + open a public issue, so other users aren't exposed before a fix ships. See SECURITY.md for the + full policy. diff --git a/README.md b/README.md index 832dcb1..2274e56 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ your local network. ๐Ÿ’ฌ **Community: [Discord](https://discord.gg/kaPNvzMuGU)** โ€” chat, support, and **Android beta access** ยท **[r/Punktfunk](https://www.reddit.com/r/Punktfunk/)**. +๐Ÿ”’ **Security:** found a vulnerability? Report it privately to **security@punktfunk.com** โ€” see +[SECURITY.md](SECURITY.md). Please don't open a public issue. + punktfunk pairs a **virtual-display streaming host** with native clients on every platform. It speaks the existing **GameStream** protocol, so any [Moonlight](https://moonlight-stream.org/) client works day one โ€” and adds its own faster **`punktfunk/1`** protocol that breaks the ~1 Gbps FEC wall with a @@ -138,7 +141,6 @@ clients/ web/ web console (TanStack) over the management API โ€” status ยท devices ยท pairing ยท GPUs ยท performance ยท logs packaging/ apt ยท rpm / COPR ยท Arch ยท Flatpak ยท Bazzite bootc image docs-site/ public documentation site (Fumadocs) โ€” https://docs.punktfunk.unom.io -design/ design notes & deep-dive plans (index: design/README.md) include/punktfunk_core.h cbindgen-generated C header (checked in) tools/ latency-probe ยท loss-harness (measurement) ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cf328f2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,69 @@ +# Security Policy + +punktfunk is a low-latency desktop/game streaming stack. A host is effectively remote control of a +machine, so we take security reports seriously and appreciate responsible disclosure. + +## Reporting a vulnerability + +**Please report security issues privately by email to security@punktfunk.com.** + +Do **not** open a public issue, pull request, or chat/forum post for a suspected vulnerability โ€” that +exposes other users before a fix exists. + +### What to include + +The more of this you can give us, the faster we can act: + +- The component and version (e.g. `punktfunk-host 0.6.0`, Windows or Linux, which client). +- The impact โ€” what an attacker can do, and from what position (same LAN, a local service account, + admin, a paired client, โ€ฆ). +- Steps to reproduce, a proof-of-concept, or a crash/log if you have one. +- Any suggested fix or mitigation (optional). + +## What to expect + +We're a small team, so timelines are best-effort, but we commit to: + +- **Acknowledge** your report within **3 business days**. +- Give an **initial assessment** (severity + whether we can reproduce) within about **7 days**. +- Keep you updated, and tell you when a fix ships. +- **Credit** you in the advisory / release notes when the fix is public โ€” unless you'd rather stay + anonymous. + +We practice **coordinated disclosure**: please give us reasonable time to release a fix before +publishing details. We aim to resolve valid issues within **90 days** and will agree a disclosure +date with you. + +## Scope + +In scope โ€” the code in this repository: + +- The host (`punktfunk-host`), its Windows drivers, and the protocol/crypto core (`punktfunk-core`). +- The native clients (Apple, Linux, Windows, Android), the web management console, and the management + API. + +Known limits โ€” documented behavior, not vulnerabilities (see +https://docs.punktfunk.unom.io/docs/security): + +- **Admin/SYSTEM already on the host = out of scope.** An attacker who is already administrator or + SYSTEM on the host owns the machine regardless of punktfunk. +- **The virtual display is a real monitor** โ€” any process already in the interactive desktop session + can capture it via the normal OS screen-capture APIs, exactly as it could a physical monitor. +- **GameStream/Moonlight compatibility** (`--gamestream`) uses legacy encryption and is documented as + opt-in, trusted-LAN-only. +- **Public-internet exposure is unsupported** โ€” issues that only arise from exposing the host to the + WAN are expected; keep the host on a trusted LAN or a VPN. + +If you're unsure whether something is in scope, report it anyway โ€” we'd rather hear about it. + +## Safe harbor + +We consider good-faith security research that follows this policy to be authorized, and we won't +pursue legal action against researchers who: + +- make a good-faith effort to avoid privacy violations, data loss, and service disruption, +- only test systems they own or have explicit permission to test, +- give us reasonable time to remediate before public disclosure, +- don't exfiltrate more data than needed to demonstrate the issue. + +Thank you for helping keep punktfunk and its users safe. diff --git a/crates/punktfunk-host/src/windows/install.rs b/crates/punktfunk-host/src/windows/install.rs index 81d051d..fd6bd91 100644 --- a/crates/punktfunk-host/src/windows/install.rs +++ b/crates/punktfunk-host/src/windows/install.rs @@ -392,6 +392,21 @@ fn web_setup(args: &[String]) -> Result<()> { register_web_task(&cmd)?; // 4. firewall: inbound TCP 47992. The console serves HTTPS (HTTP/1.1 over TLS) with the host's // identity cert. (No UDP/HTTP-3: browsers won't use QUIC against a self-signed/no-SAN cert.) + // Scoped to the same profiles as the streaming ports โ€” Domain + Private by default, Public + // only with `--allow-public-network`. Delete any prior rule first so an upgrade re-scopes it + // instead of stacking a second (possibly all-profiles) rule behind the new one. + let fw_profile = + crate::service::firewall_profile_arg(crate::service::allow_public_network(args)); + run_quiet( + "netsh", + &[ + "advfirewall", + "firewall", + "delete", + "rule", + "name=punktfunk web console (TCP 47992)", + ], + ); if !run_quiet( "netsh", &[ @@ -404,6 +419,7 @@ fn web_setup(args: &[String]) -> Result<()> { "action=allow", "protocol=TCP", "localport=47992", + fw_profile, ], ) { eprintln!("warning: could not add the firewall rule for TCP 47992"); diff --git a/docs-site/content/docs/host-cli.md b/docs-site/content/docs/host-cli.md index e61873a..0b8b0a1 100644 --- a/docs-site/content/docs/host-cli.md +++ b/docs-site/content/docs/host-cli.md @@ -19,8 +19,8 @@ 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. +HTTP; its legacy control encryption can reuse GCM nonces), so enable it **only on a trusted LAN**. The +native plane is immune to those issues. ```sh punktfunk-host serve --gamestream diff --git a/docs-site/content/docs/running-as-a-service.md b/docs-site/content/docs/running-as-a-service.md index a4090ee..e2c3ddb 100644 --- a/docs-site/content/docs/running-as-a-service.md +++ b/docs-site/content/docs/running-as-a-service.md @@ -8,8 +8,8 @@ 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`. +> host** (no GameStream โ€” its pairing runs over plain HTTP and its legacy encryption is weaker), drop +> `--gamestream` from the unit's `ExecStart` and use bare `serve`. ## A. A desktop you log into @@ -101,9 +101,15 @@ registers + starts the service for you (`/VERYSILENT` for unattended). Upgrades handled through Add/Remove Programs. Prefer the CLI? Run `punktfunk-host service install` from an elevated prompt โ€” see -[Windows service](https://git.unom.io/unom/punktfunk/src/branch/main/docs/windows-service.md). For -hardware encode you need a GPU โ€” NVIDIA (NVENC), AMD (AMF), or Intel (QSV); the host falls back to -software H.264 without one. +[Windows Host](/docs/windows-host). For hardware encode you need a GPU โ€” NVIDIA (NVENC), AMD (AMF), or +Intel (QSV); the host falls back to software H.264 without one. + +> **Firewall scope.** The installer opens the streaming + console ports on **Private and Domain** +> networks only โ€” not **Public**. If your LAN is (mis)classified Public, clients won't connect until +> you set it to Private (Windows Settings โ†’ Network), and the host logs a warning when it's on a Public +> network. For a trusted network Windows insists is Public, tick **"Allow connections on Public +> networks"** at install (or pass `--allow-public-network` to `service install`). See +> [Security & Safe Use](/docs/security) for the reasoning. ## Verifying diff --git a/docs-site/content/docs/security.md b/docs-site/content/docs/security.md index 77ab019..873b3a9 100644 --- a/docs-site/content/docs/security.md +++ b/docs-site/content/docs/security.md @@ -54,12 +54,21 @@ If you want to stream from outside your home, tunnel in instead of opening up: - **Don't** map a router port to the host. A port-forward turns "trusted LAN service" into "internet-facing service" with none of the protections that implies. -A note for **portable machines**: the installer opens the streaming ports on the firewall for *all* -network profiles, including Public. That's convenient at home but means that if you take a laptop host -onto an untrusted network โ€” a cafรฉ, a hotel, a conference โ€” other devices on that network can reach the -ports and attempt to pair. Pairing still protects you (an attacker who doesn't know the PIN can't get -in), but the safest habit is to stop the host service, or firewall it off, when you're on a network you -don't control. +A note on **Windows network profiles**: the installer opens the streaming and console ports only on +**Private and Domain** networks โ€” the profiles Windows uses for networks you've marked as trusted. On a +network Windows classifies as **Public** (cafรฉs, hotels, conferences โ€” the default for unknown +networks), the ports stay **closed**, so a laptop host won't accept connections there. That's the safe +default, and it's the behavior you want on the move. Two things follow from it: + +- **If your home network is *misclassified* as Public, clients won't connect.** Set it to Private + (Windows Settings โ†’ Network & internet โ†’ your network โ†’ **Private network**). The host also logs a + warning at startup when it detects it's on a Public network, so this doesn't fail silently. +- **If you have a trusted network that Windows insists on marking Public** (some headless or + no-gateway LAN setups), you can opt in during install โ€” the **"Allow connections on Public + networks"** checkbox (off by default). Only do this for a network you actually trust. + +Either way, pairing is what ultimately gates access โ€” but keeping the host off untrusted networks is +the first line, and on the move the safest habit is still to stop the service when you don't need it. ## What actually protects you @@ -109,9 +118,7 @@ We mitigate this deliberately: full-system. (This is why punktfunk dropped ViGEmBus.) - **Sealed internal channels.** The desktop-frame ring and the gamepad input/output channels are passed between the host and its drivers as duplicated handles to unnamed objects, so another local - service can't open them by name to read your screen or forge controller input. (Details: - [`idd-push-security.md`](https://git.unom.io/unom/punktfunk/src/branch/main/design/idd-push-security.md) - and [`gamepad-channel-sealing.md`](https://git.unom.io/unom/punktfunk/src/branch/main/design/gamepad-channel-sealing.md).) + service can't open them by name to read your screen or forge controller input. - **Secrets are locked down.** The management token, the host identity key, and the console password are stored with Administrators/SYSTEM-only permissions. @@ -142,12 +149,20 @@ applies: keep it on a trusted LAN or a VPN, require pairing, and don't expose it - **Keep the host updated** โ€” security fixes ship in new builds. - **On portable hosts**, stop the service when you're on an untrusted network. -## For the technically curious +## Reporting a vulnerability -The deeper security design lives in the repository, and it's candid about residual limits: +Found a security issue? **Email [security@punktfunk.com](mailto:security@punktfunk.com).** Please +don't open a public issue, pull request, or chat post for a suspected vulnerability โ€” that exposes +other users before a fix is available. -- [`design/idd-push-security.md`](https://git.unom.io/unom/punktfunk/src/branch/main/design/idd-push-security.md) โ€” the sealed frame channel (why the Windows capture path is isolated), and its honest floor. -- [`design/gamepad-channel-sealing.md`](https://git.unom.io/unom/punktfunk/src/branch/main/design/gamepad-channel-sealing.md) โ€” the sealed gamepad channel. -- [`design/security-review-2026-06-28.md`](https://git.unom.io/unom/punktfunk/src/branch/main/design/security-review-2026-06-28.md) and [`design/security-review.md`](https://git.unom.io/unom/punktfunk/src/branch/main/design/security-review.md) โ€” the standing security reviews. +Helpful things to include: -Found a security issue? Please report it privately rather than opening a public issue. +- The component and version โ€” e.g. `punktfunk-host 0.6.0`, Windows or Linux, and which client. +- The impact, and the attacker's position (same LAN, a paired client, a local service account, + admin, โ€ฆ). +- Steps to reproduce, a proof-of-concept, or a crash/log if you have one. + +We acknowledge reports within **3 business days** and practice coordinated disclosure โ€” we'll keep +you posted, agree a disclosure date, and credit you when the fix ships (unless you'd rather stay +anonymous). The full policy is in +[`SECURITY.md`](https://git.unom.io/unom/punktfunk/src/branch/main/SECURITY.md). diff --git a/docs-site/content/docs/status.md b/docs-site/content/docs/status.md index aaad3de..36b67ac 100644 --- a/docs-site/content/docs/status.md +++ b/docs-site/content/docs/status.md @@ -4,8 +4,7 @@ description: "Where the work stands across the core, the host, and the native cl --- A high-level view of where punktfunk stands. The ordered plan of work is on the -[Roadmap](/docs/roadmap), and milestone-level detail lives in -[`CLAUDE.md`](https://git.unom.io/unom/punktfunk/src/branch/main/CLAUDE.md). +[Roadmap](/docs/roadmap). ## Milestones at a glance diff --git a/packaging/windows/punktfunk-host.iss b/packaging/windows/punktfunk-host.iss index 37519ff..86e9e45 100644 --- a/packaging/windows/punktfunk-host.iss +++ b/packaging/windows/punktfunk-host.iss @@ -146,6 +146,11 @@ Name: "installhdrlayer"; Description: "Install the HDR Vulkan layer (lets Vulkan ; in host.env; a hand-customized value is left alone). Checked = the Moonlight-compatible unified ; host (the common Windows setup); unchecked = the secure native-only host (punktfunk clients only). Name: "gamestream"; Description: "Enable GameStream (Moonlight) compatibility - lets stock Moonlight clients connect (uses legacy plain-HTTP pairing; for trusted LANs)" +; Firewall scope, forwarded as `--allow-public-network` to `service install` / `web setup`. Unchecked +; (default) = accept connections on Private + Domain networks only (the trusted-network profiles +; punktfunk is meant for). Check ONLY for a network you trust that Windows classifies as Public (e.g. +; some headless / no-gateway LAN setups) - it opens the streaming + console ports on Public too. +Name: "allowpublicfw"; Description: "Allow connections on Public networks (only for a trusted network Windows marks as Public)"; Flags: unchecked Name: "startservice"; Description: "Start the punktfunk host service now (also starts on every boot)" ; The per-user status tray (punktfunk-tray.exe): shows running/stopped/failed at a glance and ; offers open-console / start / stop / restart without a terminal. HKLM Run = every user who signs @@ -239,7 +244,7 @@ Filename: "powershell.exe"; \ ; Register (or re-point, on upgrade - idempotent) the SYSTEM service from its FINAL {app} location: ; service install records current_exe() as the SCM binPath, so it must run from {app}, not {tmp}. ; --gamestream=on|off carries the wizard's GameStream task choice into host.env's PUNKTFUNK_HOST_CMD. -Filename: "{app}\punktfunk-host.exe"; Parameters: "service install {code:GamestreamParam}"; WorkingDir: "{app}"; \ +Filename: "{app}\punktfunk-host.exe"; Parameters: "service install {code:GamestreamParam}{code:PublicFwParam}"; WorkingDir: "{app}"; \ StatusMsg: "Registering the punktfunk host service..."; Flags: runhidden waituntilterminated Filename: "{app}\punktfunk-host.exe"; Parameters: "service start"; WorkingDir: "{app}"; \ StatusMsg: "Starting the punktfunk host service..."; Flags: runhidden waituntilterminated; Tasks: startservice @@ -247,7 +252,7 @@ Filename: "{app}\punktfunk-host.exe"; Parameters: "service start"; WorkingDir: " ; Provision the console AFTER the host service is up (so the mgmt token exists): write the ACL'd ; login password, register the PunktfunkWeb scheduled task (boot, SYSTEM, restart-on-failure), ; open TCP 47992, and start it. {code:WebSetupParams} appends -PasswordFile only on a fresh install. -Filename: "{app}\punktfunk-host.exe"; Parameters: "web setup {code:WebSetupParams}"; WorkingDir: "{app}"; \ +Filename: "{app}\punktfunk-host.exe"; Parameters: "web setup {code:WebSetupParams}{code:PublicFwParam}"; WorkingDir: "{app}"; \ StatusMsg: "Setting up the punktfunk web console..."; Flags: runhidden waituntilterminated #endif ; Launch the status tray as the SIGNED-IN user (not the elevated install user) right away, so the @@ -291,6 +296,17 @@ begin Result := '--gamestream=off'; end; +{ Firewall scope: the "allowpublicfw" task opens the streaming + console ports on Public networks too + (default = Private/Domain only). Forwarded to both `service install` and `web setup`. Returns a + LEADING SPACE so it concatenates after the preceding {code:...} param without a gap. } +function PublicFwParam(Param: String): String; +begin + if WizardIsTaskSelected('allowpublicfw') then + Result := ' --allow-public-network' + else + Result := ''; +end; + #ifdef WithWeb var WebPwPage: TInputQueryWizardPage;