diff --git a/cmd/clean.go b/cmd/clean.go index bc5cf36..f61be4c 100644 --- a/cmd/clean.go +++ b/cmd/clean.go @@ -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) }, } diff --git a/cmd/ls.go b/cmd/ls.go index 6d299bc..726f834 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -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)) }, } diff --git a/cmd/run.go b/cmd/run.go index c96a157..55ae31c 100644 --- a/cmd/run.go +++ b/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. } diff --git a/cmd/shell.go b/cmd/shell.go index ad20826..4f9469d 100644 --- a/cmd/shell.go +++ b/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. }, } diff --git a/cmd/utils.go b/cmd/utils.go index 96d48fa..b4ed45f 100644 --- a/cmd/utils.go +++ b/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,11 +183,13 @@ 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, + ShowDisplay: vmDebugFlag, } vi, err := vm.NewVM(slog.Default().With("caller", "vm"), vmCfg) @@ -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, diff --git a/go.mod b/go.mod index a541f26..f3700d3 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index e7aa11c..3db1e60 100644 --- a/go.sum +++ b/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= diff --git a/imgbuilder/imgbuilder.go b/imgbuilder/imgbuilder.go index 4c15f33..723de2c 100644 --- a/imgbuilder/imgbuilder.go +++ b/imgbuilder/imgbuilder.go @@ -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") } diff --git a/utils/net.go b/utils/net.go new file mode 100644 index 0000000..63be41b --- /dev/null +++ b/utils/net.go @@ -0,0 +1,7 @@ +package utils + +import "net" + +func IsIPv6IP(ip net.IP) bool { + return ip.To4() == nil && ip.To16() != nil +} diff --git a/vm/filemanager.go b/vm/filemanager.go index efba4a9..8243d87 100644 --- a/vm/filemanager.go +++ b/vm/filemanager.go @@ -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 +} diff --git a/vm/net.go b/vm/net.go new file mode 100644 index 0000000..990023d --- /dev/null +++ b/vm/net.go @@ -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 +} diff --git a/vm/vm.go b/vm/vm.go index 88cc660..71fd8db 100644 --- a/vm/vm.go +++ b/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,14 +79,17 @@ 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 + ShowDisplay bool + InstallBaseUtilities bool } func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { @@ -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 "."