[PR #474] feat(input): fake-uinput shim for Steam Input in containers #489

Open
opened 2026-10-15 13:00:58 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/games-on-whales/wolf/pull/474
Author: @zerofltexx
Created: 8/9/2026
Status: 🔄 Open

Base: beta ← Head: feat/fake-uinput-steam-input


📝 Commits (2)

  • d82c982 feat(input): fake-uinput shim for Steam Input in containers
  • a202377 feat(api): serve the device endpoints on a restricted socket

📊 Changes

20 files changed (+767 additions, -3 deletions)

View changed files

📝 85-wolf.rules (+10 -0)
📝 CMakeLists.txt (+1 -0)
📝 docker/startup.sh (+6 -0)
📝 docker/wolf.Dockerfile (+10 -1)
📝 docs/modules/ROOT/nav.adoc (+1 -0)
➕ docs/modules/dev/pages/fake-uinput.adoc (+87 -0)
➕ src/fake-uinput/CMakeLists.txt (+18 -0)
➕ src/fake-uinput/fake-uinput.cpp (+207 -0)
📝 src/moonlight-server/api/api.cpp (+27 -0)
📝 src/moonlight-server/api/api.hpp (+34 -1)
📝 src/moonlight-server/api/endpoints.cpp (+80 -0)
➕ src/moonlight-server/api/fake_uinput.hpp (+122 -0)
📝 src/moonlight-server/api/unix_socket_server.cpp (+34 -1)
📝 src/moonlight-server/events/events.hpp (+6 -0)
📝 src/moonlight-server/runners/docker.cpp (+20 -0)
📝 src/moonlight-server/runners/docker.hpp (+9 -0)
📝 src/moonlight-server/sessions/common.cpp (+32 -0)
📝 src/moonlight-server/state/default/config.v7.toml (+4 -0)
📝 tests/CMakeLists.txt (+1 -0)
➕ tests/testFakeUinput.cpp (+58 -0)

📄 Description

Implements #81, picking up from #88.

How it works

An app opts in with WOLF_FAKE_UINPUT=1 in its runner env. The default config ships that line commented out, since an opted in container gets the real /dev/uinput.

For an opted in app Wolf preloads a shim (src/fake-uinput) that intercepts ioctl(UI_DEV_CREATE / UI_DEV_DESTROY) and posts the device to POST /api/v1/runtime/{plug,unplug}-udev-device. Wolf reads it back from sysfs and does the mknod plus fake-udev over its existing PlugDeviceEvent path, so the container never needs CAP_MKNOD. Only devices this session's shim created reach this session's container.

An env var rather than a config field so it survives the round trip through wolf-ui lobbies, like GOW_REQUIRED_DEVICES and RUN_SWAY. Docs in docs/modules/dev/pages/fake-uinput.adoc.

Isolation

Devices the shim creates land on the host kernel, where logind would ACL them to the desktop user. The name rules in 85-wolf.rules can't catch them because the app picks the name, and Steam Input calls its pad Microsoft X-Box 360 pad. Renaming breaks SDL, which maps controllers by name. So the shim stamps phys with wolf-fake-uinput/0 before the UI_DEV_CREATE it already intercepts, and the rules match on that to park the device on seat9 and strip the ACL.

This narrows the exposure without closing it. The container holds the real /dev/uinput and could create a device by another route, carrying no marker.

Testing

wolftests passes on both compilers with BUILD_SHARED_LIBS on and off, including aarch64. New coverage in tests/testFakeUinput.cpp. scripts/wolf-input-diag.sh reports a shim created pad as reachable by the desktop user without the rule and root only with it. Ran a session with Steam Input on, gyro and per game bindings work, and the streamed pad doesn't reach the host desktop.

The shim is x86 only, since Steam needs a 32 bit build of it and that uses -m32.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/games-on-whales/wolf/pull/474 **Author:** [@zerofltexx](https://github.com/zerofltexx) **Created:** 8/9/2026 **Status:** 🔄 Open **Base:** `beta` ← **Head:** `feat/fake-uinput-steam-input` --- ### 📝 Commits (2) - [`d82c982`](https://github.com/games-on-whales/wolf/commit/d82c98223cc450b6ad0aae8857957c0f13c23a54) feat(input): fake-uinput shim for Steam Input in containers - [`a202377`](https://github.com/games-on-whales/wolf/commit/a202377e0a639c26bd9423f4ee9281b0397d04ec) feat(api): serve the device endpoints on a restricted socket ### 📊 Changes **20 files changed** (+767 additions, -3 deletions) <details> <summary>View changed files</summary> 📝 `85-wolf.rules` (+10 -0) 📝 `CMakeLists.txt` (+1 -0) 📝 `docker/startup.sh` (+6 -0) 📝 `docker/wolf.Dockerfile` (+10 -1) 📝 `docs/modules/ROOT/nav.adoc` (+1 -0) ➕ `docs/modules/dev/pages/fake-uinput.adoc` (+87 -0) ➕ `src/fake-uinput/CMakeLists.txt` (+18 -0) ➕ `src/fake-uinput/fake-uinput.cpp` (+207 -0) 📝 `src/moonlight-server/api/api.cpp` (+27 -0) 📝 `src/moonlight-server/api/api.hpp` (+34 -1) 📝 `src/moonlight-server/api/endpoints.cpp` (+80 -0) ➕ `src/moonlight-server/api/fake_uinput.hpp` (+122 -0) 📝 `src/moonlight-server/api/unix_socket_server.cpp` (+34 -1) 📝 `src/moonlight-server/events/events.hpp` (+6 -0) 📝 `src/moonlight-server/runners/docker.cpp` (+20 -0) 📝 `src/moonlight-server/runners/docker.hpp` (+9 -0) 📝 `src/moonlight-server/sessions/common.cpp` (+32 -0) 📝 `src/moonlight-server/state/default/config.v7.toml` (+4 -0) 📝 `tests/CMakeLists.txt` (+1 -0) ➕ `tests/testFakeUinput.cpp` (+58 -0) </details> ### 📄 Description Implements #81, picking up from #88. ## How it works An app opts in with `WOLF_FAKE_UINPUT=1` in its runner env. The default config ships that line commented out, since an opted in container gets the real `/dev/uinput`. For an opted in app Wolf preloads a shim (`src/fake-uinput`) that intercepts `ioctl(UI_DEV_CREATE / UI_DEV_DESTROY)` and posts the device to `POST /api/v1/runtime/{plug,unplug}-udev-device`. Wolf reads it back from sysfs and does the `mknod` plus fake-udev over its existing `PlugDeviceEvent` path, so the container never needs `CAP_MKNOD`. Only devices this session's shim created reach this session's container. An env var rather than a config field so it survives the round trip through wolf-ui lobbies, like `GOW_REQUIRED_DEVICES` and `RUN_SWAY`. Docs in `docs/modules/dev/pages/fake-uinput.adoc`. ## Isolation Devices the shim creates land on the host kernel, where logind would ACL them to the desktop user. The name rules in `85-wolf.rules` can't catch them because the app picks the name, and Steam Input calls its pad `Microsoft X-Box 360 pad`. Renaming breaks SDL, which maps controllers by name. So the shim stamps `phys` with `wolf-fake-uinput/0` before the `UI_DEV_CREATE` it already intercepts, and the rules match on that to park the device on `seat9` and strip the ACL. This narrows the exposure without closing it. The container holds the real `/dev/uinput` and could create a device by another route, carrying no marker. ## Testing `wolftests` passes on both compilers with `BUILD_SHARED_LIBS` on and off, including aarch64. New coverage in `tests/testFakeUinput.cpp`. `scripts/wolf-input-diag.sh` reports a shim created pad as reachable by the desktop user without the rule and root only with it. Ran a session with Steam Input on, gyro and per game bindings work, and the streamed pad doesn't reach the host desktop. The shim is x86 only, since Steam needs a 32 bit build of it and that uses `-m32`. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#489
No description provided.