Raw block device passthrough support

This commit is contained in:
AlexSSD7 2023-08-29 15:31:17 +01:00
commit 003b562e48
7 changed files with 133 additions and 26 deletions

View file

@ -4,7 +4,11 @@ package vm
import (
"os/exec"
"path/filepath"
"strings"
"syscall"
"github.com/pkg/errors"
)
func prepareVMCmd(cmd *exec.Cmd) {
@ -17,3 +21,26 @@ func prepareVMCmd(cmd *exec.Cmd) {
func terminateProcess(pid int) error {
return syscall.Kill(-pid, syscall.SIGTERM)
}
// This is never used except for a band-aid that would check
// that there are no double-mounts.
func checkDeviceSeemsMounted(devPathPrefix string) (bool, error) {
absDevPathPrefix, err := filepath.Abs(devPathPrefix)
if err != nil {
return false, errors.Wrap(err, "get abs path")
}
mounts, err := exec.Command("mount").Output()
if err != nil {
return false, errors.Wrap(err, "run mount command")
}
for _, line := range strings.Split(string(mounts), "\n") {
// I know, I know, this is a rare band-aid.
if strings.HasPrefix(line, devPathPrefix) || strings.HasPrefix(line, absDevPathPrefix) {
return true, nil
}
}
return false, nil
}

15
vm/passthrough.go Normal file
View file

@ -0,0 +1,15 @@
package vm
type USBDevicePassthroughConfig struct {
VendorID uint16
ProductID uint16
}
type BlockDevicePassthroughConfig struct {
Path string
}
type PassthroughConfig struct {
USB []USBDevicePassthroughConfig
Block []BlockDevicePassthroughConfig
}

View file

@ -103,10 +103,10 @@ func (vm *VM) sshSetup() (ssh.Signer, error) {
installSSHDCmd := ""
if vm.installSSH {
installSSHDCmd = "apk add openssh; "
installSSHDCmd = "ifconfig eth0 up && ifconfig lo up && udhcpc; apk add openssh; "
}
cmd := `do_setup () { sh -c "set -ex; setup-alpine -q; ` + installSSHDCmd + `mkdir -p ~/.ssh; echo ` + shellescape.Quote(string(sshPublicKey)) + ` > ~/.ssh/authorized_keys; rc-update add sshd; rc-service sshd start"; echo "SERIAL"" ""STATUS: $?"; }; do_setup` + "\n"
cmd := `do_setup () { sh -c "set -ex; ` + installSSHDCmd + `mkdir -p ~/.ssh; echo ` + shellescape.Quote(string(sshPublicKey)) + ` > ~/.ssh/authorized_keys; rc-update add sshd; rc-service sshd start"; echo "SERIAL"" ""STATUS: $?"; }; do_setup` + "\n"
err = vm.writeSerial([]byte(cmd))
if err != nil {

View file

@ -9,11 +9,6 @@ import (
"github.com/pkg/errors"
)
type USBDevicePassthroughConfig struct {
VendorID uint16
ProductID uint16
}
type PortForwardingRule struct {
HostIP net.IP
HostPort uint16

View file

@ -54,6 +54,8 @@ type VM struct {
// These are to be interacted with using `atomic` package
disposed uint32
canceled uint32
originalCfg VMConfig
}
type DriveConfig struct {
@ -67,7 +69,7 @@ type VMConfig struct {
MemoryAlloc uint32 // In KiB.
USBDevices []USBDevicePassthroughConfig
PassthroughConfig PassthroughConfig
ExtraPortForwardingRules []PortForwardingRule
// Timeouts
@ -149,26 +151,43 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
cmdArgs = append(cmdArgs, "-device", "virtio-gpu-device")
}
if len(cfg.USBDevices) != 0 {
cmdArgs = append(cmdArgs, "-device", "nec-usb-xhci")
for _, dev := range cfg.USBDevices {
cmdArgs = append(cmdArgs, "-device", "usb-host,vendorid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.VendorID))+",productid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.ProductID)))
}
}
for i, extraDrive := range cfg.Drives {
_, err = os.Stat(extraDrive.Path)
if err != nil {
return nil, errors.Wrapf(err, "stat extra drive #%v path", i)
}
driveArgs := "file=" + shellescape.Quote(extraDrive.Path) + ",format=qcow2,if=virtio"
driveArgs := "file=" + shellescape.Quote(extraDrive.Path) + ",format=qcow2,if=none,id=disk" + fmt.Sprint(i)
if extraDrive.SnapshotMode {
driveArgs += ",snapshot=on"
}
cmdArgs = append(cmdArgs, "-drive", driveArgs)
cmdArgs = append(cmdArgs, "-drive", driveArgs, "-device", "virtio-blk-pci,drive=disk"+fmt.Sprint(i)+",bootindex="+fmt.Sprint(i))
}
if len(cfg.PassthroughConfig.USB) != 0 {
cmdArgs = append(cmdArgs, "-device", "nec-usb-xhci")
for _, dev := range cfg.PassthroughConfig.USB {
cmdArgs = append(cmdArgs, "-device", "usb-host,vendorid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.VendorID))+",productid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.ProductID)))
}
}
for _, dev := range cfg.PassthroughConfig.Block {
// It's always a user's responsibility to ensure that no drives are mounted
// in both host and guest system. This should serve as the last resort.
{
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
if err != nil {
return nil, errors.Wrapf(err, "check whether device seems to be mounted (path '%v')", dev.Path)
}
if seemsMounted {
return nil, fmt.Errorf("device '%v' is already mounted in the host system", dev.Path)
}
}
cmdArgs = append(cmdArgs, "-drive", "file="+shellescape.Quote(dev.Path)+",format=raw,aio=native,cache=none")
}
// We're not using clean `cdromImagePath` here because it is set to "."
@ -235,6 +254,8 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
osUpTimeout: osUpTimeout,
sshUpTimeout: sshUpTimeout,
originalCfg: cfg,
}
vm.resetSerialStdout()
@ -252,6 +273,8 @@ func (vm *VM) Run() error {
return errors.Wrap(err, "start qemu cmd")
}
go vm.runPeriodicHostMountChecker()
var globalErrsMu sync.Mutex
var globalErrs []error
@ -499,3 +522,30 @@ func (vm *VM) DialSCP() (*scp.Client, error) {
func (vm *VM) SSHUpNotifyChan() chan struct{} {
return vm.sshReadyCh
}
// It's always a user's responsibility to ensure that no drives are mounted
// in both host and guest system. This should serve as the last resort.
func (vm *VM) runPeriodicHostMountChecker() {
if len(vm.originalCfg.PassthroughConfig.Block) == 0 {
return
}
for {
select {
case <-vm.ctx.Done():
return
case <-time.After(time.Second):
for _, dev := range vm.originalCfg.PassthroughConfig.Block {
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
if err != nil {
vm.logger.Warn("Failed to check if a passed device seems to be mounted", "dev-path", dev.Path)
continue
}
if seemsMounted {
panic(fmt.Sprintf("CRITICAL: Passed-through device '%v' appears to have been mounted on the host OS. Forcefully exiting now to prevent data corruption.", dev.Path))
}
}
}
}
}