Skip to content

Commit 8bd3636

Browse files
committed
Implement "Dual" port forwarder (SSH for TCP, GRPC for UDP)
This new default forwarder uses SSH for TCP, as SSH now outperforms GRPC when VSOCK is available. GRPC is used for UDP, as SSH does not support UDP. Fix issue 4074 Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1 parent dce57ae commit 8bd3636

File tree

16 files changed

+473
-91
lines changed

16 files changed

+473
-91
lines changed

hack/test-templates/test-misc.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ user:
111111
# Ubuntu has identical /bin/bash and /usr/bin/bash
112112
shell: /usr/bin/bash
113113

114+
portForwardTypes:
115+
any: grpc
116+
114117
portForwards:
115118
- guestPort: 80
116119
hostPort: 9090

pkg/driver/vz/vz_driver_darwin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ var knownYamlProperties = []string{
5555
"OS",
5656
"Param",
5757
"Plain",
58+
"PortForwardTypes",
5859
"PortForwards",
5960
"Probes",
6061
"PropagateProxyEnv",

pkg/driver/wsl2/wsl_driver_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var knownYamlProperties = []string{
3939
"MountType",
4040
"Param",
4141
"Plain",
42+
"PortForwardTypes",
4243
"PortForwards",
4344
"Probes",
4445
"PropagateProxyEnv",

pkg/hostagent/hostagent.go

Lines changed: 91 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"errors"
1212
"fmt"
1313
"io"
14+
"maps"
1415
"net"
1516
"os"
1617
"os/exec"
@@ -113,6 +114,82 @@ func WithCloudInitProgress(enabled bool) Opt {
113114
}
114115
}
115116

117+
// resolvePortForwardTypes resolves port forwarding types.
118+
// The returned result may not contain [limatype.ProtoAny] keys, and [limatype.PortForwardTypeNone] values.
119+
func resolvePortForwardTypes(portForwardTypes map[limatype.Proto]limatype.PortForwardType, portForwards []limatype.PortForward) (map[limatype.Proto]limatype.PortForwardType, error) {
120+
// The default port forwarding mode since Lima v2.0 is "dual": {tcp: ssh, udp: grpc}.
121+
// The default values are set in [limayaml.FillDefault], not here.
122+
if err := limayaml.ValidatePortForwardTypes(portForwardTypes); err != nil {
123+
return nil, err
124+
}
125+
126+
res := maps.Clone(portForwardTypes)
127+
128+
// Fix up keys
129+
for k, v := range res {
130+
if k == limatype.ProtoAny {
131+
for _, proto := range []limatype.Proto{limatype.ProtoTCP, limatype.ProtoUDP} {
132+
if res[proto] != limatype.PortForwardTypeNone {
133+
res[proto] = v
134+
}
135+
}
136+
delete(res, k)
137+
}
138+
}
139+
140+
// Fix up values
141+
for k, v := range res {
142+
if v == limatype.PortForwardTypeNone {
143+
delete(res, k)
144+
}
145+
}
146+
147+
// Apply "ignore all ports" rules from portForwards
148+
for _, rule := range portForwards {
149+
if rule.Ignore && rule.GuestPortRange[0] == 1 && rule.GuestPortRange[1] == 65535 {
150+
switch rule.Proto {
151+
case limatype.ProtoTCP:
152+
delete(res, limatype.ProtoTCP)
153+
case limatype.ProtoUDP:
154+
delete(res, limatype.ProtoUDP)
155+
case limatype.ProtoAny:
156+
delete(res, limatype.ProtoTCP)
157+
delete(res, limatype.ProtoUDP)
158+
}
159+
} else {
160+
break
161+
}
162+
}
163+
164+
// Apply LIMA_SSH_PORT_FORWARDER env var for backward compatibility
165+
if envVar := os.Getenv("LIMA_SSH_PORT_FORWARDER"); envVar != "" {
166+
logrus.WithField("LIMA_SSH_PORT_FORWARDER", envVar).Warnf("LIMA_SSH_PORT_FORWARDER=false is deprecated; use portForwardTypes config instead")
167+
b, err := strconv.ParseBool(envVar)
168+
if err != nil {
169+
return nil, fmt.Errorf("invalid LIMA_SSH_PORT_FORWARDER value %q", envVar)
170+
}
171+
if b {
172+
if _, ok := res[limatype.ProtoTCP]; ok {
173+
res[limatype.ProtoTCP] = limatype.PortForwardTypeSSH
174+
}
175+
// No UDP support in SSH port forwarder
176+
delete(res, limatype.ProtoUDP)
177+
} else {
178+
for _, proto := range []limatype.Proto{limatype.ProtoTCP, limatype.ProtoUDP} {
179+
if _, ok := res[proto]; ok {
180+
res[proto] = limatype.PortForwardTypeGRPC
181+
}
182+
}
183+
}
184+
}
185+
186+
if err := limayaml.ValidatePortForwardTypes(res); err != nil {
187+
return nil, err
188+
}
189+
190+
return res, nil
191+
}
192+
116193
// New creates the HostAgent.
117194
//
118195
// stdout is for emitting JSON lines of Events.
@@ -202,26 +279,15 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
202279
AdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts),
203280
}
204281

