Device passthrough and root checks
This commit is contained in:
parent
433deeab5e
commit
64d3891c48
5 changed files with 151 additions and 101 deletions
65
cmd/utils.go
65
cmd/utils.go
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -17,41 +16,13 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/AlexSSD7/linsk/nettap"
|
"github.com/AlexSSD7/linsk/nettap"
|
||||||
|
"github.com/AlexSSD7/linsk/osspecifics"
|
||||||
"github.com/AlexSSD7/linsk/share"
|
"github.com/AlexSSD7/linsk/share"
|
||||||
"github.com/AlexSSD7/linsk/storage"
|
"github.com/AlexSSD7/linsk/storage"
|
||||||
"github.com/AlexSSD7/linsk/vm"
|
"github.com/AlexSSD7/linsk/vm"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkIfRoot() (bool, error) {
|
|
||||||
currentUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "get current user")
|
|
||||||
}
|
|
||||||
return currentUser.Username == "root", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func doUSBRootCheck() {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
// Administrator privileges are not required in Windows.
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// As for everything else, we will likely need root privileges
|
|
||||||
// for the USB passthrough.
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := checkIfRoot()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to check whether the command is ran by root", "error", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
slog.Warn("USB passthrough on your OS usually requires this program to be ran as root")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createStoreOrExit() *storage.Storage {
|
func createStoreOrExit() *storage.Storage {
|
||||||
store, err := storage.NewStorage(slog.With("caller", "storage"), dataDirFlag)
|
store, err := storage.NewStorage(slog.With("caller", "storage"), dataDirFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -92,8 +63,18 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
||||||
}
|
}
|
||||||
|
|
||||||
passthroughConfig = *passthroughConfigPtr
|
passthroughConfig = *passthroughConfigPtr
|
||||||
|
}
|
||||||
|
|
||||||
doUSBRootCheck()
|
if len(passthroughConfig.USB) != 0 {
|
||||||
|
// Log USB-related warnings.
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// TODO: To document: installation of libusbK driver with Zadig utility.
|
||||||
|
slog.Warn("USB passthrough is unstable on Windows and requires installation of libusbK driver. Please consider using raw block device passthrough instead.")
|
||||||
|
case "darwin":
|
||||||
|
slog.Warn("USB passthrough is unstable on macOS. Please consider using raw block device passthrough instead.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tapRuntimeCtx *share.NetTapRuntimeContext
|
var tapRuntimeCtx *share.NetTapRuntimeContext
|
||||||
|
|
@ -314,6 +295,15 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDevicePassthroughConfig(val string) (*vm.PassthroughConfig, error) {
|
func getDevicePassthroughConfig(val string) (*vm.PassthroughConfig, error) {
|
||||||
|
isRoot, err := osspecifics.CheckRunAsRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "check whether the program is run as root")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRoot {
|
||||||
|
return nil, fmt.Errorf("device passthrough of any type requires root (admin) privileges")
|
||||||
|
}
|
||||||
|
|
||||||
valSplit := strings.Split(val, ":")
|
valSplit := strings.Split(val, ":")
|
||||||
if want, have := 2, len(valSplit); want != have {
|
if want, have := 2, len(valSplit); want != have {
|
||||||
return nil, fmt.Errorf("bad device passthrough syntax: wrong items split by ':' count: want %v, have %v", want, have)
|
return nil, fmt.Errorf("bad device passthrough syntax: wrong items split by ':' count: want %v, have %v", want, have)
|
||||||
|
|
@ -344,16 +334,11 @@ func getDevicePassthroughConfig(val string) (*vm.PassthroughConfig, error) {
|
||||||
}, nil
|
}, nil
|
||||||
case "dev":
|
case "dev":
|
||||||
devPath := filepath.Clean(valSplit[1])
|
devPath := filepath.Clean(valSplit[1])
|
||||||
// TODO: This is for Linux only. Should support Windows as well.
|
|
||||||
// stat, err := os.Stat(devPath)
|
|
||||||
// if err != nil {
|
|
||||||
// slog.Error("Failed to stat the device path", "error", err.Error(), "path", devPath)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// isDev := stat.Mode()&os.ModeDevice != 0
|
err := osspecifics.CheckValidDevicePath(devPath)
|
||||||
// if !isDev {
|
if err != nil {
|
||||||
// slog.Error("Provided path is not a path to a valid block device", "path", devPath, "file-mode", stat.Mode())
|
return nil, errors.Wrapf(err, "check whether device path is valid '%v'", devPath)
|
||||||
// }
|
}
|
||||||
|
|
||||||
return &vm.PassthroughConfig{Block: []vm.BlockDevicePassthroughConfig{{
|
return &vm.PassthroughConfig{Block: []vm.BlockDevicePassthroughConfig{{
|
||||||
Path: devPath,
|
Path: devPath,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package vm
|
package osspecifics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
@ -11,20 +14,20 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prepareVMCmd(cmd *exec.Cmd) {
|
func SetNewProcessGroupCmd(cmd *exec.Cmd) {
|
||||||
// This is to prevent Ctrl+C propagating to the child process.
|
// This is to prevent Ctrl+C propagating to the child process.
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func terminateProcess(pid int) error {
|
func TerminateProcess(pid int) error {
|
||||||
return syscall.Kill(-pid, syscall.SIGTERM)
|
return syscall.Kill(-pid, syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is never used except for a band-aid that would check
|
// This is never used except for a band-aid that would check
|
||||||
// that there are no double-mounts.
|
// that there are no double-mounts.
|
||||||
func checkDeviceSeemsMounted(devPathPrefix string) (bool, error) {
|
func CheckDeviceSeemsMounted(devPathPrefix string) (bool, error) {
|
||||||
// Quite a bit hacky implementation, but it's to be used as a failsafe band-aid anyway.
|
// Quite a bit hacky implementation, but it's to be used as a failsafe band-aid anyway.
|
||||||
absDevPathPrefix, err := filepath.Abs(devPathPrefix)
|
absDevPathPrefix, err := filepath.Abs(devPathPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -45,3 +48,26 @@ func checkDeviceSeemsMounted(devPathPrefix string) (bool, error) {
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckValidDevicePath(devPath string) error {
|
||||||
|
stat, err := os.Stat(devPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "stat path")
|
||||||
|
}
|
||||||
|
|
||||||
|
isDev := stat.Mode()&os.ModeDevice != 0
|
||||||
|
if !isDev {
|
||||||
|
fmt.Errorf("file mode is not device (%v)", stat.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckRunAsRoot() (bool, error) {
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "get current user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentUser.Username == "root", nil
|
||||||
|
}
|
||||||
89
osspecifics/osspecifics_windows.go
Normal file
89
osspecifics/osspecifics_windows.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// go:build windows
|
||||||
|
|
||||||
|
package osspecifics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetNewProcessGroupCmd(cmd *exec.Cmd) {
|
||||||
|
// This is to prevent Ctrl+C propagating to the child process.
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TerminateProcess(pid int) error {
|
||||||
|
return exec.Command("TASKKILL", "/T", "/F", "/PID", fmt.Sprint(pid)).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
var physicalDriveCheckRegexp = regexp.MustCompile(`^\\\\.\\PhysicalDrive(\d+)$`)
|
||||||
|
var physicalDriveFindRegexp = regexp.MustCompile(`PhysicalDrive(\d+)`)
|
||||||
|
|
||||||
|
// This is never used except for a band-aid that would check
|
||||||
|
// that there are no double-mounts.
|
||||||
|
func CheckDeviceSeemsMounted(path string) (bool, error) {
|
||||||
|
// Quite a bit hacky implementation, but it's to be used as a failsafe band-aid anyway.
|
||||||
|
matches := physicalDriveFindRegexp.FindAllStringSubmatch(path, 1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return false, fmt.Errorf("bad device path '%v'", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
match := matches[0]
|
||||||
|
|
||||||
|
if want, have := 2, len(match); want != have {
|
||||||
|
return false, fmt.Errorf("bad match items length: want %v, have %v (%v)", want, have, match)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := exec.Command("wmic", "path", "Win32_LogicalDiskToPartition", "get", "Antecedent").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "exec wmic cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(string(out), fmt.Sprintf("Disk #%v", match[1])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckValidDevicePath(devPath string) error {
|
||||||
|
if !physicalDriveCheckRegexp.MatchString(devPath) {
|
||||||
|
// Not including the device path in Errorf() as it is supposed to
|
||||||
|
// be included outside when the error is handled.
|
||||||
|
return fmt.Errorf("invalid device path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckRunAsRoot() (bool, error) {
|
||||||
|
var sid *windows.SID
|
||||||
|
|
||||||
|
err := windows.AllocateAndInitializeSid(
|
||||||
|
&windows.SECURITY_NT_AUTHORITY,
|
||||||
|
2,
|
||||||
|
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
||||||
|
windows.DOMAIN_ALIAS_RID_ADMINS,
|
||||||
|
0, 0, 0, 0, 0, 0,
|
||||||
|
&sid)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "allocate and initiliaze win sid")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = windows.FreeSid(sid) }()
|
||||||
|
|
||||||
|
// This appears to cast a null pointer so I'm not sure why this
|
||||||
|
// works, but this guy says it does and it Works for Me™:
|
||||||
|
// https://github.com/golang/go/issues/28804#issuecomment-438838144
|
||||||
|
|
||||||
|
member, err := windows.Token(0).IsMember(sid)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "check win sid membership")
|
||||||
|
}
|
||||||
|
|
||||||
|
return member, nil
|
||||||
|
}
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
// go:build windows
|
|
||||||
|
|
||||||
package vm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func prepareVMCmd(cmd *exec.Cmd) {
|
|
||||||
// This is to prevent Ctrl+C propagating to the child process.
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func terminateProcess(pid int) error {
|
|
||||||
return exec.Command("TASKKILL", "/T", "/F", "/PID", fmt.Sprint(pid)).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
var physicalDriveRegexp = regexp.MustCompile(`PhysicalDrive(\d+)`)
|
|
||||||
|
|
||||||
// This is never used except for a band-aid that would check
|
|
||||||
// that there are no double-mounts.
|
|
||||||
func checkDeviceSeemsMounted(path string) (bool, error) {
|
|
||||||
// Quite a bit hacky implementation, but it's to be used as a failsafe band-aid anyway.
|
|
||||||
matches := physicalDriveRegexp.FindAllStringSubmatch(path, 1)
|
|
||||||
if len(matches) == 0 {
|
|
||||||
return false, fmt.Errorf("bad device path '%v'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
match := matches[0]
|
|
||||||
|
|
||||||
if want, have := 2, len(match); want != have {
|
|
||||||
return false, fmt.Errorf("bad match items length: want %v, have %v (%v)", want, have, match)
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := exec.Command("wmic", "path", "Win32_LogicalDiskToPartition", "get", "Antecedent").Output()
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, "exec wmic cmd")
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Contains(string(out), fmt.Sprintf("Disk #%v", match[1])), nil
|
|
||||||
}
|
|
||||||
15
vm/vm.go
15
vm/vm.go
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/AlexSSD7/linsk/nettap"
|
"github.com/AlexSSD7/linsk/nettap"
|
||||||
|
"github.com/AlexSSD7/linsk/osspecifics"
|
||||||
"github.com/AlexSSD7/linsk/sshutil"
|
"github.com/AlexSSD7/linsk/sshutil"
|
||||||
"github.com/AlexSSD7/linsk/utils"
|
"github.com/AlexSSD7/linsk/utils"
|
||||||
"github.com/alessio/shellescape"
|
"github.com/alessio/shellescape"
|
||||||
|
|
@ -115,9 +116,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
var accel string
|
var accel string
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
// TODO: whpx accel is broken in Windows. Long term solution looks to be use Hyper-V.
|
// TODO: To document: For Windows, we need to install QEMU using an installer and add it to PATH.
|
||||||
|
|
||||||
// For Windows, we need to install QEMU using an installer and add it to PATH.
|
|
||||||
// Then, we should enable Windows Hypervisor Platform in "Turn Windows features on or off".
|
// Then, we should enable Windows Hypervisor Platform in "Turn Windows features on or off".
|
||||||
// IMPORTANT: We should also install libusbK drivers for USB devices we want to pass through.
|
// IMPORTANT: We should also install libusbK drivers for USB devices we want to pass through.
|
||||||
// This can be easily done with a program called Zadiag by Akeo.
|
// This can be easily done with a program called Zadiag by Akeo.
|
||||||
|
|
@ -216,14 +215,14 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.PassthroughConfig.Block) != 0 {
|
if len(cfg.PassthroughConfig.Block) != 0 {
|
||||||
logger.Warn("Detected raw block device passthrough. Please note that it's YOUR responsibility to ensure that no device is mounted in your OS and the VM at the same time. Otherwise, you run serious risks. No further warnings will be issued.")
|
logger.Warn("Using raw block device passthrough. Please note that it's YOUR responsibility to ensure that no device is mounted in your OS and the VM at the same time. Otherwise, you run serious risks. No further warnings will be issued.")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dev := range cfg.PassthroughConfig.Block {
|
for _, dev := range cfg.PassthroughConfig.Block {
|
||||||
// It's always a user's responsibility to ensure that no drives are mounted
|
// 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.
|
// in both host and guest system. This should serve as the last resort.
|
||||||
{
|
{
|
||||||
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
|
seemsMounted, err := osspecifics.CheckDeviceSeemsMounted(dev.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "check whether device seems to be mounted (path '%v')", dev.Path)
|
return nil, errors.Wrapf(err, "check whether device seems to be mounted (path '%v')", dev.Path)
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +274,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
cmd.Stderr = stderrBuf
|
cmd.Stderr = stderrBuf
|
||||||
|
|
||||||
// This function is OS-specific.
|
// This function is OS-specific.
|
||||||
prepareVMCmd(cmd)
|
osspecifics.SetNewProcessGroupCmd(cmd)
|
||||||
|
|
||||||
userReader := bufio.NewReader(userRead)
|
userReader := bufio.NewReader(userRead)
|
||||||
|
|
||||||
|
|
@ -456,7 +455,7 @@ func (vm *VM) Cancel() error {
|
||||||
if vm.cmd.Process == nil {
|
if vm.cmd.Process == nil {
|
||||||
interruptErr = fmt.Errorf("process is not started")
|
interruptErr = fmt.Errorf("process is not started")
|
||||||
} else {
|
} else {
|
||||||
interruptErr = terminateProcess(vm.cmd.Process.Pid)
|
interruptErr = osspecifics.TerminateProcess(vm.cmd.Process.Pid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,7 +581,7 @@ func (vm *VM) runPeriodicHostMountChecker() {
|
||||||
return
|
return
|
||||||
case <-time.After(time.Second):
|
case <-time.After(time.Second):
|
||||||
for _, dev := range vm.originalCfg.PassthroughConfig.Block {
|
for _, dev := range vm.originalCfg.PassthroughConfig.Block {
|
||||||
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
|
seemsMounted, err := osspecifics.CheckDeviceSeemsMounted(dev.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vm.logger.Warn("Failed to check if a passed device seems to be mounted", "dev-path", dev.Path)
|
vm.logger.Warn("Failed to check if a passed device seems to be mounted", "dev-path", dev.Path)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue