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)