Skip to content

Commit 1f5b94d

Browse files
authored
Merge pull request #2710 from AkihiroSuda/tunnel
limactl: add `tunnel` command (experimental)
2 parents 54125da + b34267c commit 1f5b94d

File tree

10 files changed

+232
-61
lines changed

10 files changed

+232
-61
lines changed

Diff for: cmd/limactl/main.go

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

Diff for: cmd/limactl/tunnel.go

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

Diff for: pkg/freeport/freeport.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Package freeport provides functions to find free localhost ports.
2+
package freeport
3+
4+
import (
5+
"fmt"
6+
"net"
7+
)
8+
9+
func TCP() (int, error) {
10+
lAddr0, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:0")
11+
if err != nil {
12+
return 0, err
13+
}
14+
l, err := net.ListenTCP("tcp4", lAddr0)
15+
if err != nil {
16+
return 0, err
17+
}
18+
defer l.Close()
19+
lAddr := l.Addr()
20+
lTCPAddr, ok := lAddr.(*net.TCPAddr)
21+
if !ok {
22+
return 0, fmt.Errorf("expected *net.TCPAddr, got %v", lAddr)
23+
}
24+
port := lTCPAddr.Port
25+
if port <= 0 {
26+
return 0, fmt.Errorf("unexpected port %d", port)
27+
}
28+
return port, nil
29+
}
30+
31+
func UDP() (int, error) {
32+
lAddr0, err := net.ResolveUDPAddr("udp4", "127.0.0.1:0")
33+
if err != nil {
34+
return 0, err
35+
}
36+
l, err := net.ListenUDP("udp4", lAddr0)
37+
if err != nil {
38+
return 0, err
39+
}
40+
defer l.Close()
41+
lAddr := l.LocalAddr()
42+
lUDPAddr, ok := lAddr.(*net.UDPAddr)
43+
if !ok {
44+
return 0, fmt.Errorf("expected *net.UDPAddr, got %v", lAddr)
45+
}
46+
port := lUDPAddr.Port
47+
if port <= 0 {
48+
return 0, fmt.Errorf("unexpected port %d", port)
49+
}
50+
return port, nil
51+
}

Diff for: pkg/freeport/freeport_unix.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build !windows
2+
3+
package freeport
4+
5+
import "errors"
6+
7+
func VSock() (int, error) {
8+
return 0, errors.New("freeport.VSock is not implemented for non-Windows hosts")
9+
}

Diff for: pkg/freeport/freeport_windows.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package freeport
2+
3+
import "github.com/lima-vm/lima/pkg/windows"
4+
5+
func VSock() (int, error) {
6+
return windows.GetRandomFreeVSockPort(0, 2147483647)
7+
}

Diff for: pkg/hostagent/hostagent.go

+5-48
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/lima-vm/lima/pkg/cidata"
2323
"github.com/lima-vm/lima/pkg/driver"
2424
"github.com/lima-vm/lima/pkg/driverutil"
25+
"github.com/lima-vm/lima/pkg/freeport"
2526
guestagentapi "github.com/lima-vm/lima/pkg/guestagent/api"
2627
guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client"
2728
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
108109

109110
var udpDNSLocalPort, tcpDNSLocalPort int
110111
if *inst.Config.HostResolver.Enabled {
111-
udpDNSLocalPort, err = findFreeUDPLocalPort()
112+
udpDNSLocalPort, err = freeport.UDP()
112113
if err != nil {
113114
return nil, err
114115
}
115-
tcpDNSLocalPort, err = findFreeTCPLocalPort()
116+
tcpDNSLocalPort, err = freeport.TCP()
116117
if err != nil {
117118
return nil, err
118119
}
@@ -123,7 +124,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
123124
if *inst.Config.VMType == limayaml.VZ {
124125
vSockPort = 2222
125126
} else if *inst.Config.VMType == limayaml.WSL2 {
126-
port, err := getFreeVSockPort()
127+
port, err := freeport.VSock()
127128
if err != nil {
128129
logrus.WithError(err).Error("failed to get free VSock port")
129130
}
@@ -250,57 +251,13 @@ func determineSSHLocalPort(confLocalPort int, instName string) (int, error) {
250251
// use hard-coded value for "default" instance, for backward compatibility
251252
return 60022, nil
252253
}
253-
sshLocalPort, err := findFreeTCPLocalPort()
254+
sshLocalPort, err := freeport.TCP()
254255
if err != nil {
255256
return 0, fmt.Errorf("failed to find a free port, try setting `ssh.localPort` manually: %w", err)
256257
}
257258
return sshLocalPort, nil
258259
}
259260

260-
func findFreeTCPLocalPort() (int, error) {
261-
lAddr0, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:0")
262-
if err != nil {
263-
return 0, err
264-
}
265-
l, err := net.ListenTCP("tcp4", lAddr0)
266-
if err != nil {
267-
return 0, err
268-
}
269-
defer l.Close()
270-
lAddr := l.Addr()
271-
lTCPAddr, ok := lAddr.(*net.TCPAddr)
272-
if !ok {
273-
return 0, fmt.Errorf("expected *net.TCPAddr, got %v", lAddr)
274-
}
275-
port := lTCPAddr.Port
276-
if port <= 0 {
277-
return 0, fmt.Errorf("unexpected port %d", port)
278-
}
279-
return port, nil
280-
}
281-
282-
func findFreeUDPLocalPort() (int, error) {
283-
lAddr0, err := net.ResolveUDPAddr("udp4", "127.0.0.1:0")
284-
if err != nil {
285-
return 0, err
286-
}
287-
l, err := net.ListenUDP("udp4", lAddr0)
288-
if err != nil {
289-
return 0, err
290-
}
291-
defer l.Close()
292-
lAddr := l.LocalAddr()
293-
lUDPAddr, ok := lAddr.(*net.UDPAddr)
294-
if !ok {
295-
return 0, fmt.Errorf("expected *net.UDPAddr, got %v", lAddr)
296-
}
297-
port := lUDPAddr.Port
298-
if port <= 0 {
299-
return 0, fmt.Errorf("unexpected port %d", port)
300-
}
301-
return port, nil
302-
}
303-
304261
func (a *HostAgent) emitEvent(_ context.Context, ev events.Event) {
305262
a.eventEncMu.Lock()
306263
defer a.eventEncMu.Unlock()

Diff for: pkg/hostagent/port_darwin.go

-4
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,3 @@ func (plf *pseudoLoopbackForwarder) Close() error {
155155
_ = plf.ln.Close()
156156
return plf.onClose()
157157
}
158-
159-
func getFreeVSockPort() (int, error) {
160-
return 0, nil
161-
}

Diff for: pkg/hostagent/port_others.go

-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,3 @@ import (
1111
func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error {
1212
return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
1313
}
14-
15-
func getFreeVSockPort() (int, error) {
16-
return 0, nil
17-
}

Diff for: pkg/hostagent/port_windows.go

-5
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,9 @@ package hostagent
33
import (
44
"context"
55

6-
"github.com/lima-vm/lima/pkg/windows"
76
"github.com/lima-vm/sshocker/pkg/ssh"
87
)
98

109
func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error {
1110
return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
1211
}
13-
14-
func getFreeVSockPort() (int, error) {
15-
return windows.GetRandomFreeVSockPort(0, 2147483647)
16-
}

Diff for: website/content/en/docs/releases/experimental/_index.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The following features are experimental and subject to change:
1717
The following commands are experimental and subject to change:
1818

1919
- `limactl snapshot *`
20+
- `limactl tunnel`
2021

2122
## Graduated
2223

0 commit comments

Comments
 (0)