diff --git a/cmd/imgbuilder/builder/build.go b/cmd/imgbuilder/builder/build.go index da6a9c4..2e19dee 100644 --- a/cmd/imgbuilder/builder/build.go +++ b/cmd/imgbuilder/builder/build.go @@ -153,7 +153,7 @@ func (bc *BuildContext) BuildWithInterruptHandler() error { bc.logger.Info("Installation in progress") - err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "samba"}) + err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "vsftpd"}) if err != nil { return errors.Wrap(err, "run alpine setup cmd") } @@ -191,7 +191,7 @@ func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error { _ = sess.Close() }() - cmd := "ifconfig eth0 up && ifconfig lo up && udhcpc && true > /etc/apk/repositories && setup-apkrepos -1 && printf 'y' | setup-disk -m sys /dev/vda" + cmd := "ifconfig eth0 up && ifconfig lo up && udhcpc && true > /etc/apk/repositories && setup-apkrepos -c -1 && printf 'y' | setup-disk -m sys /dev/vda" if len(pkgs) != 0 { pkgsQuoted := make([]string, len(pkgs)) @@ -202,7 +202,7 @@ func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error { cmd += " && mount /dev/vda3 /mnt && chroot /mnt apk add " + strings.Join(pkgsQuoted, " ") } - cmd += `&& chroot /mnt ash -c 'echo "PasswordAuthentication no" >> /etc/ssh/sshd_config && addgroup -g 1000 linsk && adduser -G linsk linsk -S -u 1000'` + cmd += `&& chroot /mnt ash -c 'echo "PasswordAuthentication no" >> /etc/ssh/sshd_config && addgroup -g 1000 linsk && adduser -D -h /mnt -G linsk linsk -u 1000'` err = sess.Run(cmd) if err != nil { diff --git a/cmd/root.go b/cmd/root.go index a671ccf..1906076 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ func Execute() { } var vmDebugFlag bool +var unrestrictedNetworkingFlag bool func init() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil))) @@ -32,4 +33,5 @@ func init() { rootCmd.AddCommand(shellCmd) rootCmd.PersistentFlags().BoolVar(&vmDebugFlag, "vmdebug", false, "Enable VM debug mode. This will open an accessible VM monitor. You can log in with root user and no password.") + rootCmd.PersistentFlags().BoolVar(&unrestrictedNetworkingFlag, "unsafe-unrestricted-networking", false, "(UNSAFE) Enable unrestricted networking. This will allow the VM to connect to the internet.") } diff --git a/cmd/run.go b/cmd/run.go index fedd9eb..ca7a8ef 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -21,12 +21,29 @@ var runCmd = &cobra.Command{ vmMountDevName := args[1] fsType := args[2] - networkSharePort, err := getClosestAvailPort(9000) + ftpPassivePortCount := uint16(9) + + networkSharePort, err := getClosestAvailPortWithSubsequent(9000, 10) if err != nil { slog.Error("Failed to get closest available host port for network file share", "error", err) os.Exit(1) } + ports := []vm.PortForwardingRule{{ + HostIP: net.ParseIP("127.0.0.1"), // TODO: Make this changeable. + HostPort: networkSharePort, + VMPort: 21, + }} + + for i := uint16(0); i < ftpPassivePortCount; i++ { + p := networkSharePort + 1 + i + ports = append(ports, vm.PortForwardingRule{ + HostIP: net.ParseIP("127.0.0.1"), // TODO: Make this changeable. + HostPort: p, + VMPort: p, + }) + } + // TODO: `slog` library prints entire stack traces for errors which makes reading errors challenging. os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int { @@ -45,27 +62,21 @@ var runCmd = &cobra.Command{ return 1 } - // TODO: Use FTP instead of SMB + shareURI := "ftp://linsk:" + sharePWD + "@localhost:" + fmt.Sprint(networkSharePort) - shareURI := "smb://linsk:" + sharePWD + "@127.0.0.1:" + 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://localhost:%v\nUsername: linsk\nPassword: %v\n\nShare URI: %v\n================\n", 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: SMB\nServer Address: smb://127.0.0.1:%v\nUsername: linsk\nPassword: %v\n\nShare URI: %v\n================\n", networkSharePort, sharePWD, shareURI) - - err = fm.StartSMB([]byte(sharePWD)) + err = fm.StartFTP([]byte(sharePWD), networkSharePort+1, ftpPassivePortCount) if err != nil { - slog.Error("Failed to start SMB server", "error", err) + slog.Error("Failed to start FTP server", "error", err) return 1 } - slog.Info("Started the network share successfully", "type", "smb") + slog.Info("Started the network share successfully", "type", "ftp") <-ctx.Done() return 0 - }, []vm.PortForwardingRule{{ - HostIP: net.ParseIP("127.0.0.1"), // TODO: Make this changeable. - HostPort: networkSharePort, - VMPort: 445, - }}, false)) + }, ports, unrestrictedNetworkingFlag)) }, } diff --git a/cmd/shell.go b/cmd/shell.go index d5a8a77..5f691cb 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -132,9 +132,7 @@ var shellCmd = &cobra.Command{ } var forwardPortsFlagStr string -var unrestrictedNetworkingFlag bool func init() { - shellCmd.Flags().BoolVar(&unrestrictedNetworkingFlag, "unsafe-unrestricted-networking", false, "(UNSAFE) Enable unrestricted networking. This will allow the VM to connect to the internet.") shellCmd.Flags().StringVar(&forwardPortsFlagStr, "forward-ports", "", "Extra TCP port forwarding rules. Syntax: ':' OR '::'. Multiple rules split by comma are accepted.") } diff --git a/cmd/utils.go b/cmd/utils.go index a6c27ea..91209f5 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -163,29 +163,60 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag } } -func getClosestAvailPort(port uint16) (uint16, error) { - for i := port; i < 65535; i++ { - ln, err := net.Listen("tcp", ":"+fmt.Sprint(i)) +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. - continue + return false, nil } } } - return 0, errors.Wrapf(err, "net listen (port %v)", port) + return false, errors.Wrapf(err, "net listen (port %v)", port) } err = ln.Close() if err != nil { - return 0, errors.Wrap(err, "close ephemeral listener") + return false, errors.Wrap(err, "close ephemeral listener") } - return i, nil + return true, nil } - return 0, fmt.Errorf("no available port found") + 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) } diff --git a/vm/filemanager.go b/vm/filemanager.go index b3b87b9..f992928 100644 --- a/vm/filemanager.go +++ b/vm/filemanager.go @@ -203,7 +203,7 @@ func (fm *FileManager) Mount(devName string, mo MountOptions) error { return nil } -func (fm *FileManager) StartSMB(pwd []byte) error { +func (fm *FileManager) StartFTP(pwd []byte, passivePortStart uint16, passivePortCount uint16) error { scpClient, err := fm.vm.DialSCP() if err != nil { return errors.Wrap(err, "dial scp") @@ -211,22 +211,22 @@ func (fm *FileManager) StartSMB(pwd []byte) error { defer scpClient.Close() - sambaCfg := `[global] -workgroup = WORKGROUP -dos charset = cp866 -unix charset = utf-8 - -[linsk] -browseable = yes -writeable = yes -path = /mnt -force user = linsk -force group = linsk -create mask = 0664` + ftpdCfg := `anonymous_enable=NO +local_enable=YES +write_enable=YES +local_umask=022 +chroot_local_user=YES +allow_writeable_chroot=YES +listen=YES +seccomp_sandbox=NO +pasv_min_port=` + fmt.Sprint(passivePortStart) + ` +pasv_max_port=` + fmt.Sprint(passivePortStart+passivePortCount) + ` +pasv_address=127.0.0.1 +` - err = scpClient.CopyFile(fm.vm.ctx, strings.NewReader(sambaCfg), "/etc/samba/smb.conf", "0400") + err = scpClient.CopyFile(fm.vm.ctx, strings.NewReader(ftpdCfg), "/etc/vsftpd/vsftpd.conf", "0400") if err != nil { - return errors.Wrap(err, "copy samba config file") + return errors.Wrap(err, "copy ftpd .conf file") } scpClient.Close() @@ -238,9 +238,9 @@ create mask = 0664` defer func() { _ = sc.Close() }() - _, err = runSSHCmd(sc, "rc-update add samba && rc-service samba start") + _, err = runSSHCmd(sc, "rc-update add vsftpd && rc-service vsftpd start") if err != nil { - return errors.Wrap(err, "add and start samba service") + return errors.Wrap(err, "add and start ftpd service") } sess, err := sc.NewSession() @@ -260,25 +260,25 @@ create mask = 0664` // TODO: Timeout for this command - err = sess.Start("smbpasswd -a linsk") + err = sess.Start("passwd linsk") if err != nil { - return errors.Wrap(err, "start change samba password cmd") + return errors.Wrap(err, "start change user password cmd") } go func() { _, err = stdinPipe.Write(pwd) if err != nil { - fm.vm.logger.Error("Failed to write SMB password to smbpasswd stdin", "error", err) + fm.vm.logger.Error("Failed to write FTP password to passwd stdin", "error", err) } _, err = stdinPipe.Write(pwd) if err != nil { - fm.vm.logger.Error("Failed to write repeated SMB password to smbpasswd stdin", "error", err) + fm.vm.logger.Error("Failed to write repeated FTP password to passwd stdin", "error", err) } }() err = sess.Wait() if err != nil { - return utils.WrapErrWithLog(err, "wait for change samba password cmd", stderr.String()) + return utils.WrapErrWithLog(err, "wait for change user password cmd", stderr.String()) } return nil