Wire most of the work from today together
This commit is contained in:
parent
40aa08c86c
commit
e57519e58d
12 changed files with 328 additions and 132 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexSSD7/linsk/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ var cleanCmd = &cobra.Command{
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if strings.ToLower(string(answer)) != "y\n" {
|
||||
if utils.ClearUnprintableChars(strings.ToLower(string(answer)), false) != "y" {
|
||||
fmt.Fprintf(os.Stderr, "Aborted.\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
|
@ -37,6 +38,8 @@ var cleanCmd = &cobra.Command{
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// TODO: Clean network tap allocations, if any.
|
||||
|
||||
slog.Info("Deleted data directory", "path", rmPath)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -15,7 +16,7 @@ var lsCmd = &cobra.Command{
|
|||
Short: "Start a VM and list all user drives within the VM. Uses lsblk command under the hood.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager, trc *share.NetTapRuntimeContext) int {
|
||||
lsblkOut, err := fm.Lsblk()
|
||||
if err != nil {
|
||||
slog.Error("Failed to list block devices in the VM", "error", err.Error())
|
||||
|
|
@ -29,6 +30,6 @@ var lsCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
return 0
|
||||
}, nil, false))
|
||||
}, nil, false, false))
|
||||
},
|
||||
}
|
||||
|
|
|
|||
95
cmd/run.go
95
cmd/run.go
|
|
@ -4,9 +4,11 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -20,46 +22,30 @@ var runCmd = &cobra.Command{
|
|||
vmMountDevName := args[1]
|
||||
fsType := args[2]
|
||||
|
||||
ftpPassivePortCount := uint16(9)
|
||||
newBackendFunc := share.GetBackend(shareBackendFlag)
|
||||
if newBackendFunc == nil {
|
||||
slog.Error("Unknown file share backend", "type", shareBackendFlag)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
networkSharePort, err := getClosestAvailPortWithSubsequent(9000, 10)
|
||||
cfg, err := share.RawUserConfiguration{
|
||||
ListenIP: shareListenIPFlag,
|
||||
|
||||
FTPExtIP: ftpExtIPFlag,
|
||||
SMBExtMode: smbUseExternAddrFlag,
|
||||
}.Process(shareBackendFlag, slog.With("caller", "share-config"))
|
||||
if err != nil {
|
||||
slog.Error("Failed to get closest available host port for network file share", "error", err.Error())
|
||||
slog.Error("Failed to process raw configuration", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ftpListenIP := net.ParseIP(ftpListenAddrFlag)
|
||||
if ftpListenIP == nil {
|
||||
slog.Error("Invalid FTP listen address specified", "value", ftpListenAddrFlag)
|
||||
backend, vmOpts, err := newBackendFunc(cfg)
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize share backend", "backend", shareBackendFlag, "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ftpExtIP := net.ParseIP(ftpExtIPFlag)
|
||||
if ftpExtIP == nil {
|
||||
slog.Error("Invalid FTP external IP specified", "value", ftpExtIPFlag)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ftpListenAddrFlag != defaultFTPListenAddr && ftpExtIPFlag == defaultFTPListenAddr {
|
||||
slog.Warn("No external FTP IP address via --ftp-extip was configured. This is a requirement in almost all scenarios if you want to connect remotely.")
|
||||
}
|
||||
|
||||
ports := []vm.PortForwardingRule{{
|
||||
HostIP: ftpListenIP,
|
||||
HostPort: networkSharePort,
|
||||
VMPort: 21,
|
||||
}}
|
||||
|
||||
for i := uint16(0); i < ftpPassivePortCount; i++ {
|
||||
p := networkSharePort + 1 + i
|
||||
ports = append(ports, vm.PortForwardingRule{
|
||||
HostIP: ftpListenIP,
|
||||
HostPort: p,
|
||||
VMPort: p,
|
||||
})
|
||||
}
|
||||
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager, tapCtx *share.NetTapRuntimeContext) int {
|
||||
slog.Info("Mounting the device", "dev", vmMountDevName, "fs", fsType, "luks", luksFlag)
|
||||
|
||||
err := fm.Mount(vmMountDevName, vm.MountOptions{
|
||||
|
|
@ -77,32 +63,53 @@ var runCmd = &cobra.Command{
|
|||
return 1
|
||||
}
|
||||
|
||||
err = fm.StartFTP(sharePWD, networkSharePort+1, ftpPassivePortCount, ftpExtIP)
|
||||
shareURI, err := backend.Apply(ctx, sharePWD, &share.VMShareContext{
|
||||
Instance: i,
|
||||
FileManager: fm,
|
||||
NetTapCtx: tapCtx,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to start FTP server", "error", err.Error())
|
||||
slog.Error("Failed to apply (start) file share backend", "backend", shareBackendFlag, "error", err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
slog.Info("Started the network share successfully", "type", "ftp")
|
||||
|
||||
shareURI := "ftp://linsk:" + sharePWD + "@" + ftpExtIP.String() + ":" + fmt.Sprint(networkSharePort)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "================\n[Network File Share Config]\nThe network file share was started. Please use the credentials below to connect to the file server.\n\nType: FTP\nServer Address: ftp://%v:%v\nUsername: linsk\nPassword: %v\n\nShare URI: %v\n================\n", ftpExtIP.String(), networkSharePort, sharePWD, shareURI)
|
||||
fmt.Fprintf(os.Stderr, "============================\n[Network File Share Config]\nThe network file share was started. Please use the credentials below to connect to the file server.\n\nType: "+strings.ToUpper(shareBackendFlag)+"\nURL: %v\nUsername: linsk\nPassword: %v\n===========================\n", shareURI, sharePWD)
|
||||
|
||||
<-ctx.Done()
|
||||
return 0
|
||||
}, ports, unrestrictedNetworkingFlag))
|
||||
}, vmOpts.Ports, unrestrictedNetworkingFlag, vmOpts.EnableTap))
|
||||
},
|
||||
}
|
||||
|
||||
var luksFlag bool
|
||||
var ftpListenAddrFlag string
|
||||
var shareListenIPFlag string
|
||||
var ftpExtIPFlag string
|
||||
|
||||
const defaultFTPListenAddr = "127.0.0.1"
|
||||
var shareBackendFlag string
|
||||
var smbUseExternAddrFlag bool
|
||||
|
||||
func init() {
|
||||
runCmd.Flags().BoolVarP(&luksFlag, "luks", "l", false, "Use cryptsetup to open a LUKS volume (password will be prompted).")
|
||||
runCmd.Flags().StringVar(&ftpListenAddrFlag, "ftp-listen", defaultFTPListenAddr, "Specifies the address to bind the FTP ports to. NOTE: Changing bind address is not enough to connect remotely. You should also specify --ftp-extip.")
|
||||
runCmd.Flags().StringVar(&ftpExtIPFlag, "ftp-extip", defaultFTPListenAddr, "Specifies the external IP the FTP server should advertise.")
|
||||
|
||||
var defaultShareType string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
defaultShareType = "smb"
|
||||
default:
|
||||
defaultShareType = "ftp"
|
||||
}
|
||||
|
||||
runCmd.Flags().StringVar(&shareBackendFlag, "share-backend", defaultShareType, "Specifies the file share backend to use. The default value is OS-specific.")
|
||||
runCmd.Flags().StringVar(&shareListenIPFlag, "share-listen", share.GetDefaultListenIPStr(), "Specifies the IP to bind the network share port to. NOTE: For FTP, changing the bind address is not enough to connect remotely. You should also specify --ftp-extip.")
|
||||
|
||||
smbExternDefault := false
|
||||
if runtime.GOOS == "windows" {
|
||||
smbExternDefault = true
|
||||
}
|
||||
|
||||
runCmd.Flags().StringVar(&ftpExtIPFlag, "ftp-extip", share.GetDefaultListenIPStr(), "Specifies the external IP the FTP server should advertise.")
|
||||
runCmd.Flags().BoolVar(&smbUseExternAddrFlag, "smb-extern", smbExternDefault, "Specifies whether Linsk emulate external networking for the VM's SMB server. This is the default for Windows as there is no way to specify ports in Windows SMB client.")
|
||||
|
||||
// TODO: log the use of smbUseExternAddrFlag when SMB is not enabled.
|
||||
}
|
||||
|
|
|
|||
10
cmd/shell.go
10
cmd/shell.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
|
@ -39,11 +40,7 @@ var shellCmd = &cobra.Command{
|
|||
forwardPortRules = append(forwardPortRules, fpr)
|
||||
}
|
||||
|
||||
if !unrestrictedNetworkingFlag {
|
||||
slog.Warn("IMPORTANT: By default, Linsk shell starts a VM with restricted networking as it's done with `run` command. This means that you will have no internet access in the shell. If you want to have access to the internet, please add `--vm-unrestricted-networking` flag to your `linsk shell` command.")
|
||||
}
|
||||
|
||||
os.Exit(runVM(passthroughArg, func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||
os.Exit(runVM(passthroughArg, func(ctx context.Context, i *vm.VM, fm *vm.FileManager, trc *share.NetTapRuntimeContext) int {
|
||||
sc, err := i.DialSSH()
|
||||
if err != nil {
|
||||
slog.Error("Failed to dial VM SSH", "error", err.Error())
|
||||
|
|
@ -130,7 +127,8 @@ var shellCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
return 0
|
||||
}, forwardPortRules, unrestrictedNetworkingFlag))
|
||||
}, forwardPortRules, true, false))
|
||||
// TODO: Enable tap option.
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
187
cmd/utils.go
187
cmd/utils.go
|
|
@ -3,7 +3,6 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
|
|
@ -17,6 +16,8 @@ import (
|
|||
|
||||
"log/slog"
|
||||
|
||||
"github.com/AlexSSD7/linsk/nettap"
|
||||
"github.com/AlexSSD7/linsk/share"
|
||||
"github.com/AlexSSD7/linsk/storage"
|
||||
"github.com/AlexSSD7/linsk/vm"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -64,7 +65,7 @@ func createStore() *storage.Storage {
|
|||
return store
|
||||
}
|
||||
|
||||
func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManager) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool) int {
|
||||
func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManager, *share.NetTapRuntimeContext) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool, withNetTap bool) int {
|
||||
store := createStore()
|
||||
|
||||
vmImagePath, err := store.CheckVMImageExists()
|
||||
|
|
@ -80,7 +81,7 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
|
||||
biosPath, err := store.CheckDownloadVMBIOS()
|
||||
if err != nil {
|
||||
slog.Error("Failed to check/download VM BIOS", "error", err)
|
||||
slog.Error("Failed to check/download VM BIOS", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +92,85 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
doUSBRootCheck()
|
||||
}
|
||||
|
||||
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())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapNameToUse := nettap.NewRandomTapName()
|
||||
// TODO: Run two instances at the same time and check whether nothing is wrongfully pruned.
|
||||
knownAllocs, err := store.ListNetTapAllocations()
|
||||
if err != nil {
|
||||
slog.Error("Failed to list net tap allocations", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapManager, err = nettap.NewTapManager(slog.Default())
|
||||
if err != nil {
|
||||
slog.Error("Failed to create net tap manager", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = tapManager.CreateNewTap(tapNameToUse)
|
||||
if err != nil {
|
||||
releaseErr := store.ReleaseNetTapAllocation(tapNameToUse)
|
||||
if releaseErr != nil {
|
||||
slog.Error("Failed to release net tap allocation", "error", releaseErr.Error(), "tapname", tapNameToUse)
|
||||
}
|
||||
|
||||
slog.Error("Failed to create new tap", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapNet, err := nettap.GenerateNet()
|
||||
if err != nil {
|
||||
slog.Error("Failed to generate tap net plan", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = tapManager.ConfigureNet(tapNameToUse, tapNet.HostCIDR)
|
||||
if err != nil {
|
||||
slog.Error("Failed to configure tap net", "error", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tapRuntimeCtx = &share.NetTapRuntimeContext{
|
||||
Manager: tapManager,
|
||||
Name: tapNameToUse,
|
||||
Net: tapNet,
|
||||
}
|
||||
|
||||
tapsConfig = []vm.TapConfig{{
|
||||
Name: tapNameToUse,
|
||||
}}
|
||||
|
||||
// TODO: Clean the tap up before exiting.
|
||||
}
|
||||
|
||||
vmCfg := vm.VMConfig{
|
||||
Drives: []vm.DriveConfig{{
|
||||
Path: vmImagePath,
|
||||
|
|
@ -103,10 +183,12 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
PassthroughConfig: passthroughConfig,
|
||||
ExtraPortForwardingRules: forwardPortsRules,
|
||||
|
||||
UnrestrictedNetworking: unrestrictedNetworking,
|
||||
Taps: tapsConfig,
|
||||
|
||||
OSUpTimeout: time.Duration(vmOSUpTimeoutFlag) * time.Second,
|
||||
SSHUpTimeout: time.Duration(vmSSHSetupTimeoutFlag) * time.Second,
|
||||
|
||||
UnrestrictedNetworking: unrestrictedNetworking,
|
||||
ShowDisplay: vmDebugFlag,
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +259,23 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
exitCode := fn(ctx, vi, fm)
|
||||
startupFailed := false
|
||||
|
||||
if tapRuntimeCtx != nil {
|
||||
err := vi.ConfigureInterfaceStaticNet(context.Background(), "eth1", tapRuntimeCtx.Net.GuestCIDR)
|
||||
if err != nil {
|
||||
slog.Error("Failed to configure tag interface network", "error", err.Error())
|
||||
startupFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
|
||||
if !startupFailed {
|
||||
exitCode = fn(ctx, vi, fm, tapRuntimeCtx)
|
||||
} else {
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
err = vi.Cancel()
|
||||
if err != nil {
|
||||
|
|
@ -201,64 +299,6 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
|||
}
|
||||
}
|
||||
|
||||
func checkPortAvailable(port uint16, subsequent uint16) (bool, error) {
|
||||
if port+subsequent < port {
|
||||
return false, fmt.Errorf("subsequent ports exceed allowed port range")
|
||||
}
|
||||
|
||||
if subsequent == 0 {
|
||||
ln, err := net.Listen("tcp", ":"+fmt.Sprint(port))
|
||||
if err != nil {
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
|
||||
if sysErr.Err == syscall.EADDRINUSE {
|
||||
// The port is in use.
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, errors.Wrapf(err, "net listen (port %v)", port)
|
||||
}
|
||||
|
||||
err = ln.Close()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "close ephemeral listener")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for i := uint16(0); i < subsequent; i++ {
|
||||
ok, err := checkPortAvailable(port+i, 0)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "check subsequent port available (base: %v, seq: %v)", port, i)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getClosestAvailPortWithSubsequent(port uint16, subsequent uint16) (uint16, error) {
|
||||
// We use 10 as port range
|
||||
for i := port; i < 65535; i += subsequent {
|
||||
ok, err := checkPortAvailable(i, subsequent)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "check port available (%v)", i)
|
||||
}
|
||||
|
||||
if ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no available port (with %v subsequent ones) found", subsequent)
|
||||
}
|
||||
|
||||
func getDevicePassthroughConfig(val string) vm.PassthroughConfig {
|
||||
valSplit := strings.Split(val, ":")
|
||||
if want, have := 2, len(valSplit); want != have {
|
||||
|
|
@ -294,17 +334,18 @@ func getDevicePassthroughConfig(val string) vm.PassthroughConfig {
|
|||
}
|
||||
case "dev":
|
||||
devPath := filepath.Clean(valSplit[1])
|
||||
stat, err := os.Stat(devPath)
|
||||
if err != nil {
|
||||
slog.Error("Failed to stat the device path", "error", err.Error(), "path", devPath)
|
||||
os.Exit(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)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
isDev := stat.Mode()&os.ModeDevice != 0
|
||||
if !isDev {
|
||||
slog.Error("Provided path is not a path to a valid block device", "path", devPath, "file-mode", stat.Mode())
|
||||
os.Exit(1)
|
||||
}
|
||||
// isDev := stat.Mode()&os.ModeDevice != 0
|
||||
// if !isDev {
|
||||
// slog.Error("Provided path is not a path to a valid block device", "path", devPath, "file-mode", stat.Mode())
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
return vm.PassthroughConfig{Block: []vm.BlockDevicePassthroughConfig{{
|
||||
Path: devPath,
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -7,9 +7,11 @@ require (
|
|||
github.com/alessio/shellescape v1.4.2
|
||||
github.com/bramvdbogaerde/go-scp v1.2.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cobra v1.7.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/crypto v0.12.0
|
||||
|
|
@ -17,7 +19,11 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
)
|
||||
|
|
|
|||
14
go.sum
14
go.sum
|
|
@ -9,6 +9,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
||||
|
|
@ -20,20 +24,30 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
|
|
|||
|
|
@ -157,7 +157,8 @@ func (bc *BuildContext) BuildWithInterruptHandler() error {
|
|||
|
||||
bc.logger.Info("VM OS installation in progress")
|
||||
|
||||
err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd"})
|
||||
// TODO: Compile select features only.
|
||||
err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd", "samba"})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "run alpine setup cmd")
|
||||
}
|
||||
|
|
|
|||
7
utils/net.go
Normal file
7
utils/net.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package utils
|
||||
|
||||
import "net"
|
||||
|
||||
func IsIPv6IP(ip net.IP) bool {
|
||||
return ip.To4() == nil && ip.To16() != nil
|
||||
}
|
||||
|
|
@ -247,3 +247,64 @@ pasv_address=` + extIP.String() + `
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fm *FileManager) StartSMB(pwd string) error {
|
||||
// This timeout is for the SCP client exclusively.
|
||||
scpCtx, scpCtxCancel := context.WithTimeout(fm.vm.ctx, time.Second*5)
|
||||
defer scpCtxCancel()
|
||||
|
||||
scpClient, err := fm.vm.DialSCP()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dial scp")
|
||||
}
|
||||
|
||||
defer scpClient.Close()
|
||||
|
||||
sambaCfg := `[global]
|
||||
workgroup = WORKGROUP
|
||||
dos charset = cp866
|
||||
unix charset = utf-8
|
||||
|
||||
read raw = yes
|
||||
write raw = yes
|
||||
socket options = TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=131072 SO_SNDBUF=131072
|
||||
min receivefile size = 16384
|
||||
use sendfile = true
|
||||
aio read size = 16384
|
||||
aio write size = 16384
|
||||
server signing = no
|
||||
|
||||
[linsk]
|
||||
browseable = yes
|
||||
writeable = yes
|
||||
path = /mnt
|
||||
force user = linsk
|
||||
force group = linsk
|
||||
create mask = 0664`
|
||||
|
||||
err = scpClient.CopyFile(scpCtx, strings.NewReader(sambaCfg), "/etc/samba/smb.conf", "0400")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "copy samba config file")
|
||||
}
|
||||
|
||||
scpClient.Close()
|
||||
|
||||
sc, err := fm.vm.DialSSH()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dial ssh")
|
||||
}
|
||||
|
||||
defer func() { _ = sc.Close() }()
|
||||
|
||||
_, err = sshutil.RunSSHCmd(fm.vm.ctx, sc, "rc-update add samba && rc-service samba start")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "add and start samba service")
|
||||
}
|
||||
|
||||
err = sshutil.ChangeSambaPass(fm.vm.ctx, sc, "linsk", pwd)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "change samba pass")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
37
vm/net.go
Normal file
37
vm/net.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AlexSSD7/linsk/sshutil"
|
||||
"github.com/AlexSSD7/linsk/utils"
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (vi *VM) ConfigureInterfaceStaticNet(ctx context.Context, iface string, cidr string) error {
|
||||
ip, _, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid cidr")
|
||||
}
|
||||
|
||||
if !utils.IsIPv6IP(ip) {
|
||||
return fmt.Errorf("ipv6 addresses accepted only (have '%v')", ip)
|
||||
}
|
||||
|
||||
sc, err := vi.DialSSH()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dial ssh")
|
||||
}
|
||||
|
||||
defer func() { _ = sc.Close() }()
|
||||
|
||||
_, err = sshutil.RunSSHCmd(ctx, sc, "ifconfig "+shellescape.Quote(iface)+" up && ip addr add "+shellescape.Quote(cidr)+" dev "+shellescape.Quote(iface))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "run net conf cmds")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
24
vm/vm.go
24
vm/vm.go
|
|
@ -18,6 +18,7 @@ import (
|
|||
|
||||
"log/slog"
|
||||
|
||||
"github.com/AlexSSD7/linsk/nettap"
|
||||
"github.com/AlexSSD7/linsk/sshutil"
|
||||
"github.com/AlexSSD7/linsk/utils"
|
||||
"github.com/alessio/shellescape"
|
||||
|
|
@ -64,6 +65,10 @@ type DriveConfig struct {
|
|||
SnapshotMode bool
|
||||
}
|
||||
|
||||
type TapConfig struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type VMConfig struct {
|
||||
CdromImagePath string
|
||||
BIOSPath string
|
||||
|
|
@ -74,12 +79,15 @@ type VMConfig struct {
|
|||
PassthroughConfig PassthroughConfig
|
||||
ExtraPortForwardingRules []PortForwardingRule
|
||||
|
||||
// Networking
|
||||
UnrestrictedNetworking bool
|
||||
Taps []TapConfig
|
||||
|
||||
// Timeouts
|
||||
OSUpTimeout time.Duration
|
||||
SSHUpTimeout time.Duration
|
||||
|
||||
// Mostly debug-related options.
|
||||
UnrestrictedNetworking bool
|
||||
ShowDisplay bool
|
||||
InstallBaseUtilities bool
|
||||
}
|
||||
|
|
@ -150,6 +158,17 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
|||
|
||||
cmdArgs = append(cmdArgs, "-device", "e1000,netdev=net0", "-netdev", netdevOpts)
|
||||
|
||||
for i, tap := range cfg.Taps {
|
||||
err := nettap.ValidateTapName(tap.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "validate network tap #%v name", i)
|
||||
}
|
||||
|
||||
netdevName := "net" + fmt.Sprint(1+i)
|
||||
|
||||
cmdArgs = append(cmdArgs, "-device", "e1000,netdev="+netdevName, "-netdev", "tap,id="+netdevName+",ifname="+shellescape.Quote(tap.Name)+",script=no,downscript=no")
|
||||
}
|
||||
|
||||
if !cfg.ShowDisplay {
|
||||
cmdArgs = append(cmdArgs, "-display", "none")
|
||||
} else if runtime.GOARCH == "arm64" {
|
||||
|
|
@ -193,6 +212,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
|||
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.
|
||||
// TODO: Windows support.
|
||||
{
|
||||
seemsMounted, err := checkDeviceSeemsMounted(dev.Path)
|
||||
if err != nil {
|
||||
|
|
@ -204,7 +224,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
|||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-drive", "file="+shellescape.Quote(dev.Path)+",format=raw,aio=native,cache=none")
|
||||
cmdArgs = append(cmdArgs, "-drive", "file="+shellescape.Quote(strings.ReplaceAll(dev.Path, "\\", "/"))+",format=raw,cache=none")
|
||||
}
|
||||
|
||||
// We're not using clean `cdromImagePath` here because it is set to "."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue