Wire most of the work from today together

This commit is contained in:
AlexSSD7 2023-08-31 16:23:40 +01:00
commit e57519e58d
12 changed files with 328 additions and 132 deletions

View file

@ -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)
},
}

View file

@ -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))
},
}

View file

@ -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.
}

View file

@ -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.
},
}

View file

@ -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,