205-
ignoreTCP := false
206-
ignoreUDP := false
207-
for _, rule := range inst.Config.PortForwards {
208-
if rule.Ignore && rule.GuestPortRange[0] == 1 && rule.GuestPortRange[1] == 65535 {
209-
switch rule.Proto {
210-
case limatype.ProtoTCP:
211-
ignoreTCP = true
212-
logrus.Info("TCP port forwarding is disabled (except for SSH)")
213-
case limatype.ProtoUDP:
214-
ignoreUDP = true
215-
logrus.Info("UDP port forwarding is disabled")
216-
case limatype.ProtoAny:
217-
ignoreTCP = true
218-
ignoreUDP = true
219-
logrus.Info("TCP (except for SSH) and UDP port forwarding is disabled")
220-
}
221-
} else {
222-
break
223-
}
282+
portForwardTypes, err := resolvePortForwardTypes(inst.Config.PortForwardTypes, inst.Config.PortForwards)
283+
if err != nil {
284+
return nil, err
224285
}
286+
logrus.WithField("portForwardTypes", portForwardTypes).Info("Resolved port forwarding types")
287+
sshFwdIgnoreTCP := portForwardTypes[limatype.ProtoTCP] != limatype.PortForwardTypeSSH
288+
grpcFwdIgnoreTCP := portForwardTypes[limatype.ProtoTCP] != limatype.PortForwardTypeGRPC
289+
grpcFwdIgnoreUDP := portForwardTypes[limatype.ProtoUDP] != limatype.PortForwardTypeGRPC
290+
225291
rules := make([]limatype.PortForward, 0, 3+len(inst.Config.PortForwards))
226292
// Block ports 22 and sshLocalPort on all IPs
227293
for _, port := range []int{sshGuestPort, sshLocalPort} {
@@ -244,8 +310,8 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
244310
instName: instName,
245311
instSSHAddress: inst.SSHAddress,
246312
sshConfig: sshConfig,
247-
portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, ignoreTCP, inst.VMType),
248-
grpcPortForwarder: portfwd.NewPortForwarder(rules, ignoreTCP, ignoreUDP),
313+
portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, sshFwdIgnoreTCP, inst.VMType),
314+
grpcPortForwarder: portfwd.NewPortForwarder(rules, grpcFwdIgnoreTCP, grpcFwdIgnoreUDP),
249315
driver: limaDriver,
250316
signalCh: signalCh,
251317
eventEnc: json.NewEncoder(stdout),
@@ -796,26 +862,10 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestag
796862
for _, f := range ev.Errors {
797863
logrus.Warnf("received error from the guest: %q", f)
798864
}
799-
// History of the default value of useSSHFwd:
800-
// - v0.1.0: true (effectively)
801-
// - v1.0.0: false
802-
// - v1.0.1: true
803-
// - v1.1.0-beta.0: false
804-
useSSHFwd := false
805-
if envVar := os.Getenv("LIMA_SSH_PORT_FORWARDER"); envVar != "" {
806-
b, err := strconv.ParseBool(envVar)
807-
if err != nil {
808-
logrus.WithError(err).Warnf("invalid LIMA_SSH_PORT_FORWARDER value %q", envVar)
809-
} else {
810-
useSSHFwd = b
811-
}
812-
}
813-
if useSSHFwd {
814-
a.portForwarder.OnEvent(ctx, ev)
815-
} else {
816-
dialContext := portfwd.DialContextToGRPCTunnel(client)
817-
a.grpcPortForwarder.OnEvent(ctx, dialContext, ev)
818-
}
865+
866+
a.portForwarder.OnEvent(ctx, ev)
867+
dialContext := portfwd.DialContextToGRPCTunnel(client)
868+
a.grpcPortForwarder.OnEvent(ctx, dialContext, ev)
819869
}
820870

