Skip to content

Commit 58c6249

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 6ec5d15 commit 58c6249

File tree

14 files changed

+466
-80
lines changed

14 files changed

+466
-80
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: 90 additions & 40 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,25 +862,9 @@ 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-
a.grpcPortForwarder.OnEvent(ctx, client, ev)
817-
}
865+
866+
a.portForwarder.OnEvent(ctx, ev)
867+
a.grpcPortForwarder.OnEvent(ctx, client, ev)
818868
}
819869

820870
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/limatype/lima_yaml.go

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,29 @@ type LimaYAML struct {
2121
Arch *Arch `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"nullable"`
2222
Images []Image `yaml:"images,omitempty" json:"images,omitempty" jsonschema:"nullable"`
2323
// Deprecated: Use vmOpts.qemu.cpuType instead.
24-
CPUType CPUType `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"`
25-
CPUs *int `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"`
26-
Memory *string `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
27-
Disk *string `yaml:"disk,omitempty" json:"disk,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
28-
AdditionalDisks []Disk `yaml:"additionalDisks,omitempty" json:"additionalDisks,omitempty" jsonschema:"nullable"`
29-
Mounts []Mount `yaml:"mounts,omitempty" json:"mounts,omitempty"`
30-
MountTypesUnsupported []string `yaml:"mountTypesUnsupported,omitempty" json:"mountTypesUnsupported,omitempty" jsonschema:"nullable"`
31-
MountType *MountType `yaml:"mountType,omitempty" json:"mountType,omitempty" jsonschema:"nullable"`
32-
MountInotify *bool `yaml:"mountInotify,omitempty" json:"mountInotify,omitempty" jsonschema:"nullable"`
33-
SSH SSH `yaml:"ssh,omitempty" json:"ssh,omitempty"` // REQUIRED (FIXME)
34-
Firmware Firmware `yaml:"firmware,omitempty" json:"firmware,omitempty"`
35-
Audio Audio `yaml:"audio,omitempty" json:"audio,omitempty"`
36-
Video Video `yaml:"video,omitempty" json:"video,omitempty"`
37-
Provision []Provision `yaml:"provision,omitempty" json:"provision,omitempty"`
38-
UpgradePackages *bool `yaml:"upgradePackages,omitempty" json:"upgradePackages,omitempty" jsonschema:"nullable"`
39-
Containerd Containerd `yaml:"containerd,omitempty" json:"containerd,omitempty"`
40-
GuestInstallPrefix *string `yaml:"guestInstallPrefix,omitempty" json:"guestInstallPrefix,omitempty" jsonschema:"nullable"`
41-
Probes []Probe `yaml:"probes,omitempty" json:"probes,omitempty"`
42-
PortForwards []PortForward `yaml:"portForwards,omitempty" json:"portForwards,omitempty"`
43-
CopyToHost []CopyToHost `yaml:"copyToHost,omitempty" json:"copyToHost,omitempty"`
44-
Message string `yaml:"message,omitempty" json:"message,omitempty"`
45-
Networks []Network `yaml:"networks,omitempty" json:"networks,omitempty" jsonschema:"nullable"`
24+
CPUType CPUType `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"`
25+
CPUs *int `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"`
26+
Memory *string `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
27+
Disk *string `yaml:"disk,omitempty" json:"disk,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
28+
AdditionalDisks []Disk `yaml:"additionalDisks,omitempty" json:"additionalDisks,omitempty" jsonschema:"nullable"`
29+
Mounts []Mount `yaml:"mounts,omitempty" json:"mounts,omitempty"`
30+
MountTypesUnsupported []string `yaml:"mountTypesUnsupported,omitempty" json:"mountTypesUnsupported,omitempty" jsonschema:"nullable"`
31+
MountType *MountType `yaml:"mountType,omitempty" json:"mountType,omitempty" jsonschema:"nullable"`
32+
MountInotify *bool `yaml:"mountInotify,omitempty" json:"mountInotify,omitempty" jsonschema:"nullable"`
33+
SSH SSH `yaml:"ssh,omitempty" json:"ssh,omitempty"` // REQUIRED (FIXME)
34+
Firmware Firmware `yaml:"firmware,omitempty" json:"firmware,omitempty"`
35+
Audio Audio `yaml:"audio,omitempty" json:"audio,omitempty"`
36+
Video Video `yaml:"video,omitempty" json:"video,omitempty"`
37+
Provision []Provision `yaml:"provision,omitempty" json:"provision,omitempty"`
38+
UpgradePackages *bool `yaml:"upgradePackages,omitempty" json:"upgradePackages,omitempty" jsonschema:"nullable"`
39+
Containerd Containerd `yaml:"containerd,omitempty" json:"containerd,omitempty"`
40+
GuestInstallPrefix *string `yaml:"guestInstallPrefix,omitempty" json:"guestInstallPrefix,omitempty" jsonschema:"nullable"`
41+
Probes []Probe `yaml:"probes,omitempty" json:"probes,omitempty"`
42+
PortForwardTypes map[Proto]PortForwardType `yaml:"portForwardTypes,omitempty" json:"portForwardTypes,omitempty" jsonschema:"nullable"`
43+
PortForwards []PortForward `yaml:"portForwards,omitempty" json:"portForwards,omitempty"`
44+
CopyToHost []CopyToHost `yaml:"copyToHost,omitempty" json:"copyToHost,omitempty"`
45+
Message string `yaml:"message,omitempty" json:"message,omitempty"`
46+
Networks []Network `yaml:"networks,omitempty" json:"networks,omitempty" jsonschema:"nullable"`
4647
// `network` was deprecated in Lima v0.7.0, removed in Lima v0.14.0. Use `networks` instead.
4748
Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
4849
Param map[string]string `yaml:"param,omitempty" json:"param,omitempty"`
@@ -284,6 +285,14 @@ const (
284285
ProtoAny Proto = "any"
285286
)
286287

288+
type PortForwardType = string
289+
290+
const (
291+
PortForwardTypeSSH PortForwardType = "ssh"
292+
PortForwardTypeGRPC PortForwardType = "grpc"
293+
PortForwardTypeNone PortForwardType = "none"
294+
)
295+
287296
type PortForward struct {
288297
GuestIPMustBeZero bool `yaml:"guestIPMustBeZero,omitempty" json:"guestIPMustBeZero,omitempty"`
289298
GuestIP net.IP `yaml:"guestIP,omitempty" json:"guestIP,omitempty"`

0 commit comments

Comments
 (0)