summary refs log tree commit diff
path: root/pkgs/os-specific/linux/spectrum
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/os-specific/linux/spectrum')
-rw-r--r--pkgs/os-specific/linux/spectrum/default.nix18
-rw-r--r--pkgs/os-specific/linux/spectrum/linux/vm.nix25
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/default.nix58
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/etc/group3
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/etc/passwd3
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/etc/wayfire/wf-shell-defaults.ini2
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/rc-services.nix26
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/services.nix28
-rw-r--r--pkgs/os-specific/linux/spectrum/rootfs/stage1.nix44
-rw-r--r--pkgs/os-specific/linux/spectrum/spectrum-vm/default.nix35
-rwxr-xr-xpkgs/os-specific/linux/spectrum/spectrum-vm/spectrum-vm.in71
-rw-r--r--pkgs/os-specific/linux/spectrum/testhost/default.nix218
-rw-r--r--pkgs/os-specific/linux/spectrum/vm/app/default.nix58
-rw-r--r--pkgs/os-specific/linux/spectrum/vm/comp/default.nix73
-rw-r--r--pkgs/os-specific/linux/spectrum/vm/default.nix9
-rw-r--r--pkgs/os-specific/linux/spectrum/vm/net/default.nix167
16 files changed, 838 insertions, 0 deletions
diff --git a/pkgs/os-specific/linux/spectrum/default.nix b/pkgs/os-specific/linux/spectrum/default.nix
new file mode 100644
index 00000000000..c4cccab3787
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/default.nix
@@ -0,0 +1,18 @@
+{ newScope, linux_cros }:
+
+let
+  self = with self; {
+    callPackage = newScope self;
+
+    sys-vms = callPackage ./vm { };
+
+    spectrum-vm = callPackage ./spectrum-vm { linux = linux_vm; };
+
+    spectrum-testhost = callPackage ./testhost { };
+
+    linux_vm = callPackage ./linux/vm.nix { linux = linux_cros; };
+
+    makeRootfs = callPackage ./rootfs { };
+  };
+in
+self
diff --git a/pkgs/os-specific/linux/spectrum/linux/vm.nix b/pkgs/os-specific/linux/spectrum/linux/vm.nix
new file mode 100644
index 00000000000..c657cb443e8
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/linux/vm.nix
@@ -0,0 +1,25 @@
+{ lib, linux, kernelPatches, structuredExtraConfig ? {} }:
+
+with lib.kernel;
+
+linux.override {
+  structuredExtraConfig = {
+    VIRTIO_PCI = yes;
+    VIRTIO_BLK = yes;
+    VIRTIO_WL = yes;
+    VIRTIO_NET = yes;
+    DEVTMPFS_MOUNT = yes;
+    SQUASHFS = yes;
+
+    # VOP is needed to work around a Kconfig bug:
+    # https://lore.kernel.org/lkml/87wob4tf9b.fsf@alyssa.is/
+    VOP = yes;
+    VOP_BUS = yes;
+    HW_RANDOM = yes;
+    HW_RANDOM_VIRTIO = yes;
+
+    NET_9P = yes;
+    NET_9P_VIRTIO = yes;
+    "9P_FS" = yes;
+  } // structuredExtraConfig;
+}
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/default.nix b/pkgs/os-specific/linux/spectrum/rootfs/default.nix
new file mode 100644
index 00000000000..56f2d15b103
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/default.nix
@@ -0,0 +1,58 @@
+{ runCommandNoCC, writeScript, writeReferencesToFile, makeFontsConf, lib
+, dash, execline, s6, s6-rc, s6-portable-utils, s6-linux-utils, s6-linux-init, busybox
+, mesa, squashfs-tools-ng, makeDBusConf, connman
+}:
+
+{ services, rcServices ? {}, fonts ? [], path ? [] }:
+
+let
+  stage1 = import ./stage1.nix {
+    inherit writeScript lib
+      execline s6 s6-rc s6-portable-utils s6-linux-utils s6-linux-init busybox mesa
+      path;
+  };
+
+  makeServicesDir = import ./services.nix {
+    inherit runCommandNoCC writeScript lib execline;
+  };
+
+  makeRcServicesDir = import ./rc-services.nix {
+    inherit runCommandNoCC lib s6-rc;
+  };
+
+  fontsConf = makeFontsConf { fontDirectories = fonts; };
+
+  squashfs = runCommandNoCC "root-squashfs" {} ''
+    cd ${rootfs}
+    (
+        grep -v ^${rootfs} ${writeReferencesToFile rootfs}
+        printf "%s\n" *
+    ) \
+        | xargs tar -cP --owner root:0 --group root:0 --hard-dereference \
+        | ${squashfs-tools-ng}/bin/tar2sqfs -c gzip -X level=1 $out
+  '';
+
+  rootfs = runCommandNoCC "rootfs" { passthru = { inherit squashfs; }; } ''
+    mkdir $out
+    cd $out
+
+    mkdir -p bin sbin dev proc run sys tmp var/lib
+    ln -s /run var/run
+    ln -s ${dash}/bin/dash bin/sh
+    ln -s ${stage1} sbin/init
+    cp -r ${./etc} etc
+    chmod u+w etc
+    ln -s ${makeDBusConf {
+      suidHelper = "/run/dbus-daemon-launch-helper";
+      serviceDirectories = [ connman ];
+    }} etc/dbus-1
+
+    mkdir etc/fonts
+    ln -s ${fontsConf} etc/fonts/fonts.conf
+
+    touch etc/login.defs
+    cp -r ${makeServicesDir { inherit services; }} etc/service
+    cp -r ${makeRcServicesDir { services = rcServices; }} etc/s6-rc
+  '';
+in
+rootfs
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/etc/group b/pkgs/os-specific/linux/spectrum/rootfs/etc/group
new file mode 100644
index 00000000000..df4940a5516
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/etc/group
@@ -0,0 +1,3 @@
+root:x:0:root
+messagebus:x:4:messagebus
+user:x:1000:user
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/etc/passwd b/pkgs/os-specific/linux/spectrum/rootfs/etc/passwd
new file mode 100644
index 00000000000..ddb1f854a6e
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/etc/passwd
@@ -0,0 +1,3 @@
+root:x:0:0:System administrator:/:/bin/sh
+messagebus:x:4:4:D-Bus system message bus daemon user:/run/dbus:/bin/sh
+user:x:1000:1000:User:/:/bin/sh
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/etc/wayfire/wf-shell-defaults.ini b/pkgs/os-specific/linux/spectrum/rootfs/etc/wayfire/wf-shell-defaults.ini
new file mode 100644
index 00000000000..7ba621225aa
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/etc/wayfire/wf-shell-defaults.ini
@@ -0,0 +1,2 @@
+[panel]
+widgets_right =
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/rc-services.nix b/pkgs/os-specific/linux/spectrum/rootfs/rc-services.nix
new file mode 100644
index 00000000000..4c942189c5e
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/rc-services.nix
@@ -0,0 +1,26 @@
+{ runCommandNoCC, lib, s6-rc }:
+
+{ services ? [] }:
+
+let
+  inherit (lib) concatStrings escapeShellArg mapAttrsToList optionalString;
+
+  source = runCommandNoCC "s6-services-source" {} ''
+    mkdir $out
+    ${concatStrings (mapAttrsToList (name: attrs: ''
+      mkdir $out/${name}
+      ${concatStrings (mapAttrsToList (key: value: ''
+        cp ${value} $out/${name}/${key}
+      '') attrs)}
+    '') services)}
+  '';
+
+  s6RcCompile = { fdhuser ? null }: source:
+    runCommandNoCC "s6-rc-compile" {} ''
+      ${s6-rc}/bin/s6-rc-compile \
+        ${optionalString (fdhuser != null) "-h ${escapeShellArg fdhuser}"} \
+        $out ${source}
+    '';
+in
+
+s6RcCompile {} source
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/services.nix b/pkgs/os-specific/linux/spectrum/rootfs/services.nix
new file mode 100644
index 00000000000..b2b09faa06e
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/services.nix
@@ -0,0 +1,28 @@
+{ runCommandNoCC, writeScript, lib, execline }:
+
+{ services ? [] }:
+
+let
+  services' = {
+    ".s6-svscan" = {
+      finish = writeScript "init-stage3" ''
+        #! ${execline}/bin/execlineb -P
+        foreground { s6-nuke -th }
+        s6-sleep -m -- 2000
+        foreground { s6-nuke -k }
+        wait { }
+        s6-linux-init-hpr -fr
+      '';
+    } // services.".s6-svscan" or {};
+  } // services;
+in
+
+runCommandNoCC "services" {} ''
+  mkdir $out
+  ${lib.concatStrings (lib.mapAttrsToList (name: attrs: ''
+    mkdir $out/${name}
+    ${lib.concatStrings (lib.mapAttrsToList (key: value: ''
+      cp ${value} $out/${name}/${key}
+    '') attrs)}
+  '') services')}
+''
diff --git a/pkgs/os-specific/linux/spectrum/rootfs/stage1.nix b/pkgs/os-specific/linux/spectrum/rootfs/stage1.nix
new file mode 100644
index 00000000000..de10d60ffb6
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/rootfs/stage1.nix
@@ -0,0 +1,44 @@
+{ writeScript, lib
+, execline, s6, s6-rc, s6-portable-utils, s6-linux-utils, s6-linux-init, busybox, mesa
+, path ? []
+}:
+
+let
+  path' = path ++ [
+    s6 s6-rc s6-portable-utils s6-linux-utils s6-linux-init busybox execline
+  ];
+in
+
+writeScript "init-stage1" ''
+  #! ${execline}/bin/execlineb -P
+  export PATH ${lib.makeBinPath path'}
+  ${s6}/bin/s6-setsid -qb --
+
+  umask 022
+  if { s6-mount -t tmpfs -o mode=0755 tmpfs /run }
+  if { s6-hiercopy /etc/service /run/service }
+  emptyenv -p
+
+  background {
+    s6-setsid --
+
+    if { s6-rc-init -c /etc/s6-rc /run/service }
+
+    if { s6-mkdir -p /run/user/0 /dev/pts /dev/shm }
+    if { install -o user -g user -d /run/user/1000 }
+
+    if { s6-mount -t devpts -o gid=4,mode=620 none /dev/pts }
+    if { s6-mount -t tmpfs none /dev/shm }
+    if { s6-mount -t tmpfs none /var/lib }
+    if { s6-mount -t proc none /proc }
+    if { s6-mount -t sysfs none /sys }
+
+    if { s6-ln -s ${mesa.drivers} /run/opengl-driver }
+
+    s6-rc change ok-all
+  }
+
+  unexport !
+  cd /run/service
+  s6-svscan
+''
diff --git a/pkgs/os-specific/linux/spectrum/spectrum-vm/default.nix b/pkgs/os-specific/linux/spectrum/spectrum-vm/default.nix
new file mode 100644
index 00000000000..c56d2537c63
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/spectrum-vm/default.nix
@@ -0,0 +1,35 @@
+{ stdenv, lib, makeWrapper, utillinux, crosvm, linux, sys-vms }:
+
+stdenv.mkDerivation {
+  name = "spectrum-vm";
+
+  src = ./spectrum-vm.in;
+
+  nativeBuildInputs = [ makeWrapper ];
+
+  unpackPhase = ''
+    cp $src spectrum-vm.in
+  '';
+
+  configurePhase = ''
+    substituteAll spectrum-vm.in spectrum-vm
+    chmod +x spectrum-vm
+  '';
+
+  getopt = "${lib.getBin utillinux}/bin/getopt";
+  crosvm = "${lib.getBin crosvm}/bin/crosvm";
+  kernel = "${sys-vms.comp.linux}/bzImage";
+  rootfs = sys-vms.comp.rootfs.squashfs;
+
+  installPhase = ''
+    mkdir -p $out/bin
+    cp spectrum-vm $out/bin
+  '';
+
+  meta = with lib; {
+    description = "Utility for testing Spectrum VM components";
+    maintainers = with maintainers; [ qyliss ];
+    license = licenses.gpl3Plus;
+    inherit (crosvm.meta) platforms;
+  };
+}
diff --git a/pkgs/os-specific/linux/spectrum/spectrum-vm/spectrum-vm.in b/pkgs/os-specific/linux/spectrum/spectrum-vm/spectrum-vm.in
new file mode 100755
index 00000000000..a72c3896141
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/spectrum-vm/spectrum-vm.in
@@ -0,0 +1,71 @@
+#!@shell@
+set -ue
+
+ex_usage() {
+    cat <<EOF
+Usage: $(basename "$0") [OPTION]... [-- CROSVM_OPTIONS]
+
+  -c COMMAND         shell command to run inside VM
+  -C, --crosvm PATH  path to custom crosvm executable
+  -k, --kernel PATH  path to custom kernel image
+  -f, --rootfs PATH  path to custom root file system image
+  -- CROSVM_OPTIONS  extra arguments to pass to crosvm
+EOF
+    exit "$1"
+}
+
+args="$(@getopt@ -s sh -l crosvm:,help,kernel:,rootfs: -o c:C:hk:f: -- "$@" || exit 1)"
+eval set -- "$args"
+
+command=
+crosvm=@crosvm@
+kernel=@kernel@
+rootfs=@rootfs@
+
+while :
+do
+    case "$1" in
+	-c)
+	    shift
+	    command="$1"
+	    shift
+	    ;;
+        -C|--crosvm)
+            shift
+            crosvm="$1"
+	    shift
+            ;;
+	-h|--help)
+	    ex_usage 0
+	    ;;
+        -k|--kernel)
+            shift
+            kernel="$1"
+	    shift
+            ;;
+        -f|--rootfs)
+            shift
+            rootfs="$1"
+	    shift
+            ;;
+        --)
+            shift
+            break
+            ;;
+    esac
+done
+
+if [ -n "${XDG_RUNTIME_DIR-}" ]
+then
+    set -- -s "$XDG_RUNTIME_DIR" "$@"
+    if [ -n "${WAYLAND_DISPLAY-}" ]
+    then set -- --wayland-sock "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "$@"
+    fi
+fi
+
+exec "$crosvm" run \
+    -p init=/sbin/init \
+    -p "spectrumcmd=$(printf %s "$command" | base64 -w0)" \
+    --root "$rootfs" \
+    "$@" \
+    "$kernel"
diff --git a/pkgs/os-specific/linux/spectrum/testhost/default.nix b/pkgs/os-specific/linux/spectrum/testhost/default.nix
new file mode 100644
index 00000000000..de62f0add67
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/testhost/default.nix
@@ -0,0 +1,218 @@
+{ lib, runCommandNoCC, writeScript, writeScriptBin, writeShellScript, writeText
+, coreutils, cloud-hypervisor, crosvm, curl, execline, gnutar, gnused, iproute
+, iptables, jq, kmod, mktuntap, rsync, s6, s6-rc, sys-vms, utillinux
+}:
+
+let
+  inherit (lib) concatStrings escapeShellArg makeBinPath mapAttrsToList
+    optionalString;
+
+  compose2 = f: g: a: b: f (g a b);
+
+  concatMapAttrs = compose2 concatStrings mapAttrsToList;
+
+  makeServicesDir = { services }:
+    runCommandNoCC "services" {} ''
+      mkdir $out
+      ${concatMapAttrs (name: attrs: ''
+        mkdir $out/${name}
+        ${concatMapAttrs (key: value: ''
+          cp -r ${value} $out/${name}/${key}
+        '') attrs}
+      '') services}
+    '';
+
+  s6RcCompile = { fdhuser ? null }: source:
+    runCommandNoCC "s6-rc-compile" {} ''
+      ${s6-rc}/bin/s6-rc-compile \
+        ${optionalString (fdhuser != null) "-h ${escapeShellArg fdhuser}"} \
+        dest ${source}
+      tar -C dest -cf $out .
+    '';
+
+  compiledRcServicesDir = s6RcCompile {} (makeServicesDir {
+    services = {
+      vm-app = {
+        run = writeScript "app-run" ''
+          #! ${execline}/bin/execlineb -S0
+          # fdclose 0
+
+          # Checking the return value of the bridge creation is
+          # important, because if it fails due to the bridge already
+          # existing that means something else could already be using
+          # this bridge.
+          if { ip link add name br0 type bridge }
+          if { ip link set br0 up }
+
+          # Calculate the MACs for our TAP and the router's TAP.
+          # MAC address format, by octet:
+          #
+          #  0-3  Static OUI for Spectrum
+          #    4  Most significant bit is used to differentiate
+          #       routers from clients.  Other bits are reserved.
+          #  5-6  Last two octets of client's IP (in 100.64.0.0/16).
+          #
+          backtick -i router_mac {
+            pipeline { printf %.4x ${toString sys-vms.app.vmID} }
+            sed s/^\\(..\\)\\(..\\)$/0A:B3:EC:80:\\1:\\2/
+          }
+          backtick -i client_mac {
+            pipeline { printf %.4x ${toString sys-vms.app.vmID} }
+            sed s/^\\(..\\)\\(..\\)$/0A:B3:EC:00:\\1:\\2/
+          }
+          multisubstitute {
+            importas -iu router_mac router_mac
+            importas -iu client_mac client_mac
+          }
+
+          # Create the net VM end, and attach it to the net VM.
+          #
+          # Use a hardcoded name for now because if we use a dynamic
+          # one iproute2 has no way of telling us the name that was
+          # chosen:
+          # https://lore.kernel.org/netdev/20210406134240.wwumpnrzfjbttnmd@eve.qyliss.net/
+          define other_tap_name vmtapnet
+          # Try to delete the device in case the VM was powered off
+          # (as the finish script wouldn't have been run in that
+          # case.)  Since we check the return value of ip tuntap add,
+          # in the case of a race condition between deleting the
+          # device and creating it again, we'll just fail and try
+          # again.
+          foreground { ip link delete $other_tap_name }
+          if { ip tuntap add name $other_tap_name mode tap }
+          if { ip link set $other_tap_name master br0 }
+          if { ip link set $other_tap_name up }
+          if {
+            pipeline {
+              jq -n "$ARGS.named"
+                --arg tap $other_tap_name
+                --arg mac $router_mac
+            }
+            curl -iX PUT
+              -H "Accept: application/json"
+              -H "Content-Type: application/json"
+              --data-binary @-
+              --unix-socket ../vm-net/env/cloud-hypervisor.sock
+              http://localhost/api/v1/vm.add-net
+          }
+
+          mktuntap -pvBi vmtap%d 6
+          importas -iu tap_name TUNTAP_NAME
+          if { ip link set $tap_name master br0 }
+          if { ip link set $tap_name up }
+          if { iptables -t nat -A POSTROUTING -o $tap_name -j MASQUERADE }
+
+          ${crosvm}/bin/crosvm run -p init=/sbin/init -p notifyport=''${port}
+            # --serial type=file,path=/tmp/app.log
+            --cid 4
+            --tap-fd 6,mac=''${client_mac}
+            --root ${sys-vms.app.rootfs.squashfs} ${sys-vms.app.linux}/bzImage
+        '';
+        finish = writeScript "app-finish" ''
+          #! ${execline}/bin/execlineb -S0
+          # TODO: remove from vm-net
+          foreground { ip link delete vmtapnet }
+          ip link delete br0
+        '';
+        type = writeText "app-type" ''
+          longrun
+        '';
+        dependencies = writeText "app-dependencies" ''
+          vm-net
+        '';
+      };
+
+      vm-net = {
+        run = writeScript "net-run" ''
+          #! ${execline}/bin/execlineb -S0
+          # This is only necessary for when running s6 from a tty.
+          # (i.e. when debugging or running the demo).
+          redirfd -w 0 /dev/null
+
+          define PCI_LOCATION 0000:00:19.0
+          define PCI_PATH /sys/bus/pci/devices/''${PCI_LOCATION}
+
+          # Unbind the network device from the driver it's already
+          # attached to, if any.
+          foreground {
+            redirfd -w 1 ''${PCI_PATH}/driver/unbind
+            printf "%s" $PCI_LOCATION
+          }
+
+          # Tell the VFIO driver it should support our device.  This
+          # is allowed to fail because it might already know that, in
+          # which case it'll return EEXIST.
+          if { modprobe vfio-pci }
+          backtick -in device_id {
+            if { dd bs=2 skip=1 count=2 status=none if=''${PCI_PATH}/vendor }
+            if { printf " " }
+            dd bs=2 skip=1 count=2 status=none if=''${PCI_PATH}/device
+          }
+          importas -iu device_id device_id
+          foreground {
+            redirfd -w 1 /sys/bus/pci/drivers/vfio-pci/new_id
+            printf "%s" $device_id
+          }
+
+          # Bind the device to the VFIO driver.  This is allowed to
+          # fail because the new_id operation we just tried will have
+          # bound it automatically for us if it succeeded.  In such a
+          # case, the kernel will return ENODEV (conistency!).
+          foreground {
+            redirfd -w 1 /sys/bus/pci/drivers/vfio-pci/bind
+            printf "%s" $PCI_LOCATION
+          }
+
+          # Because we allow both new_id and bind to fail, we need to
+          # manually make sure now that at least one of them succeeded
+          # and the device is actually attached to the vfio-driver.
+          if { test -e /sys/bus/pci/drivers/vfio-pci/''${PCI_LOCATION} }
+
+          foreground { mkdir env }
+
+          ${cloud-hypervisor}/bin/cloud-hypervisor
+            --api-socket env/cloud-hypervisor.sock
+            --console off
+            # --serial tty
+            --cmdline "console=ttyS0 panic=30 root=/dev/vda"
+            --device path=''${PCI_PATH}
+            --disk path=${sys-vms.net.rootfs.squashfs},readonly=on
+            --kernel ${sys-vms.net.linux.dev}/vmlinux
+        '';
+        type = writeText "net-type" ''
+          longrun
+        '';
+      };
+    };
+  });
+
+  servicesDir = makeServicesDir {
+    services = {
+      ".s6-svscan" = {
+        finish = writeShellScript ".s6-svscan-finish" "";
+      };
+    };
+  };
+in
+
+writeScriptBin "spectrum-testhost" ''
+  #! ${execline}/bin/execlineb -S0
+  export PATH ${makeBinPath [
+    coreutils curl execline gnused gnutar iproute iptables jq kmod mktuntap rsync
+    s6 s6-rc
+  ]}
+
+  if { redirfd -w 1 /proc/sys/net/ipv4/ip_forward echo 1 }
+
+  importas -iu runtime_dir XDG_RUNTIME_DIR
+  backtick -in TOP { mktemp -dp $runtime_dir spectrum.XXXXXXXXXX }
+  importas -iu top TOP
+  if { echo $top }
+  if { rsync -r --chmod=Du+w ${servicesDir}/ ''${top}/service }
+  background {
+    if { mkdir -p ''${top}/s6-rc/compiled }
+    if { tar -C ''${top}/s6-rc/compiled -xf ${compiledRcServicesDir} }
+    s6-rc-init -c ''${top}/s6-rc/compiled -l ''${top}/s6-rc/live ''${top}/service
+  }
+  s6-svscan ''${top}/service
+''
diff --git a/pkgs/os-specific/linux/spectrum/vm/app/default.nix b/pkgs/os-specific/linux/spectrum/vm/app/default.nix
new file mode 100644
index 00000000000..f8ff480932c
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/vm/app/default.nix
@@ -0,0 +1,58 @@
+{ runCommand, writeScript, writeText, makeRootfs
+, busybox, execline, linux_vm, jq, iproute
+}:
+
+runCommand "vm-app" rec {
+  linux = linux_vm;
+
+  login = writeScript "login" ''
+    #! ${execline}/bin/execlineb -s0
+    unexport !
+    ${busybox}/bin/login -p -f root $@
+  '';
+
+  rootfs = makeRootfs {
+    rcServices.ok-all = {
+      type = writeText "ok-all-type" ''
+        bundle
+      '';
+      contents = writeText "ok-all-contents" ''
+        net
+      '';
+    };
+
+    rcServices.net = {
+      type = writeText "net-type" ''
+        oneshot
+      '';
+      up = writeText "net-up" ''
+        backtick -i LOCAL_IP {
+          pipeline { ip -j link show eth0 }
+          pipeline { jq -r ".[0].address | split(\":\") | .[4:6] | \"0x\" + .[]" }
+          xargs printf "100.64.%d.%d"
+        }
+        importas -iu LOCAL_IP LOCAL_IP
+
+        if { ip address add ''${LOCAL_IP}/32 dev eth0 }
+        if { ip link set eth0 up }
+        if { ip route add 169.254.0.1 dev eth0 }
+        ip route add default via 169.254.0.1 dev eth0
+      '';
+    };
+
+    services.getty.run = writeScript "getty-run" ''
+      #! ${execline}/bin/execlineb -P
+      ${busybox}/bin/getty -i -n -l ${login} 38400 ttyS0
+    '';
+
+    path = [ iproute jq ];
+  };
+
+  inherit (rootfs) squashfs;
+  vmID = 0;
+} ''
+  mkdir $out
+  echo "$vmID" > $out/vm-id
+  ln -s $linux/bzImage $out/kernel
+  ln -s $squashfs $out/squashfs
+''
diff --git a/pkgs/os-specific/linux/spectrum/vm/comp/default.nix b/pkgs/os-specific/linux/spectrum/vm/comp/default.nix
new file mode 100644
index 00000000000..eb6317ed6f2
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/vm/comp/default.nix
@@ -0,0 +1,73 @@
+{ lib, makeRootfs, runCommand, writeScript, writeText
+, busybox, emacs-nox, execline, gcc, linux_vm, s6, sommelier, source-code-pro
+, tinywl, westonLite, zsh
+}:
+
+runCommand "vm-comp" rec {
+  linux = linux_vm;
+
+  path = [
+    busybox emacs-nox execline gcc s6 sommelier tinywl westonLite zsh
+  ];
+
+  login = writeScript "login" ''
+    #! ${execline}/bin/execlineb -s0
+    unexport !
+    ${busybox}/bin/login -p -f root $@
+  '';
+
+  rootfs = makeRootfs {
+    services.getty.run = writeScript "getty-run" ''
+      #! ${execline}/bin/execlineb -P
+      ${busybox}/bin/getty -i -n -l ${login} 38400 ttyS0
+    '';
+
+    rcServices.ok-all = {
+      type = writeText "ok-all-type" ''
+        bundle
+      '';
+      contents = writeText "ok-all-contents" ''
+        compositor
+      '';
+    };
+
+    rcServices.compositor = {
+      type = writeText "compositor-type" ''
+        longrun
+      '';
+      run = writeScript "compositor-run" ''
+        #! ${execline}/bin/execlineb -S0
+
+        s6-applyuidgid -u 1000 -g 1000
+
+        export HOME /
+        export PATH ${lib.makeBinPath path}
+        export XDG_RUNTIME_DIR /run/user/1000
+        export XKB_DEFAULT_LAYOUT dvorak
+
+        ${sommelier}/bin/sommelier
+        ${tinywl}/bin/tinywl -s "weston-terminal --shell $(command -v zsh)"
+      '';
+      dependencies = writeText "compositor-dependencies" ''
+        wl0
+      '';
+    };
+
+    rcServices.wl0 = {
+      type = writeText "wl0-type" ''
+        oneshot
+      '';
+      up = writeText "wl0-run" ''
+        chown user /dev/wl0
+      '';
+    };
+
+    fonts = [ source-code-pro ];
+  };
+
+  inherit (rootfs) squashfs;
+} ''
+  mkdir $out
+  ln -s $linux/bzImage $out/kernel
+  ln -s $squashfs $out/squashfs
+''
diff --git a/pkgs/os-specific/linux/spectrum/vm/default.nix b/pkgs/os-specific/linux/spectrum/vm/default.nix
new file mode 100644
index 00000000000..f5d591a960a
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/vm/default.nix
@@ -0,0 +1,9 @@
+{ callPackage }:
+
+{
+  app = callPackage ./app { };
+
+  comp = callPackage ./comp { };
+
+  net = callPackage ./net { };
+}
diff --git a/pkgs/os-specific/linux/spectrum/vm/net/default.nix b/pkgs/os-specific/linux/spectrum/vm/net/default.nix
new file mode 100644
index 00000000000..5921b62fcf7
--- /dev/null
+++ b/pkgs/os-specific/linux/spectrum/vm/net/default.nix
@@ -0,0 +1,167 @@
+{ lib, makeRootfs, runCommand, writeScript, writeText
+, busybox, connman, dbus, execline, iptables, iproute, jq, linux_vm, mdevd
+}:
+
+runCommand "vm-net" rec {
+  linux = linux_vm.override {
+    structuredExtraConfig = with lib.kernel; {
+      E1000E = yes;
+      IGB = yes;
+      PACKET = yes;
+
+      IP_NF_NAT = yes;
+      IP_NF_IPTABLES = yes;
+      IP_NF_TARGET_MASQUERADE = yes;
+      NF_CONNTRACK = yes;
+    };
+  };
+
+  login = writeScript "login" ''
+    #! ${execline}/bin/execlineb -s0
+    unexport !
+    ${busybox}/bin/login -p -f root $@
+  '';
+
+  rootfs = makeRootfs {
+    rcServices.ok-all = {
+      type = writeText "ok-all-type" ''
+        bundle
+      '';
+      contents = writeText "ok-all-contents" ''
+        mdevd-coldplug
+      '';
+    };
+
+    rcServices.mdevd = {
+      type = writeText "mdevd-type" ''
+        longrun
+      '';
+      run = writeScript "mdevd-run" ''
+        #! ${execline}/bin/execlineb -P
+        ${mdevd}/bin/mdevd -D3 -f ${writeText "mdevd.conf" ''
+          $INTERFACE=.* 0:0 660 ! @${writeScript "interface" ''
+            #! ${execline}/bin/execlineb -S0
+
+            multisubstitute {
+              importas -i DEVPATH DEVPATH
+              importas -i INTERFACE INTERFACE
+            }
+
+            ifte
+
+            {
+              # This interface is connected to another VM.
+
+              # Our IP is encoded in the NIC-specific portion of the
+              # interface's MAC address.
+              backtick -i CLIENT_IP {
+                pipeline { ip -j link show $INTERFACE }
+                pipeline { jq -r ".[0].address | split(\":\") | .[4:6] | \"0x\" + .[]" }
+                xargs printf "100.64.%d.%d"
+              }
+              importas -iu CLIENT_IP CLIENT_IP
+
+              if { ip address add 169.254.0.1/32 dev $INTERFACE }
+              if { ip link set $INTERFACE up }
+              ip route add $CLIENT_IP dev $INTERFACE
+            }
+
+            {
+              if { test $INTERFACE != lo }
+              # This is a physical connection to a network device.
+              if { iptables -t nat -A POSTROUTING -o $INTERFACE -j MASQUERADE }
+              s6-rc -u change connman
+            }
+
+            grep -iq ^0A:B3:EC: /sys/class/net/''${INTERFACE}/address
+          ''}
+        ''}
+      '';
+      notification-fd = writeText "mdevd-notification-fd" ''
+        3
+      '';
+      dependencies = writeText "mdevd-dependencies" ''
+        sysctl
+      '';
+    };
+
+    rcServices.mdevd-coldplug = {
+      type = writeText "mdevd-coldplug-type" ''
+        oneshot
+      '';
+      up = writeText "mdevd-run" ''
+        ${mdevd}/bin/mdevd-coldplug
+      '';
+      dependencies = writeText "mdevd-coldplug-dependencies" ''
+        mdevd
+      '';
+    };
+
+    rcServices.dbus = {
+      type = writeText "dbus-daemon" ''
+        longrun
+      '';
+      run = writeScript "dbus-daemon-run" ''
+        #! ${execline}/bin/execlineb -S0
+        foreground { mkdir /run/dbus }
+        # Busybox cp doesn't have -n to avoid copying to paths that
+        # already exist, but we can abuse -u for the same effect,
+        # since every file in the store is from Jan 1 1970.
+        foreground { cp -u ${dbus}/libexec/dbus-daemon-launch-helper /run }
+        foreground { chgrp messagebus /run/dbus-daemon-launch-helper }
+        foreground { chmod 4550 /run/dbus-daemon-launch-helper }
+        ${dbus}/bin/dbus-daemon
+          --nofork --nosyslog --nopidfile --config-file=/etc/dbus-1/system.conf
+      '';
+    };
+
+    rcServices.connman = {
+      type = writeText "connman-type" ''
+        longrun
+      '';
+      run = writeScript "connman-run" ''
+        #! ${execline}/bin/execlineb -S0
+        backtick -in HARDWARE_INTERFACES {
+          pipeline {
+            find -L /sys/class/net -mindepth 2 -maxdepth 2 -name address -print0
+          }
+
+          # Filter out other VMs and the loopback device.
+          pipeline { xargs -0 grep -iL ^\\(0A:B3:EC:\\|00:00:00:00:00:00$\\) }
+
+          # Extract the interface names from the address file paths.
+          awk -F/ "{if (NR > 1) printf \",\"; printf \"%s\", $5}"
+        }
+        importas -iu HARDWARE_INTERFACES HARDWARE_INTERFACES
+
+        ${connman}/bin/connmand -ni $HARDWARE_INTERFACES
+      '';
+      dependencies = writeText "connman-dependencies" ''
+        dbus
+      '';
+    };
+
+    rcServices.sysctl = {
+      type = writeText "sysctl-type" ''
+        oneshot
+      '';
+      up = writeText "sysctl-up" ''
+        redirfd -w 1 /proc/sys/net/ipv4/ip_forward
+        echo 1
+      '';
+    };
+
+    services.getty.run = writeScript "getty-run" ''
+      #! ${execline}/bin/execlineb -P
+      ${busybox}/bin/getty -i -n -l ${login} 38400 ttyS0
+    '';
+
+    path = [ iproute iptables jq ];
+  };
+
+  inherit (rootfs) squashfs;
+} ''
+  mkdir $out
+  ln -s $linux/bzImage $out/kernel
+  ln -s $squashfs $out/squashfs
+''