Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hack/test-templates/test-misc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/driver/vz/vz_driver_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var knownYamlProperties = []string{
"OS",
"Param",
"Plain",
"PortForwardTypes",
"PortForwards",
"Probes",
"PropagateProxyEnv",
Expand Down
1 change: 1 addition & 0 deletions pkg/driver/wsl2/wsl_driver_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var knownYamlProperties = []string{
"MountType",
"Param",
"Plain",
"PortForwardTypes",
"PortForwards",
"Probes",
"PropagateProxyEnv",
Expand Down
132 changes: 91 additions & 41 deletions pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
"maps"
"net"
"os"
"os/exec"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Copy link
Contributor

@norio-nomura norio-nomura Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the bool variables *FwdIgnore* created based on this portForwardTypes.

They are only used to suppress log messages (logrus.Infof("Not forwarding ...) when Forwarder decides not to forward by applying rules.

And they are not involved in the forwarder applying/not applying the rules.

Therefore, the current implementation is not related to the newly introduced settings, first the SSH Forwarder binds the local port, then the gRPC Forwarder fails to bind, and the gRPC Forwarder only runs on the rules that the SSH Forwarder does not support.

Copy link
Member Author

@AkihiroSuda AkihiroSuda Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed (EDIT: reverted due to CI failures on Windows)

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} {
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down
138 changes: 138 additions & 0 deletions pkg/hostagent/hostagent_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
53 changes: 31 additions & 22 deletions pkg/limatype/lima_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
Loading