From 40aa08c86caaed2c622e38acb30b2cff25d89e89 Mon Sep 17 00:00:00 2001 From: AlexSSD7 Date: Thu, 31 Aug 2023 16:19:03 +0100 Subject: [PATCH] Clean segregated file share backends --- share/backend.go | 19 +++++++++++ share/cfg.go | 57 +++++++++++++++++++++++++++++++++ share/ftp.go | 67 +++++++++++++++++++++++++++++++++++++++ share/ports.go | 72 ++++++++++++++++++++++++++++++++++++++++++ share/smb.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ share/types.go | 23 ++++++++++++++ 6 files changed, 320 insertions(+) create mode 100644 share/backend.go create mode 100644 share/cfg.go create mode 100644 share/ftp.go create mode 100644 share/ports.go create mode 100644 share/smb.go create mode 100644 share/types.go diff --git a/share/backend.go b/share/backend.go new file mode 100644 index 0000000..adb7669 --- /dev/null +++ b/share/backend.go @@ -0,0 +1,19 @@ +package share + +import "context" + +type NewBackendFunc func(uc *UserConfiguration) (Backend, *VMShareOptions, error) + +type Backend interface { + Apply(ctx context.Context, sharePWD string, vc *VMShareContext) (string, error) +} + +var backends = map[string]NewBackendFunc{ + "ftp": NewFTPBackend, + "smb": NewSMBBackend, +} + +// Will return nil if no backend is found. +func GetBackend(id string) NewBackendFunc { + return backends[id] +} diff --git a/share/cfg.go b/share/cfg.go new file mode 100644 index 0000000..a78b36b --- /dev/null +++ b/share/cfg.go @@ -0,0 +1,57 @@ +package share + +import ( + "fmt" + "net" + + "log/slog" +) + +var defaultListenIP = net.ParseIP("127.0.0.1") + +func GetDefaultListenIPStr() string { + return defaultListenIP.String() +} + +type UserConfiguration struct { + listenIP net.IP + ftpExtIP net.IP + + smbExtMode bool +} + +type RawUserConfiguration struct { + ListenIP string + + // Backend-specific + FTPExtIP string + SMBExtMode bool +} + +func (rc RawUserConfiguration) Process(backend string, warnLogger *slog.Logger) (*UserConfiguration, error) { + listenIP := net.ParseIP(rc.ListenIP) + if listenIP == nil { + return nil, fmt.Errorf("invalid listen ip '%v'", rc.ListenIP) + } + + ftpExtIP := net.ParseIP(rc.FTPExtIP) + if ftpExtIP == nil { + return nil, fmt.Errorf("invalid ftp ext ip '%v'", rc.FTPExtIP) + } + + if backend == "ftp" { + if !listenIP.Equal(defaultListenIP) && ftpExtIP.Equal(defaultListenIP) { + 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.") + } + } else { + if !ftpExtIP.Equal(defaultListenIP) { + slog.Warn("FTP external IP address specification is ineffective with non-FTP backends", "selected", backend) + } + } + + return &UserConfiguration{ + listenIP: listenIP, + ftpExtIP: ftpExtIP, + smbExtMode: rc.SMBExtMode, + }, nil +} diff --git a/share/ftp.go b/share/ftp.go new file mode 100644 index 0000000..445a5d7 --- /dev/null +++ b/share/ftp.go @@ -0,0 +1,67 @@ +package share + +import ( + "context" + "fmt" + "net" + + "log/slog" + + "github.com/AlexSSD7/linsk/vm" + "github.com/pkg/errors" +) + +type FTPBackend struct { + sharePort uint16 + passivePortCount uint16 + extIP net.IP +} + +func NewFTPBackend(uc *UserConfiguration) (Backend, *VMShareOptions, error) { + // TODO: Make this changeable? + passivePortCount := uint16(9) + + sharePort, err := getNetworkSharePort(9) + if err != nil { + return nil, nil, errors.Wrap(err, "get network share port") + } + + ports := []vm.PortForwardingRule{{ + HostIP: uc.listenIP, + HostPort: sharePort, + VMPort: 21, + }} + + for i := uint16(0); i < passivePortCount; i++ { + p := sharePort + 1 + i + ports = append(ports, vm.PortForwardingRule{ + HostIP: uc.listenIP, + HostPort: p, + VMPort: p, + }) + } + + return &FTPBackend{ + sharePort: sharePort, + passivePortCount: passivePortCount, + extIP: uc.ftpExtIP, + }, &VMShareOptions{ + Ports: ports, + EnableTap: false, + }, nil +} + +func (b *FTPBackend) Apply(ctx context.Context, sharePWD string, vc *VMShareContext) (string, error) { + if vc.NetTapCtx != nil { + return "", fmt.Errorf("net taps are unsupported in ftp") + } + + err := vc.FileManager.StartFTP(sharePWD, b.sharePort+1, b.passivePortCount, b.extIP) + if err != nil { + return "", errors.Wrap(err, "start ftp server") + } + + slog.Info("Started the network share successfully", "type", "ftp") + + return "ftp://" + b.extIP.String() + ":" + fmt.Sprint(b.sharePort), nil +} diff --git a/share/ports.go b/share/ports.go new file mode 100644 index 0000000..12feb97 --- /dev/null +++ b/share/ports.go @@ -0,0 +1,72 @@ +package share + +import ( + "fmt" + "net" + "os" + "syscall" + + "github.com/pkg/errors" +) + +func getNetworkSharePort(subsequent uint16) (uint16, error) { + return getClosestAvailPortWithSubsequent(9000, subsequent) +} + +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 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 +} diff --git a/share/smb.go b/share/smb.go new file mode 100644 index 0000000..7a112a0 --- /dev/null +++ b/share/smb.go @@ -0,0 +1,82 @@ +package share + +import ( + "context" + "fmt" + "net" + "runtime" + "strings" + + "log/slog" + + "github.com/AlexSSD7/linsk/vm" + "github.com/pkg/errors" +) + +const smbPort = 445 + +// TODO: Test SMB backend on macOS. + +type SMBBackend struct { + listenIP net.IP + sharePort *uint16 +} + +func NewSMBBackend(uc *UserConfiguration) (Backend, *VMShareOptions, error) { + var ports []vm.PortForwardingRule + var sharePortPtr *uint16 + if !uc.smbExtMode { + sharePort, err := getNetworkSharePort(0) + if err != nil { + return nil, nil, errors.Wrap(err, "get network share port") + } + + sharePortPtr = &sharePort + + ports = append(ports, vm.PortForwardingRule{ + HostIP: uc.listenIP, + HostPort: sharePort, + VMPort: smbPort, + }) + } + + return &SMBBackend{ + listenIP: uc.listenIP, + sharePort: sharePortPtr, + }, &VMShareOptions{ + Ports: ports, + EnableTap: uc.smbExtMode, + }, nil +} + +func (b *SMBBackend) Apply(ctx context.Context, sharePWD string, vc *VMShareContext) (string, error) { + if b.sharePort != nil && vc.NetTapCtx != nil { + return "", fmt.Errorf("conflict: configured to use a forwarded port but a net tap configuration was detected") + } + + if b.sharePort == nil && vc.NetTapCtx == nil { + return "", fmt.Errorf("no net tap configuration found") + } + + err := vc.FileManager.StartSMB(sharePWD) + if err != nil { + return "", errors.Wrap(err, "start smb server") + } + + slog.Info("Started the network share successfully", "type", "smb", "ext", vc.NetTapCtx != nil) + + var shareURL string + if b.sharePort != nil { + shareURL = "smb://" + net.JoinHostPort(b.listenIP.String(), fmt.Sprint(*b.sharePort)) + } else if vc.NetTapCtx != nil { + if runtime.GOOS == "windows" { + shareURL = `\\` + strings.ReplaceAll(vc.NetTapCtx.Net.GuestIP.String(), ":", "-") + ".ipv6-literal.net" + `\linsk` + } else { + shareURL = "smb://" + net.JoinHostPort(vc.NetTapCtx.Net.GuestIP.String(), fmt.Sprint(smbPort)) + } + } else { + return "", fmt.Errorf("no port forwarding and net tap configured") + } + + return shareURL, nil +} diff --git a/share/types.go b/share/types.go new file mode 100644 index 0000000..51fe01c --- /dev/null +++ b/share/types.go @@ -0,0 +1,23 @@ +package share + +import ( + "github.com/AlexSSD7/linsk/nettap" + "github.com/AlexSSD7/linsk/vm" +) + +type NetTapRuntimeContext struct { + Manager *nettap.TapManager + Name string + Net nettap.TapNet +} + +type VMShareOptions struct { + Ports []vm.PortForwardingRule + EnableTap bool +} + +type VMShareContext struct { + Instance *vm.VM + FileManager *vm.FileManager + NetTapCtx *NetTapRuntimeContext +}