mirror of
https://github.com/joleuger/vuinputd.git
synced 2026-07-21 02:08:28 +00:00
137 lines
7.2 KiB
Markdown
137 lines
7.2 KiB
Markdown
# Design Document
|
||
|
||
## 1. Introduction
|
||
|
||
This project provides a safe, general-purpose way to run [Sunshine](https://github.com/LizardByte/Sunshine) and other applications that use `/dev/uinput` **inside containers** — including `systemd-nspawn`, Docker, LXC, Podman, and similar runtimes.
|
||
|
||
Applications like Sunshine require creating virtual input devices (`/dev/uinput`) for keyboards, mice, and controllers.
|
||
Naively bind-mounting `/dev/uinput` from the host into a container breaks isolation: a container could create devices visible to other containers or even the host, leading to unwanted input injection and security risks.
|
||
|
||
`vuinputd` introduces a **mediated `/dev/uinput` proxy** that preserves isolation without kernel changes.
|
||
|
||
---
|
||
|
||
## 2. Architecture
|
||
|
||
Normally, applications open `/dev/uinput` directly to create virtual event devices such as `/dev/input/event9`:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
uinput apps->>uinput (kernel): open /dev/uinput and setup
|
||
create participant eventx
|
||
uinput (kernel)->>eventx: create /dev/input/eventx
|
||
uinput (kernel)->>libinput/game: announce new device via udev
|
||
libinput/game->>eventx: open /dev/input/eventx
|
||
```
|
||
|
||
vuinputd provides a virtual /dev/vuinput implemented via CUSE (Character Device in Userspace).
|
||
This device can be bind-mounted into a container as /dev/uinput, so applications operate normally:
|
||
```mermaid
|
||
|
||
sequenceDiagram
|
||
box transparent Host
|
||
participant uinput (kernel)
|
||
participant vuinputd
|
||
participant vuinput (host)
|
||
end
|
||
|
||
box transparent Container
|
||
participant uinput (container)
|
||
participant uinput apps
|
||
participant eventX
|
||
participant libinput/game
|
||
end
|
||
|
||
vuinputd->>vuinput (host): create /dev/vuinput with cuse
|
||
uinput apps->>uinput (container): open /dev/uinput and setup
|
||
uinput (container)-->vuinput (host): is equal (bind mount)
|
||
vuinput (host)->>vuinputd: forward data
|
||
vuinputd->>uinput (kernel): forward data
|
||
uinput (kernel)->>eventX: create /dev/input/eventX
|
||
uinput (kernel)->>vuinputd: announce new device via udev
|
||
vuinputd->>libinput/game: announce new device via udev
|
||
libinput/game->>eventX: open /dev/input/eventX
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Design Decisions
|
||
|
||
### 3.1 Where `/dev/uinput` lives
|
||
|
||
* **Decision**: Provide a fake `/dev/vuinput` backed by host proxy. This character device can be bind mounted inside containers to `/dev/uinput`.
|
||
* **Why**: Prevents containers from creating devices visible system-wide.
|
||
|
||
### 3.2 Prevent host from using devices
|
||
|
||
* **Decision**: udev rules strip `ID_INPUT_KEYBOARD` and `ID_INPUT_MOUSE`, set `ID_SEAT=seat_vuinput`.
|
||
* **Why**: Ensures devices are invisible to host input subsystems while still available in containers.
|
||
|
||
### 3.3 udev events in containers
|
||
|
||
* **Decision**: Proxy forwards udev events into the container via netlink.
|
||
* **Why**: Without this, SDL2 and libinput might not recognize devices correctly; with it, containers behave as if devices were created locally.
|
||
|
||
### 3.4 Where to run the proxy
|
||
|
||
* **Decision**: Run proxy on host, one instance per container.
|
||
* **Why**: Only host can safely access `/dev/uinput` and enforce mediation.
|
||
|
||
### 3.5 Security trade-off
|
||
|
||
* **Decision**: Accept that host always sees devices, but enforce rules to stop it consuming them.
|
||
* **Why**: Full input namespaces don’t exist in Linux today; mediation is the practical compromise.
|
||
|
||
### 3.6 Compatibility
|
||
* **Runtimes supported:** Works with systemd-nspawn, Docker, LXC, Podman, and other container engines.
|
||
* **Applications supported:** Any program that writes to `/dev/uinput`, including Sunshine, custom input injectors, and game streaming servers.
|
||
---
|
||
|
||
## 4. Security Considerations
|
||
|
||
`vuinputd` must currently run with **root privileges** to:
|
||
|
||
* Access `/dev/uinput` and create CUSE devices.
|
||
* Send and receive **udev/netlink** messages.
|
||
* Manage per-container device nodes under `/dev/input`.
|
||
|
||
While this design is necessary for mediation, it introduces potential attack surfaces:
|
||
|
||
### ⚠️ Risks
|
||
|
||
* **Privilege escalation:** a compromised container could exploit bugs in the proxy.
|
||
* **Input injection:** if isolation fails, input devices may leak between containers.
|
||
* **Unsafe FUSE/`unsafe` code:** any memory or pointer error could lead to denial-of-service or privilege abuse.
|
||
|
||
### 🛡️ Mitigations (planned / recommended)
|
||
|
||
* [ ] Drop capabilities after startup (e.g. keep only `CAP_SYS_ADMIN` where needed).
|
||
* [ ] Run under a dedicated **system user** (`vuinputd`) with limited filesystem access.
|
||
* [ ] Enforce **container identity** using cgroup, namespace, or pidfd checks.
|
||
* [ ] Use **seccomp** or `systemd` sandboxing (`ProtectSystem`, `ProtectKernelTunables`, `RestrictNamespaces`, etc.).
|
||
* [ ] Eventually migrate to **Rust-native FUSE/Netlink** bindings to remove unsafe dependencies.
|
||
|
||
## 5. Alternative Approaches
|
||
|
||
### 5.1 trace accesses of /dev/uinput with eBPF
|
||
|
||
**Idea (short):** attach an eBPF program to the syscall tracepoint for `ioctl` (`tracepoint/syscalls/sys_enter_ioctl`), filter by container cgroup, and send small events (pid, tgid, fd, cmd, timestamp, short payload sample) to userspace using the BPF ring buffer. A privileged host agent consumes the ringbuf events, duplicates the target FD via `pidfd_getfd()` and proceeds with UI_GET_SYSNAME / sysfs resolution to retrieve the sys-path and the dev-path. Having the dev-path and the pid of the container, the solution could proceed as in the current solution.
|
||
|
||
#### 1) Trace hook: `tracepoint/syscalls/sys_enter_ioctl`
|
||
|
||
Use the *syscall tracepoint* `syscalls:sys_enter_ioctl`. Tracepoints are stable, exported kernel probe points and the syscall tracepoint provides the syscall arguments (fd, cmd, arg) in a stable layout. This avoids fragile kprobe offsets on architecture-specific syscall wrappers. See the kernel tracepoint docs.
|
||
|
||
#### 2) BPF map: ring buffer (kernel → userspace)
|
||
|
||
Use the BPF ring buffer (`BPF_MAP_TYPE_RINGBUF`) to cheaply publish fixed-size events to userspace. The ring buffer provides `bpf_ringbuf_reserve()` / `bpf_ringbuf_submit()` semantics from the kernel side and is the recommended modern replacement for perf-buf for high-rate kernel→user events. See the kernel documentation for the ring buffer API.
|
||
|
||
#### 3) Useful eBPF helpers
|
||
|
||
Inside the trace program you will typically use:
|
||
|
||
* `bpf_get_current_pid_tgid()` to record tgid/pid,
|
||
* `bpf_get_current_cgroup_id()` to filter to the container cgroup you care about,
|
||
* `bpf_copy_from_user()` to safely copy up to `N` bytes from the user pointer (`arg`) into the event buffer.
|
||
|
||
#### 4) Use of `pidfd_getfd`
|
||
The **`pidfd_getfd()`** syscall (introduced in Linux 5.6, see `man pidfd_getfd(2)`) allows one process to **duplicate a file descriptor from another process** into its own FD table. It takes a *pidfd* (obtained via `pidfd_open()` or from `CLONE_PIDFD`), the target FD number in the remote process, and optional flags. The resulting descriptor refers to the **same open file description**—sharing offset, status flags, and driver state—exactly as if the target process had called `dup()`. Permission checks apply: the caller must either share credentials (same UID) or hold `CAP_SYS_PTRACE` or an equivalent capability over the target. This makes `pidfd_getfd()` the canonical and race-free way to inspect or reuse another process’s device handles (for example, to run `UI_GET_SYSNAME` on a client apps' fd on `/dev/uinput` ) without invasive ptrace tricks.
|