From 76039b2e148f3f885dbb9b273b5f56fdf054f372 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 04cbf59088a..6bbb9dfdbb5 100644 --- a/cmd/limactl/show-ssh.go +++ b/cmd/limactl/show-ssh.go @@ -98,7 +98,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, instName, format, opts) } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 8d7a8d6830e..d9ba09cd97a 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -100,7 +100,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 } @@ -242,13 +242,16 @@ func writeSSHConfigFile(instName, instDir, instSSHAddress string, sshLocalPort i 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 a3b196d9eec..50d6673bf6a 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -398,6 +398,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 6ce45311ac1..ebe8132fa53 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -84,6 +84,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), @@ -342,6 +343,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), @@ -556,6 +558,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 a899ed35658..ccdcc65a1a7 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -167,7 +167,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 56d03c0d22c..266a7f36016 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -167,6 +167,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 @@ -493,6 +498,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 7daf4537e1e..a04afb078eb 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -94,7 +94,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 3720be52ecc..461cda94a32 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -142,6 +142,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 fde227cf1dd448daa789561b86a53228269a5c23 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 ae9a295d2bf..18533f1da88 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -80,9 +80,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 0db6308fe2033fc33c7f1900b69239cac2cc6a2a 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 d9ba09cd97a..07c2e29ac27 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -208,7 +208,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, @@ -546,7 +546,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) } } } @@ -561,13 +561,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) } } @@ -591,7 +591,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) @@ -679,11 +679,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...) @@ -694,7 +694,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", @@ -713,7 +713,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, "/") { @@ -721,7 +721,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 { @@ -736,7 +736,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 { @@ -756,7 +756,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 646dc5d4eb3..abc3f911acf 100644 --- a/pkg/hostagent/mount.go +++ b/pkg/hostagent/mount.go @@ -58,7 +58,7 @@ func (a *HostAgent) setupMount(m limayaml.Mount) (*mount, error) { Driver: *m.SSHFS.SFTPDriver, SSHConfig: a.sshConfig, LocalPath: location, - Host: "127.0.0.1", + Host: a.instSSHAddress, Port: a.sshLocalPort, RemotePath: mountPoint, Readonly: !(*m.Writable), diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go index bfb86d1ce3c..0e82fb19199 100644 --- a/pkg/hostagent/port.go +++ b/pkg/hostagent/port.go @@ -12,6 +12,7 @@ import ( type portForwarder struct { sshConfig *ssh.SSHConfig + sshHostAddr string sshHostPort int rules []limayaml.PortForward ignore bool @@ -22,9 +23,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, @@ -91,7 +93,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) } } @@ -107,7 +109,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 615a2bd03f9..c6ca72a41e5 100644 --- a/pkg/hostagent/port_darwin.go +++ b/pkg/hostagent/port_darwin.go @@ -15,9 +15,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 { @@ -30,7 +30,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. @@ -45,7 +45,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 { @@ -60,12 +60,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 c038e6c377d..2512a85759a 100644 --- a/pkg/hostagent/port_others.go +++ b/pkg/hostagent/port_others.go @@ -8,6 +8,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 6a3d74e14b7..37f86809162 100644 --- a/pkg/hostagent/port_windows.go +++ b/pkg/hostagent/port_windows.go @@ -6,6 +6,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 2951080b0bc7c61a9614ddb17c1b4689a01b0a60 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 18533f1da88..5c55d0ec685 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -61,6 +61,7 @@ func copyAction(cmd *cobra.Command, args []string) error { scpFlags = append(scpFlags, "-r") } legacySSH := sshutil.DetectOpenSSHVersion().LessThan(*semver.New("8.0.0")) + localhostOnly := true for _, arg := range args { path := strings.Split(arg, ":") switch len(path) { @@ -84,6 +85,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) @@ -101,14 +105,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(inst.Dir, *inst.Config.User.Name, false, false, false, false) + sshOpts, err = sshutil.SSHOpts(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(false) + sshOpts, err = sshutil.CommonOpts(false, localhostOnly) if err != nil { return err } diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index 65d8fad7b29..19da8fa8992 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -169,6 +169,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 6bbb9dfdbb5..f68ce54f21e 100644 --- a/cmd/limactl/show-ssh.go +++ b/cmd/limactl/show-ssh.go @@ -92,6 +92,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 446089c65e5..290d6908bcb 100644 --- a/cmd/limactl/tunnel.go +++ b/cmd/limactl/tunnel.go @@ -109,6 +109,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 07c2e29ac27..fc89b8ed6dd 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -146,6 +146,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) @@ -578,7 +579,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 af141e660a6..8da0f759016 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -126,7 +126,7 @@ var sshInfo struct { // // The result always contains the IdentityFile option. // The result never contains the Port option. -func CommonOpts(useDotSSH bool) ([]string, error) { +func CommonOpts(useDotSSH, localhost bool) ([]string, error) { configDir, err := dirnames.LimaConfigDir() if err != nil { return nil, err @@ -181,14 +181,20 @@ func CommonOpts(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", ) @@ -223,12 +229,12 @@ func CommonOpts(useDotSSH bool) ([]string, error) { } // SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist. -func SSHOpts(instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) { +func SSHOpts(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(useDotSSH) + opts, err := CommonOpts(useDotSSH, hostAddress == "127.0.0.1") if err != nil { return nil, err } From 76af7db3a71cf603cfba4fd6c1013f4b9cbf04c5 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 f7d6b4a1458..da835da05d9 100644 --- a/pkg/hostagent/events/events.go +++ b/pkg/hostagent/events/events.go @@ -13,7 +13,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 fc89b8ed6dd..46a49f103d9 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -382,8 +382,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 899f07c7147..8e5d5c869ec 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -285,7 +285,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 d501de00e1ef7ff2f640fa440b67dd0ab5acf933 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 | 4 ++++ pkg/driverutil/instance.go | 4 ++++ pkg/ext/ext_driver.go | 17 +++++++++++++++++ pkg/limayaml/defaults.go | 2 ++ pkg/limayaml/limayaml.go | 1 + pkg/limayaml/validate.go | 9 +++++++-- pkg/store/instance.go | 8 ++++++++ 7 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 pkg/ext/ext_driver.go diff --git a/pkg/driverutil/driverutil.go b/pkg/driverutil/driverutil.go index a0d12787332..eeb814edf6e 100644 --- a/pkg/driverutil/driverutil.go +++ b/pkg/driverutil/driverutil.go @@ -1,6 +1,7 @@ package driverutil import ( + "github.com/lima-vm/lima/pkg/ext" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/vz" "github.com/lima-vm/lima/pkg/wsl2" @@ -15,5 +16,8 @@ func Drivers() []string { if wsl2.Enabled { drivers = append(drivers, limayaml.WSL2) } + if ext.Enabled { + drivers = append(drivers, limayaml.EXT) + } return drivers } diff --git a/pkg/driverutil/instance.go b/pkg/driverutil/instance.go index f93b3854115..5cf083ae4f4 100644 --- a/pkg/driverutil/instance.go +++ b/pkg/driverutil/instance.go @@ -2,6 +2,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" @@ -16,5 +17,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..a94148ee7ae --- /dev/null +++ b/pkg/ext/ext_driver.go @@ -0,0 +1,17 @@ +package ext + +import ( + "github.com/lima-vm/lima/pkg/driver" +) + +const Enabled = true + +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 50d6673bf6a..77c9cd481da 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -1051,6 +1051,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 ccdcc65a1a7..de1d6f4d8fb 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -75,6 +75,7 @@ const ( QEMU VMType = "qemu" VZ VMType = "vz" WSL2 VMType = "wsl2" + EXT VMType = "ext" ) var ( diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 266a7f36016..2baccd8006e 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -83,11 +83,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 { @@ -167,6 +169,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 a04afb078eb..95c02f10972 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -189,6 +189,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 c0b16ae37697ce629019ce800f0942f8296a75d7 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 2baccd8006e..813410dfd9b 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -504,7 +504,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 9210b692f5d466d453cf8eb1f38b35c872fe97fb 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 813410dfd9b..059743c81bc 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -1,6 +1,7 @@ package limayaml import ( + "context" "errors" "fmt" "net" @@ -11,6 +12,7 @@ import ( "runtime" "strings" "unicode" + "time" "github.com/coreos/go-semver/semver" "github.com/docker/go-units" @@ -503,12 +505,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 781590ae42be12c3196d73917c417463779499bf 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 2f290768499..890a93c8362 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -117,22 +117,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{ @@ -214,20 +216,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 c6678ce9054ce31e41d8a3f875d62c3cf3cdfd73 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 890a93c8362..bb18a3c177f 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -117,7 +117,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", @@ -216,7 +216,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 8e5d5c869ec..4af18a49f2c 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -98,6 +98,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 f283e6f2699c8c1b3f00253a9f9149917216d222 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 | 69 ++++++++++++++++++++++++++++++++++++++ pkg/hostagent/sudo.go | 43 ++++++++++++++++++++++++ 3 files changed, 117 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 46a49f103d9..8cc42ce2cfa 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -500,6 +500,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..b9010fcef41 --- /dev/null +++ b/pkg/hostagent/provision.go @@ -0,0 +1,69 @@ +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..cae95c0c7e6 --- /dev/null +++ b/pkg/hostagent/sudo.go @@ -0,0 +1,43 @@ +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 b7a6efaf42d9a59cd67ed0b52527e6724d0d3c80 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 8cc42ce2cfa..a1388f443d5 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -138,8 +138,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 88724bec6dc1ac0da3e297b11a8039d6675684bd 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 5c55d0ec685..d1b55d1b9d3 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -85,7 +85,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 8da0f759016..cb3e07c4d1a 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io/fs" + "net" "os" "os/exec" "path/filepath" @@ -121,6 +122,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. // @@ -234,7 +243,7 @@ func SSHOpts(instDir, username string, useDotSSH bool, hostAddress string, forwa 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(useDotSSH, hostAddress == "127.0.0.1") + opts, err := CommonOpts(useDotSSH, IsLocalhost(hostAddress)) if err != nil { return nil, err } diff --git a/pkg/sshutil/sshutil_test.go b/pkg/sshutil/sshutil_test.go index ef2b754da23..e452a160f1d 100644 --- a/pkg/sshutil/sshutil_test.go +++ b/pkg/sshutil/sshutil_test.go @@ -15,6 +15,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 dea8f7e9eae9f5556417d6368b8da58c0f01137b 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 --- examples/experimental/ext.yaml | 14 ++++++++++++++ pkg/limayaml/validate.go | 3 +++ templates/README.md | 3 +++ website/content/en/docs/config/vmtype/_index.md | 11 +++++++++++ 4 files changed, 31 insertions(+) create mode 100644 examples/experimental/ext.yaml diff --git a/examples/experimental/ext.yaml b/examples/experimental/ext.yaml new file mode 100644 index 00000000000..bea4f51d651 --- /dev/null +++ b/examples/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/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 059743c81bc..a1b280b0943 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -547,6 +547,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") + } if *y.Arch == RISCV64 { logrus.Warn("`arch: riscv64` is experimental") } diff --git a/templates/README.md b/templates/README.md index 8cdd2b0c708..6700b995d1b 100644 --- a/templates/README.md +++ b/templates/README.md @@ -30,6 +30,9 @@ Distro: - [`experimental/gentoo`](./experimental/gentoo.yaml): [experimental] Gentoo - [`experimental/opensuse-tumbleweed`](./experimental/opensuse-tumbleweed.yaml): [experimental] openSUSE Tumbleweed +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/website/content/en/docs/config/vmtype/_index.md b/website/content/en/docs/config/vmtype/_index.md index 3c407a5db3e..9a9980f6910 100644 --- a/website/content/en/docs/config/vmtype/_index.md +++ b/website/content/en/docs/config/vmtype/_index.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 3ab272868c9ef2be17072d69e4b991fee504f2fb 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 a1b280b0943..f23507fada1 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -1,7 +1,6 @@ package limayaml import ( - "context" "errors" "fmt" "net" @@ -12,7 +11,6 @@ import ( "runtime" "strings" "unicode" - "time" "github.com/coreos/go-semver/semver" "github.com/docker/go-units" @@ -506,16 +504,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 }