Skip to content

Plain mode: support port forwarding #3699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 40 additions & 0 deletions cmd/limactl/editflags/editflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ 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'")
_ = 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
})
}

func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) {
Expand Down Expand Up @@ -206,6 +215,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
false,
false,
},

{
"rosetta",
func(_ *flag.Flag) (string, error) {
Expand Down Expand Up @@ -261,6 +271,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},
{
"static-port-forward",
func(_ *flag.Flag) (string, error) {
ss, err := flags.GetStringSlice("static-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
},
false,
false,
},
}
var exprs []string
for _, def := range defs {
Expand Down
15 changes: 15 additions & 0 deletions hack/test-nonplain-static-port-forward.sh
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions hack/test-plain-static-port-forward.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions hack/test-templates/nonplain-static-port-forward.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions hack/test-templates/plain-static-port-forward.yaml
Original file line number Diff line number Diff line change
@@ -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
30 changes: 29 additions & 1 deletion pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ 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.")
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")
Expand Down Expand Up @@ -478,9 +478,14 @@ sudo chown -R "${USER}" /run/host-services`
return errors.Join(unlockErrs...)
})
}

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)
}
Expand Down Expand Up @@ -543,6 +548,8 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
}
}

a.addStaticPortForwards(ctx)

localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock)
remoteUnix := "/run/lima-guestagent.sock"

Expand Down Expand Up @@ -606,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
Expand Down
13 changes: 12 additions & 1 deletion pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,15 +915,26 @@ func fixUpForPlainMode(y *LimaYAML) {
if !*y.Plain {
return
}
deleteNonStaticPortForwards(&y.PortForwards)
y.Mounts = nil
y.PortForwards = nil
y.Containerd.System = ptr.Of(false)
y.Containerd.User = ptr.Of(false)
y.Rosetta.BinFmt = ptr.Of(false)
y.Rosetta.Enabled = ptr.Of(false)
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 {
Expand Down
117 changes: 117 additions & 0 deletions pkg/limayaml/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading