Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/version-consistency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ name: version-consistency
# vcpkg does not read /VERSION, so it drifts silently
# without this check (it sat at 1.0.0 through 1.1.0).
#
# Bundled-driver pins: src/platform/windows/driver_pins.h is what the
# dashboard's driver banner compares installed drivers against, and
# installer.iss carries the same ViGEmBus / HIDMaestro versions for the
# payload it ships. They must agree.
#
# A PR-time text-consistency gate, not a full re-build.

on:
pull_request:
paths:
- "VERSION"
- "src/core/version.h"
- "src/platform/windows/driver_pins.h"
- "installer.iss"
- "vcpkg.json"
- ".github/workflows/version-consistency.yml"
Expand All @@ -29,6 +35,7 @@ on:
paths:
- "VERSION"
- "src/core/version.h"
- "src/platform/windows/driver_pins.h"
- "installer.iss"
- "vcpkg.json"

Expand Down Expand Up @@ -98,9 +105,21 @@ jobs:
echo "vcpkg.json version = $vcpkg_ver"
[ "$vcpkg_ver" = "$ver" ] || { echo "::error::vcpkg.json version=$vcpkg_ver, expected $ver"; fail=1; }

# ── bundled driver pins ───────────────────────────────────────
pins=src/platform/windows/driver_pins.h
pin_vigem=$(grep -E '^#define[[:space:]]+SATELLITE_VIGEMBUS_BUNDLED_VERSION' "$pins" | sed -E 's/.*"([^"]+)".*/\1/')
pin_hm=$(grep -E '^#define[[:space:]]+SATELLITE_HIDMAESTRO_SDK_VERSION' "$pins" | sed -E 's/.*"([^"]+)".*/\1/')
pin_hm_drv=$(grep -E '^#define[[:space:]]+SATELLITE_HIDMAESTRO_BUNDLED_DRIVER_VERSION' "$pins" | sed -E 's/.*"([^"]+)".*/\1/')
iss_vigem=$(grep -E '^#define[[:space:]]+ViGEmBusVersion' installer.iss | sed -E 's/.*"([^"]+)".*/\1/')
iss_hm=$(grep -E '^#define[[:space:]]+HmVersion' installer.iss | sed -E 's/.*"([^"]+)".*/\1/')
echo "driver_pins.h vigem=$pin_vigem hidmaestro-sdk=$pin_hm hidmaestro-driver=$pin_hm_drv; installer.iss vigem=$iss_vigem hidmaestro=$iss_hm"
[ -n "$pin_vigem" ] && [ "$pin_vigem" = "$iss_vigem" ] || { echo "::error::ViGEmBus pin drift: driver_pins.h=$pin_vigem installer.iss=$iss_vigem"; fail=1; }
[ -n "$pin_hm" ] && [ "$pin_hm" = "$iss_hm" ] || { echo "::error::HIDMaestro SDK pin drift: driver_pins.h=$pin_hm installer.iss=$iss_hm"; fail=1; }
[[ "$pin_hm_drv" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]] || { echo "::error::SATELLITE_HIDMAESTRO_BUNDLED_DRIVER_VERSION '$pin_hm_drv' is not a dotted version"; fail=1; }

if [ "$fail" -ne 0 ]; then
echo
echo "::error::Version sources drifted. Bump /VERSION, src/core/version.h and vcpkg.json together; installer.iss derives automatically."
echo "::error::Version sources drifted. Bump /VERSION, src/core/version.h and vcpkg.json together; installer.iss derives automatically. Driver pins: keep src/platform/windows/driver_pins.h and the installer.iss #defines in step."
exit 1
fi
echo "All version sources agree on $ver."
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,59 @@ working. The HIDMaestro backend, the composite audio personas and the
unified build story (one script contract shared by CI and local builds)
land here too.

No elevation prompt when a controller connects. Creating a HIDMaestro
virtual device needs an administrator token, and until now Satellite got
one by spawning `satellite-hm-helper.exe` with `runas` on the first
HIDMaestro plug of each session: one UAC prompt, at the exact moment
nobody is at the PC, because the pad is on the phone across the room. Setup
now registers the same helper as the LocalSystem service `SatelliteHmBroker`
("Satellite Controller Broker"), demand-start, and Satellite talks to it over
the well-known pipe `\\.\pipe\satellite-hm-broker` instead. Nothing about
the hot path changes; the broker only does what the spawned helper did
(create the SwDevice, duplicate the section and event handles into the
satellite process, tear down on disconnect). What changes is who holds the
token: the pipe's DACL admits interactive logons only, the broker admits a
connection only when the client is the installed `satellite.exe` beside it,
and Satellite accepts the pipe only when its server PID is the registered
service's PID, so a squatted pipe name is refused rather than trusted.
Setup grants interactive users SERVICE_START and installs a named-pipe
service trigger, so the service is started by whoever connects first and
exits after five idle minutes; a PC with no controller plugged runs no
broker. The `runas` path stays as the fallback when the service is absent
(`/HIDMAESTRO=skip` installs, hand-removed service, a second concurrent
session refused as `busy`), so the one remaining UAC prompt is the one an
administrator opted into. The helper gains `service` (SCM-hosted) and
`broker` (console, for debugging) modes; `hello` answers `"broker":true`
over the service so the log says which path a session took.

Driver status on the Windows dashboard, and an upgrade path that never reboots
behind your back. `GET /api/backend/status` and `GET /api/server/capabilities`
now fill each backend's `driverVersion` on Windows (the ViGEmBus.sys file
version; the HIDMaestro driver-store INF `DriverVer`) and gain three additive
fields beside it: `bundledVersion` (what this Satellite build's installer
ships for that driver, null where it ships nothing), `versionState`
(`current` / `outdated` / `newer` / `unknown`, derived server-side so no client
has to compare version strings) and `restartPending` (ViGEmBus only: the bus
device node reports `DN_NEED_RESTART`, which is what a driver upgrade that
returned 3010 leaves behind until Windows restarts). The dashboard renders a
driver banner off those fields on Windows hosts: a quiet green strip when both
drivers are installed and current, and an amber or red banner naming what is
missing, outdated, unresponsive or waiting on a restart, with the fix as the
action. If a Satellite update is already available or downloaded, the banner
routes to it, since the installer carries both drivers; otherwise it links the
installer for the running version. The upgrade path itself had one real hole:
the in-app updater ran the installer `/VERYSILENT` without `/NORESTART`, and a
bundled ViGEmBus upgrade over an older driver (1.21.x is common in the field)
can return 3010, which a very-silent Inno run answers by rebooting the PC
without asking. The updater now passes `/NORESTART`, the installer logs driver
failures instead of raising a modal when it runs under `/OTA` (there is nobody
at the keyboard to dismiss one), and the banner's restart-pending row is how
the user learns the reboot is owed. The pinned versions live in one header
(`src/platform/windows/driver_pins.h`) and `version-consistency.yml` fails the
build if `installer.iss` drifts from it; bumping the HIDMaestro SDK now also
means re-reading the INF version its `HIDMaestro.Core.dll` embeds (the recipe
is in `redist/README.md`).

Controller audio, split per direction and no longer paying for silence: one
`controllerAudio` switch turned both directions on together, so a host that
wanted the pad's microphone had to accept its speaker too, and the speaker
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ if(WIN32)
# CM_Get_DevNode_PropertyW, for walking an audio endpoint's PnP parent
# chain: setupapi forwards CM_Locate_DevNodeW but not the property API.
cfgmgr32
version
# WinRT: RoActivateInstance / RoGetActivationFactory + HSTRING for the
# actionable pairing toast (src/platform/windows/toast.cpp).
runtimeobject
Expand Down Expand Up @@ -885,6 +886,7 @@ satellite_add_pure_test(test_origin_guard tests/test_origin_guard.cpp)
satellite_add_pure_test(test_status_json tests/test_status_json.cpp)
satellite_add_pure_test(test_backend_registry tests/test_backend_registry.cpp
src/core/backend_registry.cpp)
satellite_add_pure_test(test_driver_inf tests/test_driver_inf.cpp)
satellite_add_pure_test(test_gamepad_mux tests/test_gamepad_mux.cpp)

# The audio reorder window (core/audio/audio_jitter.h) is header-only and
Expand Down
67 changes: 54 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,60 @@ Setup's standard `/SILENT` / `/VERYSILENT`:
| *(none)* / `/VIGEM=auto` | Default. Install the bundled ViGEmBus only if missing or older than 1.22.0. |
| `/VIGEM=bundled` | Force-run the bundled installer regardless of what's already there. |
| `/VIGEM=skip` | Don't touch the driver. Use this on locked-down machines or when ViGEmBus is managed externally. |
| *(none)* / `/HIDMAESTRO=auto` | Default. Deploy/refresh the bundled HIDMaestro driver (idempotent, no reboot). |
| `/HIDMAESTRO=skip` | Don't touch the HIDMaestro driver. |

A reboot is sometimes required on first ViGEmBus install (MSI exit code 3010).
The Satellite installer surfaces this as a "Restart now / later" prompt
on the final wizard page. Until you reboot, virtual-gamepad output may not
work even though the driver is installed. HIDMaestro never needs a reboot.

At runtime, the first HIDMaestro controller you plug in each Satellite
session shows one Windows elevation prompt: creating the virtual device
needs administrator rights, which Satellite (running unelevated) delegates
to `satellite-hm-helper.exe` for that session. ViGEm-only sessions never
see a prompt.
| *(none)* / `/HIDMAESTRO=auto` | Default. Deploy/refresh the bundled HIDMaestro driver (idempotent, no reboot) and register the Satellite Controller Broker service. |
| `/HIDMAESTRO=skip` | Don't touch the HIDMaestro driver, and don't register the broker service (an existing one is removed). |

A reboot is sometimes required on first ViGEmBus install or on an upgrade
over an older ViGEmBus (MSI exit code 3010). The Satellite installer surfaces
this as a "Restart now / later" prompt on the final wizard page. Until you
reboot, virtual-gamepad output may not work even though the driver is
installed. HIDMaestro never needs a reboot.

### Upgrading

Running a newer `SatelliteSetup.exe` over an existing install (or letting
the in-app updater do it) upgrades everything in place:

- The app and web UI are replaced; your config, pairings and the autostart /
desktop-icon choices are kept.
- **ViGEmBus** is upgraded only if the installed `ViGEmBus.sys` is older than
the bundled 1.22.0 (compared by file version). Same or newer is left alone.
An upgrade that replaces a loaded kernel driver can require a restart.
- **HIDMaestro** is re-deployed from the bundled helper every time the
component is selected. The SDK compares the installed driver's manifest hash
to the embedded one, so a same-version run is a ~50 ms no-op and a bumped
pin is a real redeploy. No reboot.
- Component selection follows Inno Setup's memory of your previous install: a
"Full" install picks up new components (HIDMaestro was added in 2.0.0); a
"Custom" install keeps exactly the components you picked before, so the new
driver is not added silently. The dashboard tells you either way.
- The in-app updater runs the installer `/VERYSILENT /NORESTART`, so it never
reboots the PC by itself. Driver failures are written to the setup log under
`%TEMP%` instead of raising a dialog nobody is there to dismiss.

The dashboard shows a **driver banner** on Windows: which of the two drivers is
installed, its version against the version this Satellite build bundles, and
whether ViGEmBus is waiting on a restart to finish an upgrade. It stays a small
green strip while everything is current and turns amber or red with the fix as
the action: install the pending Satellite update (which carries both drivers)
or re-run the installer for the running version.

Creating a HIDMaestro virtual device needs administrator rights, and
Satellite itself runs unelevated. Setup therefore registers the bundled
`satellite-hm-helper.exe` as the **Satellite Controller Broker** service
(`SatelliteHmBroker`, LocalSystem, demand-start). When a HIDMaestro
controller connects, Satellite reaches the broker over a named pipe, the
broker creates the device and hands the shared-memory handles back, and no
elevation prompt appears, whether or not anyone is at the PC. The service
starts on the first connection (Windows' named-pipe service trigger, or
Satellite starting it directly, which setup permits) and exits after five
idle minutes, so it costs nothing while no controller is plugged. The broker
admits only interactive logons and only the installed `satellite.exe`
beside it; Satellite in turn accepts the pipe only when its server is the
registered service's process. If the service is missing (installed with
`/HIDMAESTRO=skip`, or removed by hand), Satellite falls back to spawning the
helper elevated for the session, which is the one case that still shows a
UAC prompt. ViGEm-only sessions never involve either path.

### Controller audio

Expand Down
12 changes: 11 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,21 @@ In scope:
- The pairing flow (PIN paths A/B on `:9443`) + the loopback-only admin
HTTP/SSE web UI exposed by `satellite`.

