From df5213b494f8507946c10c0837079cd3a77cd3d3 Mon Sep 17 00:00:00 2001 From: Praful Khanduri Date: Sun, 6 Jul 2025 22:25:56 +0530 Subject: [PATCH 1/4] Plain mode: support port forwarding Signed-off-by: Praful Khanduri --- cmd/limactl/editflags/editflags.go | 36 ++++++++++++++++++++++++++++++ pkg/hostagent/hostagent.go | 28 +++++++++++++++++++++-- pkg/limayaml/defaults.go | 1 - pkg/limayaml/validate.go | 10 +++++++++ templates/default.yaml | 4 ++-- 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go index ae9fa593e4f..9520fa4d045 100644 --- a/cmd/limactl/editflags/editflags.go +++ b/cmd/limactl/editflags/editflags.go @@ -96,6 +96,11 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) { }) flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.") + + flags.StringSlice("port-forward", nil, commentPrefix+"Port forwards (host:guest), works even in plain mode, e.g., '8080:80,2222:22'") + _ = cmd.RegisterFlagCompletionFunc("port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return []string{"8080:80", "2222:22", "3000:3000"}, cobra.ShellCompDirectiveNoFileComp + }) } func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) { @@ -206,6 +211,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { false, false, }, + { "rosetta", func(_ *flag.Flag) (string, error) { @@ -261,6 +267,36 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { {"disk", d(".disk= \"%sGiB\""), false, false}, {"vm-type", d(".vmType = %q"), true, false}, {"plain", d(".plain = %s"), true, false}, + { + "port-forward", + func(_ *flag.Flag) (string, error) { + ss, err := flags.GetStringSlice("port-forward") + if err != nil { + return "", err + } + if len(ss) == 0 { + return "", nil + } + + expr := `.portForwards = [` + for i, s := range ss { + parts := strings.Split(s, ":") + if len(parts) != 2 { + return "", fmt.Errorf("invalid port forward format %q, expected HOST:GUEST", s) + } + hostPort := strings.TrimSpace(parts[0]) + guestPort := strings.TrimSpace(parts[1]) + expr += fmt.Sprintf(`{"hostPort": %s, "guestPort": %s}`, hostPort, guestPort) + if i < len(ss)-1 { + expr += "," + } + } + expr += `]` + return expr, nil + }, + false, + false, + }, } var exprs []string for _, def := range defs { diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index c0452c8ba72..8dbe257dd6d 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -420,7 +420,11 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { if *a.instConfig.Plain { - logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.") + if len(a.instConfig.PortForwards) > 0 { + logrus.Info("Running in plain mode. Mounts, containerd, etc. will be ignored. Guest agent will not be running. Port forwarding is enabled via static SSH tunnels.") + } else { + logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.") + } } a.onClose = append(a.onClose, func() error { logrus.Debugf("shutting down the SSH master") @@ -478,7 +482,7 @@ sudo chown -R "${USER}" /run/host-services` return errors.Join(unlockErrs...) }) } - if !*a.instConfig.Plain { + if !*a.instConfig.Plain || len(a.instConfig.PortForwards) > 0 && *a.instConfig.Plain { go a.watchGuestAgentEvents(ctx) } if err := a.waitForRequirements("optional", a.optionalRequirements()); err != nil { @@ -543,6 +547,26 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } + if *a.instConfig.Plain { + logrus.Debugf("Setting up static TCP port forwarding for plain mode") + for _, rule := range a.instConfig.PortForwards { + if rule.GuestSocket == "" { + guest := &guestagentapi.IPPort{ + Ip: rule.GuestIP.String(), + Port: int32(rule.GuestPort), + Protocol: rule.Proto, + } + local, remote := a.portForwarder.forwardingAddresses(guest) + if local != "" { + logrus.Infof("Setting up static TCP forwarding from %s to %s", remote, local) + if err := forwardTCP(ctx, a.sshConfig, a.sshLocalPort, local, remote, verbForward); err != nil { + logrus.WithError(err).Warnf("failed to set up static TCP forwarding %s -> %s", remote, local) + } + } + } + } + } + localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) remoteUnix := "/run/lima-guestagent.sock" diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 629cbcc3f97..e3ea818d959 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -916,7 +916,6 @@ func fixUpForPlainMode(y *LimaYAML) { return } y.Mounts = nil - y.PortForwards = nil y.Containerd.System = ptr.Of(false) y.Containerd.User = ptr.Of(false) y.Rosetta.BinFmt = ptr.Of(false) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 9b959f25d1a..111473af5f4 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -415,6 +415,16 @@ func Validate(y *LimaYAML, warn bool) error { } } } + if y.Plain != nil && *y.Plain { + const portRangeWarnThreshold = 10 + for i, rule := range y.PortForwards { + guestRange := rule.GuestPortRange[1] - rule.GuestPortRange[0] + 1 + hostRange := rule.HostPortRange[1] - rule.HostPortRange[0] + 1 + if guestRange > portRangeWarnThreshold || hostRange > portRangeWarnThreshold { + logrus.Warnf("[plain mode] portForwards[%d] covers a range of more than %d ports (guest: %d, host: %d). All ports will be forwarded unconditionally, which may be inefficient.", i, portRangeWarnThreshold, guestRange, hostRange) + } + } + } return errs } diff --git a/templates/default.yaml b/templates/default.yaml index 11a88640239..d25eef99288 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -422,7 +422,7 @@ networks: # Port forwarding rules. Forwarding between ports 22 and ssh.localPort cannot be overridden. # Rules are checked sequentially until the first one matches. -# portForwards: +portForwards: null # - guestPort: 443 # hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"; allows privileged port forwarding # # default: hostPort: 443 (same as guestPort) @@ -613,4 +613,4 @@ nestedVirtualization: null # ===================================================================== # # END OF TEMPLATE -# ===================================================================== # +# ===================================================================== # \ No newline at end of file From 34f00b030f20d1f6d2bc60bbac4e91690e110f5d Mon Sep 17 00:00:00 2001 From: Praful Khanduri Date: Fri, 11 Jul 2025 18:56:36 +0530 Subject: [PATCH 2/4] Add static bool field for static port forwarding Signed-off-by: Praful Khanduri --- cmd/limactl/editflags/editflags.go | 18 +++-- pkg/hostagent/hostagent.go | 54 +++++++------ pkg/limayaml/defaults.go | 12 +++ pkg/limayaml/defaults_test.go | 117 +++++++++++++++++++++++++++++ pkg/limayaml/limayaml.go | 1 + templates/default.yaml | 2 +- 6 files changed, 171 insertions(+), 33 deletions(-) diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go index 9520fa4d045..29cc8805e95 100644 --- a/cmd/limactl/editflags/editflags.go +++ b/cmd/limactl/editflags/editflags.go @@ -97,9 +97,13 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) { flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.") - flags.StringSlice("port-forward", nil, commentPrefix+"Port forwards (host:guest), works even in plain mode, e.g., '8080:80,2222:22'") + flags.StringSlice("port-forward", nil, commentPrefix+"Port forwards (host:guest), e.g., '8080:80,2222:22'") _ = cmd.RegisterFlagCompletionFunc("port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return []string{"8080:80", "2222:22", "3000:3000"}, cobra.ShellCompDirectiveNoFileComp + return []string{"8080:80", "3000:3000"}, cobra.ShellCompDirectiveNoFileComp + }) + flags.StringSlice("static-port-forward", nil, commentPrefix+"Static port forwards (host:guest), works even in plain mode, e.g., '8080:80,2222:22'") + _ = cmd.RegisterFlagCompletionFunc("static-port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return []string{"8080:80", "3000:3000"}, cobra.ShellCompDirectiveNoFileComp }) } @@ -268,9 +272,9 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { {"vm-type", d(".vmType = %q"), true, false}, {"plain", d(".plain = %s"), true, false}, { - "port-forward", + "static-port-forward", func(_ *flag.Flag) (string, error) { - ss, err := flags.GetStringSlice("port-forward") + ss, err := flags.GetStringSlice("static-port-forward") if err != nil { return "", err } @@ -278,15 +282,15 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { return "", nil } - expr := `.portForwards = [` + expr := `.portForwards += [` for i, s := range ss { parts := strings.Split(s, ":") if len(parts) != 2 { - return "", fmt.Errorf("invalid port forward format %q, expected HOST:GUEST", s) + return "", fmt.Errorf("invalid static port forward format %q, expected HOST:GUEST", s) } hostPort := strings.TrimSpace(parts[0]) guestPort := strings.TrimSpace(parts[1]) - expr += fmt.Sprintf(`{"hostPort": %s, "guestPort": %s}`, hostPort, guestPort) + expr += fmt.Sprintf(`{"hostPort": %s, "guestPort": %s, "static": true}`, hostPort, guestPort) if i < len(ss)-1 { expr += "," } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 8dbe257dd6d..6f3fdfac154 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -420,11 +420,7 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { if *a.instConfig.Plain { - if len(a.instConfig.PortForwards) > 0 { - logrus.Info("Running in plain mode. Mounts, containerd, etc. will be ignored. Guest agent will not be running. Port forwarding is enabled via static SSH tunnels.") - } else { - logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.") - } + logrus.Info("Running in plain mode. Mounts, dynamic port forwarding, containerd, etc. will be ignored. Guest agent will not be running. Static port forwarding is allowed.") } a.onClose = append(a.onClose, func() error { logrus.Debugf("shutting down the SSH master") @@ -482,9 +478,14 @@ sudo chown -R "${USER}" /run/host-services` return errors.Join(unlockErrs...) }) } - if !*a.instConfig.Plain || len(a.instConfig.PortForwards) > 0 && *a.instConfig.Plain { + + if !*a.instConfig.Plain { go a.watchGuestAgentEvents(ctx) + } else { + logrus.Info("Running in plain mode, skipping guest agent events watcher") + a.addStaticPortForwards(ctx) } + if err := a.waitForRequirements("optional", a.optionalRequirements()); err != nil { errs = append(errs, err) } @@ -547,25 +548,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } - if *a.instConfig.Plain { - logrus.Debugf("Setting up static TCP port forwarding for plain mode") - for _, rule := range a.instConfig.PortForwards { - if rule.GuestSocket == "" { - guest := &guestagentapi.IPPort{ - Ip: rule.GuestIP.String(), - Port: int32(rule.GuestPort), - Protocol: rule.Proto, - } - local, remote := a.portForwarder.forwardingAddresses(guest) - if local != "" { - logrus.Infof("Setting up static TCP forwarding from %s to %s", remote, local) - if err := forwardTCP(ctx, a.sshConfig, a.sshLocalPort, local, remote, verbForward); err != nil { - logrus.WithError(err).Warnf("failed to set up static TCP forwarding %s -> %s", remote, local) - } - } - } - } - } + a.addStaticPortForwards(ctx) localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) remoteUnix := "/run/lima-guestagent.sock" @@ -630,6 +613,27 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } +func (a *HostAgent) addStaticPortForwards(ctx context.Context) { + for _, rule := range a.instConfig.PortForwards { + if rule.Static { + if rule.GuestSocket == "" { + guest := &guestagentapi.IPPort{ + Ip: rule.GuestIP.String(), + Port: int32(rule.GuestPort), + Protocol: rule.Proto, + } + local, remote := a.portForwarder.forwardingAddresses(guest) + if local != "" { + logrus.Infof("Setting up static TCP forwarding from %s to %s", remote, local) + if err := forwardTCP(ctx, a.sshConfig, a.sshLocalPort, local, remote, verbForward); err != nil { + logrus.WithError(err).Warnf("failed to set up static TCP forwarding %s -> %s", remote, local) + } + } + } + } + } +} + func isGuestAgentSocketAccessible(ctx context.Context, client *guestagentclient.GuestAgentClient) bool { _, err := client.Info(ctx) return err == nil diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index e3ea818d959..d3150781567 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -915,6 +915,7 @@ func fixUpForPlainMode(y *LimaYAML) { if !*y.Plain { return } + deleteNonStaticPortForwards(&y.PortForwards) y.Mounts = nil y.Containerd.System = ptr.Of(false) y.Containerd.User = ptr.Of(false) @@ -923,6 +924,17 @@ func fixUpForPlainMode(y *LimaYAML) { y.TimeZone = ptr.Of("") } +// deleteNonStaticPortForwards removes all non-static port forwarding rules in case of Plain mode. +func deleteNonStaticPortForwards(portForwards *[]PortForward) { + staticPortForwards := make([]PortForward, 0, len(*portForwards)) + for _, rule := range *portForwards { + if rule.Static { + staticPortForwards = append(staticPortForwards, rule) + } + } + *portForwards = staticPortForwards +} + func executeGuestTemplate(format, instDir string, user User, param map[string]string) (bytes.Buffer, error) { tmpl, err := template.New("").Parse(format) if err == nil { diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index 70f6810c855..7166f1a40f5 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -762,3 +762,120 @@ func TestContainerdDefault(t *testing.T) { archives := defaultContainerdArchives() assert.Assert(t, len(archives) > 0) } + +func TestStaticPortForwarding(t *testing.T) { + tests := []struct { + name string + config LimaYAML + expected []PortForward + }{ + { + name: "plain mode with static port forwards", + config: LimaYAML{ + Plain: ptr.Of(true), + PortForwards: []PortForward{ + { + GuestPort: 8080, + HostPort: 8080, + Static: true, + }, + { + GuestPort: 9000, + HostPort: 9000, + Static: false, + }, + { + GuestPort: 8081, + HostPort: 8081, + }, + }, + }, + expected: []PortForward{ + { + GuestPort: 8080, + HostPort: 8080, + Static: true, + }, + }, + }, + { + name: "non-plain mode with static port forwards", + config: LimaYAML{ + Plain: ptr.Of(false), + PortForwards: []PortForward{ + { + GuestPort: 8080, + HostPort: 8080, + Static: true, + }, + { + GuestPort: 9000, + HostPort: 9000, + Static: false, + }, + }, + }, + expected: []PortForward{ + { + GuestPort: 8080, + HostPort: 8080, + Static: true, + }, + { + GuestPort: 9000, + HostPort: 9000, + Static: false, + }, + }, + }, + { + name: "plain mode with no static port forwards", + config: LimaYAML{ + Plain: ptr.Of(true), + PortForwards: []PortForward{ + { + GuestPort: 8080, + HostPort: 8080, + Static: false, + }, + { + GuestPort: 9000, + HostPort: 9000, + }, + }, + }, + expected: []PortForward{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fixUpForPlainMode(&tt.config) + + if *tt.config.Plain { + for _, pf := range tt.config.PortForwards { + if !pf.Static { + t.Errorf("Non-static port forward found in plain mode: %+v", pf) + } + } + } + + assert.Equal(t, len(tt.config.PortForwards), len(tt.expected), + "Expected %d port forwards, got %d", len(tt.expected), len(tt.config.PortForwards)) + + for i, expected := range tt.expected { + if i >= len(tt.config.PortForwards) { + t.Errorf("Missing port forward at index %d", i) + continue + } + actual := tt.config.PortForwards[i] + assert.Equal(t, expected.Static, actual.Static, + "Port forward %d: expected Static=%v, got %v", i, expected.Static, actual.Static) + assert.Equal(t, expected.GuestPort, actual.GuestPort, + "Port forward %d: expected GuestPort=%d, got %d", i, expected.GuestPort, actual.GuestPort) + assert.Equal(t, expected.HostPort, actual.HostPort, + "Port forward %d: expected HostPort=%d, got %d", i, expected.HostPort, actual.HostPort) + } + }) + } +} diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index d72e5b602c8..350c4a63253 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -285,6 +285,7 @@ type PortForward struct { Proto Proto `yaml:"proto,omitempty" json:"proto,omitempty"` Reverse bool `yaml:"reverse,omitempty" json:"reverse,omitempty"` Ignore bool `yaml:"ignore,omitempty" json:"ignore,omitempty"` + Static bool `yaml:"static,omitempty" json:"static,omitempty"` // if true, the port forward is static and will not be removed when the instance is stopped } type CopyToHost struct { diff --git a/templates/default.yaml b/templates/default.yaml index d25eef99288..913b4483cf3 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -422,7 +422,7 @@ networks: # Port forwarding rules. Forwarding between ports 22 and ssh.localPort cannot be overridden. # Rules are checked sequentially until the first one matches. -portForwards: null +#portForwards: null # - guestPort: 443 # hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"; allows privileged port forwarding # # default: hostPort: 443 (same as guestPort) From d90763812728c981e7991d826037619457c857a7 Mon Sep 17 00:00:00 2001 From: Praful Khanduri Date: Fri, 11 Jul 2025 19:02:33 +0530 Subject: [PATCH 3/4] Add e2e test for static port forwards Signed-off-by: Praful Khanduri --- .github/workflows/test.yml | 25 +++++++++++++++++++ cmd/limactl/editflags/editflags.go | 6 ++--- hack/test-nonplain-static-port-forward.sh | 15 +++++++++++ hack/test-plain-static-port-forward.sh | 22 ++++++++++++++++ .../nonplain-static-port-forward.yaml | 16 ++++++++++++ .../plain-static-port-forward.yaml | 21 ++++++++++++++++ templates/default.yaml | 2 +- 7 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 hack/test-nonplain-static-port-forward.sh create mode 100755 hack/test-plain-static-port-forward.sh create mode 100644 hack/test-templates/nonplain-static-port-forward.yaml create mode 100644 hack/test-templates/plain-static-port-forward.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49ad89019f2..108d1a8735b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -612,3 +612,28 @@ jobs: sudo chown $(whoami) /dev/kvm - name: Smoke test run: limactl start --tty=false + + static-port-forwarding: + name: "Static Port Forwarding Tests" + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Install test dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 qemu-utils + qemu-system-x86_64 --version + sudo modprobe kvm + # `sudo usermod -aG kvm $(whoami)` does not take an effect on GHA + sudo chown $(whoami) /dev/kvm + - name: Make + run: make + - name: Install + run: sudo make install + - name: Run plain mode static port forwarding test + run: bash hack/test-plain-static-port-forward.sh + - name: Run non-plain mode static port forwarding test + run: bash hack/test-nonplain-static-port-forward.sh diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go index 29cc8805e95..1ec1c0df887 100644 --- a/cmd/limactl/editflags/editflags.go +++ b/cmd/limactl/editflags/editflags.go @@ -288,9 +288,9 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { if len(parts) != 2 { return "", fmt.Errorf("invalid static port forward format %q, expected HOST:GUEST", s) } - hostPort := strings.TrimSpace(parts[0]) - guestPort := strings.TrimSpace(parts[1]) - expr += fmt.Sprintf(`{"hostPort": %s, "guestPort": %s, "static": true}`, hostPort, guestPort) + guestPort := strings.TrimSpace(parts[0]) + hostPort := strings.TrimSpace(parts[1]) + expr += fmt.Sprintf(`{"guestPort": %s, "hostPort": %s}`, guestPort, hostPort) if i < len(ss)-1 { expr += "," } diff --git a/hack/test-nonplain-static-port-forward.sh b/hack/test-nonplain-static-port-forward.sh new file mode 100644 index 00000000000..33edaf4da2b --- /dev/null +++ b/hack/test-nonplain-static-port-forward.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euxo pipefail + +INSTANCE=nonplain-static-port-forward +TEMPLATE=hack/test-templates/nonplain-static-port-forward.yaml + +limactl delete -f $INSTANCE || true + +limactl start --name=$INSTANCE --tty=false $TEMPLATE + +limactl shell $INSTANCE -- bash -c 'until [ -e /run/nginx.pid ]; do sleep 1; done' + +curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'nginx port forwarding works!' + +limactl delete -f $INSTANCE \ No newline at end of file diff --git a/hack/test-plain-static-port-forward.sh b/hack/test-plain-static-port-forward.sh new file mode 100755 index 00000000000..1349957e121 --- /dev/null +++ b/hack/test-plain-static-port-forward.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euxo pipefail + +INSTANCE=plain-static-port-forward +TEMPLATE=hack/test-templates/plain-static-port-forward.yaml + +limactl delete -f $INSTANCE || true + +limactl start --name=$INSTANCE --tty=false $TEMPLATE + +limactl shell $INSTANCE -- bash -c 'until [ -e /run/nginx.pid ]; do sleep 1; done' + +curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'nginx port forwarding works!' + +if curl -sSf http://127.0.0.1:9080; then + echo 'ERROR: Port 9080 should not be forwarded in plain mode!' + exit 1 +else + echo 'Port 9080 is correctly NOT forwarded in plain mode.' +fi + +limactl delete -f $INSTANCE \ No newline at end of file diff --git a/hack/test-templates/nonplain-static-port-forward.yaml b/hack/test-templates/nonplain-static-port-forward.yaml new file mode 100644 index 00000000000..ad2e98483f3 --- /dev/null +++ b/hack/test-templates/nonplain-static-port-forward.yaml @@ -0,0 +1,16 @@ +images: +- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img" + arch: "x86_64" + +provision: +- mode: system + script: | + apt-get update + apt-get install -y nginx + systemctl enable nginx + systemctl start nginx + +portForwards: +- guestPort: 80 + hostPort: 9090 + static: true diff --git a/hack/test-templates/plain-static-port-forward.yaml b/hack/test-templates/plain-static-port-forward.yaml new file mode 100644 index 00000000000..30f1626dd3c --- /dev/null +++ b/hack/test-templates/plain-static-port-forward.yaml @@ -0,0 +1,21 @@ +images: +- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img" + arch: "x86_64" + +plain: true + +provision: +- mode: system + script: | + apt-get update + apt-get install -y nginx + systemctl enable nginx + systemctl start nginx + +portForwards: +- guestPort: 80 + hostPort: 9090 + static: true +- guestPort: 9080 + hostPort: 9080 + static: false diff --git a/templates/default.yaml b/templates/default.yaml index 913b4483cf3..aeea939c81d 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -422,7 +422,7 @@ networks: # Port forwarding rules. Forwarding between ports 22 and ssh.localPort cannot be overridden. # Rules are checked sequentially until the first one matches. -#portForwards: null +# portForwards: # - guestPort: 443 # hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"; allows privileged port forwarding # # default: hostPort: 443 (same as guestPort) From 9764a705e452b883f28352abbf76a8d08031bbee Mon Sep 17 00:00:00 2001 From: Praful Khanduri Date: Sat, 12 Jul 2025 17:20:56 +0530 Subject: [PATCH 4/4] updated e2e tests Signed-off-by: Praful Khanduri Skip static port forwards on gRPC because conflicts Signed-off-by: Praful Khanduri Revert unintended changes to templates/default.yaml Signed-off-by: Praful Khanduri fixed failing golangci lint err Signed-off-by: Praful Khanduri --- .github/workflows/test.yml | 25 ---- cmd/limactl/editflags/editflags.go | 82 +++++++---- cmd/limactl/editflags/editflags_test.go | 134 ++++++++++++++++++ hack/test-nonplain-static-port-forward.sh | 18 ++- hack/test-plain-static-port-forward.sh | 31 ++-- hack/test-templates.sh | 14 ++ .../nonplain-static-port-forward.yaml | 16 --- .../plain-static-port-forward.yaml | 21 --- hack/test-templates/static-port-forward.yaml | 64 +++++++++ pkg/hostagent/hostagent.go | 61 +++++--- pkg/limayaml/limayaml.go | 2 +- pkg/portfwd/forward.go | 16 +++ templates/default.yaml | 2 +- 13 files changed, 362 insertions(+), 124 deletions(-) mode change 100644 => 100755 hack/test-nonplain-static-port-forward.sh delete mode 100644 hack/test-templates/nonplain-static-port-forward.yaml delete mode 100644 hack/test-templates/plain-static-port-forward.yaml create mode 100644 hack/test-templates/static-port-forward.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 108d1a8735b..49ad89019f2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -612,28 +612,3 @@ jobs: sudo chown $(whoami) /dev/kvm - name: Smoke test run: limactl start --tty=false - - static-port-forwarding: - name: "Static Port Forwarding Tests" - runs-on: ubuntu-24.04 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - name: Install test dependencies - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 qemu-utils - qemu-system-x86_64 --version - sudo modprobe kvm - # `sudo usermod -aG kvm $(whoami)` does not take an effect on GHA - sudo chown $(whoami) /dev/kvm - - name: Make - run: make - - name: Install - run: sudo make install - - name: Run plain mode static port forwarding test - run: bash hack/test-plain-static-port-forward.sh - - name: Run non-plain mode static port forwarding test - run: bash hack/test-nonplain-static-port-forward.sh diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go index 1ec1c0df887..1b14f8fcdee 100644 --- a/cmd/limactl/editflags/editflags.go +++ b/cmd/limactl/editflags/editflags.go @@ -97,13 +97,9 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) { flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.") - flags.StringSlice("port-forward", nil, commentPrefix+"Port forwards (host:guest), e.g., '8080:80,2222:22'") + flags.StringArray("port-forward", nil, commentPrefix+"Port forwards (host:guest), e.g., '8080:80' or '9090:9090,static=true' for static port-forwards") _ = cmd.RegisterFlagCompletionFunc("port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return []string{"8080:80", "3000:3000"}, cobra.ShellCompDirectiveNoFileComp - }) - flags.StringSlice("static-port-forward", nil, commentPrefix+"Static port forwards (host:guest), works even in plain mode, e.g., '8080:80,2222:22'") - _ = cmd.RegisterFlagCompletionFunc("static-port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return []string{"8080:80", "3000:3000"}, cobra.ShellCompDirectiveNoFileComp + return []string{"8080:80", "3000:3000", "8080:80,static=true"}, cobra.ShellCompDirectiveNoFileComp }) } @@ -113,6 +109,56 @@ func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) { } } +func ParsePortForward(spec string) (hostPort, guestPort string, isStatic bool, err error) { + parts := strings.Split(spec, ",") + if len(parts) > 2 { + return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST or HOST:GUEST,static=true", spec) + } + + portParts := strings.Split(strings.TrimSpace(parts[0]), ":") + if len(portParts) != 2 { + return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST", parts[0]) + } + + hostPort = strings.TrimSpace(portParts[0]) + guestPort = strings.TrimSpace(portParts[1]) + + if len(parts) == 2 { + staticPart := strings.TrimSpace(parts[1]) + if strings.HasPrefix(staticPart, "static=") { + staticValue := strings.TrimPrefix(staticPart, "static=") + isStatic, err = strconv.ParseBool(staticValue) + if err != nil { + return "", "", false, fmt.Errorf("invalid value for static parameter: %q", staticValue) + } + } else { + return "", "", false, fmt.Errorf("invalid parameter %q, expected 'static=' followed by a boolean value", staticPart) + } + } + + return hostPort, guestPort, isStatic, nil +} + +func BuildPortForwardExpression(portForwards []string) (string, error) { + if len(portForwards) == 0 { + return "", nil + } + + expr := `.portForwards += [` + for i, spec := range portForwards { + hostPort, guestPort, isStatic, err := ParsePortForward(spec) + if err != nil { + return "", err + } + expr += fmt.Sprintf(`{"guestPort": %q, "hostPort": %q, "static": %v}`, guestPort, hostPort, isStatic) + if i < len(portForwards)-1 { + expr += "," + } + } + expr += `]` + return expr, nil +} + // YQExpressions returns YQ expressions. func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { type def struct { @@ -272,31 +318,13 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { {"vm-type", d(".vmType = %q"), true, false}, {"plain", d(".plain = %s"), true, false}, { - "static-port-forward", + "port-forward", func(_ *flag.Flag) (string, error) { - ss, err := flags.GetStringSlice("static-port-forward") + ss, err := flags.GetStringArray("port-forward") if err != nil { return "", err } - if len(ss) == 0 { - return "", nil - } - - expr := `.portForwards += [` - for i, s := range ss { - parts := strings.Split(s, ":") - if len(parts) != 2 { - return "", fmt.Errorf("invalid static port forward format %q, expected HOST:GUEST", s) - } - guestPort := strings.TrimSpace(parts[0]) - hostPort := strings.TrimSpace(parts[1]) - expr += fmt.Sprintf(`{"guestPort": %s, "hostPort": %s}`, guestPort, hostPort) - if i < len(ss)-1 { - expr += "," - } - } - expr += `]` - return expr, nil + return BuildPortForwardExpression(ss) }, false, false, diff --git a/cmd/limactl/editflags/editflags_test.go b/cmd/limactl/editflags/editflags_test.go index 2dceac15596..1634da8211a 100644 --- a/cmd/limactl/editflags/editflags_test.go +++ b/cmd/limactl/editflags/editflags_test.go @@ -23,3 +23,137 @@ func TestCompleteMemoryGiB(t *testing.T) { assert.DeepEqual(t, []float32{1, 2, 4}, completeMemoryGiB(8<<30)) assert.DeepEqual(t, []float32{1, 2, 4, 8, 10}, completeMemoryGiB(20<<30)) } + +func TestBuildPortForwardExpression(t *testing.T) { + tests := []struct { + name string + portForwards []string + expected string + expectError bool + }{ + { + name: "empty port forwards", + portForwards: []string{}, + expected: "", + }, + { + name: "single dynamic port forward", + portForwards: []string{"8080:80"}, + expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": false}]`, + }, + { + name: "single static port forward", + portForwards: []string{"8080:80,static=true"}, + expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": true}]`, + }, + { + name: "multiple mixed port forwards", + portForwards: []string{"8080:80", "2222:22,static=true", "3000:3000"}, + expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": false},{"guestPort": "22", "hostPort": "2222", "static": true},{"guestPort": "3000", "hostPort": "3000", "static": false}]`, + }, + { + name: "invalid format - missing colon", + portForwards: []string{"8080"}, + expectError: true, + }, + { + name: "invalid format - too many colons", + portForwards: []string{"8080:80:extra"}, + expectError: true, + }, + { + name: "invalid static parameter", + portForwards: []string{"8080:80,invalid=true"}, + expectError: true, + }, + { + name: "too many parameters", + portForwards: []string{"8080:80,static=true,extra=value"}, + expectError: true, + }, + { + name: "whitespace handling", + portForwards: []string{" 8080 : 80 , static=true "}, + expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": true}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := BuildPortForwardExpression(tt.portForwards) + if tt.expectError { + assert.Check(t, err != nil) + } else { + assert.NilError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestParsePortForward(t *testing.T) { + tests := []struct { + name string + spec string + hostPort string + guestPort string + isStatic bool + expectError bool + }{ + { + name: "dynamic port forward", + spec: "8080:80", + hostPort: "8080", + guestPort: "80", + isStatic: false, + }, + { + name: "static port forward", + spec: "8080:80,static=true", + hostPort: "8080", + guestPort: "80", + isStatic: true, + }, + { + name: "whitespace handling", + spec: " 8080 : 80 , static=true ", + hostPort: "8080", + guestPort: "80", + isStatic: true, + }, + { + name: "invalid format - missing colon", + spec: "8080", + expectError: true, + }, + { + name: "invalid format - too many colons", + spec: "8080:80:extra", + expectError: true, + }, + { + name: "invalid parameter", + spec: "8080:80,invalid=true", + expectError: true, + }, + { + name: "too many parameters", + spec: "8080:80,static=true,extra=value", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hostPort, guestPort, isStatic, err := ParsePortForward(tt.spec) + if tt.expectError { + assert.Check(t, err != nil) + } else { + assert.NilError(t, err) + assert.Equal(t, tt.hostPort, hostPort) + assert.Equal(t, tt.guestPort, guestPort) + assert.Equal(t, tt.isStatic, isStatic) + } + }) + } +} diff --git a/hack/test-nonplain-static-port-forward.sh b/hack/test-nonplain-static-port-forward.sh old mode 100644 new mode 100755 index 33edaf4da2b..92a94b95420 --- a/hack/test-nonplain-static-port-forward.sh +++ b/hack/test-nonplain-static-port-forward.sh @@ -1,15 +1,25 @@ -#!/bin/bash +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + set -euxo pipefail INSTANCE=nonplain-static-port-forward -TEMPLATE=hack/test-templates/nonplain-static-port-forward.yaml +TEMPLATE=hack/test-templates/static-port-forward.yaml limactl delete -f $INSTANCE || true limactl start --name=$INSTANCE --tty=false $TEMPLATE limactl shell $INSTANCE -- bash -c 'until [ -e /run/nginx.pid ]; do sleep 1; done' +limactl shell $INSTANCE -- bash -c 'until systemctl is-active --quiet test-server-9080; do sleep 1; done' +limactl shell $INSTANCE -- bash -c 'until systemctl is-active --quiet test-server-9070; do sleep 1; done' -curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'nginx port forwarding works!' +curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'Static port forwarding (9090) works in normal mode!' +curl -sSf http://127.0.0.1:9080 | grep -i 'Dynamic port 9080' && echo 'Dynamic port forwarding (9080) works in normal mode!' +curl -sSf http://127.0.0.1:9070 | grep -i 'Dynamic port 9070' && echo 'Dynamic port forwarding (9070) works in normal mode!' -limactl delete -f $INSTANCE \ No newline at end of file +limactl delete -f $INSTANCE +echo "All tests passed for normal mode - both static and dynamic ports work!" +# EOF diff --git a/hack/test-plain-static-port-forward.sh b/hack/test-plain-static-port-forward.sh index 1349957e121..fbe70d62742 100755 --- a/hack/test-plain-static-port-forward.sh +++ b/hack/test-plain-static-port-forward.sh @@ -1,22 +1,35 @@ -#!/bin/bash +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + set -euxo pipefail INSTANCE=plain-static-port-forward -TEMPLATE=hack/test-templates/plain-static-port-forward.yaml +TEMPLATE=hack/test-templates/static-port-forward.yaml limactl delete -f $INSTANCE || true -limactl start --name=$INSTANCE --tty=false $TEMPLATE +limactl start --name=$INSTANCE --plain=true --tty=false $TEMPLATE limactl shell $INSTANCE -- bash -c 'until [ -e /run/nginx.pid ]; do sleep 1; done' -curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'nginx port forwarding works!' +curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'Static port forwarding (9090) works in plain mode!' + +if curl -sSf http://127.0.0.1:9080 2>/dev/null; then + echo 'ERROR: Dynamic port 9080 should not be forwarded in plain mode!' + exit 1 +else + echo 'Dynamic port 9080 is correctly NOT forwarded in plain mode.' +fi -if curl -sSf http://127.0.0.1:9080; then - echo 'ERROR: Port 9080 should not be forwarded in plain mode!' - exit 1 +if curl -sSf http://127.0.0.1:9070 2>/dev/null; then + echo 'ERROR: Dynamic port 9070 should not be forwarded in plain mode!' + exit 1 else - echo 'Port 9080 is correctly NOT forwarded in plain mode.' + echo 'Dynamic port 9070 is correctly NOT forwarded in plain mode.' fi -limactl delete -f $INSTANCE \ No newline at end of file +limactl delete -f $INSTANCE +echo "All tests passed for plain mode - only static ports work!" +# EOF diff --git a/hack/test-templates.sh b/hack/test-templates.sh index 2935907c99e..a6e692a5fe5 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -50,6 +50,7 @@ declare -A CHECKS=( ["snapshot-online"]="" ["snapshot-offline"]="" ["port-forwards"]="1" + ["static-port-forwards"]="" ["vmnet"]="" ["disk"]="" ["user-v2"]="" @@ -90,6 +91,12 @@ case "$NAME" in CHECKS["param-env-variables"]="1" CHECKS["set-user"]="1" ;; +"static-port-forward") + CHECKS["static-port-forwards"]="1" + CHECKS["port-forwards"]="" + CHECKS["container-engine"]="" + CHECKS["restart"]="" + ;; "docker") CONTAINER_ENGINE="docker" ;; @@ -383,6 +390,13 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then set +x fi +if [[ -n ${CHECKS["static-port-forwards"]} ]]; then + INFO "Testing static port forwarding functionality" + "${scriptdir}/test-plain-static-port-forward.sh" "$NAME" + "${scriptdir}/test-nonplain-static-port-forward.sh" "$NAME" + INFO "All static port forwarding tests passed!" +fi + if [[ -n ${CHECKS["vmnet"]} ]]; then INFO "Testing vmnet functionality" guestip="$(limactl shell "$NAME" ip -4 -j addr show dev lima0 | jq -r '.[0].addr_info[0].local')" diff --git a/hack/test-templates/nonplain-static-port-forward.yaml b/hack/test-templates/nonplain-static-port-forward.yaml deleted file mode 100644 index ad2e98483f3..00000000000 --- a/hack/test-templates/nonplain-static-port-forward.yaml +++ /dev/null @@ -1,16 +0,0 @@ -images: -- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img" - arch: "x86_64" - -provision: -- mode: system - script: | - apt-get update - apt-get install -y nginx - systemctl enable nginx - systemctl start nginx - -portForwards: -- guestPort: 80 - hostPort: 9090 - static: true diff --git a/hack/test-templates/plain-static-port-forward.yaml b/hack/test-templates/plain-static-port-forward.yaml deleted file mode 100644 index 30f1626dd3c..00000000000 --- a/hack/test-templates/plain-static-port-forward.yaml +++ /dev/null @@ -1,21 +0,0 @@ -images: -- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img" - arch: "x86_64" - -plain: true - -provision: -- mode: system - script: | - apt-get update - apt-get install -y nginx - systemctl enable nginx - systemctl start nginx - -portForwards: -- guestPort: 80 - hostPort: 9090 - static: true -- guestPort: 9080 - hostPort: 9080 - static: false diff --git a/hack/test-templates/static-port-forward.yaml b/hack/test-templates/static-port-forward.yaml new file mode 100644 index 00000000000..67998d54312 --- /dev/null +++ b/hack/test-templates/static-port-forward.yaml @@ -0,0 +1,64 @@ +images: +- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img" + arch: "x86_64" + +provision: +- mode: system + script: | + apt-get update + apt-get install -y nginx python3 + systemctl enable nginx + systemctl start nginx + + cat > /etc/systemd/system/test-server-9080.service << 'EOF' + [Unit] + Description=Test Server on Port 9080 + After=network.target + + [Service] + Type=simple + User=root + ExecStart=/usr/bin/python3 -m http.server 9080 --bind 127.0.0.1 + Restart=always + + [Install] + WantedBy=multi-user.target + EOF + + cat > /etc/systemd/system/test-server-9070.service << 'EOF' + [Unit] + Description=Test Server on Port 9070 + After=network.target + + [Service] + Type=simple + User=root + ExecStart=/usr/bin/python3 -m http.server 9070 --bind 127.0.0.1 + Restart=always + + [Install] + WantedBy=multi-user.target + EOF + + mkdir -p /var/www/html-9080 + mkdir -p /var/www/html-9070 + + echo '

Dynamic port 9080

' > /var/www/html-9080/index.html + echo '

Dynamic port 9070

' > /var/www/html-9070/index.html + + systemctl daemon-reload + systemctl enable test-server-9080 + systemctl enable test-server-9070 + systemctl start test-server-9080 + systemctl start test-server-9070 + +portForwards: +- guestPort: 80 + hostPort: 9090 + static: true +- guestPort: 9080 + hostPort: 9080 + static: false +- guestPort: 9070 + hostPort: 9070 + static: false diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 6f3fdfac154..d9fb97f2def 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -479,11 +479,14 @@ sudo chown -R "${USER}" /run/host-services` }) } + staticPortForwards, err := a.separateStaticPortForwards() + if err != nil { + errs = append(errs, err) + } + + a.addStaticPortForwardsFromList(ctx, staticPortForwards) if !*a.instConfig.Plain { go a.watchGuestAgentEvents(ctx) - } else { - logrus.Info("Running in plain mode, skipping guest agent events watcher") - a.addStaticPortForwards(ctx) } if err := a.waitForRequirements("optional", a.optionalRequirements()); err != nil { @@ -548,8 +551,6 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } - a.addStaticPortForwards(ctx) - localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) remoteUnix := "/run/lima-guestagent.sock" @@ -613,27 +614,47 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } -func (a *HostAgent) addStaticPortForwards(ctx context.Context) { - for _, rule := range a.instConfig.PortForwards { - if rule.Static { - if rule.GuestSocket == "" { - guest := &guestagentapi.IPPort{ - Ip: rule.GuestIP.String(), - Port: int32(rule.GuestPort), - Protocol: rule.Proto, - } - local, remote := a.portForwarder.forwardingAddresses(guest) - if local != "" { - logrus.Infof("Setting up static TCP forwarding from %s to %s", remote, local) - if err := forwardTCP(ctx, a.sshConfig, a.sshLocalPort, local, remote, verbForward); err != nil { - logrus.WithError(err).Warnf("failed to set up static TCP forwarding %s -> %s", remote, local) - } +func (a *HostAgent) addStaticPortForwardsFromList(ctx context.Context, staticPortForwards []limayaml.PortForward) { + for _, rule := range staticPortForwards { + if rule.GuestSocket == "" { + guest := &guestagentapi.IPPort{ + Ip: rule.GuestIP.String(), + Port: int32(rule.GuestPort), + Protocol: rule.Proto, + } + local, remote := a.portForwarder.forwardingAddresses(guest) + if local != "" { + logrus.Infof("Setting up static TCP forwarding from %s to %s", remote, local) + if err := forwardTCP(ctx, a.sshConfig, a.sshLocalPort, local, remote, verbForward); err != nil { + logrus.WithError(err).Warnf("failed to set up static TCP forwarding %s -> %s", remote, local) } } } } } +func (a *HostAgent) separateStaticPortForwards() ([]limayaml.PortForward, error) { + staticPortForwards := make([]limayaml.PortForward, 0, len(a.instConfig.PortForwards)) + nonStaticPortForwards := make([]limayaml.PortForward, 0, len(a.instConfig.PortForwards)) + + for i := range len(a.instConfig.PortForwards) { + rule := a.instConfig.PortForwards[i] + if rule.Static { + logrus.Debugf("Found static port forward: guest=%d host=%d", rule.GuestPort, rule.HostPort) + staticPortForwards = append(staticPortForwards, rule) + } else { + logrus.Debugf("Found non-static port forward: guest=%d host=%d", rule.GuestPort, rule.HostPort) + nonStaticPortForwards = append(nonStaticPortForwards, rule) + } + } + + logrus.Debugf("Static port forwards: %d, Non-static port forwards: %d", len(staticPortForwards), len(nonStaticPortForwards)) + + a.instConfig.PortForwards = nonStaticPortForwards + + return staticPortForwards, nil +} + func isGuestAgentSocketAccessible(ctx context.Context, client *guestagentclient.GuestAgentClient) bool { _, err := client.Info(ctx) return err == nil diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index 350c4a63253..115ec168002 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -285,7 +285,7 @@ type PortForward struct { Proto Proto `yaml:"proto,omitempty" json:"proto,omitempty"` Reverse bool `yaml:"reverse,omitempty" json:"reverse,omitempty"` Ignore bool `yaml:"ignore,omitempty" json:"ignore,omitempty"` - Static bool `yaml:"static,omitempty" json:"static,omitempty"` // if true, the port forward is static and will not be removed when the instance is stopped + Static bool `yaml:"static,omitempty" json:"static,omitempty"` } type CopyToHost struct { diff --git a/pkg/portfwd/forward.go b/pkg/portfwd/forward.go index 7d6c001c5c4..05fa37e0691 100644 --- a/pkg/portfwd/forward.go +++ b/pkg/portfwd/forward.go @@ -35,6 +35,10 @@ func NewPortForwarder(rules []limayaml.PortForward, ignoreTCP, ignoreUDP bool) * func (fw *Forwarder) OnEvent(ctx context.Context, client *guestagentclient.GuestAgentClient, ev *api.Event) { for _, f := range ev.LocalPortsAdded { + // Before forwarding, check if any static rule matches this port otherwise it will be forwarded twice and cause a port conflict + if fw.isPortStaticallyForwarded(f) { + continue + } local, remote := fw.forwardingAddresses(f) if local == "" { if !fw.ignoreTCP && f.Protocol == "tcp" { @@ -91,6 +95,18 @@ func (fw *Forwarder) forwardingAddresses(guest *api.IPPort) (hostAddr, guestAddr return "", guest.HostString() } +func (fw *Forwarder) isPortStaticallyForwarded(guest *api.IPPort) bool { + for _, rule := range fw.rules { + if !rule.Static { + continue + } + if guest.Port >= int32(rule.GuestPortRange[0]) && guest.Port <= int32(rule.GuestPortRange[1]) { + return true + } + } + return false +} + func hostAddress(rule limayaml.PortForward, guest *api.IPPort) string { if rule.HostSocket != "" { return rule.HostSocket diff --git a/templates/default.yaml b/templates/default.yaml index aeea939c81d..11a88640239 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -613,4 +613,4 @@ nestedVirtualization: null # ===================================================================== # # END OF TEMPLATE -# ===================================================================== # \ No newline at end of file +# ===================================================================== #