2023-09-02 20:03:44 +01:00
// Linsk - A utility to access Linux-native file systems on non-Linux operating systems.
// Copyright (c) 2023 The Linsk Authors.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2023-08-25 15:12:19 +01:00
package cmd
import (
2023-09-02 12:07:30 +01:00
"context"
2023-08-26 16:26:35 +01:00
"fmt"
2023-08-25 15:12:19 +01:00
"os"
2023-08-30 09:36:33 +01:00
"path/filepath"
"strconv"
"strings"
2023-08-29 11:51:06 +01:00
"time"
2023-08-25 15:12:19 +01:00
2023-08-25 16:54:58 +01:00
"log/slog"
2023-09-02 11:47:58 +01:00
"github.com/AlexSSD7/linsk/cmd/runvm"
2023-08-31 16:23:40 +01:00
"github.com/AlexSSD7/linsk/nettap"
2023-09-01 11:44:49 +01:00
"github.com/AlexSSD7/linsk/osspecifics"
2023-08-31 16:23:40 +01:00
"github.com/AlexSSD7/linsk/share"
2023-08-30 09:19:02 +01:00
"github.com/AlexSSD7/linsk/storage"
2023-08-26 09:16:52 +01:00
"github.com/AlexSSD7/linsk/vm"
2023-08-25 15:12:19 +01:00
"github.com/pkg/errors"
)
2023-08-31 20:10:26 +01:00
func createStoreOrExit ( ) * storage . Storage {
2023-08-30 09:19:02 +01:00
store , err := storage . NewStorage ( slog . With ( "caller" , "storage" ) , dataDirFlag )
if err != nil {
slog . Error ( "Failed to create Linsk data storage" , "error" , err . Error ( ) , "data-dir" , dataDirFlag )
os . Exit ( 1 )
}
2023-08-30 09:43:41 +01:00
return store
}
2023-09-02 12:09:26 +01:00
func runVM ( passthroughArg string , fn runvm . Func , forwardPortsRules [ ] vm . PortForwardingRule , unrestrictedNetworking bool , withNetTap bool ) int {
2023-08-31 20:10:26 +01:00
store := createStoreOrExit ( )
2023-08-30 12:39:38 +01:00
vmImagePath , err := store . CheckVMImageExists ( )
2023-08-30 09:19:02 +01:00
if err != nil {
2023-08-30 12:39:38 +01:00
slog . Error ( "Failed to check whether VM image exists" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-30 12:39:38 +01:00
}
if vmImagePath == "" {
slog . Error ( "VM image does not exist. You need to build it first before attempting to start Linsk. Please run `linsk build` first." )
2023-08-31 20:10:26 +01:00
return 1
2023-08-30 09:19:02 +01:00
}
2023-09-02 12:07:30 +01:00
biosPath , err := store . CheckDownloadVMBIOS ( context . Background ( ) )
2023-08-30 13:32:00 +01:00
if err != nil {
2023-08-31 16:23:40 +01:00
slog . Error ( "Failed to check/download VM BIOS" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-30 13:32:00 +01:00
}
2023-08-29 15:31:17 +01:00
var passthroughConfig vm . PassthroughConfig
2023-08-26 11:27:38 +01:00
if passthroughArg != "" {
2023-08-31 20:10:26 +01:00
passthroughConfigPtr , err := getDevicePassthroughConfig ( passthroughArg )
if err != nil {
slog . Error ( "Failed to get device passthrough config" , "error" , err . Error ( ) )
return 1
}
passthroughConfig = * passthroughConfigPtr
2023-09-01 11:44:49 +01:00
}
if len ( passthroughConfig . USB ) != 0 {
// Log USB-related warnings.
2023-08-31 20:10:26 +01:00
2023-09-01 16:29:01 +01:00
// Unfortunately USB passthrough is unstable in macOS and Windows. On Windows, you also need to install external
// libusbK driver, which nullifies the UX. This is a problem with how QEMU works, and unfortunately there isn't
// much we can do about it from our side.
2023-09-02 11:28:23 +01:00
switch {
case osspecifics . IsWindows ( ) :
2023-09-01 11:44:49 +01:00
// 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." )
2023-09-02 11:28:23 +01:00
case osspecifics . IsMacOS ( ) :
2023-09-01 11:44:49 +01:00
slog . Warn ( "USB passthrough is unstable on macOS. Please consider using raw block device passthrough instead." )
}
2023-08-26 11:27:38 +01:00
}
2023-08-25 16:54:58 +01:00
2023-08-31 16:23:40 +01:00
var tapRuntimeCtx * share . NetTapRuntimeContext
var tapsConfig [ ] vm . TapConfig
if withNetTap {
tapManager , err := nettap . NewTapManager ( slog . With ( "caller" , "nettap-manager" ) )
if err != nil {
slog . Error ( "Failed to create new network tap manager" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
2023-09-02 10:07:17 +01:00
tapNameToUse , err := nettap . NewUniqueTapName ( )
2023-09-01 14:41:19 +01:00
if err != nil {
slog . Error ( "Failed to generate new network tap name" , "error" , err . Error ( ) )
return 1
}
2023-08-31 16:23:40 +01:00
knownAllocs , err := store . ListNetTapAllocations ( )
if err != nil {
slog . Error ( "Failed to list net tap allocations" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
removedTaps , err := tapManager . PruneTaps ( knownAllocs )
if err != nil {
slog . Error ( "Failed to prune dangling network taps" , "error" , err . Error ( ) )
} else {
// This is optional, meaning that we won't exit in panic if this fails.
for _ , removedTap := range removedTaps {
err = store . ReleaseNetTapAllocation ( removedTap )
if err != nil {
slog . Error ( "Failed to release a danging net tap allocation" , "error" , err . Error ( ) )
}
}
}
err = store . SaveNetTapAllocation ( tapNameToUse , os . Getpid ( ) )
if err != nil {
slog . Error ( "Failed to save net tap allocation" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
tapManager , err = nettap . NewTapManager ( slog . Default ( ) )
if err != nil {
slog . Error ( "Failed to create net tap manager" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
err = tapManager . CreateNewTap ( tapNameToUse )
if err != nil {
releaseErr := store . ReleaseNetTapAllocation ( tapNameToUse )
if releaseErr != nil {
2023-08-31 20:10:26 +01:00
slog . Error ( "Failed to release net tap allocation" , "error" , releaseErr . Error ( ) , "tap-name" , tapNameToUse )
// Non-critical error.
2023-08-31 16:23:40 +01:00
}
slog . Error ( "Failed to create new tap" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
2023-08-31 20:10:26 +01:00
defer func ( ) {
err := tapManager . DeleteTap ( tapNameToUse )
if err != nil {
slog . Error ( "Failed to clean up net tap" , "error" , err . Error ( ) , "tap-name" , tapNameToUse )
} else {
err = store . ReleaseNetTapAllocation ( tapNameToUse )
if err != nil {
slog . Error ( "Failed to release net tap allocation" , "error" , err . Error ( ) , "tap-name" , tapNameToUse )
}
}
} ( )
2023-08-31 16:23:40 +01:00
tapNet , err := nettap . GenerateNet ( )
if err != nil {
slog . Error ( "Failed to generate tap net plan" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
err = tapManager . ConfigureNet ( tapNameToUse , tapNet . HostCIDR )
if err != nil {
slog . Error ( "Failed to configure tap net" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-31 16:23:40 +01:00
}
tapRuntimeCtx = & share . NetTapRuntimeContext {
Manager : tapManager ,
Name : tapNameToUse ,
Net : tapNet ,
}
tapsConfig = [ ] vm . TapConfig { {
Name : tapNameToUse ,
} }
}
2023-09-02 12:07:30 +01:00
vmCfg := vm . Config {
2023-08-27 15:53:44 +01:00
Drives : [ ] vm . DriveConfig { {
2023-08-30 12:39:38 +01:00
Path : vmImagePath ,
2023-08-27 15:53:44 +01:00
SnapshotMode : true ,
} } ,
2023-08-27 13:44:57 +01:00
2023-08-29 10:59:50 +01:00
MemoryAlloc : vmMemAllocFlag ,
2023-08-30 13:32:00 +01:00
BIOSPath : biosPath ,
2023-08-29 10:59:50 +01:00
2023-08-29 15:31:17 +01:00
PassthroughConfig : passthroughConfig ,
2023-08-27 13:44:57 +01:00
ExtraPortForwardingRules : forwardPortsRules ,
2023-08-31 16:23:40 +01:00
UnrestrictedNetworking : unrestrictedNetworking ,
Taps : tapsConfig ,
2023-08-29 11:51:06 +01:00
OSUpTimeout : time . Duration ( vmOSUpTimeoutFlag ) * time . Second ,
SSHUpTimeout : time . Duration ( vmSSHSetupTimeoutFlag ) * time . Second ,
2023-08-31 16:23:40 +01:00
ShowDisplay : vmDebugFlag ,
2023-08-27 13:44:57 +01:00
}
vi , err := vm . NewVM ( slog . Default ( ) . With ( "caller" , "vm" ) , vmCfg )
2023-08-25 16:54:58 +01:00
if err != nil {
2023-08-29 10:59:50 +01:00
slog . Error ( "Failed to create vm instance" , "error" , err . Error ( ) )
2023-08-31 20:10:26 +01:00
return 1
2023-08-25 15:12:19 +01:00
}
2023-08-25 16:54:58 +01:00
2023-09-02 11:47:58 +01:00
return runvm . RunVM ( vi , true , tapRuntimeCtx , fn )
2023-08-25 15:12:19 +01:00
}
2023-08-26 16:26:35 +01:00
2023-08-31 20:10:26 +01:00
func getDevicePassthroughConfig ( val string ) ( * vm . PassthroughConfig , error ) {
2023-09-01 11:44:49 +01:00
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" )
}
2023-08-30 09:36:33 +01:00
valSplit := strings . Split ( val , ":" )
if want , have := 2 , len ( valSplit ) ; want != have {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad device passthrough syntax: wrong items split by ':' count: want %v, have %v" , want , have )
2023-08-30 09:36:33 +01:00
}
switch valSplit [ 0 ] {
case "usb" :
usbValsSplit := strings . Split ( valSplit [ 1 ] , "," )
if want , have := 2 , len ( usbValsSplit ) ; want != have {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad usb device passthrough syntax: wrong args split by ',' count: want %v, have %v" , want , have )
2023-08-30 09:36:33 +01:00
}
vendorID , err := strconv . ParseUint ( usbValsSplit [ 0 ] , 16 , 32 )
if err != nil {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad usb vendor id '%v'" , usbValsSplit [ 0 ] )
2023-08-30 09:36:33 +01:00
}
productID , err := strconv . ParseUint ( usbValsSplit [ 1 ] , 16 , 32 )
if err != nil {
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "bad usb product id '%v'" , usbValsSplit [ 1 ] )
2023-08-30 09:36:33 +01:00
}
2023-08-31 20:10:26 +01:00
return & vm . PassthroughConfig {
2023-08-30 09:36:33 +01:00
USB : [ ] vm . USBDevicePassthroughConfig { {
VendorID : uint16 ( vendorID ) ,
ProductID : uint16 ( productID ) ,
} } ,
2023-08-31 20:10:26 +01:00
} , nil
2023-08-30 09:36:33 +01:00
case "dev" :
devPath := filepath . Clean ( valSplit [ 1 ] )
2023-09-01 11:44:49 +01:00
err := osspecifics . CheckValidDevicePath ( devPath )
if err != nil {
return nil , errors . Wrapf ( err , "check whether device path is valid '%v'" , devPath )
}
2023-08-30 09:36:33 +01:00
2023-08-31 20:10:26 +01:00
return & vm . PassthroughConfig { Block : [ ] vm . BlockDevicePassthroughConfig { {
2023-08-30 09:36:33 +01:00
Path : devPath ,
2023-08-31 20:10:26 +01:00
} } } , nil
2023-08-30 09:36:33 +01:00
default :
2023-08-31 20:10:26 +01:00
return nil , fmt . Errorf ( "unknown device passthrough type '%v'" , val )
2023-08-30 09:36:33 +01:00
}
}