Improved usage guide for nixos

This commit is contained in:
Johannes Leupolz 2026-05-09 08:04:06 +00:00
parent 2c8d0f9af1
commit ef060ce9d6

View file

@ -1,13 +1,285 @@
# Usage Guide for NixOS # 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/) * [ShaneTRS](https://github.com/ShaneTRS/nixos-config/)
* [griffi-gh](https://github.com/girl-pp-ua/nixos-infra/) * [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. 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 \
<your-application>
```
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`.