821871
if err := client.Events(ctx, onEvent); err != nil {

pkg/hostagent/hostagent_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package hostagent
5+
6+
import (
7+
"testing"
8+
9+
"gotest.tools/v3/assert"
10+
11+
"github.com/lima-vm/lima/v2/pkg/limatype"
12+
)
13+
14+
func TestResolvePortForwardTypes(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
portForwardTypes map[limatype.Proto]limatype.PortForwardType
18+
portForwards []limatype.PortForward
19+
env map[string]string
20+
expected map[limatype.Proto]limatype.PortForwardType
21+
expectedErr string
22+
}{
23+
{
24+
name: "default",
25+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
26+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
27+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
28+
},
29+
portForwards: nil,
30+
env: nil,
31+
expected: map[limatype.Proto]limatype.PortForwardType{
32+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
33+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
34+
},
35+
},
36+
{
37+
name: "grpc only via config",
38+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
39+
limatype.ProtoAny: limatype.PortForwardTypeGRPC,
40+
},
41+
portForwards: nil,
42+
env: nil,
43+
expected: map[limatype.Proto]limatype.PortForwardType{
44+
limatype.ProtoTCP: limatype.PortForwardTypeGRPC,
45+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
46+
},
47+
},
48+
{
49+
name: "ssh only via env",
50+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
51+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
52+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
53+
},
54+
portForwards: nil,
55+
env: map[string]string{
56+
"LIMA_SSH_PORT_FORWARDER": "true",
57+
},
58+
expected: map[limatype.Proto]limatype.PortForwardType{
59+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
60+
// No UDP support in SSH port forwarder
61+
},
62+
},
63+
{
64+
name: "grpc only via env",
65+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
66+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
67+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
68+
},
69+
portForwards: nil,
70+
env: map[string]string{
71+
"LIMA_SSH_PORT_FORWARDER": "false",
72+
},
73+
expected: map[limatype.Proto]limatype.PortForwardType{
74+
limatype.ProtoTCP: limatype.PortForwardTypeGRPC,
75+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
76+
},
77+
},
78+
{
79+
name: "disable tcp via portForwards",
80+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
81+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
82+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
83+
},
84+
portForwards: []limatype.PortForward{
85+
{
86+
Ignore: true,
87+
Proto: limatype.ProtoTCP,
88+
GuestPortRange: [2]int{1, 65535},
89+
},
90+
},
91+
env: nil,
92+
expected: map[limatype.Proto]limatype.PortForwardType{
93+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
94+
},
95+
},
96+
{
97+
name: "disable tcp via portForwards, with any in portForwardTypes",
98+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
99+
limatype.ProtoAny: limatype.PortForwardTypeGRPC,
100+
},
101+
portForwards: []limatype.PortForward{
102+
{
103+
Ignore: true,
104+
Proto: limatype.ProtoTCP,
105+
GuestPortRange: [2]int{1, 65535},
106+
},
107+
},
108+
env: nil,
109+
expected: map[limatype.Proto]limatype.PortForwardType{
110+
limatype.ProtoUDP: limatype.PortForwardTypeGRPC,
111+
},
112+
},
113+
{
114+
name: "conflict between any and tcp",
115+
portForwardTypes: map[limatype.Proto]limatype.PortForwardType{
116+
limatype.ProtoAny: limatype.PortForwardTypeGRPC,
117+
limatype.ProtoTCP: limatype.PortForwardTypeSSH,
118+
},
119+
portForwards: nil,
120+
env: nil,
121+
expectedErr: "conflicting port forward types for proto",
122+
},
123+
}
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
for envK, envV := range tt.env {
127+
t.Setenv(envK, envV)
128+
}
129+
got, err := resolvePortForwardTypes(tt.portForwardTypes, tt.portForwards)
130+
if tt.expectedErr != "" {
131+
assert.ErrorContains(t, err, tt.expectedErr)
132+
return
133+
}
134+
assert.NilError(t, err)
135+
assert.DeepEqual(t, got, tt.expected)
136+
})
137+
}
138+
}

pkg/hostagent/port.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,12 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev *api.Event) {
101101
}
102102
}
103103
for _, f := range ev.AddedLocalPorts {
104-
if f.Protocol != "tcp" {
104+
if pf.ignore || f.Protocol != "tcp" {
105105
continue
106106
}
107107
local, remote := pf.forwardingAddresses(f)
108108
if local == "" {
109-
if !pf.ignore {
110-
logrus.Infof("Not forwarding TCP %s", remote)
111-
}
109+
logrus.Infof("Not forwarding TCP %s", remote)
112110
continue
113111
}
114112
logrus.Infof("Forwarding TCP from %s to %s", remote, local)

0 commit comments

Comments
 (0)