diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da3ee770dba..0ef30a16a60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -516,10 +516,18 @@ jobs: matrix: template: - default.yaml + create_arg: + - "" + - "--network=vzNAT" + include: + - template: default.yaml + create_arg: "--network=vzNAT" + additional_env: | + _LIMA_DIRECT_IP_PORT_FORWARDER=true steps: - name: "Adjust LIMACTL_CREATE_ARGS" # --cpus=1 is needed for running vz on GHA: https://github.com/lima-vm/lima/pull/1511#issuecomment-1574937888 - run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --cpus 1 --memory 1" >>$GITHUB_ENV + run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --cpus 1 --memory 1 ${{ matrix.create_arg }}" >>$GITHUB_ENV - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: @@ -536,12 +544,15 @@ jobs: run: brew install bash coreutils w3m socat - name: Uninstall qemu run: brew uninstall --ignore-dependencies --force qemu + - name: Set additional environment variables + if: matrix.additional_env != null + run: echo "${{ matrix.additional_env }}" >>$GITHUB_ENV - name: Test run: ./hack/test-templates.sh templates/${{ matrix.template }} - if: failure() uses: ./.github/actions/upload_failure_logs_if_exists with: - suffix: ${{ matrix.template }} + suffix: ${{ matrix.template }}${{ matrix.create_arg }} # gomodjail is a library sandbox for Go # https://github.com/AkihiroSuda/gomodjail diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index 017aa1f579b..c358039054d 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -282,10 +282,11 @@ func shellAction(cmd *cobra.Command, args []string) error { if olderSSH { logLevel = "QUIET" } + sshAddress, sshPort := inst.SSHAddressPort() sshArgs = append(sshArgs, []string{ "-o", fmt.Sprintf("LogLevel=%s", logLevel), - "-p", strconv.Itoa(inst.SSHLocalPort), - inst.SSHAddress, + "-p", strconv.Itoa(sshPort), + sshAddress, "--", script, }...) diff --git a/cmd/limactl/show-ssh.go b/cmd/limactl/show-ssh.go index ecaf98e43da..0ed7f5eb6ca 100644 --- a/cmd/limactl/show-ssh.go +++ b/cmd/limactl/show-ssh.go @@ -109,8 +109,9 @@ func showSSHAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - opts = append(opts, "Hostname=127.0.0.1") - opts = append(opts, fmt.Sprintf("Port=%d", inst.SSHLocalPort)) + sshAddress, sshPort := inst.SSHAddressPort() + opts = append(opts, fmt.Sprintf("Hostname=%s", sshAddress)) + opts = append(opts, fmt.Sprintf("Port=%d", sshPort)) return sshutil.Format(w, "ssh", instName, format, opts) } diff --git a/cmd/limactl/tunnel.go b/cmd/limactl/tunnel.go index 68b16cbf674..c5290774d69 100644 --- a/cmd/limactl/tunnel.go +++ b/cmd/limactl/tunnel.go @@ -103,13 +103,14 @@ func tunnelAction(cmd *cobra.Command, args []string) error { } sshArgs := append([]string{}, sshExe.Args...) sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...) + sshAddress, sshPort := inst.SSHAddressPort() sshArgs = append(sshArgs, []string{ "-q", // quiet "-f", // background "-N", // no command "-D", fmt.Sprintf("127.0.0.1:%d", port), - "-p", strconv.Itoa(inst.SSHLocalPort), - inst.SSHAddress, + "-p", strconv.Itoa(sshPort), + sshAddress, }...) sshCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...) sshCmd.Stdout = stderr diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl index 4c586dbf442..4e792f9a8a3 100755 --- a/hack/test-port-forwarding.pl +++ b/hack/test-port-forwarding.pl @@ -191,6 +191,7 @@ my $sudo = $test->{guest_port} < 1024 ? "sudo " : ""; print $lima "${sudo}${cmd} >$listener.${id} 2>/dev/null &\n"; + print "Running in guest: ${sudo}${cmd} >$listener.${id} 2>/dev/null &\n"; } # Make sure the guest- and hostagents had enough time to set up the forwards @@ -211,7 +212,7 @@ my $tcp_dest = $test->{host_ip} =~ /:/ ? "TCP6:[$test->{host_ip}]:$test->{host_port}" : "TCP:$test->{host_ip}:$test->{host_port}"; $cmd = $test->{host_socket} eq "" ? "socat -u STDIN $tcp_dest,connect-timeout=$connectionTimeout" : "socat -u STDIN UNIX-CONNECT:$test->{host_socket}"; } - print "Running: $cmd\n"; + print "Running in host: $cmd\n"; open(my $netcat, "| $cmd") or die "Can't run '$cmd': $!"; print $netcat "$test->{log_msg}\n"; # Don't check for errors on close; macOS nc seems to return non-zero exit code even on success diff --git a/hack/test-templates.sh b/hack/test-templates.sh index a8ae97da0b1..f2987bf1d68 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -336,30 +336,20 @@ if [[ -n ${CHECKS["ssh-over-vsock"]} ]]; then if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "vz" ]]; then INFO "Testing SSH over vsock" set -x - INFO "Testing LIMA_SSH_OVER_VSOCK=true environment" - limactl stop "${NAME}" - # Detection of the SSH server on VSOCK may fail; however, a failing log indicates that controlling detection via the environment variable works as expected. - if ! LIMA_SSH_OVER_VSOCK=true limactl start "${NAME}" 2>&1 | grep -i -E "(started vsock forwarder|Failed to detect SSH server on vsock)"; then - set +x - diagnose "${NAME}" - ERROR "LIMA_SSH_OVER_VSOCK=true did not enable vsock forwarder" - exit 1 - fi - INFO 'Testing LIMA_SSH_OVER_VSOCK="" environment' + INFO "Testing LIMA_SSH_OVER_VSOCK=false environment" limactl stop "${NAME}" - # Detection of the SSH server on VSOCK may fail; however, a failing log indicates that controlling detection via the environment variable works as expected. - if ! LIMA_SSH_OVER_VSOCK="" limactl start "${NAME}" 2>&1 | grep -i -E "(started vsock forwarder|Failed to detect SSH server on vsock)"; then + if ! LIMA_SSH_OVER_VSOCK=false limactl start "${NAME}" 2>&1 | grep -i "skipping detection of SSH server on vsock port"; then set +x diagnose "${NAME}" - ERROR "LIMA_SSH_OVER_VSOCK= did not enable vsock forwarder" + ERROR "LIMA_SSH_OVER_VSOCK=false did not disable vsock forwarder" exit 1 fi - INFO "Testing LIMA_SSH_OVER_VSOCK=false environment" + INFO "Testing LIMA_SSH_OVER_VSOCK=true environment" limactl stop "${NAME}" - if ! LIMA_SSH_OVER_VSOCK=false limactl start "${NAME}" 2>&1 | grep -i "skipping detection of SSH server on vsock port"; then + if ! LIMA_SSH_OVER_VSOCK=true limactl start "${NAME}" 2>&1 | grep -i -E "(started vsock forwarder|Failed to detect SSH server on vsock)"; then set +x diagnose "${NAME}" - ERROR "LIMA_SSH_OVER_VSOCK=false did not disable vsock forwarder" + ERROR "LIMA_SSH_OVER_VSOCK=true did not enable vsock forwarder" exit 1 fi set +x @@ -445,6 +435,13 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then if limactl shell "${NAME}" command -v dnf; then limactl shell "${NAME}" sudo dnf install -y nc socat fi + # print routing table for debugging + case "${OS_HOST}" in + "Darwin") netstat -rn ;; + "GNU/Linux") ip route show ;; + "Msys") route print ;; + *) ;; + esac if "${scriptdir}/test-port-forwarding.pl" "${NAME}" socat $PORT_FORWARDING_CONNECTION_TIMEOUT; then INFO "Port forwarding rules work" else diff --git a/pkg/hostagent/api/api.go b/pkg/hostagent/api/api.go index d893b6acddd..6fe82989584 100644 --- a/pkg/hostagent/api/api.go +++ b/pkg/hostagent/api/api.go @@ -3,9 +3,13 @@ package api +import "net" + type Info struct { // indicate instance is started by launchd or systemd if not empty AutoStartedIdentifier string `json:"autoStartedIdentifier,omitempty"` - // SSHLocalPort is the local port on the host for SSH access to the VM. + // Guest IP address directly accessible from the host. + GuestIP net.IP `json:"guestIP,omitempty"` + // SSH local port on the host forwarded to the guest's port 22. SSHLocalPort int `json:"sshLocalPort,omitempty"` } diff --git a/pkg/hostagent/events/events.go b/pkg/hostagent/events/events.go index 3afa14319ff..3819c3d05fa 100644 --- a/pkg/hostagent/events/events.go +++ b/pkg/hostagent/events/events.go @@ -4,6 +4,7 @@ package events import ( + "net" "time" ) @@ -16,6 +17,9 @@ type Status struct { Errors []string `json:"errors,omitempty"` + // Guest IP address directly accessible from the host. + GuestIP net.IP `json:"guestIP,omitempty"` + // SSH local port on the host forwarded to the guest's port 22. SSHLocalPort int `json:"sshLocalPort,omitempty"` // Cloud-init progress information diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 9355a2c91e9..1eeea518337 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -83,6 +83,11 @@ type HostAgent struct { statusMu sync.RWMutex currentStatus events.Status + + // Guest IP address on the same subnet as the host. + guestIPv4 net.IP + guestIPv6 net.IP + guestIPMu sync.RWMutex } type options struct { @@ -258,6 +263,27 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o return a, nil } +func (a *HostAgent) WriteSSHConfigFile(ctx context.Context) error { + sshExe, err := sshutil.NewSSHExe() + if err != nil { + return err + } + sshOpts, err := sshutil.SSHOpts( + ctx, + sshExe, + a.instDir, + *a.instConfig.User.Name, + *a.instConfig.SSH.LoadDotSSHPubKeys, + *a.instConfig.SSH.ForwardAgent, + *a.instConfig.SSH.ForwardX11, + *a.instConfig.SSH.ForwardX11Trusted) + if err != nil { + return err + } + sshAddress, sshPort := a.sshAddressPort() + return writeSSHConfigFile(sshExe.Exe, a.instName, a.instDir, sshAddress, sshPort, sshOpts) +} + func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error { if instDir == "" { return fmt.Errorf("directory is unknown for the instance %q", instName) @@ -336,6 +362,17 @@ func (a *HostAgent) emitCloudInitProgressEvent(ctx context.Context, progress *ev a.emitEvent(ctx, ev) } +func (a *HostAgent) emitGuestIPEvent(ctx context.Context, ip string) { + a.statusMu.RLock() + currentStatus := a.currentStatus + a.statusMu.RUnlock() + + currentStatus.GuestIP = net.ParseIP(ip) + + ev := events.Event{Status: currentStatus} + a.emitEvent(ctx, ev) +} + func generatePassword(length int) (string, error) { // avoid any special symbols, to make it easier to copy/paste return password.Generate(length, length/4, 0, false, false) @@ -481,9 +518,31 @@ func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error return a.driver.Stop(ctx) } +// GuestIP returns the guest's IPv4 address if available; otherwise the IPv6 address. +// It returns nil if the guest is not reachable by a direct IP. +func (a *HostAgent) GuestIP() net.IP { + a.guestIPMu.RLock() + defer a.guestIPMu.RUnlock() + if a.guestIPv4 != nil { + return a.guestIPv4 + } else if a.guestIPv6 != nil { + return a.guestIPv6 + } + return nil +} + +// GuestIPs returns the guest's IPv4 and IPv6 addresses if available; otherwise nil. +func (a *HostAgent) GuestIPs() (ipv4, ipv6 net.IP) { + a.guestIPMu.RLock() + defer a.guestIPMu.RUnlock() + return a.guestIPv4, a.guestIPv6 +} + func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { + guestIP := a.GuestIP() info := &hostagentapi.Info{ AutoStartedIdentifier: autostart.AutoStartedIdentifier(), + GuestIP: guestIP, SSHLocalPort: a.sshLocalPort, } return info, nil @@ -492,6 +551,12 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { func (a *HostAgent) sshAddressPort() (sshAddress string, sshPort int) { sshAddress = a.instSSHAddress sshPort = a.sshLocalPort + guestIP := a.GuestIP() + if guestIP != nil { + sshAddress = guestIP.String() + sshPort = 22 + logrus.Debugf("Using the guest IP address %q directly", sshAddress) + } return sshAddress, sshPort } @@ -513,7 +578,8 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { return nil } logrus.Debugf("shutting down the SSH master") - if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil { + sshAddress, sshPort := a.sshAddressPort() + if exitMasterErr := ssh.ExitMaster(sshAddress, sshPort, a.sshConfig); exitMasterErr != nil { logrus.WithError(exitMasterErr).Warn("failed to exit SSH master") } return nil @@ -529,7 +595,8 @@ sudo mkdir -p -m 700 /run/host-services sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock sudo chown -R "${USER}" /run/host-services` faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock" - stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc) + sshAddress, sshPort := a.sshAddressPort() + stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, a.sshConfig, faScript, faDesc) logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) if err != nil { errs = append(errs, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)) @@ -836,7 +903,53 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestag if useSSHFwd { a.portForwarder.OnEvent(ctx, ev) } else { - dialContext := portfwd.DialContextToGRPCTunnel(client) + useDirectIPPortForwarding := false + if envVar := os.Getenv("_LIMA_DIRECT_IP_PORT_FORWARDER"); envVar != "" { + b, err := strconv.ParseBool(envVar) + if err != nil { + logrus.WithError(err).Warnf("invalid _LIMA_DIRECT_IP_PORT_FORWARDER value %q", envVar) + } else { + useDirectIPPortForwarding = b + } + } + var dialContext func(ctx context.Context, network string, guestAddress string) (net.Conn, error) + if useDirectIPPortForwarding { + logrus.Warn("Direct IP Port forwarding is enabled. It may fall back to GRPC Port Forwarding in some cases.") + dialContext = func(ctx context.Context, network, guestAddress string) (net.Conn, error) { + guestIPv4, guestIPv6 := a.GuestIPs() + if guestIPv4 == nil && guestIPv6 == nil { + return portfwd.DialContextToGRPCTunnel(client)(ctx, network, guestAddress) + } + // Check if the host part of guestAddress is either unspecified address or matches the known guest IP. + // If so, replace it with the known guest IP to avoid issues with dual-stack setups and DNS resolution. + // Otherwise, fall back to the gRPC tunnel. + if host, _, err := net.SplitHostPort(guestAddress); err != nil { + return nil, err + } else if ip := net.ParseIP(host); ip.IsUnspecified() || ip.Equal(guestIPv4) || ip.Equal(guestIPv6) { + if ip.To4() != nil { + if guestIPv4 != nil { + conn, err := DialContextToGuestIP(guestIPv4)(ctx, network, guestAddress) + if err == nil { + return conn, nil + } + logrus.WithError(err).Warn("failed to connect to the guest IPv4 directly, falling back to gRPC tunnel") + } + } else if ip.To16() != nil { + if guestIPv6 != nil { + conn, err := DialContextToGuestIP(guestIPv6)(ctx, network, guestAddress) + if err == nil { + return conn, nil + } + logrus.WithError(err).Warn("failed to connect to the guest IPv6 directly, falling back to gRPC tunnel") + } + } + // If we reach here, it means we couldn't find a suitable guest IP + } + return portfwd.DialContextToGRPCTunnel(client)(ctx, network, guestAddress) + } + } else { + dialContext = portfwd.DialContextToGRPCTunnel(client) + } a.grpcPortForwarder.OnEvent(ctx, dialContext, ev) } } @@ -850,6 +963,24 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestag return io.EOF } +// DialContextToGuestIP returns a DialContext function that connects to the guest IP directly. +// If the guest IP is not known, it returns nil. +func DialContextToGuestIP(guestIP net.IP) func(ctx context.Context, network, address string) (net.Conn, error) { + if guestIP == nil { + return nil + } + return func(ctx context.Context, network, address string) (net.Conn, error) { + var d net.Dialer + _, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + // Host part of address is ignored, because it already has been checked by forwarding rules + // and we want to connect to the guest IP directly. + return d.DialContext(ctx, network, net.JoinHostPort(guestIP.String(), port)) + } +} + const ( verbForward = "forward" verbCancel = "cancel" diff --git a/pkg/hostagent/requirements.go b/pkg/hostagent/requirements.go index 2873cebd786..20d27fef232 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -4,8 +4,11 @@ package hostagent import ( + "context" + "encoding/json" "errors" "fmt" + "net" "runtime" "strings" "time" @@ -122,20 +125,25 @@ func (a *HostAgent) waitForRequirement(r requirement) error { AdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs), } } - stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description) + sshAddress, sshPort := a.sshAddressPort() + stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, sshConfig, script, r.description) logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) if err != nil { return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err) } + if r.stdoutParser != nil { + return r.stdoutParser(stdout) + } return nil } type requirement struct { - description string - script string - debugHint string - fatal bool - noMaster bool + description string + script string + debugHint string + fatal bool + noMaster bool + stdoutParser func(string) error } func (a *HostAgent) essentialRequirements() []requirement { @@ -151,7 +159,27 @@ Make sure that the YAML field "ssh.localPort" is not used by other processes on If any private key under ~/.ssh is protected with a passphrase, you need to have ssh-agent to be running. `, noMaster: true, - }) + }, + ) + + if runtime.GOOS == "darwin" { + // Limit the Guest IP address detection only to macOS for now. + req = append(req, + requirement{ + description: "detect guest IP address", + script: `#!/bin/bash +ip -j addr +`, + debugHint: `Detecting the guest IP address on the interface in same subnet on the host. +This is only supported on macOS for now. +If the interface does not have IPv4 address, SSH connection against the guest OS will be made via the localhost port forwarding.`, + noMaster: true, + stdoutParser: a.detectGuestIPAddress, + }, + ) + } else { + logrus.Info("Skipping the guest IP address detection because it is only tested on macOS for now") + } startControlMasterReq := requirement{ description: "Explicitly start ssh ControlMaster", script: `#!/bin/bash @@ -280,3 +308,82 @@ Check "/var/log/cloud-init-output.log" in the guest to see where the process is }) return req } + +// detectGuestIPAddress detects the guest IP address on the interface in same subnet on the host +// by parsing the output of "ip -j addr" command in the guest. +func (a *HostAgent) detectGuestIPAddress(stdout string) error { + var guestIfs []struct { + IFNAME string `json:"ifname"` + ADDRS []struct { + Family string `json:"family"` + Local net.IP `json:"local"` + Scope string `json:"scope"` + } `json:"addr_info"` + } + if err := json.Unmarshal([]byte(stdout), &guestIfs); err != nil { + return fmt.Errorf("failed to parse ip addr output %q: %w", stdout, err) + } + var ( + guestIPv4 net.IP + guestIPv6 net.IP + ) + hostIfs, err := net.Interfaces() + if err != nil { + return fmt.Errorf("failed to get network interfaces: %w", err) + } + for _, hostIf := range hostIfs { + if hostIf.Flags&net.FlagUp == 0 { + continue + } + hostAddrs, err := hostIf.Addrs() + if err != nil { + return fmt.Errorf("failed to get addresses for interface %q: %w", hostIf.Name, err) + } + for _, hostAddr := range hostAddrs { + hostIPNet, ok := hostAddr.(*net.IPNet) + if !ok { + continue + } + for _, guestIf := range guestIfs { + if hostIPv4 := hostIPNet.IP.To4(); hostIPv4 != nil { + for _, guestAddr := range guestIf.ADDRS { + if guestAddr.Scope != "global" { + continue + } else if guestAddr.Family != "inet" { + continue + } else if hostIPNet.Contains(guestAddr.Local) { + guestIPv4 = guestAddr.Local + } + } + } else if hostIPv6 := hostIPNet.IP.To16(); hostIPv6 != nil { + for _, guestAddr := range guestIf.ADDRS { + if guestAddr.Scope != "global" { + continue + } else if guestAddr.Family != "inet6" { + continue + } else if hostIPNet.Contains(guestAddr.Local) { + guestIPv6 = guestAddr.Local + } + } + } + } + } + } + if guestIPv4 == nil && guestIPv6 == nil { + logrus.Infof("The guest IPv4/IPv6 address is not found") + return nil + } + if guestIPv4 != nil { + logrus.Infof("The guest IPv4 address is %q", guestIPv4) + } + if guestIPv6 != nil { + logrus.Infof("The guest IPv6 address is %q", guestIPv6) + } + a.guestIPMu.Lock() + a.guestIPv4 = guestIPv4 + a.guestIPv6 = guestIPv6 + a.guestIPMu.Unlock() + ctx := context.Background() + a.emitGuestIPEvent(ctx, a.GuestIP().String()) + return a.WriteSSHConfigFile(ctx) +} diff --git a/pkg/instance/start.go b/pkg/instance/start.go index 8e5c3806f81..83340f7903a 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -290,6 +290,7 @@ func watchHostAgentEvents(ctx context.Context, inst *limatype.Instance, haStdout var ( printedSSHLocalPort bool + printedGuestIP bool receivedRunningEvent bool cloudInitCompleted bool err error @@ -304,6 +305,14 @@ func watchHostAgentEvents(ctx context.Context, inst *limatype.Instance, haStdout inst.SSHLocalPort = ev.Status.SSHLocalPort } + if !printedGuestIP && ev.Status.GuestIP != nil { + logrus.Infof("Guest IP Address: %s", ev.Status.GuestIP.String()) + printedGuestIP = true + + // Update the instance's Guest IP address + inst.GuestIP = ev.Status.GuestIP + } + if showProgress && ev.Status.CloudInitProgress != nil { progress := ev.Status.CloudInitProgress if progress.Active && progress.LogLine == "" { diff --git a/pkg/limatype/lima_instance.go b/pkg/limatype/lima_instance.go index fada2f34e98..0702ac6e9d9 100644 --- a/pkg/limatype/lima_instance.go +++ b/pkg/limatype/lima_instance.go @@ -6,6 +6,7 @@ package limatype import ( "encoding/json" "errors" + "net" "os" "path/filepath" @@ -48,6 +49,8 @@ type Instance struct { LimaVersion string `json:"limaVersion"` Param map[string]string `json:"param,omitempty"` AutoStartedIdentifier string `json:"autoStartedIdentifier,omitempty"` + // Guest IP address directly accessible from the host. + GuestIP net.IP `json:"guestIP,omitempty"` } // Protect protects the instance to prohibit accidental removal. @@ -108,3 +111,13 @@ func (inst *Instance) UnmarshalJSON(data []byte) error { } return nil } + +func (inst *Instance) SSHAddressPort() (sshAddress string, sshPort int) { + sshAddress = inst.SSHAddress + sshPort = inst.SSHLocalPort + if inst.GuestIP != nil { + sshAddress = inst.GuestIP.String() + sshPort = 22 + } + return sshAddress, sshPort +} diff --git a/pkg/store/instance.go b/pkg/store/instance.go index 54e7a9c1f3e..91a248c3ab1 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -82,6 +82,7 @@ func Inspect(ctx context.Context, instName string) (*limatype.Instance, error) { inst.Status = limatype.StatusBroken inst.Errors = append(inst.Errors, fmt.Errorf("failed to get Info from %q: %w", haSock, err)) } else { + inst.GuestIP = info.GuestIP inst.SSHLocalPort = info.SSHLocalPort inst.AutoStartedIdentifier = info.AutoStartedIdentifier } @@ -347,10 +348,11 @@ func PrintInstances(w io.Writer, instances []*limatype.Instance, format string, if strings.HasPrefix(dir, homeDir) { dir = strings.Replace(dir, homeDir, "~", 1) } + sshAddress, sshPort := instance.SSHAddressPort() fmt.Fprintf(w, "%s\t%s\t%s", instance.Name, instance.Status, - fmt.Sprintf("%s:%d", instance.SSHAddress, instance.SSHLocalPort), + fmt.Sprintf("%s:%d", sshAddress, sshPort), ) if !hideType { fmt.Fprintf(w, "\t%s", diff --git a/website/content/en/docs/config/environment-variables.md b/website/content/en/docs/config/environment-variables.md index 7881d5eddf4..6d13c5c8c0d 100644 --- a/website/content/en/docs/config/environment-variables.md +++ b/website/content/en/docs/config/environment-variables.md @@ -140,6 +140,19 @@ This page documents the environment variables used in Lima. export LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT=5 ``` +### `_LIMA_DIRECT_IP_PORT_FORWARDER` + +- **Description**: Specifies to use direct IP port forwarding instead of gRPC. +- **Default**: `false` +- **Usage**: + ```sh + export _LIMA_DIRECT_IP_PORT_FORWARDER=true + ``` +- **Note**: Direct IP port forwarding may fall back to gRPC port forwarding in some cases: + - If the guest's IP address is not available, Lima falls back to gRPC port forwarding. + - If the guest's IP address is available but the connection to the guest's IP address fails, Lima also falls back to gRPC port forwarding. + - If the guest's IP address is not accessible from the host (e.g. localhost on guest), Lima falls back to gRPC port forwarding as well. + ### `_LIMA_QEMU_UEFI_IN_BIOS` - **Description**: Commands QEMU to load x86_64 UEFI images using `-bios` instead of `pflash` drives. diff --git a/website/content/en/docs/config/port.md b/website/content/en/docs/config/port.md index 05d538cdb56..082f4d42956 100644 --- a/website/content/en/docs/config/port.md +++ b/website/content/en/docs/config/port.md @@ -88,6 +88,22 @@ lima ip addr show lima0 See [Config » Network » VMNet networks](./network/vmnet.md) for the further information. +### Direct IP Port Forwarding + +To enable direct IP port forwarding, set the `_LIMA_DIRECT_IP_PORT_FORWARDER` environment variable to `true`: + +```bash +export _LIMA_DIRECT_IP_PORT_FORWARDER=true +``` +This feature makes Lima to use direct IP port forwarding instead of gRPC port forwarding. +When this feature is enabled, Lima tries to connect to the guest's IP address directly for port forwarding. + +#### Fallback to gRPC Port Forwarding +Lima may fall back to gRPC port forwarding in the following cases: +- If the guest's IP address is not available, Lima falls back to gRPC port forwarding. +- If the guest's IP address is available but the connection to the guest's IP address fails, Lima also falls back to gRPC port forwarding. +- If the guest's IP address is not accessible from the host (e.g. localhost on guest), Lima falls back to gRPC port forwarding as well. + ## Benchmarks