- On Windows, the `SatelliteHmBroker` service boundary: the LocalSystem
broker that creates HIDMaestro virtual devices for the unelevated
`satellite.exe` over `\\.\pipe\satellite-hm-broker` (interactive-logon
DACL, client image-path check; see `docs/architecture.md`, "Elevation
story"). A way for a non-interactive or remote caller to reach it, or to
obtain anything beyond virtual controller / audio device creation through
it, is in scope.

Out of scope:

- Anything that requires the attacker to already have local privileges
on the user's PC (root, Administrator, ability to drop binaries in
`%APPDATA%`, etc.).
`%APPDATA%`, etc.). A process already running as the signed-in user can
reach the broker by design; that is the same trust it already holds over
the user's own session.
- The vendored ViGEmBus driver itself; file with [nefarius/ViGEmBus](https://github.com/nefarius/ViGEmBus).
- The bundled HIDMaestro driver/SDK itself; file with [hifihedgehog/HIDMaestro](https://github.com/hifihedgehog/HIDMaestro).
- DoS via raw network flooding. UDP without rate-limit is a known
Expand Down
32 changes: 25 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,13 +508,31 @@ ring (DS5 report 0x02, valid_flag1 bit 0 gating byte 8) as `setMicLedCallback`.
Satellite stays asInvoker. Device lifecycle needs an elevated token
(SwDevice creation, `Global\` section creation, driver deploy), so it
lives in the bundled .NET helper (`helper/hidmaestro` →
`satellite-hm-helper.exe`), spawned once per session on the FIRST
HIDMaestro plug (one UAC prompt; never from a status probe). The helper
drives the HIDMaestro SDK and duplicates the per-controller section +
event handles into the satellite process over a private named pipe
(`hidmaestro_helper_client.cpp`; the connecting client's PID is
verified against the spawned process). After that the per-frame hot
path is native only. If the helper dies, submits keep working (the
`satellite-hm-helper.exe`). The helper drives the HIDMaestro SDK and
duplicates the per-controller section + event handles into the
satellite process over a named pipe (`hidmaestro_helper_client.cpp`).
Two transports reach it, tried in order on the FIRST HIDMaestro plug
(never from a status probe):

1. The **SatelliteHmBroker** service (`satellite-hm-helper.exe
service`): LocalSystem, demand-start, registered by setup with the
hidmaestro component. It listens on `\\.\pipe\satellite-hm-broker`
with a DACL that admits interactive logons only, and admits a
connection only when the client PID is the installed `satellite.exe`
beside it in an interactive session. Satellite starts it if stopped
(setup grants SERVICE_START to interactive users, and the SCM's
named-pipe trigger covers the same case), then accepts the pipe only
if `GetNamedPipeServerProcessId` equals the service's PID from
`QueryServiceStatusEx`, so a squatted pipe name is refused. One
client session at a time (the SDK's orphan sweep is machine-global);
a second is answered `busy`. The service exits after five idle
minutes and the trigger brings it back. No UAC prompt.
2. A helper spawned `runas` with a private, per-session pipe name (one
UAC prompt; the connecting client's PID is verified against the
spawned process). This is the fallback when the service is absent or
busy.

After that the per-frame hot path is native only. If the helper dies, submits keep working (the
mapped sections outlive it); the next unplug reports unconfirmed and
quarantines the serial, matching the ViGEm zombie-target contract. The
installer's optional-but-default "hidmaestro" component deploys the
Expand Down
20 changes: 18 additions & 2 deletions docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ caller's own session.
"backends": [
{ "id": "vigem", "vendor": "Nefarius Software Solutions", "displayName": "ViGEmBus",
"kernelMode": true, "audio": false, "available": true, "errorCode": null,
"lifecycle": "eol", "eolDate": "2023-11-02", "driverVersion": null,
"lifecycle": "eol", "eolDate": "2023-11-02", "driverVersion": "1.22.0.0",
"bundledVersion": "1.22.0", "versionState": "current", "restartPending": false,
"controllers": [
{ "type": 0, "name": "xbox", "latency": "lowest", "latencyRank": 0,
"motion": false, "touchpad": false, "lightbar": false, "motionRequires": null,
Expand All @@ -309,7 +310,8 @@ caller's own session.
] },
{ "id": "hidmaestro", "vendor": "hifihedgehog", "displayName": "HIDMaestro",
"kernelMode": false, "audio": true, "available": true, "errorCode": null,
"lifecycle": "supported", "eolDate": null, "driverVersion": null,
"lifecycle": "supported", "eolDate": null, "driverVersion": "1.4.7.12",
"bundledVersion": "1.4.7.12", "versionState": "current", "restartPending": false,
"controllers": [
{ "type": 0, "name": "xbox", "latency": "medium", "latencyRank": 2,
"motion": false, "touchpad": false, "lightbar": false, "motionRequires": null,
Expand Down Expand Up @@ -382,6 +384,20 @@ copy a client renders for them is the client's own. A backend being `eol` says n
about whether it works — `available` is the runtime truth, and an `eol` backend may
well be the preferred one.

`bundledVersion`, `versionState` and `restartPending` (additive; absent on older
servers) are the install-state companions to `driverVersion`. `bundledVersion` is the
driver version the running server's own installer ships for that backend (null where it
ships none: uinput, machid, and any build without a pin). `versionState` ∈ `current` |
`outdated` | `newer` | `unknown` is `driverVersion` compared numerically against
`bundledVersion`, derived server-side so a client never parses version strings; it is
`unknown` whenever either side is null, including a missing driver (whose `errorCode`
already says so). `restartPending` is true when the driver was replaced on disk but the
loaded one is still the old one and Windows needs a restart to finish (ViGEmBus reports
this on its bus device node); it is false everywhere else. On Windows `driverVersion` is
the `ViGEmBus.sys` file version for `vigem` and the driver-store INF `DriverVer` for
`hidmaestro` (the HIDMaestro SDK release and its UMDF driver carry different version
schemes; the INF is what Device Manager shows and what a bumped pin changes).

`host` is the receiver's OWN capability inventory, readable before pairing or any
catalog round-trip so a client reflects the real receiver instead of an optimistic
default. Each entry's `supported` is the static fact (mirrors the catalog
Expand Down
Loading
Loading