From 8d2adc63e6ce9ea99f6f4213addaa95e5703fcc6 Mon Sep 17 00:00:00 2001 From: Jon Sahlberg Date: Tue, 9 Apr 2024 12:19:51 +0300 Subject: [PATCH] Initial AudioVM implementation with pipewire and pulseaudio Initial version of AudioVM with Pipewire backend and pulseaudio TCP remote communication layer for the guest VMs. Note that this is not really secure design (yet) basically all VMs can access the pulseaudio TCP service. Signed-off-by: Jon Sahlberg --- modules/common/hardware/definition.nix | 35 +++++ .../lenovo-x1/definitions/default.nix | 1 + .../lenovo-x1/definitions/x1-gen10.nix | 2 + .../lenovo-x1/definitions/x1-gen11.nix | 30 +++++ modules/microvm/default.nix | 1 + .../virtualization/microvm/audiovm.nix | 120 ++++++++++++++++++ .../microvm/virtualization/microvm/guivm.nix | 1 + targets/lenovo-x1/appvms/chromium.nix | 24 ++-- targets/lenovo-x1/appvms/element.nix | 23 ++-- targets/lenovo-x1/audiovmExtraModules.nix | 97 ++++++++++++++ targets/lenovo-x1/everything.nix | 21 ++- targets/lenovo-x1/guivmExtraModules.nix | 3 - 12 files changed, 324 insertions(+), 34 deletions(-) create mode 100644 modules/microvm/virtualization/microvm/audiovm.nix create mode 100644 targets/lenovo-x1/audiovmExtraModules.nix diff --git a/modules/common/hardware/definition.nix b/modules/common/hardware/definition.nix index 2e99fdfb59..36dbc9146f 100644 --- a/modules/common/hardware/definition.nix +++ b/modules/common/hardware/definition.nix @@ -139,5 +139,40 @@ SUBSYSTEM=="input",ATTRS{name}=="AT Translated Set 2 keyboard",GROUP="kvm" ''; }; + + audio = { + # With the current implementation, the whole PCI IOMMU group 14: + # 00:1f.x in the example from Lenovo X1 Carbon + # must be defined for passthrough to AudioVM + pciDevices = mkOption { + description = "PCI Devices to passthrough to AudioVM"; + type = types.listOf pciDevSubmodule; + default = []; + example = literalExpression '' + [ + { + path = "0000:00:1f.0"; + vendorId = "8086"; + productId = "5194"; + } + { + path = "0000:00:1f.3"; + vendorId = "8086"; + productId = "51ca"; + } + { + path = "0000:00:1f.4"; + vendorId = "8086"; + productId = "51a3"; + } + { + path = "0000:00:1f.5"; + vendorId = "8086"; + productId = "51a4"; + } + ] + ''; + }; + }; }; } diff --git a/modules/common/hardware/lenovo-x1/definitions/default.nix b/modules/common/hardware/lenovo-x1/definitions/default.nix index f18b9813f7..c3c8ce5e6a 100644 --- a/modules/common/hardware/lenovo-x1/definitions/default.nix +++ b/modules/common/hardware/lenovo-x1/definitions/default.nix @@ -12,6 +12,7 @@ in { inherit (hwDefinition) disks; inherit (hwDefinition) network; inherit (hwDefinition) gpu; + inherit (hwDefinition) audio; # Notes: # 1. This assembles udev rules for different hw configurations (i.e., different mice/touchpads) by adding diff --git a/modules/common/hardware/lenovo-x1/definitions/x1-gen10.nix b/modules/common/hardware/lenovo-x1/definitions/x1-gen10.nix index 8cfc01b10a..a4ccc39ff5 100644 --- a/modules/common/hardware/lenovo-x1/definitions/x1-gen10.nix +++ b/modules/common/hardware/lenovo-x1/definitions/x1-gen10.nix @@ -29,4 +29,6 @@ productId = "46a6"; } ]; + + audio.pciDevices = []; } diff --git a/modules/common/hardware/lenovo-x1/definitions/x1-gen11.nix b/modules/common/hardware/lenovo-x1/definitions/x1-gen11.nix index 72818febd2..2cde94f8e6 100644 --- a/modules/common/hardware/lenovo-x1/definitions/x1-gen11.nix +++ b/modules/common/hardware/lenovo-x1/definitions/x1-gen11.nix @@ -38,4 +38,34 @@ productId = "a7a1"; } ]; + + # With the current implementation, the whole PCI IOMMU group 14: + # 00:1f.x in the example from Lenovo X1 Carbon + # must be defined for passthrough to AudioVM + audio.pciDevices = [ + { + # ISA bridge: Intel Corporation Raptor Lake LPC/eSPI Controller (rev 01) + path = "0000:00:1f.0"; + vendorId = "8086"; + productId = "5194"; + } + { + # Audio device: Intel Corporation Raptor Lake-P/U/H cAVS (rev 01) + path = "0000:00:1f.3"; + vendorId = "8086"; + productId = "51ca"; + } + { + # SMBus: Intel Corporation Alder Lake PCH-P SMBus Host Controller (rev 01) + path = "0000:00:1f.4"; + vendorId = "8086"; + productId = "51a3"; + } + { + # Serial bus controller: Intel Corporation Alder Lake-P PCH SPI Controller (rev 01) + path = "0000:00:1f.5"; + vendorId = "8086"; + productId = "51a4"; + } + ]; } diff --git a/modules/microvm/default.nix b/modules/microvm/default.nix index d9c58bcfb9..9607af02a4 100644 --- a/modules/microvm/default.nix +++ b/modules/microvm/default.nix @@ -9,6 +9,7 @@ ./virtualization/microvm/netvm.nix ./virtualization/microvm/appvm.nix ./virtualization/microvm/guivm.nix + ./virtualization/microvm/audiovm.nix ./networking.nix ]; } diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix new file mode 100644 index 0000000000..0c6e650bc3 --- /dev/null +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -0,0 +1,120 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: let + configHost = config; + vmName = "audio-vm"; + macAddress = "02:00:00:03:03:03"; + audiovmBaseConfiguration = { + imports = [ + (import ./common/vm-networking.nix {inherit vmName macAddress;}) + ({ + lib, + pkgs, + ... + }: { + ghaf = { + users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; + + development = { + ssh.daemon.enable = lib.mkDefault configHost.ghaf.development.ssh.daemon.enable; + debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; + nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; + }; + systemd = { + enable = true; + withName = "audiovm-systemd"; + withNss = true; + withResolved = true; + withTimesyncd = true; + withDebug = configHost.ghaf.profiles.debug.enable; + }; + }; + + environment = { + systemPackages = [ + pkgs.pulseaudio + pkgs.pamixer + pkgs.pipewire + ]; + }; + + system.stateVersion = lib.trivial.release; + + nixpkgs.buildPlatform.system = configHost.nixpkgs.buildPlatform.system; + nixpkgs.hostPlatform.system = configHost.nixpkgs.hostPlatform.system; + + microvm = { + optimize.enable = true; + vcpu = 1; + mem = 256; + hypervisor = "qemu"; + shares = [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + } + ]; + writableStoreOverlay = lib.mkIf config.ghaf.development.debug.tools.enable "/nix/.rw-store"; + qemu = { + machine = + { + # Use the same machine type as the host + x86_64-linux = "q35"; + aarch64-linux = "virt"; + } + .${configHost.nixpkgs.hostPlatform.system}; + }; + }; + + imports = [ + ../../../common + ]; + + # Fixed IP-address for debugging subnet + systemd.network.networks."10-ethint0".addresses = [ + { + addressConfig.Address = "192.168.101.4/24"; + } + ]; + }) + ]; + }; + cfg = config.ghaf.virtualization.microvm.audiovm; + # Importing kernel builder function and building guest_audio_hardened_kernel + # Maybe this also needs actual hardenening & remove unused modules etc. +in { + options.ghaf.virtualization.microvm.audiovm = { + enable = lib.mkEnableOption "AudioVM"; + + extraModules = lib.mkOption { + description = '' + List of additional modules to be imported and evaluated as part of + AudioVM's NixOS configuration. + ''; + default = []; + }; + }; + + config = lib.mkIf cfg.enable { + microvm.vms."${vmName}" = { + autostart = true; + config = + audiovmBaseConfiguration + // { + # boot.kernelPackages = + # lib.mkIf config.ghaf.guest.kernel.hardening.audio.enable + # (pkgs.linuxPackagesFor guest_audio_hardened_kernel); + + imports = + audiovmBaseConfiguration.imports + ++ cfg.extraModules; + }; + }; + }; +} diff --git a/modules/microvm/virtualization/microvm/guivm.nix b/modules/microvm/virtualization/microvm/guivm.nix index 8dee3cdde1..7be4ee3b14 100644 --- a/modules/microvm/virtualization/microvm/guivm.nix +++ b/modules/microvm/virtualization/microvm/guivm.nix @@ -67,6 +67,7 @@ pkgs.waypipe pkgs.networkmanagerapplet pkgs.nm-launcher + pkgs.pamixer ]; }; diff --git a/targets/lenovo-x1/appvms/chromium.nix b/targets/lenovo-x1/appvms/chromium.nix index 8caaf9319a..8779a4b995 100644 --- a/targets/lenovo-x1/appvms/chromium.nix +++ b/targets/lenovo-x1/appvms/chromium.nix @@ -21,7 +21,7 @@ in { ''; in [ pkgs.chromium - pkgs.pamixer + pkgs.pulseaudio pkgs.xdg-utils xdgPdfItem xdgOpenPdf @@ -32,22 +32,24 @@ in { cores = 4; extraModules = [ { - # Enable pulseaudio for user ghaf + # Enable pulseaudio for Chromium VM + security.rtkit.enable = true; sound.enable = true; hardware.pulseaudio.enable = true; - users.extraUsers.ghaf.extraGroups = ["audio"]; + users.extraUsers.ghaf.extraGroups = ["audio" "video"]; + + hardware.pulseaudio.extraConfig = '' + load-module module-tunnel-sink sink_name=chromium-speaker server=audio-vm.ghaf:4713 format=s16le channels=2 rate=48000 + load-module module-tunnel-source source_name=chromium-mic server=audio-vm.ghaf:4713 format=s16le channels=2 rate=48000 + + # Set sink and source default max volume to about 90% (0-65536) + set-sink-volume chromium-speaker 60000 + set-source-volume chromium-mic 60000 + ''; time.timeZone = "Asia/Dubai"; microvm.qemu.extraArgs = [ - # Connect sound device to hosts pulseaudio socket - "-audiodev" - "pa,id=pa1,server=unix:/run/pulse/native" - # Add HDA sound device to guest - "-device" - "intel-hda" - "-device" - "hda-duplex,audiodev=pa1" # Lenovo X1 integrated usb webcam "-device" "qemu-xhci" diff --git a/targets/lenovo-x1/appvms/element.nix b/targets/lenovo-x1/appvms/element.nix index 635533bfd4..3158d7bea2 100644 --- a/targets/lenovo-x1/appvms/element.nix +++ b/targets/lenovo-x1/appvms/element.nix @@ -19,6 +19,21 @@ extraArgs = ["-n"]; # Do not wait for a client to connect before polling }; + # Enable pulseaudio for Element VM + security.rtkit.enable = true; + sound.enable = true; + hardware.pulseaudio.enable = true; + users.extraUsers.ghaf.extraGroups = ["audio" "video"]; + + hardware.pulseaudio.extraConfig = '' + load-module module-tunnel-sink sink_name=element-speaker server=audio-vm.ghaf:4713 format=s16le channels=2 rate=48000 + load-module module-tunnel-source source_name=element-mic server=audio-vm.ghaf:4713 format=s16le channels=2 rate=48000 + + # Set sink and source default max volume to about 90% (0-65536) + set-sink-volume element-speaker 60000 + set-source-volume element-mic 60000 + ''; + systemd.services.element-gps = { description = "Element-gps is a GPS location provider for Element websocket interface."; enable = true; @@ -42,14 +57,6 @@ # External USB GPS receiver "-device" "usb-host,vendorid=0x067b,productid=0x23a3" - # Connect sound device to hosts pulseaudio socket - "-audiodev" - "pa,id=pa1,server=unix:/run/pulse/native" - # Add HDA sound device to guest - "-device" - "intel-hda" - "-device" - "hda-duplex,audiodev=pa1" ]; } ]; diff --git a/targets/lenovo-x1/audiovmExtraModules.nix b/targets/lenovo-x1/audiovmExtraModules.nix new file mode 100644 index 0000000000..670c323e87 --- /dev/null +++ b/targets/lenovo-x1/audiovmExtraModules.nix @@ -0,0 +1,97 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +# +{ + lib, + pkgs, + microvm, + configH, + ... +}: let + # TCP port used by Pipewire-pulseaudio service + pulseaudioTcpPort = 4713; + + audiovmPCIPassthroughModule = { + microvm.devices = lib.mkForce ( + builtins.map (d: { + bus = "pci"; + inherit (d) path; + }) + configH.ghaf.hardware.definition.audio.pciDevices + ); + }; + + audiovmExtraConfigurations = { + ghaf.hardware.definition.network.pciDevices = configH.ghaf.hardware.definition.network.pciDevices; + + time.timeZone = "Asia/Dubai"; + + # Enable pipewire service for audioVM with pulseaudio support + security.rtkit.enable = true; + sound.enable = true; + + services.pipewire = { + enable = true; + # alsa.enable = true; + # alsa.support32Bit = true; + pulse.enable = true; + systemWide = true; + }; + + environment.etc."pipewire/pipewire.conf.d/10-remote-simple.conf".text = '' + context.modules = [ + { name = libpipewire-module-protocol-pulse + args = { + server.address = [ + "tcp:4713" # IPv4 and IPv6 on all addresses + ]; + pulse.min.req = 128/48000; # 2.7ms + pulse.default.req = 960/48000; # 20 milliseconds + pulse.min.frag = 128/48000; # 2.7ms + pulse.default.frag = 512/48000; # ~10 ms + pulse.default.tlength = 512/48000; # ~10 ms + pulse.min.quantum = 128/48000; # 2.7ms + } + } + ] + ''; + + hardware.pulseaudio.extraConfig = '' + # Set sink and source default max volume to about 75% (0-65536) + set-sink-volume @DEFAULT_SINK@ 48000 + set-source-volume @DEFAULT_SOURCE@ 48000 + ''; + + # Allow ghaf user to access pulseaudio and pipewire + users.extraUsers.ghaf.extraGroups = ["audio" "video" "pulse-access" "pipewire"]; + + # Dummy service to get pipewire and pulseaudio services started at boot + # Normally Pipewire and pulseaudio are started when they are needed by user, + # We don't have users in audiovm so we need to give PW/PA a slight kick.. + # This calls pulseaudios pa-info binary to get information about pulseaudio current + # state which starts pipewire-pulseaudio service in the process. + + systemd.services.pulseaudio-starter = { + after = ["pipewire.service" "network-online.target"]; + requires = ["pipewire.service"]; + wantedBy = ["default.target"]; + path = [pkgs.coreutils]; + enable = true; + serviceConfig = { + User = "ghaf"; + Group = "ghaf"; + }; + script = ''${pkgs.pulseaudio}/bin/pa-info > /dev/null 2>&1''; + }; + + # Open TCP port for the PDF XDG socket + networking.firewall.allowedTCPPorts = [pulseaudioTcpPort]; + + microvm.qemu.extraArgs = [ + ]; + }; +in [ + ./sshkeys.nix + audiovmPCIPassthroughModule + audiovmExtraConfigurations +] diff --git a/targets/lenovo-x1/everything.nix b/targets/lenovo-x1/everything.nix index fb24b326eb..d82e7cc381 100644 --- a/targets/lenovo-x1/everything.nix +++ b/targets/lenovo-x1/everything.nix @@ -43,22 +43,11 @@ }: let powerControl = pkgs.callPackage ../../packages/powercontrol {}; in { + security.polkit.enable = cfg.withPolkit; security.polkit.extraConfig = powerControl.polkitExtraConfig; services.udev.extraRules = hwDefinition.udevRules; time.timeZone = "Asia/Dubai"; - # Enable pulseaudio support for host as a service - sound.enable = true; - hardware.pulseaudio.enable = true; - hardware.pulseaudio.systemWide = true; - # Add systemd to require pulseaudio before starting chromium-vm - systemd.services."microvm@chromium-vm".after = ["pulseaudio.service"]; - systemd.services."microvm@chromium-vm".requires = ["pulseaudio.service"]; - - # Allow microvm user to access pulseaudio - hardware.pulseaudio.extraConfig = "load-module module-combine-sink module-native-protocol-unix auth-anonymous=1"; - users.extraUsers.microvm.extraGroups = ["audio" "pulse-access"]; - environment.etc.${config.ghaf.security.sshKeys.getAuthKeysFilePathInEtc} = import ./getAuthKeysSource.nix {inherit pkgs config;}; services.openssh = config.ghaf.security.sshKeys.sshAuthorizedKeysCommand; @@ -102,6 +91,13 @@ configH = config; }; }; + virtualization.microvm.audiovm = { + enable = true; + extraModules = import ./audiovmExtraModules.nix { + inherit lib pkgs microvm; + configH = config; + }; + }; virtualization.microvm.appvm = { enable = true; vms = import ./appvms/default.nix {inherit pkgs;}; @@ -130,6 +126,7 @@ vfioPciIds = mapPciIdsToString (filterDevices ( config.ghaf.hardware.definition.network.pciDevices ++ config.ghaf.hardware.definition.gpu.pciDevices + ++ config.ghaf.hardware.definition.audio.pciDevices )); in [ "intel_iommu=on,sm_on" diff --git a/targets/lenovo-x1/guivmExtraModules.nix b/targets/lenovo-x1/guivmExtraModules.nix index f360dc1f90..5d3dab9737 100644 --- a/targets/lenovo-x1/guivmExtraModules.nix +++ b/targets/lenovo-x1/guivmExtraModules.nix @@ -153,9 +153,6 @@ # Lenovo X1 AC adapter "-device" "acad" - # Connect sound device to hosts pulseaudio socket - "-audiodev" - "pa,id=pa1,server=unix:/run/pulse/native" ]; }; };