Skip to content

Commit

Permalink
limactl: add tunnel command
Browse files Browse the repository at this point in the history
```console
$ limactl tunnel default
Open <System Settings> → <Network> → <Wi-Fi> (or whatever) → <Details> → <Proxies> → <SOCKS proxy>,
and specify the following configuration:
- Server: 127.0.0.1
- Port: 54940
The instance can be connected from the host as <http://lima-default.internal> via a web browser.

$ curl --proxy socks5h://127.0.0.1:54940 http://lima-default.internal
<!DOCTYPE html>
[...]
```

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Oct 25, 2024
1 parent 7a1d74a commit b34267c
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 0 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)
}
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 b34267c

Please # to comment.