Skip to content

Commit c3d41a7

Browse files
committed
limactl: add tunnel command
```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 <[email protected]>
1 parent 2f93592 commit c3d41a7

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

cmd/limactl/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ func newApp() *cobra.Command {
153153
newSnapshotCommand(),
154154
newProtectCommand(),
155155
newUnprotectCommand(),
156+
newTunnelCommand(),
156157
)
157158
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
158159
rootCmd.AddCommand(startAtLoginCommand())

cmd/limactl/tunnel.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"runtime"
9+
"strconv"
10+
11+
"github.com/lima-vm/lima/pkg/freeport"
12+
"github.com/lima-vm/lima/pkg/sshutil"
13+
"github.com/lima-vm/lima/pkg/store"
14+
"github.com/mattn/go-shellwords"
15+
"github.com/sirupsen/logrus"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
const tunnelHelp = `Create a tunnel for Lima
20+
21+
Create a SOCKS tunnel so that the host can join the guest network.
22+
`
23+
24+
func newTunnelCommand() *cobra.Command {
25+
tunnelCmd := &cobra.Command{
26+
Use: "tunnel [flags] INSTANCE",
27+
Short: "Create a tunnel for Lima",
28+
Long: tunnelHelp,
29+
Args: WrapArgsError(cobra.ExactArgs(1)),
30+
RunE: tunnelAction,
31+
ValidArgsFunction: tunnelBashComplete,
32+
SilenceErrors: true,
33+
GroupID: advancedCommand,
34+
}
35+
36+
tunnelCmd.Flags().SetInterspersed(false)
37+
// TODO: implement l2tp, ikev2, masque, ...
38+
tunnelCmd.Flags().String("type", "socks", "Tunnel type, currently only \"socks\" is implemented")
39+
tunnelCmd.Flags().Int("socks-port", 0, "SOCKS port, defaults to a random port")
40+
return tunnelCmd
41+
}
42+
43+
func tunnelAction(cmd *cobra.Command, args []string) error {
44+
flags := cmd.Flags()
45+
tunnelType, err := flags.GetString("type")
46+
if err != nil {
47+
return err
48+
}
49+
if tunnelType != "socks" {
50+
return fmt.Errorf("unknown tunnel type: %q", tunnelType)
51+
}
52+
port, err := flags.GetInt("socks-port")
53+
if err != nil {
54+
return err
55+
}
56+
if port != 0 && (port < 1024 || port > 65535) {
57+
return fmt.Errorf("invalid socks port %d", port)
58+
}
59+
stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
60+
instName := args[0]
61+
inst, err := store.Inspect(instName)
62+
if err != nil {
63+
if errors.Is(err, os.ErrNotExist) {
64+
return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
65+
}
66+
return err
67+
}
68+
if inst.Status == store.StatusStopped {
69+
return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
70+
}
71+
72+
if port == 0 {
73+
port, err = freeport.TCP()
74+
if err != nil {
75+
return err
76+
}
77+
}
78+
79+
var (
80+
arg0 string
81+
arg0Args []string
82+
)
83+
// FIXME: deduplicate the code clone across `limactl shell` and `limactl tunnel`
84+
if sshShell := os.Getenv(envShellSSH); sshShell != "" {
85+
sshShellFields, err := shellwords.Parse(sshShell)
86+
switch {
87+
case err != nil:
88+
logrus.WithError(err).Warnf("Failed to split %s variable into shell tokens. "+
89+
"Falling back to 'ssh' command", envShellSSH)
90+
case len(sshShellFields) > 0:
91+
arg0 = sshShellFields[0]
92+
if len(sshShellFields) > 1 {
93+
arg0Args = sshShellFields[1:]
94+
}
95+
}
96+
}
97+
98+
if arg0 == "" {
99+
arg0, err = exec.LookPath("ssh")
100+
if err != nil {
101+
return err
102+
}
103+
}
104+
105+
sshOpts, err := sshutil.SSHOpts(
106+
inst.Dir,
107+
*inst.Config.SSH.LoadDotSSHPubKeys,
108+
*inst.Config.SSH.ForwardAgent,
109+
*inst.Config.SSH.ForwardX11,
110+
*inst.Config.SSH.ForwardX11Trusted)
111+
if err != nil {
112+
return err
113+
}
114+
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
115+
sshArgs = append(sshArgs, []string{
116+
"-q", // quiet
117+
"-f", // background
118+
"-N", // no command
119+
"-D", fmt.Sprintf("127.0.0.1:%d", port),
120+
"-p", strconv.Itoa(inst.SSHLocalPort),
121+
inst.SSHAddress,
122+
}...)
123+
sshCmd := exec.Command(arg0, append(arg0Args, sshArgs...)...)
124+
sshCmd.Stdout = stderr
125+
sshCmd.Stderr = stderr
126+
logrus.Debugf("executing ssh (may take a long)): %+v", sshCmd.Args)
127+
128+
if err := sshCmd.Run(); err != nil {
129+
return err
130+
}
131+
132+
switch runtime.GOOS {
133+
case "darwin":
134+
fmt.Fprintf(stdout, "Open <System Settings> → <Network> → <Wi-Fi> (or whatever) → <Details> → <Proxies> → <SOCKS proxy>,\n")
135+
fmt.Fprintf(stdout, "and specify the following configuration:\n")
136+
fmt.Fprintf(stdout, "- Server: 127.0.0.1\n")
137+
fmt.Fprintf(stdout, "- Port: %d\n", port)
138+
case "windows":
139+
fmt.Fprintf(stdout, "Open <Settings> → <Network & Internet> → <Proxy>,\n")
140+
fmt.Fprintf(stdout, "and specify the following configuration:\n")
141+
fmt.Fprintf(stdout, "- Address: socks=127.0.0.1\n")
142+
fmt.Fprintf(stdout, "- Port: %d\n", port)
143+
default:
144+
fmt.Fprintf(stdout, "Set `ALL_PROXY=socks5h://127.0.0.1:%d`, etc.\n", port)
145+
}
146+
fmt.Fprintf(stdout, "The instance can be connected from the host as <http://lima-%s.internal> via a web browser.\n", inst.Name)
147+
148+
// TODO: show the port in `limactl list --json` ?
149+
// TODO: add `--stop` flag to shut down the tunnel
150+
return nil
151+
}
152+
153+
func tunnelBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
154+
return bashCompleteInstanceNames(cmd)
155+
}

0 commit comments

Comments
 (0)