Change nixosVms from list to attrset keyed by VM name

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-02-26 20:26:55 +00:00
parent cde2c3e727
commit 85c88cfb33
8 changed files with 532 additions and 532 deletions

View file

@ -82,9 +82,8 @@ Configure VMs in your NixOS configuration:
interface = "eth0";
};
nixosVms = [
{
name = "banking";
nixosVms = {
banking = {
color = "green";
memory = 4096;
cpus = 4;
@ -99,9 +98,8 @@ Configure VMs in your NixOS configuration:
};
};
guestPrograms = with pkgs; [ firefox konsole ];
}
{
name = "shopping";
};
shopping = {
color = "yellow";
memory = 2048;
cpus = 2;
@ -116,16 +114,15 @@ Configure VMs in your NixOS configuration:
};
};
guestPrograms = with pkgs; [ firefox konsole ];
}
{
name = "personal";
};
personal = {
color = "green";
memory = 4096;
cpus = 4;
# No network.interfaces = offline VM
guestPrograms = with pkgs; [ libreoffice ];
}
];
};
};
};
}
```
@ -157,7 +154,7 @@ For mpv, make sure you use `--vo=wlshm`. Other backends probably won't work.
| `user` | string | required | User who owns TAP interfaces and runs VMs (must have explicit UID) |
| `hostNetworking.nat.enable` | bool | `false` | Enable NAT for VM internet access |
| `hostNetworking.nat.interface` | string | `""` | External interface for NAT (required if nat.enable) |
| `nixosVms` | list of VM configs | `[]` | List of NixOS-based VMs to create |
| `nixosVms` | attrsOf VM config | `{}` | NixOS-based VMs to create (keys are VM names) |
| `enableBashIntegration` | bool | `true` | Enable bash completion for vm-* commands |
| `nvidiaWeakenSandbox` | bool | `false` | Use crosvm-nvidia package with relaxed W+X memory policy for nvidia GPU support |
| `schedulerIsolation` | `"full"`, `"vm"`, or `"off"` | `"full"` | Mitigate hyperthreading attacks using scheduler thread isolation. `"full"`: vCPU threads may not share a core with any other thread. `"vm"`: vCPU threads may share a core with other vCPUs from the same VM only. `"off"`: no mitigations. |
@ -175,11 +172,10 @@ For mpv, make sure you use `--vo=wlshm`. Other backends probably won't work.
| `vmsilo-tray.logLevel` | string | `"info"` | Log level for tray proxy host and guest daemons (error, warn, info, debug, trace) |
| `isolatedPciDevices` | list of strings | `[]` | PCI devices to isolate with vfio-pci |
### VM Configuration (`nixosVms` items)
### VM Configuration (`nixosVms.<name>`)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `name` | string | required | VM name for scripts |
| `memory` | int | `1024` | Memory allocation in MB |
| `cpus` | int | `2` | Number of virtual CPUs |
| `color` | string | `"darkred"` | Window decoration color (named color or hex, e.g., `"#2ecc71"`) |
@ -330,16 +326,16 @@ programs.vmsilo = {
# Devices to isolate from host (claimed by vfio-pci)
isolatedPciDevices = [ "01:00.0" "02:00.0" ];
nixosVms = [{
name = "sys-usb";
memory = 1024;
pciDevices = [{ path = "01:00.0"; }]; # USB controller
}
{
name = "sys-net";
memory = 1024;
pciDevices = [{ path = "02:00.0"; }]; # Network card
}];
nixosVms = {
sys-usb = {
memory = 1024;
pciDevices = [{ path = "01:00.0"; }]; # USB controller
};
sys-net = {
memory = 1024;
pciDevices = [{ path = "02:00.0"; }]; # Network card
};
};
};
# Recommended: blacklist native drivers for reliability
@ -363,11 +359,11 @@ Each attrset is formatted as `key=value` pairs for crosvm `--vhost-user`.
Each VM's `color` option controls its KDE window decoration color, providing a visual indicator of which security domain a window belongs to:
```nix
nixosVms = [
{ name = "banking"; color = "#2ecc71"; ... } # Green
{ name = "shopping"; color = "#3498db"; ... } # Blue
{ name = "untrusted"; color = "red"; ... } # Red (default)
];
nixosVms = {
banking = { color = "#2ecc71"; ... }; # Green
shopping = { color = "#3498db"; ... }; # Blue
untrusted = { color = "red"; ... }; # Red (default)
};
```
The color is passed to KWin via the wayland security context. A KWin patch (included in the module) reads the color and applies it to the window's title bar and frame. Serverside decorations are forced for VM windows so the color is always visible. Text color is automatically chosen (black or white) based on the background luminance.

View file

@ -48,9 +48,8 @@
hostNetworking.nat.enable = true;
hostNetworking.nat.interface = "en+";
nixosVms = [
{
name = "testvm";
nixosVms = {
testvm = {
color = "yellow";
memory = 4096;
cpus = 4;
@ -71,9 +70,8 @@
{ documentation.nixos.enable = true; }
];
guestPrograms = commonGuestPrograms;
}
{
name = "netvm";
};
netvm = {
color = "red";
autoStart = true;
memory = 512;
@ -120,7 +118,7 @@
};
}
];
}
];
};
};
};
}

View file

@ -25,23 +25,19 @@ let
normalizedVmPciDevices = vm: map (dev: extractBdf dev.path) vm.pciDevices;
# All PCI devices across all VMs (for duplicate check)
allVmPciDevices = lib.concatMap normalizedVmPciDevices cfg.nixosVms;
allVmPciDevices = lib.concatMap normalizedVmPciDevices (lib.attrValues cfg.nixosVms);
in
{
config = lib.mkIf cfg.enable {
assertions =
let
vmNames = map (vm: vm.name) cfg.nixosVms;
vmNames = lib.attrNames cfg.nixosVms;
in
[
{
assertion = cfg.hostNetworking.nat.enable -> cfg.hostNetworking.nat.interface != "";
message = "programs.vmsilo.hostNetworking.nat.interface must be set when hostNetworking.nat.enable is true";
}
{
assertion = lib.length vmNames == lib.length (lib.unique vmNames);
message = "VM names must be unique";
}
]
# PCI passthrough assertions
++ lib.concatMap (
@ -56,7 +52,7 @@ in
message = "VM '${vm.name}' uses PCI device ${dev.path} which is not in isolatedPciDevices";
}
) vm.pciDevices
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
++ [
{
assertion = lib.length allVmPciDevices == lib.length (lib.unique allVmPciDevices);
@ -122,7 +118,7 @@ in
}) (lib.attrNames iface.routes)
) vm.network.interfaces
)
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
# dependsOn assertions: referenced VM names must exist
++ lib.concatMap (
vm:
@ -130,7 +126,7 @@ in
assertion = lib.elem depName vmNames;
message = "VM '${vm.name}' has dependsOn '${depName}' which is not a defined VM name";
}) vm.dependsOn
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
# Shared directory assertions: translateUid/translateGid incompatible with posixAcl
++ lib.concatMap (
vm:
@ -150,6 +146,6 @@ in
}
) effectiveSharedDirs
)
) cfg.nixosVms;
) (lib.attrValues cfg.nixosVms);
};
}

View file

@ -296,7 +296,7 @@ let
<Category>X-Vmsilo-${vm.name}</Category>
</Include>
</Menu>
'') cfg.nixosVms}
'') (lib.attrValues cfg.nixosVms)}
</Menu>
'';
@ -311,7 +311,7 @@ let
# Merge all VM desktop entries (--no-preserve=mode keeps dirs writable)
${lib.concatMapStringsSep "\n" (vm: ''
cp -r --no-preserve=mode ${mkDesktopEntries vm}/share/* $out/share/
'') cfg.nixosVms}
'') (lib.attrValues cfg.nixosVms)}
'';
in
{

View file

@ -160,8 +160,13 @@
in
if lib.stringLength fullName <= maxLen then fullName else "${truncatedName}-${ifIndexStr}";
# Auto-assign sequential VM IDs starting from 3 based on list position
assignVmIds = vms: lib.imap0 (idx: vm: vm // { id = idx + 3; }) vms;
# Auto-assign sequential VM IDs starting from 3 based on alphabetical name order
assignVmIds =
vmAttrs:
let
sortedNames = lib.sort (a: b: a < b) (lib.attrNames vmAttrs);
in
lib.imap0 (idx: name: vmAttrs.${name} // { id = idx + 3; }) sortedNames;
# Compute effective shared directories by merging implicit sharedHome entry
# into explicitly configured sharedDirectories.

View file

@ -135,495 +135,499 @@ let
# Type for free-form attrsets (disks, pciDevices, sharedDirectories, vhostUser)
freeformAttrsetType = lib.types.attrsOf (lib.types.nullOr attrValueType);
vmSubmodule = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "VM name. Used for TAP interface naming and scripts.";
example = "banking";
};
vmSubmodule = lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
readOnly = true;
description = "VM name (derived from attribute name).";
};
color = lib.mkOption {
type = lib.types.str;
default = "darkred";
description = ''
Window decoration color for this VM. Used in wayland security context.
Supported formats: named colors ("red", "green") or hex ("#FF0000").
'';
example = "#3498db";
};
color = lib.mkOption {
type = lib.types.str;
default = "darkred";
description = ''
Window decoration color for this VM. Used in wayland security context.
Supported formats: named colors ("red", "green") or hex ("#FF0000").
'';
example = "#3498db";
};
waylandProxy = lib.mkOption {
type = lib.types.enum [
"wayland-proxy-virtwl"
"sommelier"
];
default = "wayland-proxy-virtwl";
description = "Wayland proxy to use in the guest VM.";
example = "sommelier";
};
waylandProxy = lib.mkOption {
type = lib.types.enum [
"wayland-proxy-virtwl"
"sommelier"
];
default = "wayland-proxy-virtwl";
description = "Wayland proxy to use in the guest VM.";
example = "sommelier";
};
memory = lib.mkOption {
type = lib.types.int;
default = 1024;
description = "Memory allocation in MB.";
example = 4096;
};
memory = lib.mkOption {
type = lib.types.int;
default = 1024;
description = "Memory allocation in MB.";
example = 4096;
};
cpus = lib.mkOption {
type = lib.types.int;
default = 2;
description = "Number of virtual CPUs.";
example = 4;
};
cpus = lib.mkOption {
type = lib.types.int;
default = 2;
description = "Number of virtual CPUs.";
example = 4;
};
network = lib.mkOption {
type = networkSubmodule;
default = { };
description = "Network configuration for this VM.";
example = lib.literalExpression ''
{
nameservers = [ "8.8.8.8" ];
interfaces = {
wan = {
type = "tap";
tap.hostAddress = "10.0.0.254/24";
addresses = [ "10.0.0.1/24" ];
routes."0.0.0.0/0" = { via = "10.0.0.254"; };
network = lib.mkOption {
type = networkSubmodule;
default = { };
description = "Network configuration for this VM.";
example = lib.literalExpression ''
{
nameservers = [ "8.8.8.8" ];
interfaces = {
wan = {
type = "tap";
tap.hostAddress = "10.0.0.254/24";
addresses = [ "10.0.0.1/24" ];
routes."0.0.0.0/0" = { via = "10.0.0.254"; };
};
};
};
}
'';
};
autoShutdown = lib.mkOption {
type = lib.types.submodule {
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable auto-shutdown when idle. VM shuts down after autoShutdown.after seconds with no active commands.";
};
after = lib.mkOption {
type = lib.types.int;
default = 60;
description = "Seconds to wait after last command exits before shutting down.";
};
};
}
'';
};
default = { };
description = "Auto-shutdown configuration for idle VMs.";
};
autoStart = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Start this VM automatically instead of on-demand via socket activation. GPU VMs start when the graphical session begins; non-GPU VMs start at boot.";
};
rootOverlay = lib.mkOption {
type = lib.types.submodule {
options = {
type = lib.mkOption {
type = lib.types.enum [
"qcow2"
"tmpfs"
];
default = "qcow2";
description = "Overlay upper layer type: 'qcow2' (disk-backed, default) or 'tmpfs' (RAM-backed).";
};
size = lib.mkOption {
type = lib.types.str;
default = "10G";
description = "Maximum size of ephemeral disk (qcow2 only). Parsed by qemu-img create.";
example = "20G";
};
};
};
default = { };
description = "Root overlay configuration. Controls where VM writes go.";
};
dependsOn = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of VM names to start when this VM starts.";
};
additionalDisks = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
Additional disks to attach to the VM. Each attrset requires `path` (positional).
Other keys are passed directly to crosvm --block (e.g., ro, sparse, block-size, id).
'';
example = lib.literalExpression ''
[{
path = "/tmp/data.qcow2";
ro = false;
sparse = true;
block-size = 4096;
id = "datadisk";
}]
'';
};
rootDisk = lib.mkOption {
type = lib.types.nullOr freeformAttrsetType;
default = null;
description = ''
Custom root disk. If not set, uses the built rootfs image with rootDiskReadonly.
Requires `path` (positional). Other keys are passed directly to crosvm --block.
'';
example = lib.literalExpression ''
{
path = "/path/to/custom-root.qcow2";
ro = true;
}
'';
};
kernel = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom kernel image. If not set, uses the built rootfs kernel.";
example = "/path/to/bzImage";
};
initramfs = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom initramfs. If not set, uses the built rootfs initrd.";
example = "/path/to/initrd";
};
rootDiskReadonly = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether the built rootfs should be read-only. Ignored when rootDisk is set.";
};
copyChannel = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Include a NixOS channel in the rootfs image, built from the same nixpkgs revision used to build the VM.";
};
sharedHome = lib.mkOption {
type = lib.types.either lib.types.bool lib.types.str;
default = true;
description = ''
Share a host directory as /home/user in the guest via virtiofs.
Set to true for default path (/shared/<vmname>), a string for a custom host path, or false to disable.
'';
example = "/data/vms/banking-home";
};
kernelParams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra kernel command line parameters.";
example = [
"acpi=off"
"debug"
];
};
gpu = lib.mkOption {
type = lib.types.either lib.types.bool (
lib.types.submodule {
autoShutdown = lib.mkOption {
type = lib.types.submodule {
options = {
wayland = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable cross-domain context type for Wayland passthrough.";
};
opengl = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable virgl2 context type for OpenGL acceleration.";
};
vulkan = lib.mkOption {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable venus context type for Vulkan acceleration.";
description = "Enable auto-shutdown when idle. VM shuts down after autoShutdown.after seconds with no active commands.";
};
after = lib.mkOption {
type = lib.types.int;
default = 60;
description = "Seconds to wait after last command exits before shutting down.";
};
};
}
);
default = true;
description = ''
GPU configuration. Set to false to disable, true for default config
(wayland + opengl), or an attrset to select specific GPU features.
'';
example = lib.literalExpression ''
{
wayland = true;
opengl = true;
vulkan = true;
}
'';
};
sound = {
playback = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable sound playback.";
};
default = { };
description = "Auto-shutdown configuration for idle VMs.";
};
capture = lib.mkOption {
autoStart = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable sound capture. Implies playback.";
description = "Start this VM automatically instead of on-demand via socket activation. GPU VMs start when the graphical session begins; non-GPU VMs start at boot.";
};
};
sharedDirectories = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
rootOverlay = lib.mkOption {
type = lib.types.submodule {
options = {
path = lib.mkOption {
type = lib.mkOption {
type = lib.types.enum [
"qcow2"
"tmpfs"
];
default = "qcow2";
description = "Overlay upper layer type: 'qcow2' (disk-backed, default) or 'tmpfs' (RAM-backed).";
};
size = lib.mkOption {
type = lib.types.str;
description = "Host directory path to share with the guest.";
default = "10G";
description = "Maximum size of ephemeral disk (qcow2 only). Parsed by qemu-img create.";
example = "20G";
};
threadPoolSize = lib.mkOption {
type = lib.types.int;
default = 0;
description = "Thread pool size for virtiofsd.";
};
xattr = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable extended attributes.";
};
posixAcl = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable POSIX ACLs. Incompatible with translateUid/translateGid.";
};
readonly = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Share as read-only.";
};
inodeFileHandles = lib.mkOption {
type = lib.types.enum [
"never"
"prefer"
"mandatory"
];
default = "prefer";
description = "Inode file handles mode.";
};
cache = lib.mkOption {
type = lib.types.enum [
"auto"
"always"
"never"
"metadata"
];
default = "auto";
description = "Cache policy.";
};
allowMmap = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Allow memory-mapped I/O.";
};
enableReaddirplus = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable readdirplus. When false, passes --no-readdirplus.";
};
writeback = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable writeback caching.";
};
allowDirectIo = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Allow direct I/O.";
};
logLevel = lib.mkOption {
type = lib.types.enum [
"error"
"warn"
"info"
"debug"
"trace"
"off"
];
default = "info";
description = "virtiofsd log level.";
};
killprivV2 = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable FUSE_HANDLE_KILLPRIV_V2. Avoids duplicating work around
stripping SUID/SGID bits off binaries when written to by non-root.
'';
};
uidMap = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Map a range of UIDs from the host into the namespace,
given as ":namespace_uid:host_uid:count:". Done by user namespace.
'';
};
gidMap = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Map a range of GIDs from the host into the namespace,
given as ":namespace_gid:host_gid:count:". Done by user namespace.
'';
};
translateUid = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Translate UIDs between guest and host internally by virtiofsd,
given as "<type>:<source base>:<target base>:<count>".
Incompatible with posixAcl.
'';
};
translateGid = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Translate GIDs between guest and host internally by virtiofsd,
given as "<type>:<source base>:<target base>:<count>".
Incompatible with posixAcl.
'';
};
preserveNoatime = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Preserve O_NOATIME flag on files.";
};
};
}
);
default = { };
description = ''
Shared directories via virtiofsd. Keys are used as the virtiofs tag.
Each entry runs a dedicated virtiofsd process connected to crosvm via vhost-user.
'';
example = lib.literalExpression ''
{
home = {
path = "/tmp/host_shared_dir";
xattr = true;
posixAcl = true;
};
}
'';
};
vhostUser = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
vhost-user devices for --vhost-user. Each attrset is formatted as key=value pairs.
'';
example = lib.literalExpression ''
[{
type = "net";
socket = "/path/to/vhost-user.sock";
}]
'';
};
pciDevices = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
PCI devices to pass through via --vfio. Each attrset requires `path` (BDF or sysfs path).
BDF format ("01:00.0" or "0000:01:00.0") is auto-converted to sysfs path.
Other keys are passed as key=value pairs (e.g., iommu=on).
Devices must also be listed in isolatedPciDevices.
'';
example = lib.literalExpression ''
[{
path = "01:00.0";
iommu = "on";
}]
'';
};
guestPrograms = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "VM-specific packages to include in the rootfs.";
example = lib.literalExpression "[ pkgs.firefox ]";
};
guestConfig = lib.mkOption {
type = lib.types.either lib.types.deferredModule (lib.types.listOf lib.types.deferredModule);
default = [ ];
description = ''
VM-specific NixOS configuration modules.
Accepts a single NixOS module or a list of modules. Each module can be
an attribute set, a module function ({config, pkgs, lib, ...}: { ... }),
or a path to a module file.
'';
example = lib.literalExpression ''
# Single attrset
{ services.openssh.enable = true; }
# Single module function
{ config, pkgs, ... }: {
services.openssh.enable = true;
}
# List of modules
[
./my-guest-module.nix
{ services.openssh.enable = true; }
({ config, pkgs, ... }: { environment.systemPackages = [ pkgs.vim ]; })
]
'';
};
crosvm = lib.mkOption {
type = lib.types.submodule {
options = {
logLevel = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Per-VM log level override. If null, uses global crosvm.logLevel.";
example = "debug";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Per-VM extra arguments passed to crosvm before 'run'. Appended to global crosvm.extraArgs.";
example = [
"--syslog-tag"
"banking"
];
};
extraRunArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Per-VM extra arguments passed to crosvm after 'run'. Appended to global crosvm.extraRunArgs.";
example = [ "--disable-sandbox" ];
};
};
default = { };
description = "Root overlay configuration. Controls where VM writes go.";
};
dependsOn = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of VM names to start when this VM starts.";
};
additionalDisks = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
Additional disks to attach to the VM. Each attrset requires `path` (positional).
Other keys are passed directly to crosvm --block (e.g., ro, sparse, block-size, id).
'';
example = lib.literalExpression ''
[{
path = "/tmp/data.qcow2";
ro = false;
sparse = true;
block-size = 4096;
id = "datadisk";
}]
'';
};
rootDisk = lib.mkOption {
type = lib.types.nullOr freeformAttrsetType;
default = null;
description = ''
Custom root disk. If not set, uses the built rootfs image with rootDiskReadonly.
Requires `path` (positional). Other keys are passed directly to crosvm --block.
'';
example = lib.literalExpression ''
{
path = "/path/to/custom-root.qcow2";
ro = true;
}
'';
};
kernel = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom kernel image. If not set, uses the built rootfs kernel.";
example = "/path/to/bzImage";
};
initramfs = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Custom initramfs. If not set, uses the built rootfs initrd.";
example = "/path/to/initrd";
};
rootDiskReadonly = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether the built rootfs should be read-only. Ignored when rootDisk is set.";
};
copyChannel = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Include a NixOS channel in the rootfs image, built from the same nixpkgs revision used to build the VM.";
};
sharedHome = lib.mkOption {
type = lib.types.either lib.types.bool lib.types.str;
default = true;
description = ''
Share a host directory as /home/user in the guest via virtiofs.
Set to true for default path (/shared/<vmname>), a string for a custom host path, or false to disable.
'';
example = "/data/vms/banking-home";
};
kernelParams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra kernel command line parameters.";
example = [
"acpi=off"
"debug"
];
};
gpu = lib.mkOption {
type = lib.types.either lib.types.bool (
lib.types.submodule {
options = {
wayland = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable cross-domain context type for Wayland passthrough.";
};
opengl = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable virgl2 context type for OpenGL acceleration.";
};
vulkan = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable venus context type for Vulkan acceleration.";
};
};
}
);
default = true;
description = ''
GPU configuration. Set to false to disable, true for default config
(wayland + opengl), or an attrset to select specific GPU features.
'';
example = lib.literalExpression ''
{
wayland = true;
opengl = true;
vulkan = true;
}
'';
};
sound = {
playback = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable sound playback.";
};
capture = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable sound capture. Implies playback.";
};
};
sharedDirectories = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
path = lib.mkOption {
type = lib.types.str;
description = "Host directory path to share with the guest.";
};
threadPoolSize = lib.mkOption {
type = lib.types.int;
default = 0;
description = "Thread pool size for virtiofsd.";
};
xattr = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable extended attributes.";
};
posixAcl = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable POSIX ACLs. Incompatible with translateUid/translateGid.";
};
readonly = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Share as read-only.";
};
inodeFileHandles = lib.mkOption {
type = lib.types.enum [
"never"
"prefer"
"mandatory"
];
default = "prefer";
description = "Inode file handles mode.";
};
cache = lib.mkOption {
type = lib.types.enum [
"auto"
"always"
"never"
"metadata"
];
default = "auto";
description = "Cache policy.";
};
allowMmap = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Allow memory-mapped I/O.";
};
enableReaddirplus = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable readdirplus. When false, passes --no-readdirplus.";
};
writeback = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable writeback caching.";
};
allowDirectIo = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Allow direct I/O.";
};
logLevel = lib.mkOption {
type = lib.types.enum [
"error"
"warn"
"info"
"debug"
"trace"
"off"
];
default = "info";
description = "virtiofsd log level.";
};
killprivV2 = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable FUSE_HANDLE_KILLPRIV_V2. Avoids duplicating work around
stripping SUID/SGID bits off binaries when written to by non-root.
'';
};
uidMap = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Map a range of UIDs from the host into the namespace,
given as ":namespace_uid:host_uid:count:". Done by user namespace.
'';
};
gidMap = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Map a range of GIDs from the host into the namespace,
given as ":namespace_gid:host_gid:count:". Done by user namespace.
'';
};
translateUid = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Translate UIDs between guest and host internally by virtiofsd,
given as "<type>:<source base>:<target base>:<count>".
Incompatible with posixAcl.
'';
};
translateGid = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Translate GIDs between guest and host internally by virtiofsd,
given as "<type>:<source base>:<target base>:<count>".
Incompatible with posixAcl.
'';
};
preserveNoatime = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Preserve O_NOATIME flag on files.";
};
};
}
);
default = { };
description = ''
Shared directories via virtiofsd. Keys are used as the virtiofs tag.
Each entry runs a dedicated virtiofsd process connected to crosvm via vhost-user.
'';
example = lib.literalExpression ''
{
home = {
path = "/tmp/host_shared_dir";
xattr = true;
posixAcl = true;
};
}
'';
};
vhostUser = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
vhost-user devices for --vhost-user. Each attrset is formatted as key=value pairs.
'';
example = lib.literalExpression ''
[{
type = "net";
socket = "/path/to/vhost-user.sock";
}]
'';
};
pciDevices = lib.mkOption {
type = lib.types.listOf freeformAttrsetType;
default = [ ];
description = ''
PCI devices to pass through via --vfio. Each attrset requires `path` (BDF or sysfs path).
BDF format ("01:00.0" or "0000:01:00.0") is auto-converted to sysfs path.
Other keys are passed as key=value pairs (e.g., iommu=on).
Devices must also be listed in isolatedPciDevices.
'';
example = lib.literalExpression ''
[{
path = "01:00.0";
iommu = "on";
}]
'';
};
guestPrograms = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "VM-specific packages to include in the rootfs.";
example = lib.literalExpression "[ pkgs.firefox ]";
};
guestConfig = lib.mkOption {
type = lib.types.either lib.types.deferredModule (lib.types.listOf lib.types.deferredModule);
default = [ ];
description = ''
VM-specific NixOS configuration modules.
Accepts a single NixOS module or a list of modules. Each module can be
an attribute set, a module function ({config, pkgs, lib, ...}: { ... }),
or a path to a module file.
'';
example = lib.literalExpression ''
# Single attrset
{ services.openssh.enable = true; }
# Single module function
{ config, pkgs, ... }: {
services.openssh.enable = true;
}
# List of modules
[
./my-guest-module.nix
{ services.openssh.enable = true; }
({ config, pkgs, ... }: { environment.systemPackages = [ pkgs.vim ]; })
]
'';
};
crosvm = lib.mkOption {
type = lib.types.submodule {
options = {
logLevel = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Per-VM log level override. If null, uses global crosvm.logLevel.";
example = "debug";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Per-VM extra arguments passed to crosvm before 'run'. Appended to global crosvm.extraArgs.";
example = [
"--syslog-tag"
"banking"
];
};
extraRunArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Per-VM extra arguments passed to crosvm after 'run'. Appended to global crosvm.extraRunArgs.";
example = [ "--disable-sandbox" ];
};
};
};
default = { };
description = "Per-VM crosvm configuration.";
};
default = { };
description = "Per-VM crosvm configuration.";
};
};
};
}
);
in
{
@ -733,16 +737,17 @@ in
};
nixosVms = lib.mkOption {
type = lib.types.listOf vmSubmodule;
default = [ ];
description = "List of NixOS-based VMs to create.";
type = lib.types.attrsOf vmSubmodule;
default = { };
description = "NixOS-based VMs to create. Keys are VM names.";
example = lib.literalExpression ''
[{
name = "banking";
memory = 4096;
cpus = 4;
guestPrograms = [ pkgs.firefox ];
}]
{
banking = {
memory = 4096;
cpus = 4;
guestPrograms = [ pkgs.firefox ];
};
}
'';
};

View file

@ -18,7 +18,7 @@ let
# VM launcher scripts
${lib.concatMapStringsSep "\n" (vm: ''
ln -s ${cfg._internal.vmScripts.${vm.name}} $out/bin/vmsilo-start-${vm.name}
'') cfg.nixosVms}
'') (lib.attrValues cfg.nixosVms)}
# User-facing scripts
${lib.concatMapStringsSep "\n" (name: ''

View file

@ -30,10 +30,10 @@ let
userHome = config.users.users.${cfg.user}.home;
# Whether any VM uses sharedHome
anySharedHome = lib.any (vm: vm.sharedHome != false) cfg.nixosVms;
anySharedHome = lib.any (vm: vm.sharedHome != false) (lib.attrValues cfg.nixosVms);
# VMs with GPU enabled (gpu defaults to true, so filter out gpu = false)
gpuVms = lib.filter (vm: vm.gpu != false) cfg.nixosVms;
gpuVms = lib.filter (vm: vm.gpu != false) (lib.attrValues cfg.nixosVms);
# vmsilo-balloond service definition
mkVmsiloBalloondService = {
@ -81,7 +81,7 @@ in
SocketMode = "0600";
};
}
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
);
# Systemd system services for VMs (run as root for PCI passthrough and sandboxing)
@ -124,7 +124,7 @@ in
ExecStartPre = startPreScripts;
};
}
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
++
# Proxy template services (per-connection)
map (
@ -140,7 +140,7 @@ in
ExecStart = "${cfg._internal.proxyScripts.${vm.name}}";
};
}
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
++
# Console relay services (one per VM)
# Uses PTY so crosvm stays connected when users disconnect
@ -164,7 +164,7 @@ in
RestartSec = "1s";
};
}
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
++
# virtiofsd services (one per shared directory per VM)
lib.concatMap (
@ -231,7 +231,7 @@ in
};
}
) effectiveSharedDirs
) cfg.nixosVms
) (lib.attrValues cfg.nixosVms)
++ [ (lib.nameValuePair "vmsilo-balloond" mkVmsiloBalloondService) ]
++
# Tray proxy services (one per VM)