From 7a1d74ae05ad32f74942e3fe4c242f4fd10e83a1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Oct 2024 01:01:00 +0900 Subject: [PATCH 1/2] hostagent: split pkg/freeport Signed-off-by: Akihiro Suda --- pkg/freeport/freeport.go | 51 ++++++++++++++++++++++++++++++ pkg/freeport/freeport_unix.go | 9 ++++++ pkg/freeport/freeport_windows.go | 7 +++++ pkg/hostagent/hostagent.go | 53 +++----------------------------- pkg/hostagent/port_darwin.go | 4 --- pkg/hostagent/port_others.go | 4 --- pkg/hostagent/port_windows.go | 5 --- 7 files changed, 72 insertions(+), 61 deletions(-) create mode 100644 pkg/freeport/freeport.go create mode 100644 pkg/freeport/freeport_unix.go create mode 100644 pkg/freeport/freeport_windows.go diff --git a/pkg/freeport/freeport.go b/pkg/freeport/freeport.go new file mode 100644 index 000000000000..d4e1a71a23e1 --- /dev/null +++ b/pkg/freeport/freeport.go @@ -0,0 +1,51 @@ +// Package freeport provides functions to find free localhost ports. +package freeport + +import ( + "fmt" + "net" +) + +func TCP() (int, error) { + lAddr0, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:0") + if err != nil { + return 0, err + } + l, err := net.ListenTCP("tcp4", lAddr0) + if err != nil { + return 0, err + } + defer l.Close() + lAddr := l.Addr() + lTCPAddr, ok := lAddr.(*net.TCPAddr) + if !ok { + return 0, fmt.Errorf("expected *net.TCPAddr, got %v", lAddr) + } + port := lTCPAddr.Port + if port <= 0 { + return 0, fmt.Errorf("unexpected port %d", port) + } + return port, nil +} + +func UDP() (int, error) { + lAddr0, err := net.ResolveUDPAddr("udp4", "127.0.0.1:0") + if err != nil { + return 0, err + } + l, err := net.ListenUDP("udp4", lAddr0) + if err != nil { + return 0, err + } + defer l.Close() + lAddr := l.LocalAddr() + lUDPAddr, ok := lAddr.(*net.UDPAddr) + if !ok { + return 0, fmt.Errorf("expected *net.UDPAddr, got %v", lAddr) + } + port := lUDPAddr.Port + if port <= 0 { + return 0, fmt.Errorf("unexpected port %d", port) + } + return port, nil +} diff --git a/pkg/freeport/freeport_unix.go b/pkg/freeport/freeport_unix.go new file mode 100644 index 000000000000..180e03623cdc --- /dev/null +++ b/pkg/freeport/freeport_unix.go @@ -0,0 +1,9 @@ +//go:build !windows + +package freeport + +import "errors" + +func VSock() (int, error) { + return 0, errors.New("freeport.VSock is not implemented for non-Windows hosts") +} diff --git a/pkg/freeport/freeport_windows.go b/pkg/freeport/freeport_windows.go new file mode 100644 index 000000000000..48bef99dbe36 --- /dev/null +++ b/pkg/freeport/freeport_windows.go @@ -0,0 +1,7 @@ +package freeport + +import "github.com/lima-vm/lima/pkg/windows" + +func VSock() (int, error) { + return windows.GetRandomFreeVSockPort(0, 2147483647) +} diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index f2f18debb5fa..db9f84814e61 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -22,6 +22,7 @@ import ( "github.com/lima-vm/lima/pkg/cidata" "github.com/lima-vm/lima/pkg/driver" "github.com/lima-vm/lima/pkg/driverutil" + "github.com/lima-vm/lima/pkg/freeport" guestagentapi "github.com/lima-vm/lima/pkg/guestagent/api" guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client" hostagentapi "github.com/lima-vm/lima/pkg/hostagent/api" @@ -108,11 +109,11 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt var udpDNSLocalPort, tcpDNSLocalPort int if *inst.Config.HostResolver.Enabled { - udpDNSLocalPort, err = findFreeUDPLocalPort() + udpDNSLocalPort, err = freeport.UDP() if err != nil { return nil, err } - tcpDNSLocalPort, err = findFreeTCPLocalPort() + tcpDNSLocalPort, err = freeport.TCP() if err != nil { return nil, err } @@ -123,7 +124,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt if *inst.Config.VMType == limayaml.VZ { vSockPort = 2222 } else if *inst.Config.VMType == limayaml.WSL2 { - port, err := getFreeVSockPort() + port, err := freeport.VSock() if err != nil { logrus.WithError(err).Error("failed to get free VSock port") } @@ -250,57 +251,13 @@ func determineSSHLocalPort(confLocalPort int, instName string) (int, error) { // use hard-coded value for "default" instance, for backward compatibility return 60022, nil } - sshLocalPort, err := findFreeTCPLocalPort() + sshLocalPort, err := freeport.TCP() if err != nil { return 0, fmt.Errorf("failed to find a free port, try setting `ssh.localPort` manually: %w", err) } return sshLocalPort, nil } -func findFreeTCPLocalPort() (int, error) { - lAddr0, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:0") - if err != nil { - return 0, err - } - l, err := net.ListenTCP("tcp4", lAddr0) - if err != nil { - return 0, err - } - defer l.Close() - lAddr := l.Addr() - lTCPAddr, ok := lAddr.(*net.TCPAddr) - if !ok { - return 0, fmt.Errorf("expected *net.TCPAddr, got %v", lAddr) - } - port := lTCPAddr.Port - if port <= 0 { - return 0, fmt.Errorf("unexpected port %d", port) - } - return port, nil -} - -func findFreeUDPLocalPort() (int, error) { - lAddr0, err := net.ResolveUDPAddr("udp4", "127.0.0.1:0") - if err != nil { - return 0, err - } - l, err := net.ListenUDP("udp4", lAddr0) - if err != nil { - return 0, err - } - defer l.Close() - lAddr := l.LocalAddr() - lUDPAddr, ok := lAddr.(*net.UDPAddr) - if !ok { - return 0, fmt.Errorf("expected *net.UDPAddr, got %v", lAddr) - } - port := lUDPAddr.Port - if port <= 0 { - return 0, fmt.Errorf("unexpected port %d", port) - } - return port, nil -} - func (a *HostAgent) emitEvent(_ context.Context, ev events.Event) { a.eventEncMu.Lock() defer a.eventEncMu.Unlock() diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go index ee4b388d2d5d..615a2bd03f9a 100644 --- a/pkg/hostagent/port_darwin.go +++ b/pkg/hostagent/port_darwin.go @@ -155,7 +155,3 @@ func (plf *pseudoLoopbackForwarder) Close() error { _ = plf.ln.Close() return plf.onClose() } - -func getFreeVSockPort() (int, error) { - return 0, nil -} diff --git a/pkg/hostagent/port_others.go b/pkg/hostagent/port_others.go index da0d364463c3..c038e6c377d1 100644 --- a/pkg/hostagent/port_others.go +++ b/pkg/hostagent/port_others.go @@ -11,7 +11,3 @@ import ( func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error { return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) } - -func getFreeVSockPort() (int, error) { - return 0, nil -} diff --git a/pkg/hostagent/port_windows.go b/pkg/hostagent/port_windows.go index 8531e0df862e..6a3d74e14b7d 100644 --- a/pkg/hostagent/port_windows.go +++ b/pkg/hostagent/port_windows.go @@ -3,14 +3,9 @@ package hostagent import ( "context" - "github.com/lima-vm/lima/pkg/windows" "github.com/lima-vm/sshocker/pkg/ssh" ) func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error { return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) } - -func getFreeVSockPort() (int, error) { - return windows.GetRandomFreeVSockPort(0, 2147483647) -} From b34267cf6ba0d142a74fa2f16a3fa26b687b34bd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Oct 2024 01:42:46 +0900 Subject: [PATCH 2/2] limactl: add `tunnel` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ```console $ limactl tunnel default Open (or whatever) →
, and specify the following configuration: - Server: 127.0.0.1 - Port: 54940 The instance can be connected from the host as via a web browser. $ curl --proxy socks5h://127.0.0.1:54940 http://lima-default.internal [...] ``` Signed-off-by: Akihiro Suda --- cmd/limactl/main.go | 1 + cmd/limactl/tunnel.go | 158 ++++++++++++++++++ .../en/docs/releases/experimental/_index.md | 1 + 3 files changed, 160 insertions(+) create mode 100644 cmd/limactl/tunnel.go diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go index 24967b12bba5..2efad9207296 100644 --- a/cmd/limactl/main.go +++ b/cmd/limactl/main.go @@ -154,6 +154,7 @@ func newApp() *cobra.Command { newSnapshotCommand(), newProtectCommand(), newUnprotectCommand(), + newTunnelCommand(), ) if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { rootCmd.AddCommand(startAtLoginCommand()) diff --git a/cmd/limactl/tunnel.go b/cmd/limactl/tunnel.go new file mode 100644 index 000000000000..40ac30fe4126 --- /dev/null +++ b/cmd/limactl/tunnel.go @@ -0,0 +1,158 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "runtime" + "strconv" + + "github.com/lima-vm/lima/pkg/freeport" + "github.com/lima-vm/lima/pkg/sshutil" + "github.com/lima-vm/lima/pkg/store" + "github.com/mattn/go-shellwords" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const tunnelHelp = `Create a tunnel for Lima + +Create a SOCKS tunnel so that the host can join the guest network. +` + +func newTunnelCommand() *cobra.Command { + tunnelCmd := &cobra.Command{ + Use: "tunnel [flags] INSTANCE", + Short: "Create a tunnel for Lima", + PersistentPreRun: func(*cobra.Command, []string) { + logrus.Warn("`limactl tunnel` is experimental") + }, + Long: tunnelHelp, + Args: WrapArgsError(cobra.ExactArgs(1)), + RunE: tunnelAction, + ValidArgsFunction: tunnelBashComplete, + SilenceErrors: true, + GroupID: advancedCommand, + } + + tunnelCmd.Flags().SetInterspersed(false) + // TODO: implement l2tp, ikev2, masque, ... + tunnelCmd.Flags().String("type", "socks", "Tunnel type, currently only \"socks\" is implemented") + tunnelCmd.Flags().Int("socks-port", 0, "SOCKS port, defaults to a random port") + return tunnelCmd +} + +func tunnelAction(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + tunnelType, err := flags.GetString("type") + if err != nil { + return err + } + if tunnelType != "socks" { + return fmt.Errorf("unknown tunnel type: %q", tunnelType) + } + port, err := flags.GetInt("socks-port") + if err != nil { + return err + } + if port != 0 && (port < 1024 || port > 65535) { + return fmt.Errorf("invalid socks port %d", port) + } + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + instName := args[0] + inst, err := store.Inspect(instName) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) + } + return err + } + if inst.Status == store.StatusStopped { + return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) + } + + if port == 0 { + port, err = freeport.TCP() + if err != nil { + return err + } + } + + var ( + arg0 string + arg0Args []string + ) + // FIXME: deduplicate the code clone across `limactl shell` and `limactl tunnel` + if sshShell := os.Getenv(envShellSSH); sshShell != "" { + sshShellFields, err := shellwords.Parse(sshShell) + switch { + case err != nil: + logrus.WithError(err).Warnf("Failed to split %s variable into shell tokens. "+ + "Falling back to 'ssh' command", envShellSSH) + case len(sshShellFields) > 0: + arg0 = sshShellFields[0] + if len(sshShellFields) > 1 { + arg0Args = sshShellFields[1:] + } + } + } + + if arg0 == "" { + arg0, err = exec.LookPath("ssh") + if err != nil { + return err + } + } + + sshOpts, err := sshutil.SSHOpts( + inst.Dir, + *inst.Config.SSH.LoadDotSSHPubKeys, + *inst.Config.SSH.ForwardAgent, + *inst.Config.SSH.ForwardX11, + *inst.Config.SSH.ForwardX11Trusted) + if err != nil { + return err + } + sshArgs := sshutil.SSHArgsFromOpts(sshOpts) + sshArgs = append(sshArgs, []string{ + "-q", // quiet + "-f", // background + "-N", // no command + "-D", fmt.Sprintf("127.0.0.1:%d", port), + "-p", strconv.Itoa(inst.SSHLocalPort), + inst.SSHAddress, + }...) + sshCmd := exec.Command(arg0, append(arg0Args, sshArgs...)...) + sshCmd.Stdout = stderr + sshCmd.Stderr = stderr + logrus.Debugf("executing ssh (may take a long)): %+v", sshCmd.Args) + + if err := sshCmd.Run(); err != nil { + return err + } + + switch runtime.GOOS { + case "darwin": + fmt.Fprintf(stdout, "Open (or whatever) →
,\n") + fmt.Fprintf(stdout, "and specify the following configuration:\n") + fmt.Fprintf(stdout, "- Server: 127.0.0.1\n") + fmt.Fprintf(stdout, "- Port: %d\n", port) + case "windows": + fmt.Fprintf(stdout, "Open ,\n") + fmt.Fprintf(stdout, "and specify the following configuration:\n") + fmt.Fprintf(stdout, "- Address: socks=127.0.0.1\n") + fmt.Fprintf(stdout, "- Port: %d\n", port) + default: + fmt.Fprintf(stdout, "Set `ALL_PROXY=socks5h://127.0.0.1:%d`, etc.\n", port) + } + fmt.Fprintf(stdout, "The instance can be connected from the host as via a web browser.\n", inst.Name) + + // TODO: show the port in `limactl list --json` ? + // TODO: add `--stop` flag to shut down the tunnel + return nil +} + +func tunnelBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return bashCompleteInstanceNames(cmd) +} diff --git a/website/content/en/docs/releases/experimental/_index.md b/website/content/en/docs/releases/experimental/_index.md index 92f6d2ff0bbd..2310e41c56d4 100644 --- a/website/content/en/docs/releases/experimental/_index.md +++ b/website/content/en/docs/releases/experimental/_index.md @@ -17,6 +17,7 @@ The following features are experimental and subject to change: The following commands are experimental and subject to change: - `limactl snapshot *` +- `limactl tunnel` ## Graduated