From 14677e751c6fabd9d6accef3edb09ae0b7a7522a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 16 Oct 2025 18:20:44 +0900 Subject: [PATCH] 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 --- hack/test-templates/test-misc.yaml | 3 + pkg/driver/vz/vz_driver_darwin.go | 1 + pkg/driver/wsl2/wsl_driver_windows.go | 1 + pkg/hostagent/hostagent.go | 132 +++++++++++------ pkg/hostagent/hostagent_test.go | 138 ++++++++++++++++++ pkg/limatype/lima_yaml.go | 53 ++++--- pkg/limayaml/defaults.go | 12 ++ pkg/limayaml/defaults_test.go | 16 ++ pkg/limayaml/validate.go | 36 +++++ pkg/limayaml/validate_test.go | 76 ++++++++++ templates/default.yaml | 4 + .../en/docs/config/environment-variables.md | 6 +- website/content/en/docs/config/port.md | 69 +++++++-- .../content/en/docs/releases/deprecated.md | 1 + 14 files changed, 467 insertions(+), 81 deletions(-) create mode 100644 pkg/hostagent/hostagent_test.go diff --git a/hack/test-templates/test-misc.yaml b/hack/test-templates/test-misc.yaml index 4dddf23d732..cd878d8687c 100644 --- a/hack/test-templates/test-misc.yaml +++ b/hack/test-templates/test-misc.yaml @@ -111,6 +111,9 @@ user: # Ubuntu has identical /bin/bash and /usr/bin/bash shell: /usr/bin/bash +portForwardTypes: + any: grpc + portForwards: - guestPort: 80 hostPort: 9090 diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index 927e9b90d26..079b8815bd3 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -55,6 +55,7 @@ var knownYamlProperties = []string{ "OS", "Param", "Plain", + "PortForwardTypes", "PortForwards", "Probes", "PropagateProxyEnv", diff --git a/pkg/driver/wsl2/wsl_driver_windows.go b/pkg/driver/wsl2/wsl_driver_windows.go index 4468dbf554e..1117d2d1569 100644 --- a/pkg/driver/wsl2/wsl_driver_windows.go +++ b/pkg/driver/wsl2/wsl_driver_windows.go @@ -39,6 +39,7 @@ var knownYamlProperties = []string{ "MountType", "Param", "Plain", + "PortForwardTypes", "PortForwards", "Probes", "PropagateProxyEnv", diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 53ba5fd1039..7dba716165e 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "maps" "net" "os" "os/exec" @@ -113,6 +114,82 @@ func WithCloudInitProgress(enabled bool) Opt { } } +// resolvePortForwardTypes resolves port forwarding types. +// The returned result may not contain [limatype.ProtoAny] keys, and [limatype.PortForwardTypeNone] values. +func resolvePortForwardTypes(portForwardTypes map[limatype.Proto]limatype.PortForwardType, portForwards []limatype.PortForward) (map[limatype.Proto]limatype.PortForwardType, error) { + // The default port forwarding mode since Lima v2.0 is "dual": {tcp: ssh, udp: grpc}. + // The default values are set in [limayaml.FillDefault], not here. + if err := limayaml.ValidatePortForwardTypes(portForwardTypes); err != nil { + return nil, err + } + + res := maps.Clone(portForwardTypes) + + // Fix up keys + for k, v := range res { + if k == limatype.ProtoAny { + for _, proto := range []limatype.Proto{limatype.ProtoTCP, limatype.ProtoUDP} { + if res[proto] != limatype.PortForwardTypeNone { + res[proto] = v + } + } + delete(res, k) + } + } + + // Fix up values + for k, v := range res { + if v == limatype.PortForwardTypeNone { + delete(res, k) + } + } + + // Apply "ignore all ports" rules from portForwards + for _, rule := range portForwards { + if rule.Ignore && rule.GuestPortRange[0] == 1 && rule.GuestPortRange[1] == 65535 { + switch rule.Proto { + case limatype.ProtoTCP: + delete(res, limatype.ProtoTCP) + case limatype.ProtoUDP: + delete(res, limatype.ProtoUDP) + case limatype.ProtoAny: + delete(res, limatype.ProtoTCP) + delete(res, limatype.ProtoUDP) + } + } else { + break + } + } + + // Apply LIMA_SSH_PORT_FORWARDER env var for backward compatibility + if envVar := os.Getenv("LIMA_SSH_PORT_FORWARDER"); envVar != "" { + logrus.WithField("LIMA_SSH_PORT_FORWARDER", envVar).Warnf("LIMA_SSH_PORT_FORWARDER=false is deprecated; use portForwardTypes config instead") + b, err := strconv.ParseBool(envVar) + if err != nil { + return nil, fmt.Errorf("invalid LIMA_SSH_PORT_FORWARDER value %q", envVar) + } + if b { + if _, ok := res[limatype.ProtoTCP]; ok { + res[limatype.ProtoTCP] = limatype.PortForwardTypeSSH + } + // No UDP support in SSH port forwarder + delete(res, limatype.ProtoUDP) + } else { + for _, proto := range []limatype.Proto{limatype.ProtoTCP, limatype.ProtoUDP} { + if _, ok := res[proto]; ok { + res[proto] = limatype.PortForwardTypeGRPC + } + } + } + } + + if err := limayaml.ValidatePortForwardTypes(res); err != nil { + return nil, err + } + + return res, nil +} + // New creates the HostAgent. // // 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 AdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts), } - ignoreTCP := false - ignoreUDP := false - for _, rule := range inst.Config.PortForwards { - if rule.Ignore && rule.GuestPortRange[0] == 1 && rule.GuestPortRange[1] == 65535 { - switch rule.Proto { - case limatype.ProtoTCP: - ignoreTCP = true - logrus.Info("TCP port forwarding is disabled (except for SSH)") - case limatype.ProtoUDP: - ignoreUDP = true - logrus.Info("UDP port forwarding is disabled") - case limatype.ProtoAny: - ignoreTCP = true - ignoreUDP = true - logrus.Info("TCP (except for SSH) and UDP port forwarding is disabled") - } - } else { - break - } + portForwardTypes, err := resolvePortForwardTypes(inst.Config.PortForwardTypes, inst.Config.PortForwards) + if err != nil { + return nil, err } + logrus.WithField("portForwardTypes", portForwardTypes).Info("Resolved port forwarding types") + sshFwdIgnoreTCP := portForwardTypes[limatype.ProtoTCP] != limatype.PortForwardTypeSSH + grpcFwdIgnoreTCP := portForwardTypes[limatype.ProtoTCP] != limatype.PortForwardTypeGRPC + grpcFwdIgnoreUDP := portForwardTypes[limatype.ProtoUDP] != limatype.PortForwardTypeGRPC + rules := make([]limatype.PortForward, 0, 3+len(inst.Config.PortForwards)) // Block ports 22 and sshLocalPort on all IPs for _, port := range []int{sshGuestPort, sshLocalPort} { @@ -244,8 +310,8 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o instName: instName, instSSHAddress: inst.SSHAddress, sshConfig: sshConfig, - portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, ignoreTCP, inst.VMType), - grpcPortForwarder: portfwd.NewPortForwarder(rules, ignoreTCP, ignoreUDP), + portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, sshFwdIgnoreTCP, inst.VMType), + grpcPortForwarder: portfwd.NewPortForwarder(rules, grpcFwdIgnoreTCP, grpcFwdIgnoreUDP), driver: limaDriver, signalCh: signalCh, eventEnc: json.NewEncoder(stdout), @@ -796,26 +862,10 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestag for _, f := range ev.Errors { logrus.Warnf("received error from the guest: %q", f) } - // History of the default value of useSSHFwd: - // - v0.1.0: true (effectively) - // - v1.0.0: false - // - v1.0.1: true - // - v1.1.0-beta.0: false - useSSHFwd := false - if envVar := os.Getenv("LIMA_SSH_PORT_FORWARDER"); envVar != "" { - b, err := strconv.ParseBool(envVar) - if err != nil { - logrus.WithError(err).Warnf("invalid LIMA_SSH_PORT_FORWARDER value %q", envVar) - } else { - useSSHFwd = b - } - } - if useSSHFwd { - a.portForwarder.OnEvent(ctx, ev) - } else { - dialContext := portfwd.DialContextToGRPCTunnel(client) - a.grpcPortForwarder.OnEvent(ctx, dialContext, ev) - } + + a.portForwarder.OnEvent(ctx, ev) + dialContext := portfwd.DialContextToGRPCTunnel(client) + a.grpcPortForwarder.OnEvent(ctx, dialContext, ev) } if err := client.Events(ctx, onEvent); err != nil { diff --git a/pkg/hostagent/hostagent_test.go b/pkg/hostagent/hostagent_test.go new file mode 100644 index 00000000000..85b84ddcd84 --- /dev/null +++ b/pkg/hostagent/hostagent_test.go @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package hostagent + +import ( + "testing" + + "gotest.tools/v3/assert" + + "github.com/lima-vm/lima/v2/pkg/limatype" +) + +func TestResolvePortForwardTypes(t *testing.T) { + tests := []struct { + name string + portForwardTypes map[limatype.Proto]limatype.PortForwardType + portForwards []limatype.PortForward + env map[string]string + expected map[limatype.Proto]limatype.PortForwardType + expectedErr string + }{ + { + name: "default", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + portForwards: nil, + env: nil, + expected: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + }, + { + name: "grpc only via config", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoAny: limatype.PortForwardTypeGRPC, + }, + portForwards: nil, + env: nil, + expected: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeGRPC, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + }, + { + name: "ssh only via env", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + portForwards: nil, + env: map[string]string{ + "LIMA_SSH_PORT_FORWARDER": "true", + }, + expected: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + // No UDP support in SSH port forwarder + }, + }, + { + name: "grpc only via env", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + portForwards: nil, + env: map[string]string{ + "LIMA_SSH_PORT_FORWARDER": "false", + }, + expected: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeGRPC, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + }, + { + name: "disable tcp via portForwards", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + portForwards: []limatype.PortForward{ + { + Ignore: true, + Proto: limatype.ProtoTCP, + GuestPortRange: [2]int{1, 65535}, + }, + }, + env: nil, + expected: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + }, + { + name: "disable tcp via portForwards, with any in portForwardTypes", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoAny: limatype.PortForwardTypeGRPC, + }, + portForwards: []limatype.PortForward{ + { + Ignore: true, + Proto: limatype.ProtoTCP, + GuestPortRange: [2]int{1, 65535}, + }, + }, + env: nil, + expected: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + }, + { + name: "conflict between any and tcp", + portForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoAny: limatype.PortForwardTypeGRPC, + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + }, + portForwards: nil, + env: nil, + expectedErr: "conflicting port forward types for proto", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for envK, envV := range tt.env { + t.Setenv(envK, envV) + } + got, err := resolvePortForwardTypes(tt.portForwardTypes, tt.portForwards) + if tt.expectedErr != "" { + assert.ErrorContains(t, err, tt.expectedErr) + return + } + assert.NilError(t, err) + assert.DeepEqual(t, got, tt.expected) + }) + } +} diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index 360260caeb6..ac66882ed34 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -21,28 +21,29 @@ type LimaYAML struct { Arch *Arch `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"nullable"` Images []Image `yaml:"images,omitempty" json:"images,omitempty" jsonschema:"nullable"` // Deprecated: Use vmOpts.qemu.cpuType instead. - CPUType CPUType `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"` - CPUs *int `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"` - Memory *string `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes - Disk *string `yaml:"disk,omitempty" json:"disk,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes - AdditionalDisks []Disk `yaml:"additionalDisks,omitempty" json:"additionalDisks,omitempty" jsonschema:"nullable"` - Mounts []Mount `yaml:"mounts,omitempty" json:"mounts,omitempty"` - MountTypesUnsupported []string `yaml:"mountTypesUnsupported,omitempty" json:"mountTypesUnsupported,omitempty" jsonschema:"nullable"` - MountType *MountType `yaml:"mountType,omitempty" json:"mountType,omitempty" jsonschema:"nullable"` - MountInotify *bool `yaml:"mountInotify,omitempty" json:"mountInotify,omitempty" jsonschema:"nullable"` - SSH SSH `yaml:"ssh,omitempty" json:"ssh,omitempty"` // REQUIRED (FIXME) - Firmware Firmware `yaml:"firmware,omitempty" json:"firmware,omitempty"` - Audio Audio `yaml:"audio,omitempty" json:"audio,omitempty"` - Video Video `yaml:"video,omitempty" json:"video,omitempty"` - Provision []Provision `yaml:"provision,omitempty" json:"provision,omitempty"` - UpgradePackages *bool `yaml:"upgradePackages,omitempty" json:"upgradePackages,omitempty" jsonschema:"nullable"` - Containerd Containerd `yaml:"containerd,omitempty" json:"containerd,omitempty"` - GuestInstallPrefix *string `yaml:"guestInstallPrefix,omitempty" json:"guestInstallPrefix,omitempty" jsonschema:"nullable"` - Probes []Probe `yaml:"probes,omitempty" json:"probes,omitempty"` - PortForwards []PortForward `yaml:"portForwards,omitempty" json:"portForwards,omitempty"` - CopyToHost []CopyToHost `yaml:"copyToHost,omitempty" json:"copyToHost,omitempty"` - Message string `yaml:"message,omitempty" json:"message,omitempty"` - Networks []Network `yaml:"networks,omitempty" json:"networks,omitempty" jsonschema:"nullable"` + CPUType CPUType `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"` + CPUs *int `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"` + Memory *string `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes + Disk *string `yaml:"disk,omitempty" json:"disk,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes + AdditionalDisks []Disk `yaml:"additionalDisks,omitempty" json:"additionalDisks,omitempty" jsonschema:"nullable"` + Mounts []Mount `yaml:"mounts,omitempty" json:"mounts,omitempty"` + MountTypesUnsupported []string `yaml:"mountTypesUnsupported,omitempty" json:"mountTypesUnsupported,omitempty" jsonschema:"nullable"` + MountType *MountType `yaml:"mountType,omitempty" json:"mountType,omitempty" jsonschema:"nullable"` + MountInotify *bool `yaml:"mountInotify,omitempty" json:"mountInotify,omitempty" jsonschema:"nullable"` + SSH SSH `yaml:"ssh,omitempty" json:"ssh,omitempty"` // REQUIRED (FIXME) + Firmware Firmware `yaml:"firmware,omitempty" json:"firmware,omitempty"` + Audio Audio `yaml:"audio,omitempty" json:"audio,omitempty"` + Video Video `yaml:"video,omitempty" json:"video,omitempty"` + Provision []Provision `yaml:"provision,omitempty" json:"provision,omitempty"` + UpgradePackages *bool `yaml:"upgradePackages,omitempty" json:"upgradePackages,omitempty" jsonschema:"nullable"` + Containerd Containerd `yaml:"containerd,omitempty" json:"containerd,omitempty"` + GuestInstallPrefix *string `yaml:"guestInstallPrefix,omitempty" json:"guestInstallPrefix,omitempty" jsonschema:"nullable"` + Probes []Probe `yaml:"probes,omitempty" json:"probes,omitempty"` + PortForwardTypes map[Proto]PortForwardType `yaml:"portForwardTypes,omitempty" json:"portForwardTypes,omitempty" jsonschema:"nullable"` + PortForwards []PortForward `yaml:"portForwards,omitempty" json:"portForwards,omitempty"` + CopyToHost []CopyToHost `yaml:"copyToHost,omitempty" json:"copyToHost,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Networks []Network `yaml:"networks,omitempty" json:"networks,omitempty" jsonschema:"nullable"` // `network` was deprecated in Lima v0.7.0, removed in Lima v0.14.0. Use `networks` instead. Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"` Param map[string]string `yaml:"param,omitempty" json:"param,omitempty"` @@ -284,6 +285,14 @@ const ( ProtoAny Proto = "any" ) +type PortForwardType = string + +const ( + PortForwardTypeSSH PortForwardType = "ssh" + PortForwardTypeGRPC PortForwardType = "grpc" + PortForwardTypeNone PortForwardType = "none" +) + type PortForward struct { GuestIPMustBeZero *bool `yaml:"guestIPMustBeZero,omitempty" json:"guestIPMustBeZero,omitempty"` GuestIP net.IP `yaml:"guestIP,omitempty" json:"guestIP,omitempty"` diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index fc138addaf2..4625c6a1301 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -519,6 +519,18 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin } } + portForwardTypes := make(map[limatype.Proto]limatype.PortForwardType) + maps.Copy(portForwardTypes, d.PortForwardTypes) + maps.Copy(portForwardTypes, y.PortForwardTypes) + maps.Copy(portForwardTypes, o.PortForwardTypes) + if portForwardTypes[limatype.ProtoTCP] == "" && portForwardTypes[limatype.ProtoAny] == "" { + portForwardTypes[limatype.ProtoTCP] = limatype.PortForwardTypeSSH + } + if portForwardTypes[limatype.ProtoUDP] == "" && portForwardTypes[limatype.ProtoAny] == "" { + portForwardTypes[limatype.ProtoUDP] = limatype.PortForwardTypeGRPC + } + y.PortForwardTypes = portForwardTypes + y.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, d.PortForwards) for i := range y.PortForwards { FillPortForwardDefaults(&y.PortForwards[i], instDir, y.User, y.Param) diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index b52d407fbb2..16fb10ab015 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -121,6 +121,10 @@ func TestFillDefault(t *testing.T) { Shell: ptr.Of("/bin/bash"), UID: ptr.Of(uint32(uid)), }, + PortForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, } defaultPortForward := limatype.PortForward{ @@ -248,6 +252,10 @@ func TestFillDefault(t *testing.T) { expect.Networks[0].Metric = ptr.Of(uint32(100)) expect.DNS = slices.Clone(y.DNS) + expect.PortForwardTypes = map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + } expect.PortForwards = []limatype.PortForward{ defaultPortForward, defaultPortForward, @@ -386,6 +394,10 @@ func TestFillDefault(t *testing.T) { DNS: []net.IP{ net.ParseIP("1.1.1.1"), }, + PortForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, PortForwards: []limatype.PortForward{{ GuestIP: IPv4loopback1, GuestIPMustBeZero: ptr.Of(false), @@ -600,6 +612,10 @@ func TestFillDefault(t *testing.T) { DNS: []net.IP{ net.ParseIP("2.2.2.2"), }, + PortForwardTypes: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, PortForwards: []limatype.PortForward{{ GuestIP: IPv4loopback1, GuestIPMustBeZero: ptr.Of(false), diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index f823803bc08..c33f217241b 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -283,6 +283,11 @@ func Validate(y *limatype.LimaYAML, warn bool) error { errs = errors.Join(errs, fmt.Errorf("field `probe[%d].mode` can only be %q", i, limatype.ProbeModeReadiness)) } } + + if err := ValidatePortForwardTypes(y.PortForwardTypes); err != nil { + errs = errors.Join(errs, fmt.Errorf("field `portForwardTypes` is invalid: %w", err)) + } + for i, rule := range y.PortForwards { field := fmt.Sprintf("portForwards[%d]", i) if *rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) { @@ -648,3 +653,34 @@ func ValidateAgainstLatestConfig(ctx context.Context, yNew, yLatest []byte) erro return errs } + +func ValidatePortForwardTypes(m map[limatype.Proto]limatype.PortForwardType) error { + for k, v := range m { + switch k { + case limatype.ProtoTCP: + // NOP + case limatype.ProtoUDP: + if v == limatype.PortForwardTypeSSH { + return errors.New("port forward type \"ssh\" does not support protocol udp") + } + case limatype.ProtoAny: + if v == limatype.PortForwardTypeSSH { + return errors.New("port forward type \"ssh\" does not support protocol \"any\", due to lack of udp support") + } + for _, kk := range []limatype.Proto{limatype.ProtoTCP, limatype.ProtoUDP} { + if vv, ok := m[kk]; ok && vv != v { + return fmt.Errorf("conflicting port forward types for proto %q:%q vs %q:%q", k, v, kk, vv) + } + } + default: + return fmt.Errorf("invalid port forward type proto: %q", k) + } + switch v { + case limatype.PortForwardTypeSSH, limatype.PortForwardTypeGRPC, limatype.PortForwardTypeNone: + // NOP + default: + return fmt.Errorf("invalid port forward type: %q for proto %q", v, k) + } + } + return nil +} diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go index b992610875e..0b566f3e251 100644 --- a/pkg/limayaml/validate_test.go +++ b/pkg/limayaml/validate_test.go @@ -429,3 +429,79 @@ func TestValidateAgainstLatestConfig(t *testing.T) { }) } } + +func TestValidatePortForwardTypes(t *testing.T) { + tests := []struct { + name string + m map[limatype.Proto]limatype.PortForwardType + wantErr string + wantErrContains bool + }{ + { + name: "tcp: ssh, udp: grpc", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + limatype.ProtoUDP: limatype.PortForwardTypeGRPC, + }, + }, + { + name: "any: none", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoAny: limatype.PortForwardTypeNone, + }, + }, + { + name: "udp: ssh", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoUDP: limatype.PortForwardTypeSSH, + }, + wantErr: "port forward type \"ssh\" does not support protocol udp", + }, + { + name: "any: ssh", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoAny: limatype.PortForwardTypeSSH, + }, + wantErr: "port forward type \"ssh\" does not support protocol \"any\", due to lack of udp support", + }, + { + name: "any: grpc, tcp: ssh", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoAny: limatype.PortForwardTypeGRPC, + limatype.ProtoTCP: limatype.PortForwardTypeSSH, + }, + wantErr: "conflicting port forward types", + wantErrContains: true, + }, + { + name: "invalid key", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.Proto("invalid"): limatype.PortForwardTypeGRPC, + }, + wantErr: "invalid port forward type proto: \"invalid\"", + }, + { + name: "invalid value", + m: map[limatype.Proto]limatype.PortForwardType{ + limatype.ProtoTCP: limatype.PortForwardType("invalid"), + }, + wantErr: "invalid port forward type", + wantErrContains: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := ValidatePortForwardTypes(tc.m) + if tc.wantErr == "" { + assert.NilError(t, err) + return + } + if tc.wantErrContains { + assert.ErrorContains(t, err, tc.wantErr) + return + } + assert.Error(t, err, tc.wantErr) + }) + } +} diff --git a/templates/default.yaml b/templates/default.yaml index fcbf7cd4d71..5802beabee5 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -465,6 +465,10 @@ networks: # Needs `vmType: vz` # - vzNAT: true +# Port forwarder types. +# 🟢 Builtin default: {tcp: ssh, udp: grpc} +portForwardTypes: null + # Port forwarding rules. Forwarding between ports 22 and ssh.localPort cannot be overridden. # Rules are checked sequentially until the first one matches. # portForwards: diff --git a/website/content/en/docs/config/environment-variables.md b/website/content/en/docs/config/environment-variables.md index 7881d5eddf4..198519928bb 100644 --- a/website/content/en/docs/config/environment-variables.md +++ b/website/content/en/docs/config/environment-variables.md @@ -117,12 +117,13 @@ This page documents the environment variables used in Lima. ### `LIMA_SSH_PORT_FORWARDER` -- **Description**: Specifies to use the SSH port forwarder (slow) instead of gRPC (fast, previously unstable) -- **Default**: `false` (since v1.1.0) +- **Description**: Specifies to disable the gRPC port forwarder. See [Port Forwarding](./port.md) for the details. +- **Default**: not set (since v2.0.0) - **Usage**: ```sh export LIMA_SSH_PORT_FORWARDER=false ``` +- **Note**: Deprecated since v2.0.0. - **The history of the default value**: | Version | Default value | |---------|---------------------| @@ -130,6 +131,7 @@ This page documents the environment variables used in Lima. | v1.0.0 | `false` | | v1.0.1 | `true` | | v1.1.0 | `false` | + | v2.0.0 | not set | ### `LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT` diff --git a/website/content/en/docs/config/port.md b/website/content/en/docs/config/port.md index 05d538cdb56..431c8555e68 100644 --- a/website/content/en/docs/config/port.md +++ b/website/content/en/docs/config/port.md @@ -7,29 +7,56 @@ Lima supports automatic port-forwarding of localhost ports from guest to host. ## Port forwarding types -Lima supports two port forwarders: SSH and GRPC. +Lima supports the following port forwarders: +- Dual (SSH for TCP, GRPC for UDP) +- SSH +- GRPC The default port forwarder is shown in the following table. -| Version | Default | -| --------| ------- | -| v0.1.0 | SSH | -| v1.0.0 | GRPC | -| v1.0.1 | SSH | -| v1.1.0 | GRPC | +| Version | Default | Reason to change the default | +| --------| ------- | -------------------------------------------------------- | +| v0.1.0 | SSH | (The initial implementation.) | +| v1.0.0 | GRPC | GRPC implementation outperforms SSH. | +| v1.0.1 | SSH | GRPC implementation turned out to have stability issues. | +| v1.1.0 | GRPC | The stability issues were fixed. | +| v2.0.0 | Dual | SSH outperforms GRPC when VSOCK is available. | -The default was once changed to GRPC in Lima v1.0, but it was reverted to SSH in v1.0.1 due to stability reasons. -The default was further reverted to GRPC in Lima v1.1, as the stability issues were resolved. +### Using Dual forwarder -### Using SSH +| ⚡ Requirement | Lima >= 2.0 | +|---------------|-------------| -SSH based port forwarding was previously the default mode. +The dual forwarder uses SSH for TCP and GRPC for UDP to mix the advantages of the both forwarders. -To explicitly use SSH forwarding use the below command +```yaml +portForwardTypes: + tcp: ssh + udp: grpc +``` + +This is the default mode since Lima v2.0. +### Using SSH + +SSH-only port forwarding was previously the default mode. + +{{< tabpane text=true >}} +{{% tab header="Lima v2.0+" %}} +```yaml +portForwardTypes: + tcp: ssh + udp: none +``` +{{% /tab %}} +{{% tab header="Lima v1.x" %}} ```bash -LIMA_SSH_PORT_FORWARDER=true limactl start +# Deprecated +export LIMA_SSH_PORT_FORWARDER=true +limactl start ``` +{{% /tab %}} +{{< /tabpane >}} #### Advantages @@ -62,11 +89,21 @@ export LIMA_SSH_OVER_VSOCK=false In this model, lima uses existing GRPC communication (Host <-> Guest) to tunnel port forwarding requests. For each port forwarding request, a GRPC tunnel is created and this will be used for transmitting data -To enable this feature, set `LIMA_SSH_PORT_FORWARDER` to `false`: - +{{< tabpane text=true >}} +{{% tab header="Lima v2.0+" %}} +```yaml +portForwardTypes: + any: grpc +``` +{{% /tab %}} +{{% tab header="Lima v1.x" %}} ```bash -LIMA_SSH_PORT_FORWARDER=false limactl start +# Deprecated +export LIMA_SSH_PORT_FORWARDER=false +limactl start ``` +{{% /tab %}} +{{< /tabpane >}} #### Advantages diff --git a/website/content/en/docs/releases/deprecated.md b/website/content/en/docs/releases/deprecated.md index 23c5956d612..14775d93020 100644 --- a/website/content/en/docs/releases/deprecated.md +++ b/website/content/en/docs/releases/deprecated.md @@ -9,6 +9,7 @@ The following features are deprecated: - Loading non-strict YAMLs (i.e., YAMLs with unknown properties) - `limactl show-ssh` command (Use `ssh -F ~/.lima/default/ssh.config lima-default` instead) - Ansible provisioning mode (Use `ansible-playbook playbook.yaml` after the start instead) +- `LIMA_SSH_PORT_FORWARDER` environment variable: deprecated in Lima v2.0, in favor of [the new "dual" port forwarder](../config/port.md) that mixes SSH and gRPC implementations. ## Removed features - YAML property `network`: deprecated in [Lima v0.7.0](https://github.com/lima-vm/lima/commit/07e68230e70b21108d2db3ca5e0efd0e43842fbd)