Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

limactl: add tunnel command (experimental) #2710

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func newApp() *cobra.Command {
newSnapshotCommand(),
newProtectCommand(),
newUnprotectCommand(),
newTunnelCommand(),
)
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
rootCmd.AddCommand(startAtLoginCommand())
Expand Down
158 changes: 158 additions & 0 deletions cmd/limactl/tunnel.go
Original file line number Diff line number Diff line change
@@ -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 <System Settings> → <Network> → <Wi-Fi> (or whatever) → <Details> → <Proxies> → <SOCKS proxy>,\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 <Settings> → <Network & Internet> → <Proxy>,\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 <http://lima-%s.internal> via a web browser.\n", inst.Name)

// TODO: show the port in `limactl list --json` ?
// TODO: add `--stop` flag to shut down the tunnel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we planning to address these todo now or later release ??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later.
So probably this should be marked as experimental

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR to mark the command experimental

return nil
}

func tunnelBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
51 changes: 51 additions & 0 deletions pkg/freeport/freeport.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions pkg/freeport/freeport_unix.go
Original file line number Diff line number Diff line change
@@ -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")
}
7 changes: 7 additions & 0 deletions pkg/freeport/freeport_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package freeport

import "github.com/lima-vm/lima/pkg/windows"

func VSock() (int, error) {
return windows.GetRandomFreeVSockPort(0, 2147483647)
}
53 changes: 5 additions & 48 deletions pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 0 additions & 4 deletions pkg/hostagent/port_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,3 @@ func (plf *pseudoLoopbackForwarder) Close() error {
_ = plf.ln.Close()
return plf.onClose()
}

func getFreeVSockPort() (int, error) {
return 0, nil
}
4 changes: 0 additions & 4 deletions pkg/hostagent/port_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 0 additions & 5 deletions pkg/hostagent/port_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions website/content/en/docs/releases/experimental/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading