From ef060ce9d6658e25a7b3343554b85ff5d7a84819 Mon Sep 17 00:00:00 2001 From: Johannes Leupolz Date: Sat, 9 May 2026 08:04:06 +0000 Subject: [PATCH] Improved usage guide for nixos --- docs/USAGE-NIXOS.md | 280 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 276 insertions(+), 4 deletions(-) diff --git a/docs/USAGE-NIXOS.md b/docs/USAGE-NIXOS.md index c8408c8..d391997 100644 --- a/docs/USAGE-NIXOS.md +++ b/docs/USAGE-NIXOS.md @@ -1,13 +1,285 @@ # Usage Guide for NixOS -This guide will explain how to use `vuinputd` on NixOS. For general remarks, please refer to [USAGE.md](USAGE.md). +This guide explains how to set up and use `vuinputd` on NixOS. +For general remarks about `vuinputd` and how it works, please refer to [USAGE.md](USAGE.md). + +> **Status:** NixOS is one of the primary target platforms for `vuinputd`. Native packaging +> is planned; for now, the configuration below builds `vuinputd` directly from source as part +> of the NixOS system. -Currently, this is WIP. The plan is to make NixOS one of the main platforms for vuinputd. --- -## Configurations in the community +## Configurations in the Community + +The following community members have shared their NixOS configurations including `vuinputd`: * [ShaneTRS](https://github.com/ShaneTRS/nixos-config/) * [griffi-gh](https://github.com/girl-pp-ua/nixos-infra/) +* [Markus328](https://github.com/joleuger/vuinputd/issues/14) -Feel free to create a github issue or a PR to add your configuration or your remarks. \ No newline at end of file +Feel free to open a GitHub issue or pull request to add your own configuration or remarks. + +--- + +## NixOS Configuration + +The example below is a self-contained NixOS module that: + +- Builds `vuinputd` from source using `rustPlatform.buildRustPackage` +- Installs the required udev rules and hwdb entries +- Runs `vuinputd` as a systemd service with a tmpfs for the container-scoped `/dev/input` tree +- Applies the correct permissions to `/dev/vuinput` after the daemon starts + +Add the module to your `configuration.nix` imports and run `nixos-rebuild switch`. + +```nix +{ config, pkgs, lib, ... }: +let + vuinputd = pkgs.rustPlatform.buildRustPackage { + pname = "vuinputd"; + version = "0.3.2-git"; + + buildType = "debug"; + + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.rustPlatform.bindgenHook + ]; + + buildInputs = [ pkgs.udev pkgs.fuse3 ]; + + src = pkgs.fetchFromGitHub { + owner = "joleuger"; + repo = "vuinputd"; + rev = "8c40fdc12005319ea16dceb752a8822abfc6039a"; + hash = "sha256-8Q34B04BngZqRLyixeFq8F1t5wFnk6JpaG3EEbgKRcU="; + }; + + cargoHash = "sha256-nJw9bRh6Yn9g1H5SeoT6zxgZLCqV3AtAs9gMfE+P+CU="; + + # Recent versions of fuse3 expose additional libfuse_* types that bindgen + # needs to allowlist alongside the standard fuse_* types. + postPatch = '' + substituteInPlace cuse-lowlevel/build.rs \ + --replace-fail '.allowlist_type("(?i)^fuse.*")' '.allowlist_type("(?i)^(fuse|libfuse).*")' + ''; + + postInstall = '' + mkdir -p $out/lib/udev/rules.d + mkdir $out/lib/udev/hwdb.d + cp vuinputd/udev/*.rules $out/lib/udev/rules.d/ + cp vuinputd/udev/*.hwdb $out/lib/udev/hwdb.d/ + ''; + }; +in +{ + environment.systemPackages = with pkgs; [ + vuinputd + bubblewrap # required for running containerized applications via bwrap + ]; + + # Main vuinputd daemon. + # Before starting, a tmpfs is mounted at /run/vuinputd/vuinput/dev-input. + # This directory serves as the container-scoped /dev/input tree: input devices + # created by vuinputd are placed here instead of the host's /dev/input, so + # that containers see only their own devices. + systemd.services.vuinputd = { + enable = true; + wantedBy = [ "multi-user.target" ]; + unitConfig = { + Description = "Virtual input (/dev/vuinput) daemon"; + }; + serviceConfig = { + Type = "exec"; + ExecStartPre = pkgs.writeShellScript "mount-tmpfs-dev-input" '' + mkdir -p /run/vuinputd/vuinput/dev-input + ${pkgs.util-linux}/bin/mount -t tmpfs -o rw,dev,nosuid tmpfs /run/vuinputd/vuinput/dev-input + ''; + ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host"; + ExecStopPost = pkgs.writeShellScript "umount-dev-input" '' + ${pkgs.util-linux}/bin/umount /run/vuinputd/vuinput/dev-input + ''; + Restart = "on-failure"; + # Required to allow vuinputd to access character devices (uinput, CUSE). + DeviceAllow = "char-* rwm"; + Environment = [ + "RUST_LOG=debug" + ]; + }; + }; + + # vuinputd creates /dev/vuinput via CUSE. The device initially has restrictive + # permissions, so a one-shot service applies chmod 666 shortly after startup. + # A proper udev-based solution is planned to replace this workaround. + systemd.services.vuinputd-chmod = { + unitConfig.Description = "Chmod 666 /dev/vuinput"; + wantedBy = [ "vuinputd.service" ]; + after = [ "vuinputd.service" ]; + serviceConfig = { + ExecStart = pkgs.writeShellScript "chmod-vuinput" '' + sleep 2 && chmod 666 /dev/vuinput + ''; + }; + }; +} +``` + +### Key Configuration Notes + +**`--major` and `--minor`** +These are the device numbers assigned to the virtual `/dev/uinput` character device exposed +inside the container. The values `120` and `414795` are chosen to avoid conflicts with +real devices on the host. Refer to [USAGE.md](USAGE.md) for details on choosing these values. + +**`--placement on-host`** +Tells `vuinputd` to place the resulting `/dev/input/event*` devices on the host side (under +the tmpfs at `/run/vuinputd/vuinput/dev-input`) rather than directly in the host's `/dev/input`. +This is what enables per-container input isolation. + +**`DeviceAllow = "char-* rwm"`** +`vuinputd` needs access to `/dev/uinput` (to create real input devices on the host) and to +the CUSE subsystem (to expose the virtual `/dev/uinput` inside containers). Both are character +devices, so this broad allowlist is currently required. Reducing the attack surface here is a +[planned hardening step](https://github.com/joleuger/vuinputd/blob/main/docs/DESIGN.md). + +**`--device-policy`** +The `ExecStart` line can be extended with a `--device-policy` flag to control which input +capabilities and events the daemon exposes to containerized applications: + +| Policy | Effect | +|---|---| +| `none` | All capabilities allowed; no filtering. Useful for debugging. | +| `mute-sys-rq` | Blocks SysRq key handling. All other input passes through. **(default)** | +| `sanitized` | Keyboards and mice only; filters SysRq and VT-switching combos. Recommended for desktop/streaming workloads. | +| `strict-gamepad` | Gamepad-like devices only; blocks keyboards and mice entirely. | + +For example, to use the recommended policy for a Sunshine streaming container: + +```nix +ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host --device-policy sanitized"; +``` + +See [USAGE.md](USAGE.md) for a full description of each policy. + +**The `vuinputd-chmod` service** +The CUSE device `/dev/vuinput` is created by the kernel with root-only permissions. Until +a proper udev rule handles this, a small one-shot service applies `chmod 666` two seconds +after the daemon starts. This is a known rough edge and will be improved. + +--- + +## Running a Containerized Application + +Once `vuinputd` is running, start a containerized application by binding the virtual devices +into its namespace. The example below uses `bwrap` (Bubblewrap) as a lightweight container: + +```bash +bwrap \ + --unshare-net \ + --ro-bind / / \ + --tmpfs /tmp \ + --tmpfs /run/udev \ + --dev-bind /run/vuinputd/vuinput/dev-input /dev/input \ + --dev-bind /dev/vuinput /dev/uinput \ + +``` + +The two `--dev-bind` flags are the core of the integration: + +| Bind | Purpose | +|---|---| +| `/run/vuinputd/vuinput/dev-input` → `/dev/input` | Gives the container its own isolated `/dev/input` tree populated by `vuinputd`. | +| `/dev/vuinput` → `/dev/uinput` | Exposes the CUSE-backed virtual `/dev/uinput` at the standard path the application expects. | + +The `--tmpfs /run/udev` flag provides a writable but empty udev runtime directory inside the +sandbox. This is sufficient when using `--placement on-host`, because `vuinputd` forwards udev +events into the container directly. If you switch to `--placement in-container`, replace this +flag with a bind-mount of the actual udev runtime directory instead, and create the required +stubs inside the container: + +```bash +mkdir -p /run/udev/data/ +touch /run/udev/control +``` + +For instructions on testing this setup in an isolated VM, see +[Testing vuinputd on NixOS with Incus](https://github.com/joleuger/vuinputd/blob/main/distro-tests/nixos/README.md). + +--- + +## Verifying Operation + +To confirm that `vuinputd` and the container integration are working correctly, run the +following checks inside the container (install `libinput-tools` and `evtest` if needed): + +```bash +# Watch for device creation and input events +libinput debug-events + +# Observe udev announcements in a second terminal +udevadm monitor -p + +# Read raw events from the input device +evtest /dev/input/event* +``` + +Then trigger some input from within the container (e.g. run a test binary or move a virtual +mouse). You should see device creation reported by `libinput` and `udevadm`, and raw event +data in `evtest`. On the host, `journalctl -u vuinputd` should show corresponding log lines +about device creation and event forwarding. + +For a more detailed walkthrough with example output, see the [Verifying Operation](USAGE.md#7-verifying-operation) +section in the main usage guide. + +--- + +## Phantom Input Events and VT Handling + +On headless NixOS systems (no active graphical session), the Linux kernel's virtual terminal +(VT) layer remains active and continues to process keyboard input. This can cause injected +input forwarded by `vuinputd` to reach `getty` login prompts or trigger kernel hotkeys such +as `Ctrl+Alt+Fn`. + +The quickest mitigation is to start `vuinputd` with the `--vt-guard` flag: + +```nix +ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host --vt-guard"; +``` + +`--vt-guard` switches the active VT into graphics mode via a direct ioctl, which disables +the kernel keyboard handler for that VT without requiring a compositor or DRM device. + +For a full discussion of all available approaches (including KMSCON and the experimental +`fallbackdm`), see the [Phantom Input Events](USAGE.md#8-handling-phantom-input-events-caused-by-vts) +section in the main usage guide. + +--- + +## Troubleshooting + +If `vuinputd` does not behave as expected, refer to +[DEBUG.md](https://github.com/joleuger/vuinputd/blob/main/docs/DEBUG.md) for general +debugging guidance. + +Common NixOS-specific issues: + +- **CUSE module not loaded:** NixOS should load `cuse` automatically via udev, but if + `/dev/vuinput` does not appear after the service starts, run `modprobe cuse` and restart + the service. +- **`/dev/vuinput` is not accessible:** The `vuinputd-chmod` service applies permissions + 2 seconds after startup. If it fails, check `systemctl status vuinputd-chmod` and apply + `chmod 666 /dev/vuinput` manually for debugging. +- **Input devices not visible inside the container:** Verify that + `/run/vuinputd/vuinput/dev-input` is mounted as a tmpfs (`mount | grep vuinputd`) and that + the `bwrap` `--dev-bind` flags point to the correct paths. +- **Read-only filesystem error from `vuinputd`:** If the daemon logs an error like + `ReadOnlyFilesystem` when creating a device node, the directory where it tries to write + (typically `/dev/input` or `/run`) is not writable inside the container. Ensure the + `--dev-bind` and `--tmpfs` flags in your `bwrap` command cover all paths `vuinputd` writes + to, or switch to `--placement on-host` so writes happen on the host side instead. + + ``` + Error creating input device /dev/input/event12: Read-only file system + ``` +- **Build failures due to bindgen/fuse3 mismatch:** Ensure the `postPatch` block in the + derivation is present; it is required for recent versions of `fuse3`. \ No newline at end of file