From 6f49c8e1b43b01bc94d8d89e991352916dc40ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 15 Nov 2023 22:58:52 +0100 Subject: [PATCH 01/15] Add SSH address for supplying external IP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- cmd/limactl/show-ssh.go | 2 +- pkg/hostagent/hostagent.go | 7 +++++-- pkg/limayaml/defaults.go | 10 ++++++++++ pkg/limayaml/defaults_test.go | 3 +++ pkg/limayaml/limayaml.go | 3 ++- pkg/limayaml/validate.go | 12 ++++++++++++ pkg/store/instance.go | 2 +- templates/default.yaml | 3 +++ 8 files changed, 37 insertions(+), 5 deletions(-) diff --git a/cmd/limactl/show-ssh.go b/cmd/limactl/show-ssh.go index b5103756c74..70fc00fbd97 100644 --- a/cmd/limactl/show-ssh.go +++ b/cmd/limactl/show-ssh.go @@ -102,7 +102,7 @@ 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("Hostname=%s", inst.SSHAddress)) opts = append(opts, fmt.Sprintf("Port=%d", inst.SSHLocalPort)) return sshutil.Format(w, "ssh", instName, format, opts) } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index f48f8dd86b8..9f795c85e2f 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -103,7 +103,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt } // inst.Config is loaded with FillDefault() already, so no need to care about nil pointers. - sshLocalPort, err := determineSSHLocalPort(*inst.Config.SSH.LocalPort, instName) + sshLocalPort, err := determineSSHLocalPort(*inst.Config.SSH.Address, *inst.Config.SSH.LocalPort, instName) if err != nil { return nil, err } @@ -232,13 +232,16 @@ func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLo return os.WriteFile(fileName, b.Bytes(), 0o600) } -func determineSSHLocalPort(confLocalPort int, instName string) (int, error) { +func determineSSHLocalPort(confSSHAddress string, confLocalPort int, instName string) (int, error) { if confLocalPort > 0 { return confLocalPort, nil } if confLocalPort < 0 { return 0, fmt.Errorf("invalid ssh local port %d", confLocalPort) } + if confLocalPort == 0 && confSSHAddress != "127.0.0.1" { + return 22, nil + } if instName == "default" { // use hard-coded value for "default" instance, for backward compatibility return 60022, nil diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index f6782d39c03..a26cb417bea 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -419,6 +419,16 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) { y.TimeZone = ptr.Of(hostTimeZone()) } + if y.SSH.Address == nil { + y.SSH.Address = d.SSH.Address + } + if o.SSH.Address != nil { + y.SSH.Address = o.SSH.Address + } + if y.SSH.Address == nil { + y.SSH.Address = ptr.Of("127.0.0.1") + } + if y.SSH.LocalPort == nil { y.SSH.LocalPort = d.SSH.LocalPort } diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index b8f203838dd..ddc015f1806 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -87,6 +87,7 @@ func TestFillDefault(t *testing.T) { Archives: defaultContainerdArchives(), }, SSH: SSH{ + Address: ptr.Of("127.0.0.1"), LocalPort: ptr.Of(0), LoadDotSSHPubKeys: ptr.Of(false), ForwardAgent: ptr.Of(false), @@ -359,6 +360,7 @@ func TestFillDefault(t *testing.T) { }, }, SSH: SSH{ + Address: ptr.Of("0.0.0.0"), LocalPort: ptr.Of(888), LoadDotSSHPubKeys: ptr.Of(false), ForwardAgent: ptr.Of(true), @@ -582,6 +584,7 @@ func TestFillDefault(t *testing.T) { }, }, SSH: SSH{ + Address: ptr.Of("127.0.1.1"), LocalPort: ptr.Of(4433), LoadDotSSHPubKeys: ptr.Of(true), ForwardAgent: ptr.Of(true), diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index b954063c06c..48b695f44e2 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -180,7 +180,8 @@ type Virtiofs struct { } type SSH struct { - LocalPort *int `yaml:"localPort,omitempty" json:"localPort,omitempty" jsonschema:"nullable"` + Address *string `yaml:"address,omitempty" json:"address,omitempty" jsonschema:"nullable"` + LocalPort *int `yaml:"localPort,omitempty" json:"localPort,omitempty" jsonschema:"nullable"` // LoadDotSSHPubKeys loads ~/.ssh/*.pub in addition to $LIMA_HOME/_config/user.pub . LoadDotSSHPubKeys *bool `yaml:"loadDotSSHPubKeys,omitempty" json:"loadDotSSHPubKeys,omitempty" jsonschema:"nullable"` // default: false diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index b5ee24c87d9..86963101424 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -187,6 +187,11 @@ func Validate(y *LimaYAML, warn bool) error { } } + if y.SSH.Address != nil { + if err := validateHost("ssh.address", *y.SSH.Address); err != nil { + return err + } + } if *y.SSH.LocalPort != 0 { if err := validatePort("ssh.localPort", *y.SSH.LocalPort); err != nil { return err @@ -559,6 +564,13 @@ func ValidateParamIsUsed(y *LimaYAML) error { return nil } +func validateHost(field, host string) error { + if net.ParseIP(host) == nil { + return fmt.Errorf("field `%s` must be IP", field) + } + return nil +} + func validatePort(field string, port int) error { switch { case port < 0: diff --git a/pkg/store/instance.go b/pkg/store/instance.go index 2af8f814f93..d82d42a79cc 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -96,7 +96,7 @@ func Inspect(instName string) (*Instance, error) { inst.Arch = *y.Arch inst.VMType = *y.VMType inst.CPUType = y.CPUType[*y.Arch] - inst.SSHAddress = "127.0.0.1" + inst.SSHAddress = *y.SSH.Address inst.SSHLocalPort = *y.SSH.LocalPort // maybe 0 inst.SSHConfigFile = filepath.Join(instDir, filenames.SSHConfig) inst.HostAgentPID, err = ReadPIDFile(filepath.Join(instDir, filenames.HostAgentPID)) diff --git a/templates/default.yaml b/templates/default.yaml index eac787d7d77..dc03934dc30 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -147,6 +147,9 @@ additionalDisks: # fsType: "ext4" ssh: + # Address for the host. + # 🟢 Builtin default: "127.0.0.1" (localhost) + address: null # A localhost port of the host. Forwarded to port 22 of the guest. # 🟢 Builtin default: 0 (automatically assigned to a free port) # NOTE: when the instance name is "default", the builtin default value is set to From 898f6be17399dbb7a4f4fcb83c49fcc12729b2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 22 Nov 2023 21:47:55 +0100 Subject: [PATCH 02/15] Use SSH address also for copy command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- cmd/limactl/copy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go index 8b4a138f9f5..c3dcc94a1ba 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -111,9 +111,9 @@ func copyAction(cmd *cobra.Command, args []string) error { } if legacySSH { scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort)) - scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", *inst.Config.User.Name, path[1])) + scpArgs = append(scpArgs, fmt.Sprintf("%s@%s:%s", *inst.Config.User.Name, inst.SSHAddress, path[1])) } else { - scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", *inst.Config.User.Name, inst.SSHLocalPort, path[1])) + scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@%s:%d/%s", *inst.Config.User.Name, inst.SSHAddress, inst.SSHLocalPort, path[1])) } instances[instName] = inst default: From 6a455f83bf9666f3ed3b3b02ff2e87f903c39641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Fri, 24 Nov 2023 08:25:42 +0100 Subject: [PATCH 03/15] Use SSH address also for host agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/hostagent/hostagent.go | 24 ++++++++++++------------ pkg/hostagent/mount.go | 2 +- pkg/hostagent/port.go | 8 +++++--- pkg/hostagent/port_darwin.go | 12 ++++++------ pkg/hostagent/port_others.go | 4 ++-- pkg/hostagent/port_windows.go | 4 ++-- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 9f795c85e2f..d63d055b3a4 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -198,7 +198,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt instName: instName, instSSHAddress: inst.SSHAddress, sshConfig: sshConfig, - portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, ignoreTCP, inst.VMType), + portForwarder: newPortForwarder(sshConfig, inst.SSHAddress, sshLocalPort, rules, ignoreTCP, inst.VMType), grpcPortForwarder: portfwd.NewPortForwarder(rules, ignoreTCP, ignoreUDP), driver: limaDriver, signalCh: signalCh, @@ -536,7 +536,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { for _, rule := range a.instConfig.PortForwards { if rule.GuestSocket != "" { local := hostAddress(rule, &guestagentapi.IPPort{}) - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbForward, rule.Reverse) + _ = forwardSSH(ctx, a.sshConfig, a.instSSHAddress, a.sshLocalPort, local, rule.GuestSocket, verbForward, rule.Reverse) } } } @@ -551,13 +551,13 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { if rule.GuestSocket != "" { local := hostAddress(rule, &guestagentapi.IPPort{}) // using ctx.Background() because ctx has already been cancelled - if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil { + if err := forwardSSH(context.Background(), a.sshConfig, a.instSSHAddress, a.sshLocalPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil { errs = append(errs, err) } } } if a.driver.ForwardGuestAgent() { - if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbCancel, false); err != nil { + if err := forwardSSH(context.Background(), a.sshConfig, a.instSSHAddress, a.sshLocalPort, localUnix, remoteUnix, verbCancel, false); err != nil { errs = append(errs, err) } } @@ -581,7 +581,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { for { if a.client == nil || !isGuestAgentSocketAccessible(ctx, a.client) { if a.driver.ForwardGuestAgent() { - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) + _ = forwardSSH(ctx, a.sshConfig, a.instSSHAddress, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) } } client, err := a.getOrCreateClient(ctx) @@ -683,11 +683,11 @@ const ( verbCancel = "cancel" ) -func executeSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, command ...string) error { +func executeSSH(ctx context.Context, sshConfig *ssh.SSHConfig, addr string, port int, command ...string) error { args := sshConfig.Args() args = append(args, "-p", strconv.Itoa(port), - "127.0.0.1", + addr, "--", ) args = append(args, command...) @@ -698,7 +698,7 @@ func executeSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, command return nil } -func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string, reverse bool) error { +func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, addr string, port int, local, remote, verb string, reverse bool) error { args := sshConfig.Args() args = append(args, "-T", @@ -717,7 +717,7 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, "-N", "-f", "-p", strconv.Itoa(port), - "127.0.0.1", + addr, "--", ) if strings.HasPrefix(local, "/") { @@ -725,7 +725,7 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, case verbForward: if reverse { logrus.Infof("Forwarding %q (host) to %q (guest)", local, remote) - if err := executeSSH(ctx, sshConfig, port, "rm", "-f", remote); err != nil { + if err := executeSSH(ctx, sshConfig, addr, port, "rm", "-f", remote); err != nil { logrus.WithError(err).Warnf("Failed to clean up %q (guest) before setting up forwarding", remote) } } else { @@ -740,7 +740,7 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, case verbCancel: if reverse { logrus.Infof("Stopping forwarding %q (host) to %q (guest)", local, remote) - if err := executeSSH(ctx, sshConfig, port, "rm", "-f", remote); err != nil { + if err := executeSSH(ctx, sshConfig, addr, port, "rm", "-f", remote); err != nil { logrus.WithError(err).Warnf("Failed to clean up %q (guest) after stopping forwarding", remote) } } else { @@ -760,7 +760,7 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, if verb == verbForward && strings.HasPrefix(local, "/") { if reverse { logrus.WithError(err).Warnf("Failed to set up forward from %q (host) to %q (guest)", local, remote) - if err := executeSSH(ctx, sshConfig, port, "rm", "-f", remote); err != nil { + if err := executeSSH(ctx, sshConfig, addr, port, "rm", "-f", remote); err != nil { logrus.WithError(err).Warnf("Failed to clean up %q (guest) after forwarding failed", remote) } } else { diff --git a/pkg/hostagent/mount.go b/pkg/hostagent/mount.go index 877d759fd0f..491dd5fe9fc 100644 --- a/pkg/hostagent/mount.go +++ b/pkg/hostagent/mount.go @@ -62,7 +62,7 @@ func (a *HostAgent) setupMount(m limayaml.Mount) (*mount, error) { Driver: *m.SSHFS.SFTPDriver, SSHConfig: a.sshConfig, LocalPath: resolvedLocation, - Host: "127.0.0.1", + Host: a.instSSHAddress, Port: a.sshLocalPort, RemotePath: *m.MountPoint, Readonly: !(*m.Writable), diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go index 040d7594092..7da5abe7df7 100644 --- a/pkg/hostagent/port.go +++ b/pkg/hostagent/port.go @@ -15,6 +15,7 @@ import ( type portForwarder struct { sshConfig *ssh.SSHConfig + sshHostAddr string sshHostPort int rules []limayaml.PortForward ignore bool @@ -25,9 +26,10 @@ const sshGuestPort = 22 var IPv4loopback1 = limayaml.IPv4loopback1 -func newPortForwarder(sshConfig *ssh.SSHConfig, sshHostPort int, rules []limayaml.PortForward, ignore bool, vmType limayaml.VMType) *portForwarder { +func newPortForwarder(sshConfig *ssh.SSHConfig, sshHostAddr string, sshHostPort int, rules []limayaml.PortForward, ignore bool, vmType limayaml.VMType) *portForwarder { return &portForwarder{ sshConfig: sshConfig, + sshHostAddr: sshHostAddr, sshHostPort: sshHostPort, rules: rules, ignore: ignore, @@ -94,7 +96,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev *api.Event) { continue } logrus.Infof("Stopping forwarding TCP from %s to %s", remote, local) - if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostPort, local, remote, verbCancel); err != nil { + if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostAddr, pf.sshHostPort, local, remote, verbCancel); err != nil { logrus.WithError(err).Warnf("failed to stop forwarding tcp port %d", f.Port) } } @@ -110,7 +112,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev *api.Event) { continue } logrus.Infof("Forwarding TCP from %s to %s", remote, local) - if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostPort, local, remote, verbForward); err != nil { + if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostAddr, pf.sshHostPort, local, remote, verbForward); err != nil { logrus.WithError(err).Warnf("failed to set up forwarding tcp port %d (negligible if already forwarded)", f.Port) } } diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go index 5ac63b41388..98dae80fae5 100644 --- a/pkg/hostagent/port_darwin.go +++ b/pkg/hostagent/port_darwin.go @@ -19,9 +19,9 @@ import ( ) // forwardTCP is not thread-safe. -func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error { +func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, addr string, port int, local, remote, verb string) error { if strings.HasPrefix(local, "/") { - return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) + return forwardSSH(ctx, sshConfig, addr, port, local, remote, verb, false) } localIPStr, localPortStr, err := net.SplitHostPort(local) if err != nil { @@ -34,7 +34,7 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, } if !localIP.Equal(IPv4loopback1) || localPort >= 1024 { - return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) + return forwardSSH(ctx, sshConfig, addr, port, local, remote, verb, false) } // on macOS, listening on 127.0.0.1:80 requires root while 0.0.0.0:80 does not require root. @@ -49,7 +49,7 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, localUnix := plf.unixAddr.Name _ = plf.Close() delete(pseudoLoopbackForwarders, local) - if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, verb, false); err != nil { + if err := forwardSSH(ctx, sshConfig, addr, port, localUnix, remote, verb, false); err != nil { return err } } else { @@ -64,12 +64,12 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, } localUnix := filepath.Join(localUnixDir, "sock") logrus.Debugf("forwarding %q to %q", localUnix, remote) - if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, verb, false); err != nil { + if err := forwardSSH(ctx, sshConfig, addr, port, localUnix, remote, verb, false); err != nil { return err } plf, err := newPseudoLoopbackForwarder(localPort, localUnix) if err != nil { - if cancelErr := forwardSSH(ctx, sshConfig, port, localUnix, remote, verbCancel, false); cancelErr != nil { + if cancelErr := forwardSSH(ctx, sshConfig, addr, port, localUnix, remote, verbCancel, false); cancelErr != nil { logrus.WithError(cancelErr).Warnf("failed to cancel forwarding %q to %q", localUnix, remote) } return err diff --git a/pkg/hostagent/port_others.go b/pkg/hostagent/port_others.go index 8d218c25b35..a8fd5c4fdb7 100644 --- a/pkg/hostagent/port_others.go +++ b/pkg/hostagent/port_others.go @@ -11,6 +11,6 @@ import ( "github.com/lima-vm/sshocker/pkg/ssh" ) -func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error { - return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) +func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, addr string, port int, local, remote, verb string) error { + return forwardSSH(ctx, sshConfig, addr, port, local, remote, verb, false) } diff --git a/pkg/hostagent/port_windows.go b/pkg/hostagent/port_windows.go index d8d19f0cbd1..fcd7dd60413 100644 --- a/pkg/hostagent/port_windows.go +++ b/pkg/hostagent/port_windows.go @@ -9,6 +9,6 @@ import ( "github.com/lima-vm/sshocker/pkg/ssh" ) -func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote, verb string) error { - return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) +func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, addr string, port int, local, remote, verb string) error { + return forwardSSH(ctx, sshConfig, addr, port, local, remote, verb, false) } From f9fda1c5ffe22d382d6be5316978ce2b43eb86b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 29 Nov 2023 11:44:16 +0100 Subject: [PATCH 04/15] Use host key checking outside localhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify ssh host keys, when connecting to a remote server. The first connection will prompt, if not in known_hosts. Signed-off-by: Anders F Björklund --- cmd/limactl/copy.go | 8 ++++++-- cmd/limactl/shell.go | 1 + cmd/limactl/show-ssh.go | 1 + cmd/limactl/tunnel.go | 1 + pkg/hostagent/hostagent.go | 3 ++- pkg/sshutil/sshutil.go | 18 ++++++++++++------ 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go index c3dcc94a1ba..9fcf57baba7 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -82,6 +82,7 @@ func copyAction(cmd *cobra.Command, args []string) error { } // this assumes that ssh and scp come from the same place, but scp has no -V legacySSH := sshutil.DetectOpenSSHVersion("ssh").LessThan(*semver.New("8.0.0")) + localhostOnly := true for _, arg := range args { if runtime.GOOS == "windows" { if filepath.IsAbs(arg) { @@ -115,6 +116,9 @@ func copyAction(cmd *cobra.Command, args []string) error { } else { scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@%s:%d/%s", *inst.Config.User.Name, inst.SSHAddress, inst.SSHLocalPort, path[1])) } + if inst.SSHAddress != "127.0.0.1" { + localhostOnly = false + } instances[instName] = inst default: return fmt.Errorf("path %q contains multiple colons", arg) @@ -132,14 +136,14 @@ func copyAction(cmd *cobra.Command, args []string) error { // arguments such as ControlPath. This is preferred as we can multiplex // sessions without re-authenticating (MaxSessions permitting). for _, inst := range instances { - sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false) + sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, inst.SSHAddress, false, false, false) if err != nil { return err } } } else { // Copying among multiple hosts; we can't pass in host-specific options. - sshOpts, err = sshutil.CommonOpts("ssh", false) + sshOpts, err = sshutil.CommonOpts("ssh", false, localhostOnly) if err != nil { return err } diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index 3aba88208e3..82220dd8c70 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -205,6 +205,7 @@ func shellAction(cmd *cobra.Command, args []string) error { inst.Dir, *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, + *inst.Config.SSH.Address, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, *inst.Config.SSH.ForwardX11Trusted) diff --git a/cmd/limactl/show-ssh.go b/cmd/limactl/show-ssh.go index 70fc00fbd97..0ee1a054e0a 100644 --- a/cmd/limactl/show-ssh.go +++ b/cmd/limactl/show-ssh.go @@ -96,6 +96,7 @@ func showSSHAction(cmd *cobra.Command, args []string) error { inst.Dir, *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, + *inst.Config.SSH.Address, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, *inst.Config.SSH.ForwardX11Trusted) diff --git a/cmd/limactl/tunnel.go b/cmd/limactl/tunnel.go index 9e69028186b..06a04665361 100644 --- a/cmd/limactl/tunnel.go +++ b/cmd/limactl/tunnel.go @@ -91,6 +91,7 @@ func tunnelAction(cmd *cobra.Command, args []string) error { inst.Dir, *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, + *inst.Config.SSH.Address, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, *inst.Config.SSH.ForwardX11Trusted) diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index d63d055b3a4..254b923ea74 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -143,6 +143,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt inst.Dir, *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, + *inst.Config.SSH.Address, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, *inst.Config.SSH.ForwardX11Trusted) @@ -568,7 +569,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { if a.instConfig.MountInotify != nil && *a.instConfig.MountInotify { if a.client == nil || !isGuestAgentSocketAccessible(ctx, a.client) { if a.driver.ForwardGuestAgent() { - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) + _ = forwardSSH(ctx, a.sshConfig, a.instSSHAddress, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) } } err := a.startInotify(ctx) diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go index 1d8a5f15ce6..aede7c1a651 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -167,7 +167,7 @@ var sshInfo struct { // // The result always contains the IdentityFile option. // The result never contains the Port option. -func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) { +func CommonOpts(sshPath string, useDotSSH, localhost bool) ([]string, error) { configDir, err := dirnames.LimaConfigDir() if err != nil { return nil, err @@ -221,14 +221,20 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) { } } + if localhost { + opts = append(opts, + "StrictHostKeyChecking=no", + "UserKnownHostsFile=/dev/null", + "BatchMode=yes", + ) + } + opts = append(opts, - "StrictHostKeyChecking=no", - "UserKnownHostsFile=/dev/null", "NoHostAuthenticationForLocalhost=yes", "GSSAPIAuthentication=no", "PreferredAuthentications=publickey", "Compression=no", - "BatchMode=yes", + "PasswordAuthentication=no", "IdentitiesOnly=yes", ) @@ -274,12 +280,12 @@ func identityFileEntry(privateKeyPath string) (string, error) { } // SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist. -func SSHOpts(sshPath, instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) { +func SSHOpts(sshPath, instDir, username string, useDotSSH bool, hostAddress string, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) { controlSock := filepath.Join(instDir, filenames.SSHSock) if len(controlSock) >= osutil.UnixPathMax { return nil, fmt.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax) } - opts, err := CommonOpts(sshPath, useDotSSH) + opts, err := CommonOpts(sshPath, useDotSSH, hostAddress == "127.0.0.1") if err != nil { return nil, err } From 4bfd8159eac3bef1929fcc15cd4fc5d40672291d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Tue, 9 Jan 2024 20:10:54 +0100 Subject: [PATCH 05/15] Print IP instead of Port for non-local SSH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/hostagent/events/events.go | 3 ++- pkg/hostagent/hostagent.go | 13 +++++++++++++ pkg/instance/start.go | 8 +++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/hostagent/events/events.go b/pkg/hostagent/events/events.go index 4752d980852..114e06fe2de 100644 --- a/pkg/hostagent/events/events.go +++ b/pkg/hostagent/events/events.go @@ -16,7 +16,8 @@ type Status struct { Errors []string `json:"errors,omitempty"` - SSHLocalPort int `json:"sshLocalPort,omitempty"` + SSHIPAddress string `json:"sshIPAddress,omitempty"` + SSHLocalPort int `json:"sshLocalPort,omitempty"` } type Event struct { diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 254b923ea74..302db4f4b68 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -372,8 +372,21 @@ func (a *HostAgent) Run(ctx context.Context) error { return a.startRoutinesAndWait(ctx, errCh) } +func getIP(address string) string { + ip := net.ParseIP(address) + if ip != nil { + return address + } + ips, err := net.LookupIP(address) + if err == nil && len(ips) > 0 { + return ips[0].String() + } + return address +} + func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error) error { stBase := events.Status{ + SSHIPAddress: getIP(a.instSSHAddress), SSHLocalPort: a.sshLocalPort, } stBooting := stBase diff --git a/pkg/instance/start.go b/pkg/instance/start.go index b53de08183a..770fc6feb18 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -288,7 +288,13 @@ func watchHostAgentEvents(ctx context.Context, inst *store.Instance, haStdoutPat ) onEvent := func(ev hostagentevents.Event) bool { if !printedSSHLocalPort && ev.Status.SSHLocalPort != 0 { - logrus.Infof("SSH Local Port: %d", ev.Status.SSHLocalPort) + if ev.Status.SSHIPAddress == "127.0.0.1" { + logrus.Infof("SSH Local Port: %d", ev.Status.SSHLocalPort) + } else if ev.Status.SSHLocalPort == 22 { + logrus.Infof("SSH IP Address: %s", ev.Status.SSHIPAddress) + } else { + logrus.Infof("SSH IP Address: %s Port: %d", ev.Status.SSHIPAddress, ev.Status.SSHLocalPort) + } printedSSHLocalPort = true } From b1ed5fcbbd37942b27b9548a5d35e051a1914fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 15 Nov 2023 22:59:31 +0100 Subject: [PATCH 06/15] Add ext driver for external VM machines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/driverutil/driverutil.go | 1 + pkg/driverutil/instance.go | 4 ++++ pkg/ext/ext_driver.go | 18 ++++++++++++++++++ pkg/limayaml/defaults.go | 2 ++ pkg/limayaml/limayaml.go | 3 ++- pkg/limayaml/validate.go | 9 +++++++-- pkg/store/instance.go | 8 ++++++++ 7 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 pkg/ext/ext_driver.go diff --git a/pkg/driverutil/driverutil.go b/pkg/driverutil/driverutil.go index eb27833e7ad..7cb8c34f686 100644 --- a/pkg/driverutil/driverutil.go +++ b/pkg/driverutil/driverutil.go @@ -18,5 +18,6 @@ func Drivers() []string { if wsl2.Enabled { drivers = append(drivers, limayaml.WSL2) } + drivers = append(drivers, limayaml.EXT) return drivers } diff --git a/pkg/driverutil/instance.go b/pkg/driverutil/instance.go index d7c443ff5d2..b35c6fe1bff 100644 --- a/pkg/driverutil/instance.go +++ b/pkg/driverutil/instance.go @@ -5,6 +5,7 @@ package driverutil import ( "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/ext" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/qemu" "github.com/lima-vm/lima/pkg/vz" @@ -19,5 +20,8 @@ func CreateTargetDriverInstance(base *driver.BaseDriver) driver.Driver { if *limaDriver == limayaml.WSL2 { return wsl2.New(base) } + if *limaDriver == limayaml.EXT { + return ext.New(base) + } return qemu.New(base) } diff --git a/pkg/ext/ext_driver.go b/pkg/ext/ext_driver.go new file mode 100644 index 00000000000..49dc779f3b9 --- /dev/null +++ b/pkg/ext/ext_driver.go @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package ext + +import ( + "github.com/lima-vm/lima/pkg/driver" +) + +type LimaExtDriver struct { + *driver.BaseDriver +} + +func New(driver *driver.BaseDriver) *LimaExtDriver { + return &LimaExtDriver{ + BaseDriver: driver, + } +} diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index a26cb417bea..c6753068004 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -1109,6 +1109,8 @@ func NewVMType(driver string) VMType { return QEMU case "wsl2": return WSL2 + case "ext": + return EXT default: logrus.Warnf("Unknown driver: %s", driver) return driver diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index 48b695f44e2..ff18285ca2c 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -87,13 +87,14 @@ const ( QEMU VMType = "qemu" VZ VMType = "vz" WSL2 VMType = "wsl2" + EXT VMType = "ext" ) var ( OSTypes = []OS{LINUX} ArchTypes = []Arch{X8664, AARCH64, ARMV7L, RISCV64, S390X} MountTypes = []MountType{REVSSHFS, NINEP, VIRTIOFS, WSLMount} - VMTypes = []VMType{QEMU, VZ, WSL2} + VMTypes = []VMType{QEMU, VZ, WSL2, EXT} ) type User struct { diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 86963101424..05a30a73e99 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -92,11 +92,13 @@ func Validate(y *LimaYAML, warn bool) error { if !IsNativeArch(*y.Arch) { return fmt.Errorf("field `arch` must be %q for VZ; got %q", NewArch(runtime.GOARCH), *y.Arch) } + case EXT: + // NOP default: - return fmt.Errorf("field `vmType` must be %q, %q, %q; got %q", QEMU, VZ, WSL2, *y.VMType) + return fmt.Errorf("field `vmType` must be %q, %q, %q, %q; got %q", QEMU, VZ, WSL2, EXT, *y.VMType) } - if len(y.Images) == 0 { + if len(y.Images) == 0 && *y.VMType != EXT { return errors.New("field `images` must be set") } for i, f := range y.Images { @@ -187,6 +189,9 @@ func Validate(y *LimaYAML, warn bool) error { } } + if *y.SSH.Address == "127.0.0.1" && *y.VMType == EXT { + return errors.New("field `ssh.address` must be set, for ext") + } if y.SSH.Address != nil { if err := validateHost("ssh.address", *y.SSH.Address); err != nil { return err diff --git a/pkg/store/instance.go b/pkg/store/instance.go index d82d42a79cc..abb1f0e40a7 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -191,6 +191,14 @@ func inspectStatusWithPIDFiles(instDir string, inst *Instance, y *limayaml.LimaY inst.Status = StatusBroken inst.Errors = append(inst.Errors, err) } + if *y.VMType == limayaml.EXT { + if inst.HostAgentPID > 0 { + inst.Status = StatusRunning + } else if inst.HostAgentPID == 0 { + inst.Status = StatusStopped + } + return + } if inst.Status == StatusUnknown { switch { From 2e079a20d049525a38804f0e228de66883b33e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 22 Nov 2023 20:31:40 +0100 Subject: [PATCH 07/15] Allow using address like raspberrypi.local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/limayaml/validate.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 05a30a73e99..7526d33171b 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -570,7 +570,10 @@ func ValidateParamIsUsed(y *LimaYAML) error { } func validateHost(field, host string) error { - if net.ParseIP(host) == nil { + if net.ParseIP(host) != nil { + return nil + } + if _, err := net.LookupIP(host); err != nil { return fmt.Errorf("field `%s` must be IP", field) } return nil From ac6b13cd5ffd70ba543162bb4b3c799c387c8be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 29 Nov 2023 10:04:21 +0100 Subject: [PATCH 08/15] Add a shorter timeout for mDNS IP lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/limayaml/validate.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 7526d33171b..e071aee3f7c 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -4,6 +4,7 @@ package limayaml import ( + "context" "errors" "fmt" "net" @@ -16,6 +17,7 @@ import ( "strconv" "strings" "unicode" + "time" "github.com/containerd/containerd/identifiers" "github.com/coreos/go-semver/semver" @@ -569,12 +571,26 @@ func ValidateParamIsUsed(y *LimaYAML) error { return nil } +func lookupIP(host string) error { + var err error + if strings.HasSuffix(host, ".local") { + var r net.Resolver + const timeout = 500 * time.Millisecond // timeout for .local + ctx, cancel := context.WithTimeout(context.TODO(), timeout) + defer cancel() + _, err = r.LookupIP(ctx, "ip", host) + } else { + _, err = net.LookupIP(host) + } + return err +} + func validateHost(field, host string) error { if net.ParseIP(host) != nil { return nil } - if _, err := net.LookupIP(host); err != nil { - return fmt.Errorf("field `%s` must be IP", field) + if err := lookupIP(host); err != nil { + return fmt.Errorf("field `%s` must be IP: %w", field, err) } return nil } From 7c4c709a48a9cdeaf44a028bb366de62eb4f5cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 29 Nov 2023 10:45:26 +0100 Subject: [PATCH 09/15] Don't wait for bootscripts for external MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/hostagent/requirements.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg/hostagent/requirements.go b/pkg/hostagent/requirements.go index 7d8f24f3549..581ae65405c 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -131,22 +131,24 @@ If any private key under ~/.ssh is protected with a passphrase, you need to have if *a.instConfig.Plain { return req } - req = append(req, - requirement{ - description: "user session is ready for ssh", - script: `#!/bin/bash + if *a.y.VMType != limayaml.EXT { + req = append(req, + requirement{ + description: "user session is ready for ssh", + script: `#!/bin/bash set -eux -o pipefail if ! timeout 30s bash -c "until sudo diff -q /run/lima-ssh-ready /mnt/lima-cidata/meta-data 2>/dev/null; do sleep 3; done"; then echo >&2 "not ready to start persistent ssh session" exit 1 fi `, - debugHint: `The boot sequence will terminate any existing user session after updating + debugHint: `The boot sequence will terminate any existing user session after updating /etc/environment to make sure the session includes the new values. Terminating the session will break the persistent SSH tunnel, so it must not be created until the session reset is done. `, - }) + }) + } if *a.instConfig.MountType == limayaml.REVSSHFS && len(a.instConfig.Mounts) > 0 { req = append(req, requirement{ @@ -228,20 +230,22 @@ Also see "/var/log/cloud-init-output.log" in the guest. func (a *HostAgent) finalRequirements() []requirement { req := make([]requirement, 0) - req = append(req, - requirement{ - description: "boot scripts must have finished", - script: `#!/bin/bash + if *a.y.VMType != limayaml.EXT { + req = append(req, + requirement{ + description: "boot scripts must have finished", + script: `#!/bin/bash set -eux -o pipefail if ! timeout 30s bash -c "until sudo diff -q /run/lima-boot-done /mnt/lima-cidata/meta-data 2>/dev/null; do sleep 3; done"; then echo >&2 "boot scripts have not finished" exit 1 fi `, - debugHint: `All boot scripts, provisioning scripts, and readiness probes must + debugHint: `All boot scripts, provisioning scripts, and readiness probes must finish before the instance is considered "ready". Check "/var/log/cloud-init-output.log" in the guest to see where the process is blocked! `, - }) + }) + } return req } From 1ef9d0221b4d6d215a6061abfe28134ec3c13b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sun, 7 Jan 2024 21:06:55 +0100 Subject: [PATCH 10/15] Don't show log for already cached archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/hostagent/requirements.go | 4 ++-- pkg/instance/start.go | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/hostagent/requirements.go b/pkg/hostagent/requirements.go index 581ae65405c..d0b389c6a30 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -131,7 +131,7 @@ If any private key under ~/.ssh is protected with a passphrase, you need to have if *a.instConfig.Plain { return req } - if *a.y.VMType != limayaml.EXT { + if *a.instConfig.VMType != limayaml.EXT { req = append(req, requirement{ description: "user session is ready for ssh", @@ -230,7 +230,7 @@ Also see "/var/log/cloud-init-output.log" in the guest. func (a *HostAgent) finalRequirements() []requirement { req := make([]requirement, 0) - if *a.y.VMType != limayaml.EXT { + if *a.instConfig.VMType != limayaml.EXT { req = append(req, requirement{ description: "boot scripts must have finished", diff --git a/pkg/instance/start.go b/pkg/instance/start.go index 770fc6feb18..fd2cfa2fbbc 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -101,6 +101,10 @@ func Prepare(ctx context.Context, inst *store.Instance) (*Prepared, error) { if err := limaDriver.CreateDisk(ctx); err != nil { return nil, err } + if *inst.Config.VMType == limayaml.EXT { + // Created externally + created = true + } nerdctlArchiveCache, err := ensureNerdctlArchiveCache(ctx, inst.Config, created) if err != nil { return nil, err From bef550f67ed1fb5aa11832d64402d187b7609c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sun, 7 Jan 2024 22:27:26 +0100 Subject: [PATCH 11/15] Add external support for provision scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/hostagent/hostagent.go | 5 +++ pkg/hostagent/provision.go | 72 ++++++++++++++++++++++++++++++++++++++ pkg/hostagent/sudo.go | 46 ++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 pkg/hostagent/provision.go create mode 100644 pkg/hostagent/sudo.go diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 302db4f4b68..e1d3d676b2b 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -490,6 +490,11 @@ sudo chown -R "${USER}" /run/host-services` return errors.Join(unlockErrs...) }) } + if *a.instConfig.VMType == limayaml.EXT { + if err := a.runProvisionScripts(); err != nil { + return err + } + } if !*a.instConfig.Plain { go a.watchGuestAgentEvents(ctx) } diff --git a/pkg/hostagent/provision.go b/pkg/hostagent/provision.go new file mode 100644 index 00000000000..bad8cbe1b95 --- /dev/null +++ b/pkg/hostagent/provision.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package hostagent + +import ( + "errors" + "fmt" + + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sirupsen/logrus" +) + +func (a *HostAgent) runProvisionScripts() error { + var errs []error + + for i, f := range a.instConfig.Provision { + switch f.Mode { + case limayaml.ProvisionModeSystem, limayaml.ProvisionModeUser: + logrus.Infof("Running %s provision %d of %d", f.Mode, i+1, len(a.instConfig.Provision)) + err := a.waitForProvision( + provision{ + description: fmt.Sprintf("provision.%s/%08d", f.Mode, i), + sudo: f.Mode == limayaml.ProvisionModeSystem, + script: f.Script, + }) + if err != nil { + errs = append(errs, err) + } + case limayaml.ProvisionModeDependency, limayaml.ProvisionModeBoot: + logrus.Infof("Skipping %s provision %d of %d", f.Mode, i+1, len(a.instConfig.Provision)) + continue + default: + return fmt.Errorf("unknown provision mode %q", f.Mode) + } + } + return errors.Join(errs...) +} + +func (a *HostAgent) waitForProvision(p provision) error { + if p.sudo { + return a.waitForSystemProvision(p) + } + return a.waitForUserProvision(p) +} + +func (a *HostAgent) waitForSystemProvision(p provision) error { + logrus.Debugf("executing script %q", p.description) + stdout, stderr, err := sudoExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, p.script, p.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) + } + return nil +} + +func (a *HostAgent) waitForUserProvision(p provision) error { + logrus.Debugf("executing script %q", p.description) + stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, p.script, p.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) + } + return nil +} + +type provision struct { + description string + script string + sudo bool +} diff --git a/pkg/hostagent/sudo.go b/pkg/hostagent/sudo.go new file mode 100644 index 00000000000..73b5d4784fd --- /dev/null +++ b/pkg/hostagent/sudo.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package hostagent + +import ( + "bytes" + "errors" + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sirupsen/logrus" +) + +// sudoExecuteScript executes the given script (as root) on the remote host via stdin. +// Returns stdout and stderr. +// +// scriptName is used only for readability of error strings. +func sudoExecuteScript(host string, port int, c *ssh.SSHConfig, script, scriptName string) (stdout, stderr string, err error) { + if c == nil { + return "", "", errors.New("got nil SSHConfig") + } + interpreter, err := ssh.ParseScriptInterpreter(script) + if err != nil { + return "", "", err + } + sshBinary := c.Binary() + sshArgs := c.Args() + if port != 0 { + sshArgs = append(sshArgs, "-p", strconv.Itoa(port)) + } + sshArgs = append(sshArgs, host, "--", "sudo", interpreter) + sshCmd := exec.Command(sshBinary, sshArgs...) + sshCmd.Stdin = strings.NewReader(script) + var buf bytes.Buffer + sshCmd.Stderr = &buf + logrus.Debugf("executing ssh for script %q: %s %v", scriptName, sshCmd.Path, sshCmd.Args) + out, err := sshCmd.Output() + if err != nil { + return string(out), buf.String(), fmt.Errorf("failed to execute script %q: stdout=%q, stderr=%q: %w", scriptName, string(out), buf.String(), err) + } + return string(out), buf.String(), nil +} From 5ad23d4f04997073b249ee2f5074e54a290d1cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Tue, 16 Jan 2024 13:46:25 +0100 Subject: [PATCH 12/15] Don't generate cidata.iso for external vm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is not using cloud-init anyway, and does not need another copy of lima-guestagent and nerdctl-full.tgz Signed-off-by: Anders F Björklund --- pkg/hostagent/hostagent.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index e1d3d676b2b..605dcda18f5 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -134,8 +134,10 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt if err := cidata.GenerateCloudConfig(inst.Dir, instName, inst.Config); err != nil { return nil, err } - if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort, virtioPort); err != nil { - return nil, err + if *inst.Config.VMType != limayaml.EXT { + if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort, virtioPort); err != nil { + return nil, err + } } sshOpts, err := sshutil.SSHOpts( From ef219afd7264d490b27bae52ea35d2b63f211d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Tue, 9 Jul 2024 12:39:06 +0200 Subject: [PATCH 13/15] Use net helper for local ssh CommonOpts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- cmd/limactl/copy.go | 2 +- pkg/sshutil/sshutil.go | 11 ++++++++++- pkg/sshutil/sshutil_test.go | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go index 9fcf57baba7..d2af96bbed4 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -116,7 +116,7 @@ func copyAction(cmd *cobra.Command, args []string) error { } else { scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@%s:%d/%s", *inst.Config.User.Name, inst.SSHAddress, inst.SSHLocalPort, path[1])) } - if inst.SSHAddress != "127.0.0.1" { + if !sshutil.IsLocalhost(inst.SSHAddress) { localhostOnly = false } instances[instName] = inst diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go index aede7c1a651..7090f275521 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io/fs" + "net" "os" "os/exec" "path/filepath" @@ -162,6 +163,14 @@ var sshInfo struct { openSSHVersion semver.Version } +func IsLocalhost(address string) bool { + ip := net.ParseIP(address) + if ip == nil { + return false + } + return ip.IsLoopback() +} + // CommonOpts returns ssh option key-value pairs like {"IdentityFile=/path/to/id_foo"}. // The result may contain different values with the same key. // @@ -285,7 +294,7 @@ func SSHOpts(sshPath, instDir, username string, useDotSSH bool, hostAddress stri if len(controlSock) >= osutil.UnixPathMax { return nil, fmt.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax) } - opts, err := CommonOpts(sshPath, useDotSSH, hostAddress == "127.0.0.1") + opts, err := CommonOpts(sshPath, useDotSSH, IsLocalhost(hostAddress)) if err != nil { return nil, err } diff --git a/pkg/sshutil/sshutil_test.go b/pkg/sshutil/sshutil_test.go index 972c3037b1a..b0be5cdb814 100644 --- a/pkg/sshutil/sshutil_test.go +++ b/pkg/sshutil/sshutil_test.go @@ -18,6 +18,10 @@ func TestDefaultPubKeys(t *testing.T) { } } +func TestIsLocalhost(t *testing.T) { + assert.Equal(t, IsLocalhost("127.0.0.1"), true) +} + func TestParseOpenSSHVersion(t *testing.T) { assert.Check(t, ParseOpenSSHVersion([]byte("OpenSSH_8.4p1 Ubuntu")).Equal( semver.Version{Major: 8, Minor: 4, Patch: 1, PreRelease: "", Metadata: ""})) From ca818292f372814f66e19fd25bc7b84575598ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Tue, 9 Jul 2024 13:03:41 +0200 Subject: [PATCH 14/15] Add documentation and example for ext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/limayaml/validate.go | 3 +++ templates/README.md | 3 +++ templates/experimental/ext.yaml | 14 ++++++++++++++ website/content/en/docs/config/vmtype.md | 11 +++++++++++ 4 files changed, 31 insertions(+) create mode 100644 templates/experimental/ext.yaml diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index e071aee3f7c..748384504ea 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -613,6 +613,9 @@ func warnExperimental(y *LimaYAML) { if *y.MountType == VIRTIOFS && runtime.GOOS == "linux" { logrus.Warn("`mountType: virtiofs` on Linux is experimental") } + if *y.VMType == EXT { + logrus.Warn("`vmType: ext` is experimental") + } switch *y.Arch { case RISCV64, ARMV7L, S390X: logrus.Warnf("`arch: %s ` is experimental", *y.Arch) diff --git a/templates/README.md b/templates/README.md index 3fd1ca59a48..cb470e1d10b 100644 --- a/templates/README.md +++ b/templates/README.md @@ -33,6 +33,9 @@ Distro: - [`experimental/opensuse-tumbleweed`](./experimental/opensuse-tumbleweed.yaml): [experimental] openSUSE Tumbleweed - [`experimental/debian-sid`](./experimental/debian-sid.yaml): [experimental] Debian Sid +External: +- [`experimental/ext`](./experimental/ext.yaml): [experimental] External Raspberry Pi Zero + Container engines: - [`apptainer`](./apptainer.yaml): Apptainer - [`apptainer-rootful`](./apptainer-rootful.yaml): Apptainer (rootful) diff --git a/templates/experimental/ext.yaml b/templates/experimental/ext.yaml new file mode 100644 index 00000000000..bea4f51d651 --- /dev/null +++ b/templates/experimental/ext.yaml @@ -0,0 +1,14 @@ +vmType: ext + +arch: "aarch64" +cpus: 4 +memory: 512MiB +disk: 32GiB + +mounts: +- location: "~" +- location: "/tmp/lima" + writable: true + +ssh: + address: raspberrypi.local diff --git a/website/content/en/docs/config/vmtype.md b/website/content/en/docs/config/vmtype.md index 3c407a5db3e..9a9980f6910 100644 --- a/website/content/en/docs/config/vmtype.md +++ b/website/content/en/docs/config/vmtype.md @@ -7,6 +7,9 @@ Lima supports two ways of running guest machines: - [qemu](#qemu) - [vz](#vz) +Lima also supports connecting to external machines: +- [ext](#ext) + The vmType can be specified only on creating the instance. The vmType of existing instances cannot be changed. @@ -111,3 +114,11 @@ containerd: - When running lima using "wsl2", `${LIMA_HOME}//serial.log` will not contain kernel boot logs - WSL2 requires a `tar` formatted rootfs archive instead of a VM image - Windows doesn't ship with ssh.exe, gzip.exe, etc. which are used by Lima at various points. The easiest way around this is to run `winget install -e --id Git.MinGit` (winget is now built in to Windows as well), and add the resulting `C:\Program Files\Git\usr\bin\` directory to your path. + +## EXT +> **Warning** +> "ext" mode is experimental + +"ext" option makes use of an external machine, either a virtual machine or a physical machine. + +It is accessed using an address (for SSH), the keys are supposed to be set up for it already. From cc436f1f75c3a0dd63405285e238cd70dbae9e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Tue, 9 Jul 2024 20:28:47 +0200 Subject: [PATCH 15/15] Allow .local hosts to be offline or slow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/limayaml/validate.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 748384504ea..f8fce37b552 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -4,7 +4,6 @@ package limayaml import ( - "context" "errors" "fmt" "net" @@ -17,7 +16,6 @@ import ( "strconv" "strings" "unicode" - "time" "github.com/containerd/containerd/identifiers" "github.com/coreos/go-semver/semver" @@ -572,16 +570,11 @@ func ValidateParamIsUsed(y *LimaYAML) error { } func lookupIP(host string) error { - var err error if strings.HasSuffix(host, ".local") { - var r net.Resolver - const timeout = 500 * time.Millisecond // timeout for .local - ctx, cancel := context.WithTimeout(context.TODO(), timeout) - defer cancel() - _, err = r.LookupIP(ctx, "ip", host) - } else { - _, err = net.LookupIP(host) + // allow offline or slow mDNS + return nil } + _, err := net.LookupIP(host) return err }