diff --git a/README.md b/README.md new file mode 100644 index 0000000..46acd68 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# vuinputd +A minimal **CUSE-based proxy for `/dev/uinput`** that lets unmodified applications (like [Sunshine](https://github.com/LizardByte/Sunshine)) run inside a container while creating virtual input devices on the host. + +## Overview + +This project makes it possible to run [Sunshine](https://github.com/LizardByte/Sunshine) inside `systemd-nspawn` containers without breaking input isolation. + +Normally, Sunshine creates virtual input devices via `/dev/uinput`. If `/dev/uinput` is simply bind-mounted into a container: + +* Devices from one container can leak into another. +* Keyboards and mice may attach to host seats that are attached to a running session. + +This project solves that by introducing a **mediated input stack**: + +* A **fake `/dev/uinput`** inside the container. +* A daemon that **forward** add/remove events into the container, making SDL2 and Wayland/libinput behave correctly. +* A **host proxy** that safely creates the real devices. +* **udev rules** that tag and isolate devices per-container. + +--- + +## Architecture + +* **Container**: Sunshine writes to fake `/dev/uinput`. +* **Host Proxy**: Creates real devices on the host, labeled with container identity Forwards add/remove events into the container, so SDL2 and Wayland see devices natively. +* **udev**: Matches by identity, prevents host use. + +--- + +## Benefits + +* 🎮 **SDL2 / Wayland compatibility**: fake-udev ensures compositors and games see device events properly. +* 🔒 **Isolation**: containers only see their own devices; host also sees them, but ignores them completely. +* ♻️ **Lifecycle safety**: devices are removed cleanly when Sunshine stops. +* 🛠️ **Simple integration**: no kernel patches, just userspace tools + udev rules. + +--- + +## Documentation + +See [docs/DESIGN.md](docs/DESIGN.md) for detailed architecture, design tradeoffs, and security considerations. + +--- + +## License + +MIT diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..a85a883 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,37 @@ +# Design Document + +## 1. Introduction + +This project provides a safe way to run [Sunshine](https://github.com/LizardByte/Sunshine) inside `systemd-nspawn` containers. +Sunshine requires creating virtual input devices (`/dev/uinput`) for keyboards, mice, and controllers. + +Naively exposing `/dev/uinput` from the host into a container breaks isolation: containers could create devices visible to other containers, and the host could even start consuming those devices. + +--- + +## 2. Design Decisions + +### 2.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. + +### 2.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. + +### 2.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. + +### 2.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. + +### 2.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. diff --git a/vuinputd/src/container/inject_in_container_job.rs b/vuinputd/src/container/inject_in_container_job.rs index 5b5fefe..061351e 100644 --- a/vuinputd/src/container/inject_in_container_job.rs +++ b/vuinputd/src/container/inject_in_container_job.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use std::{collections::HashMap, future::Future, pin::Pin, time::Duration}; use async_io::Timer; diff --git a/vuinputd/src/container/mknod_input_device.rs b/vuinputd/src/container/mknod_input_device.rs index 461e397..0f0d6f8 100644 --- a/vuinputd/src/container/mknod_input_device.rs +++ b/vuinputd/src/container/mknod_input_device.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use nix::sys::stat::{makedev, mknod, stat, Mode, SFlag}; use nix::unistd::{chown, Gid, Uid}; use std::error::Error; diff --git a/vuinputd/src/container/mod.rs b/vuinputd/src/container/mod.rs index edfdd45..610b907 100644 --- a/vuinputd/src/container/mod.rs +++ b/vuinputd/src/container/mod.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + pub mod inject_in_container_job; pub mod remove_from_container_job; pub mod mknod_input_device; diff --git a/vuinputd/src/container/netlink_message.rs b/vuinputd/src/container/netlink_message.rs index 412f957..eb2a16b 100644 --- a/vuinputd/src/container/netlink_message.rs +++ b/vuinputd/src/container/netlink_message.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use std::collections::HashMap; use std::mem; use std::os::fd::{AsRawFd, OwnedFd}; diff --git a/vuinputd/src/container/remove_from_container_job.rs b/vuinputd/src/container/remove_from_container_job.rs index 153c1fe..b508fbf 100644 --- a/vuinputd/src/container/remove_from_container_job.rs +++ b/vuinputd/src/container/remove_from_container_job.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use std::{collections::HashMap, future::Future, pin::Pin, time::Duration}; use async_io::Timer; diff --git a/vuinputd/src/container/runtime_data.rs b/vuinputd/src/container/runtime_data.rs index db26d7e..3528f7f 100644 --- a/vuinputd/src/container/runtime_data.rs +++ b/vuinputd/src/container/runtime_data.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use std::fs::{self, File}; use std::io::{self, Write}; use std::path::Path; diff --git a/vuinputd/src/jobs/closure_job.rs b/vuinputd/src/jobs/closure_job.rs index b7f9136..fc868a2 100644 --- a/vuinputd/src/jobs/closure_job.rs +++ b/vuinputd/src/jobs/closure_job.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use std::future::Future; use std::pin::Pin; diff --git a/vuinputd/src/jobs/job.rs b/vuinputd/src/jobs/job.rs index e7627c1..ba0ca8b 100644 --- a/vuinputd/src/jobs/job.rs +++ b/vuinputd/src/jobs/job.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + use async_channel::{Receiver, Sender}; use futures::executor::{LocalPool, LocalSpawner}; use futures::future::RemoteHandle;