Skip to content

Commit

Permalink
Merge pull request #2710 from AkihiroSuda/tunnel
Browse files Browse the repository at this point in the history
limactl: add `tunnel` command (experimental)
  • Loading branch information
AkihiroSuda authored Oct 28, 2024
2 parents 54125da + b34267c commit 1f5b94d
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 61 deletions.
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
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

0 comments on commit 1f5b94d

Please # to comment.