Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/hostagent/hostagent.go
2611 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package hostagent
5
6
import (
7
"bufio"
8
"bytes"
9
"context"
10
"encoding/json"
11
"errors"
12
"fmt"
13
"io"
14
"net"
15
"os"
16
"os/exec"
17
"path/filepath"
18
"runtime"
19
"strconv"
20
"strings"
21
"sync"
22
"time"
23
24
"github.com/lima-vm/sshocker/pkg/ssh"
25
"github.com/sethvargo/go-password/password"
26
"github.com/sirupsen/logrus"
27
"google.golang.org/grpc/codes"
28
"google.golang.org/grpc/status"
29
30
"github.com/lima-vm/lima/v2/pkg/autostart"
31
"github.com/lima-vm/lima/v2/pkg/cidata"
32
"github.com/lima-vm/lima/v2/pkg/driver"
33
"github.com/lima-vm/lima/v2/pkg/driverutil"
34
"github.com/lima-vm/lima/v2/pkg/freeport"
35
guestagentapi "github.com/lima-vm/lima/v2/pkg/guestagent/api"
36
guestagentclient "github.com/lima-vm/lima/v2/pkg/guestagent/api/client"
37
hostagentapi "github.com/lima-vm/lima/v2/pkg/hostagent/api"
38
"github.com/lima-vm/lima/v2/pkg/hostagent/dns"
39
"github.com/lima-vm/lima/v2/pkg/hostagent/events"
40
"github.com/lima-vm/lima/v2/pkg/instance/hostname"
41
"github.com/lima-vm/lima/v2/pkg/limatype"
42
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
43
"github.com/lima-vm/lima/v2/pkg/limayaml"
44
"github.com/lima-vm/lima/v2/pkg/networks"
45
"github.com/lima-vm/lima/v2/pkg/osutil"
46
"github.com/lima-vm/lima/v2/pkg/portfwd"
47
"github.com/lima-vm/lima/v2/pkg/sshutil"
48
"github.com/lima-vm/lima/v2/pkg/store"
49
"github.com/lima-vm/lima/v2/pkg/version/versionutil"
50
)
51
52
type HostAgent struct {
53
instConfig *limatype.LimaYAML
54
sshLocalPort int
55
udpDNSLocalPort int
56
tcpDNSLocalPort int
57
instDir string
58
instName string
59
instSSHAddress string
60
sshConfig *ssh.SSHConfig
61
portForwarder *portForwarder // legacy SSH port forwarder
62
grpcPortForwarder *portfwd.Forwarder
63
64
onClose []func() error // LIFO
65
onCloseMu sync.Mutex
66
67
driver driver.Driver
68
signalCh chan os.Signal
69
70
eventEnc *json.Encoder
71
eventEncMu sync.Mutex
72
73
vSockPort int
74
virtioPort string
75
76
clientMu sync.RWMutex
77
client *guestagentclient.GuestAgentClient
78
79
guestAgentAliveCh chan struct{} // closed on establishing the connection
80
guestAgentAliveChOnce sync.Once
81
82
showProgress bool // whether to show cloud-init progress
83
84
statusMu sync.RWMutex
85
currentStatus events.Status
86
}
87
88
type options struct {
89
guestAgentBinary string
90
nerdctlArchive string // local path, not URL
91
showProgress bool
92
}
93
94
type Opt func(*options) error
95
96
func WithGuestAgentBinary(s string) Opt {
97
return func(o *options) error {
98
o.guestAgentBinary = s
99
return nil
100
}
101
}
102
103
func WithNerdctlArchive(s string) Opt {
104
return func(o *options) error {
105
o.nerdctlArchive = s
106
return nil
107
}
108
}
109
110
func WithCloudInitProgress(enabled bool) Opt {
111
return func(o *options) error {
112
o.showProgress = enabled
113
return nil
114
}
115
}
116
117
// New creates the HostAgent.
118
//
119
// stdout is for emitting JSON lines of Events.
120
func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt) (*HostAgent, error) {
121
var o options
122
for _, f := range opts {
123
if err := f(&o); err != nil {
124
return nil, err
125
}
126
}
127
inst, err := store.Inspect(ctx, instName)
128
if err != nil {
129
return nil, err
130
}
131
132
var limaVersion string
133
limaVersionFile := filepath.Join(inst.Dir, filenames.LimaVersion)
134
if b, err := os.ReadFile(limaVersionFile); err == nil {
135
limaVersion = strings.TrimSpace(string(b))
136
} else if !errors.Is(err, os.ErrNotExist) {
137
logrus.WithError(err).Warnf("Failed to read %q", limaVersionFile)
138
}
139
140
// inst.Config is loaded with FillDefault() already, so no need to care about nil pointers.
141
sshLocalPort, err := determineSSHLocalPort(*inst.Config.SSH.LocalPort, instName, limaVersion)
142
if err != nil {
143
return nil, err
144
}
145
146
var udpDNSLocalPort, tcpDNSLocalPort int
147
if *inst.Config.HostResolver.Enabled {
148
udpDNSLocalPort, err = freeport.UDP()
149
if err != nil {
150
return nil, err
151
}
152
tcpDNSLocalPort, err = freeport.TCP()
153
if err != nil {
154
return nil, err
155
}
156
}
157
158
limaDriver, err := driverutil.CreateConfiguredDriver(inst, sshLocalPort)
159
if err != nil {
160
return nil, fmt.Errorf("failed to create driver instance: %w", err)
161
}
162
sshLocalPort = inst.SSHLocalPort
163
164
vSockPort := limaDriver.Info().VsockPort
165
virtioPort := limaDriver.Info().VirtioPort
166
noCloudInit := limaDriver.Info().Features.NoCloudInit
167
rosettaEnabled := limaDriver.Info().Features.RosettaEnabled
168
rosettaBinFmt := limaDriver.Info().Features.RosettaBinFmt
169
170
// Disable Rosetta in Plain mode
171
if *inst.Config.Plain {
172
rosettaEnabled = false
173
rosettaBinFmt = false
174
}
175
176
if err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil {
177
return nil, err
178
}
179
if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort, noCloudInit, rosettaEnabled, rosettaBinFmt); err != nil {
180
return nil, err
181
}
182
183
sshExe, err := sshutil.NewSSHExe()
184
if err != nil {
185
return nil, err
186
}
187
sshOpts, err := sshutil.SSHOpts(
188
ctx,
189
sshExe,
190
inst.Dir,
191
*inst.Config.User.Name,
192
*inst.Config.SSH.LoadDotSSHPubKeys,
193
*inst.Config.SSH.ForwardAgent,
194
*inst.Config.SSH.ForwardX11,
195
*inst.Config.SSH.ForwardX11Trusted)
196
if err != nil {
197
return nil, err
198
}
199
if err = writeSSHConfigFile("ssh", inst.Name, inst.Dir, inst.SSHAddress, sshLocalPort, sshOpts); err != nil {
200
return nil, err
201
}
202
sshConfig := &ssh.SSHConfig{
203
AdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts),
204
}
205
206
ignoreTCP := false
207
ignoreUDP := false
208
for _, rule := range inst.Config.PortForwards {
209
if rule.Ignore && rule.GuestPortRange[0] == 1 && rule.GuestPortRange[1] == 65535 {
210
switch rule.Proto {
211
case limatype.ProtoTCP:
212
ignoreTCP = true
213
logrus.Info("TCP port forwarding is disabled (except for SSH)")
214
case limatype.ProtoUDP:
215
ignoreUDP = true
216
logrus.Info("UDP port forwarding is disabled")
217
case limatype.ProtoAny:
218
ignoreTCP = true
219
ignoreUDP = true
220
logrus.Info("TCP (except for SSH) and UDP port forwarding is disabled")
221
}
222
} else {
223
break
224
}
225
}
226
rules := make([]limatype.PortForward, 0, 3+len(inst.Config.PortForwards))
227
// Block ports 22 and sshLocalPort on all IPs
228
for _, port := range []int{sshGuestPort, sshLocalPort} {
229
rule := limatype.PortForward{GuestIP: net.IPv4zero, GuestPort: port, Ignore: true}
230
limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Config.User, inst.Param)
231
rules = append(rules, rule)
232
}
233
rules = append(rules, inst.Config.PortForwards...)
234
// Default forwards for all non-privileged ports from "127.0.0.1" and "::1"
235
rule := limatype.PortForward{}
236
limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Config.User, inst.Param)
237
rules = append(rules, rule)
238
239
a := &HostAgent{
240
instConfig: inst.Config,
241
sshLocalPort: sshLocalPort,
242
udpDNSLocalPort: udpDNSLocalPort,
243
tcpDNSLocalPort: tcpDNSLocalPort,
244
instDir: inst.Dir,
245
instName: instName,
246
instSSHAddress: inst.SSHAddress,
247
sshConfig: sshConfig,
248
driver: limaDriver,
249
signalCh: signalCh,
250
eventEnc: json.NewEncoder(stdout),
251
vSockPort: vSockPort,
252
virtioPort: virtioPort,
253
guestAgentAliveCh: make(chan struct{}),
254
showProgress: o.showProgress,
255
}
256
a.grpcPortForwarder = portfwd.NewPortForwarder(rules, ignoreTCP, ignoreUDP, func(ev *events.PortForwardEvent) {
257
a.emitPortForwardEvent(context.Background(), ev)
258
})
259
a.portForwarder = newPortForwarder(sshConfig, a.sshAddressPort, rules, ignoreTCP, inst.VMType, func(ev *events.PortForwardEvent) {
260
a.emitPortForwardEvent(context.Background(), ev)
261
})
262
263
// Set up vsock event callback if the driver supports it
264
if vsockEmitter, ok := limaDriver.Driver.(driver.VsockEventEmitter); ok {
265
vsockEmitter.SetVsockEventCallback(func(ev *events.VsockEvent) {
266
a.emitVsockEvent(context.Background(), ev)
267
})
268
}
269
270
return a, nil
271
}
272
273
func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {
274
if instDir == "" {
275
return fmt.Errorf("directory is unknown for the instance %q", instName)
276
}
277
b := bytes.NewBufferString(`# This SSH config file can be passed to 'ssh -F'.
278
# This file is created by Lima, but not used by Lima itself currently.
279
# Modifications to this file will be lost on restarting the Lima instance.
280
`)
281
if runtime.GOOS == "windows" {
282
// Remove ControlMaster, ControlPath, and ControlPersist options,
283
// because Cygwin-based SSH clients do not support multiplexing when executing commands.
284
// References:
285
// https://inbox.sourceware.org/cygwin/[email protected]/T/
286
// https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible
287
// By removing these options:
288
// - Avoids execution failures when the control master is not yet available.
289
// - Prevents error messages such as:
290
// > mux_client_request_session: read from master failed: Connection reset by peer
291
// > ControlSocket ....sock already exists, disabling multiplexing
292
// Only remove these options when writing the SSH config file and executing `limactl shell`, since multiplexing seems to work with port forwarding.
293
sshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)
294
}
295
if err := sshutil.Format(b, sshPath, instName, sshutil.FormatConfig,
296
append(sshOpts,
297
fmt.Sprintf("Hostname=%s", instSSHAddress),
298
fmt.Sprintf("Port=%d", sshLocalPort),
299
)); err != nil {
300
return err
301
}
302
fileName := filepath.Join(instDir, filenames.SSHConfig)
303
return os.WriteFile(fileName, b.Bytes(), 0o600)
304
}
305
306
func determineSSHLocalPort(confLocalPort int, instName, limaVersion string) (int, error) {
307
if confLocalPort > 0 {
308
return confLocalPort, nil
309
}
310
if confLocalPort < 0 {
311
return 0, fmt.Errorf("invalid ssh local port %d", confLocalPort)
312
}
313
if versionutil.LessThan(limaVersion, "2.0.0") && instName == "default" {
314
// use hard-coded value for "default" instance, for backward compatibility
315
return 60022, nil
316
}
317
sshLocalPort, err := freeport.TCP()
318
if err != nil {
319
return 0, fmt.Errorf("failed to find a free port, try setting `ssh.localPort` manually: %w", err)
320
}
321
return sshLocalPort, nil
322
}
323
324
func (a *HostAgent) emitEvent(_ context.Context, ev events.Event) {
325
a.eventEncMu.Lock()
326
defer a.eventEncMu.Unlock()
327
328
a.statusMu.Lock()
329
a.currentStatus = ev.Status
330
a.statusMu.Unlock()
331
332
if ev.Time.IsZero() {
333
ev.Time = time.Now()
334
}
335
if err := a.eventEnc.Encode(ev); err != nil {
336
logrus.WithField("event", ev).WithError(err).Error("failed to emit an event")
337
}
338
}
339
340
func (a *HostAgent) emitCloudInitProgressEvent(ctx context.Context, progress *events.CloudInitProgress) {
341
a.statusMu.RLock()
342
currentStatus := a.currentStatus
343
a.statusMu.RUnlock()
344
345
currentStatus.CloudInitProgress = progress
346
347
ev := events.Event{Status: currentStatus}
348
a.emitEvent(ctx, ev)
349
}
350
351
func (a *HostAgent) emitPortForwardEvent(ctx context.Context, pfEvent *events.PortForwardEvent) {
352
a.statusMu.RLock()
353
currentStatus := a.currentStatus
354
a.statusMu.RUnlock()
355
356
currentStatus.PortForward = pfEvent
357
358
ev := events.Event{Status: currentStatus}
359
a.emitEvent(ctx, ev)
360
}
361
362
func (a *HostAgent) emitVsockEvent(ctx context.Context, vsockEvent *events.VsockEvent) {
363
a.statusMu.RLock()
364
currentStatus := a.currentStatus
365
a.statusMu.RUnlock()
366
367
currentStatus.Vsock = vsockEvent
368
369
ev := events.Event{Status: currentStatus}
370
a.emitEvent(ctx, ev)
371
}
372
373
func generatePassword(length int) (string, error) {
374
// avoid any special symbols, to make it easier to copy/paste
375
return password.Generate(length, length/4, 0, false, false)
376
}
377
378
func (a *HostAgent) Run(ctx context.Context) error {
379
defer func() {
380
exitingEv := events.Event{
381
Status: events.Status{
382
Exiting: true,
383
},
384
}
385
a.emitEvent(ctx, exitingEv)
386
}()
387
adjustNofileRlimit()
388
389
if limayaml.FirstUsernetIndex(a.instConfig) == -1 && *a.instConfig.HostResolver.Enabled {
390
hosts := a.instConfig.HostResolver.Hosts
391
if hosts == nil {
392
hosts = make(map[string]string)
393
}
394
hosts["host.lima.internal"] = networks.SlirpGateway
395
name := hostname.FromInstName(a.instName) // TODO: support customization
396
hosts[name] = networks.SlirpIPAddress
397
srvOpts := dns.ServerOptions{
398
UDPPort: a.udpDNSLocalPort,
399
TCPPort: a.tcpDNSLocalPort,
400
Address: "127.0.0.1",
401
HandlerOptions: dns.HandlerOptions{
402
IPv6: *a.instConfig.HostResolver.IPv6,
403
StaticHosts: hosts,
404
},
405
}
406
dnsServer, err := dns.Start(srvOpts)
407
if err != nil {
408
return fmt.Errorf("cannot start DNS server: %w", err)
409
}
410
defer dnsServer.Shutdown()
411
}
412
413
errCh, err := a.driver.Start(ctx)
414
if err != nil {
415
return err
416
}
417
418
if err := a.driver.AdditionalSetupForSSH(ctx); err != nil {
419
return err
420
}
421
422
// WSL instance SSH address isn't known until after VM start
423
if a.driver.Info().Features.DynamicSSHAddress {
424
sshAddr, err := a.driver.SSHAddress(ctx)
425
if err != nil {
426
return err
427
}
428
a.instSSHAddress = sshAddr
429
}
430
431
if a.instConfig.Video.Display != nil && *a.instConfig.Video.Display == "vnc" {
432
vncdisplay, vncoptions, _ := strings.Cut(*a.instConfig.Video.VNC.Display, ",")
433
vnchost, vncnum, err := net.SplitHostPort(vncdisplay)
434
if err != nil {
435
return err
436
}
437
n, err := strconv.Atoi(vncnum)
438
if err != nil {
439
return err
440
}
441
vncport := strconv.Itoa(5900 + n)
442
vncpwdfile := filepath.Join(a.instDir, filenames.VNCPasswordFile)
443
vncpasswd, err := generatePassword(8)
444
if err != nil {
445
return err
446
}
447
if err := a.driver.ChangeDisplayPassword(ctx, vncpasswd); err != nil {
448
return err
449
}
450
if err := os.WriteFile(vncpwdfile, []byte(vncpasswd), 0o600); err != nil {
451
return err
452
}
453
if strings.Contains(vncoptions, "to=") {
454
vncport, err = a.driver.DisplayConnection(ctx)
455
if err != nil {
456
return err
457
}
458
p, err := strconv.Atoi(vncport)
459
if err != nil {
460
return err
461
}
462
vncnum = strconv.Itoa(p - 5900)
463
vncdisplay = net.JoinHostPort(vnchost, vncnum)
464
}
465
vncfile := filepath.Join(a.instDir, filenames.VNCDisplayFile)
466
if err := os.WriteFile(vncfile, []byte(vncdisplay), 0o600); err != nil {
467
return err
468
}
469
vncurl := "vnc://" + net.JoinHostPort(vnchost, vncport)
470
logrus.Infof("VNC server running at %s <%s>", vncdisplay, vncurl)
471
logrus.Infof("VNC Display: `%s`", vncfile)
472
logrus.Infof("VNC Password: `%s`", vncpwdfile)
473
}
474
475
if a.driver.Info().Features.CanRunGUI {
476
go func() {
477
err = a.startRoutinesAndWait(ctx, errCh)
478
if err != nil {
479
logrus.Error(err)
480
}
481
}()
482
return a.driver.RunGUI()
483
}
484
return a.startRoutinesAndWait(ctx, errCh)
485
}
486
487
func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error) error {
488
stBase := events.Status{
489
SSHLocalPort: a.sshLocalPort,
490
}
491
stBooting := stBase
492
a.emitEvent(ctx, events.Event{Status: stBooting})
493
ctxHA, cancelHA := context.WithCancel(ctx)
494
go func() {
495
stRunning := stBase
496
if haErr := a.startHostAgentRoutines(ctxHA); haErr != nil {
497
stRunning.Degraded = true
498
stRunning.Errors = append(stRunning.Errors, haErr.Error())
499
}
500
stRunning.Running = true
501
a.emitEvent(ctx, events.Event{Status: stRunning})
502
}()
503
// wait for either the driver to stop or a signal to shut down
504
select {
505
case driverErr := <-errCh:
506
logrus.Infof("Driver stopped due to error: %q", driverErr)
507
case sig := <-a.signalCh:
508
logrus.Infof("Received %s, shutting down the host agent", osutil.SignalName(sig))
509
}
510
// close the host agent routines before cancelling the context
511
if closeErr := a.close(); closeErr != nil {
512
logrus.WithError(closeErr).Warn("an error during shutting down the host agent")
513
}
514
cancelHA()
515
return a.driver.Stop(ctx)
516
}
517
518
func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
519
info := &hostagentapi.Info{
520
AutoStartedIdentifier: autostart.AutoStartedIdentifier(),
521
SSHLocalPort: a.sshLocalPort,
522
}
523
return info, nil
524
}
525
526
func (a *HostAgent) sshAddressPort() (sshAddress string, sshPort int) {
527
sshAddress = a.instSSHAddress
528
sshPort = a.sshLocalPort
529
return sshAddress, sshPort
530
}
531
532
func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
533
if *a.instConfig.Plain {
534
msg := "Running in plain mode. Mounts, dynamic port forwarding, containerd, etc. will be ignored. Guest agent will not be running."
535
for _, port := range a.instConfig.PortForwards {
536
if port.Static {
537
msg += " Static port forwarding is allowed." //nolint:modernize // stringsbuilder is not needed
538
break
539
}
540
}
541
logrus.Info(msg)
542
}
543
a.cleanUp(func() error {
544
// Skip ExitMaster when the control socket does not exist.
545
// On Windows, the ControlMaster is used only for SSH port forwarding.
546
if !sshutil.IsControlMasterExisting(a.instDir) {
547
return nil
548
}
549
logrus.Debugf("shutting down the SSH master")
550
if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil {
551
logrus.WithError(exitMasterErr).Warn("failed to exit SSH master")
552
}
553
return nil
554
})
555
var errs []error
556
if err := a.waitForRequirements("essential", a.essentialRequirements()); err != nil {
557
errs = append(errs, err)
558
}
559
if *a.instConfig.SSH.ForwardAgent {
560
faScript := `#!/bin/bash
561
set -eux -o pipefail
562
sudo mkdir -p -m 700 /run/host-services
563
sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock
564
sudo chown -R "${USER}" /run/host-services`
565
faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock"
566
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc)
567
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
568
if err != nil {
569
errs = append(errs, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err))
570
}
571
}
572
if *a.instConfig.MountType == limatype.REVSSHFS && !*a.instConfig.Plain {
573
mounts, err := a.setupMounts(ctx)
574
if err != nil {
575
errs = append(errs, err)
576
}
577
a.cleanUp(func() error {
578
var unmountErrs []error
579
for _, m := range mounts {
580
if unmountErr := m.close(); unmountErr != nil {
581
unmountErrs = append(unmountErrs, unmountErr)
582
}
583
}
584
return errors.Join(unmountErrs...)
585
})
586
}
587
if len(a.instConfig.AdditionalDisks) > 0 {
588
a.cleanUp(func() error {
589
var unlockErrs []error
590
for _, d := range a.instConfig.AdditionalDisks {
591
disk, inspectErr := store.InspectDisk(d.Name, d.FSType)
592
if inspectErr != nil {
593
unlockErrs = append(unlockErrs, inspectErr)
594
continue
595
}
596
logrus.Infof("Unmounting disk %q", disk.Name)
597
if unlockErr := disk.Unlock(); unlockErr != nil {
598
unlockErrs = append(unlockErrs, unlockErr)
599
}
600
}
601
return errors.Join(unlockErrs...)
602
})
603
}
604
605
staticPortForwards := a.separateStaticPortForwards()
606
a.addStaticPortForwardsFromList(ctx, staticPortForwards)
607
608
if !*a.instConfig.Plain {
609
go a.watchGuestAgentEvents(ctx)
610
go a.startTimeSync(ctx)
611
if a.showProgress {
612
cloudInitDone := make(chan struct{})
613
go func() {
614
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
615
defer cancel()
616
617
a.watchCloudInitProgress(timeoutCtx)
618
close(cloudInitDone)
619
}()
620
621
go func() {
622
<-cloudInitDone
623
logrus.Debug("Cloud-init monitoring completed, VM is fully ready")
624
}()
625
}
626
}
627
if err := a.waitForRequirements("optional", a.optionalRequirements()); err != nil {
628
errs = append(errs, err)
629
}
630
if !*a.instConfig.Plain {
631
logrus.Info("Waiting for the guest agent to be running")
632
select {
633
case <-a.guestAgentAliveCh:
634
// NOP
635
case <-time.After(time.Minute):
636
errs = append(errs, errors.New("guest agent does not seem to be running; port forwards will not work"))
637
}
638
}
639
if err := a.waitForRequirements("final", a.finalRequirements()); err != nil {
640
errs = append(errs, err)
641
}
642
// Copy all config files _after_ the requirements are done
643
for _, rule := range a.instConfig.CopyToHost {
644
sshAddress, sshPort := a.sshAddressPort()
645
if err := copyToHost(ctx, a.sshConfig, sshAddress, sshPort, rule.HostFile, rule.GuestFile); err != nil {
646
errs = append(errs, err)
647
}
648
}
649
a.cleanUp(func() error {
650
var rmErrs []error
651
for _, rule := range a.instConfig.CopyToHost {
652
if rule.DeleteOnStop {
653
logrus.Infof("Deleting %s", rule.HostFile)
654
if err := os.RemoveAll(rule.HostFile); err != nil {
655
rmErrs = append(rmErrs, err)
656
}
657
}
658
}
659
return errors.Join(rmErrs...)
660
})
661
return errors.Join(errs...)
662
}
663
664
// cleanUp registers a cleanup function to be called when the host agent is stopped.
665
// The cleanup functions are called before the context is cancelled, in the reverse order of their registration.
666
func (a *HostAgent) cleanUp(fn func() error) {
667
a.onCloseMu.Lock()
668
defer a.onCloseMu.Unlock()
669
a.onClose = append(a.onClose, fn)
670
}
671
672
func (a *HostAgent) close() error {
673
a.onCloseMu.Lock()
674
defer a.onCloseMu.Unlock()
675
logrus.Infof("Shutting down the host agent")
676
var errs []error
677
for i := len(a.onClose) - 1; i >= 0; i-- {
678
f := a.onClose[i]
679
if err := f(); err != nil {
680
errs = append(errs, err)
681
}
682
}
683
return errors.Join(errs...)
684
}
685
686
func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
687
// TODO: use vSock (when QEMU for macOS gets support for vSock)
688
689
// Setup all socket forwards and defer their teardown
690
if !(a.driver.Info().Features.SkipSocketForwarding) {
691
logrus.Debugf("Forwarding unix sockets")
692
sshAddress, sshPort := a.sshAddressPort()
693
for _, rule := range a.instConfig.PortForwards {
694
if rule.GuestSocket != "" {
695
local := hostAddress(rule, &guestagentapi.IPPort{})
696
_ = forwardSSH(ctx, a.sshConfig, sshAddress, sshPort, local, rule.GuestSocket, verbForward, rule.Reverse)
697
}
698
}
699
}
700
701
localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock)
702
remoteUnix := "/run/lima-guestagent.sock"
703
704
a.cleanUp(func() error {
705
logrus.Debugf("Stop forwarding unix sockets")
706
var errs []error
707
sshAddress, sshPort := a.sshAddressPort()
708
for _, rule := range a.instConfig.PortForwards {
709
if rule.GuestSocket != "" {
710
local := hostAddress(rule, &guestagentapi.IPPort{})
711
// using ctx.Background() because ctx has already been cancelled
712
if err := forwardSSH(context.Background(), a.sshConfig, sshAddress, sshPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil {
713
errs = append(errs, err)
714
}
715
}
716
}
717
if a.driver.ForwardGuestAgent() {
718
if err := forwardSSH(context.Background(), a.sshConfig, sshAddress, sshPort, localUnix, remoteUnix, verbCancel, false); err != nil {
719
errs = append(errs, err)
720
}
721
}
722
return errors.Join(errs...)
723
})
724
725
go func() {
726
if a.instConfig.MountInotify != nil && *a.instConfig.MountInotify {
727
if a.client == nil || !isGuestAgentSocketAccessible(ctx, a.client) {
728
if a.driver.ForwardGuestAgent() {
729
sshAddress, sshPort := a.sshAddressPort()
730
_ = forwardSSH(ctx, a.sshConfig, sshAddress, sshPort, localUnix, remoteUnix, verbForward, false)
731
}
732
}
733
err := a.startInotify(ctx)
734
if err != nil {
735
logrus.WithError(err).Warn("failed to start inotify")
736
}
737
}
738
}()
739
740
// ensure close before ctx is cancelled
741
a.cleanUp(a.grpcPortForwarder.Close)
742
743
for {
744
if a.client == nil || !isGuestAgentSocketAccessible(ctx, a.client) {
745
if a.driver.ForwardGuestAgent() {
746
sshAddress, sshPort := a.sshAddressPort()
747
_ = forwardSSH(ctx, a.sshConfig, sshAddress, sshPort, localUnix, remoteUnix, verbForward, false)
748
}
749
}
750
client, err := a.getOrCreateClient(ctx)
751
if err == nil {
752
if err := a.processGuestAgentEvents(ctx, client); err != nil {
753
if !errors.Is(err, context.Canceled) {
754
logrus.WithError(err).Warn("guest agent events closed unexpectedly")
755
}
756
}
757
} else {
758
if !strings.Contains(err.Error(), context.Canceled.Error()) {
759
logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly")
760
}
761
}
762
select {
763
case <-ctx.Done():
764
return
765
case <-time.After(10 * time.Second):
766
}
767
}
768
}
769
770
func (a *HostAgent) addStaticPortForwardsFromList(ctx context.Context, staticPortForwards []limatype.PortForward) {
771
sshAddress, sshPort := a.sshAddressPort()
772
for _, rule := range staticPortForwards {
773
if rule.GuestSocket == "" {
774
guest := &guestagentapi.IPPort{
775
Ip: rule.GuestIP.String(),
776
Port: int32(rule.GuestPort),
777
Protocol: rule.Proto,
778
}
779
local, remote := a.portForwarder.forwardingAddresses(guest)
780
if local != "" {
781
logrus.Infof("Setting up static TCP forwarding from %s to %s", remote, local)
782
if err := forwardTCP(ctx, a.sshConfig, sshAddress, sshPort, local, remote, verbForward); err != nil {
783
logrus.WithError(err).Warnf("failed to set up static TCP forwarding %s -> %s", remote, local)
784
}
785
}
786
}
787
}
788
}
789
790
// separateStaticPortForwards separates static port forwards from a.instConfig.PortForwards,
791
// updates a.instConfig.PortForwards to contain only non-static port forwards,
792
// and returns the list of static port forwards.
793
func (a *HostAgent) separateStaticPortForwards() []limatype.PortForward {
794
staticPortForwards := make([]limatype.PortForward, 0, len(a.instConfig.PortForwards))
795
nonStaticPortForwards := make([]limatype.PortForward, 0, len(a.instConfig.PortForwards))
796
797
for i := range len(a.instConfig.PortForwards) {
798
rule := a.instConfig.PortForwards[i]
799
if rule.Static {
800
logrus.Debugf("Found static port forward: guest=%d host=%d", rule.GuestPort, rule.HostPort)
801
staticPortForwards = append(staticPortForwards, rule)
802
} else {
803
logrus.Debugf("Found non-static port forward: guest=%d host=%d", rule.GuestPort, rule.HostPort)
804
nonStaticPortForwards = append(nonStaticPortForwards, rule)
805
}
806
}
807
808
logrus.Debugf("Static port forwards: %d, Non-static port forwards: %d", len(staticPortForwards), len(nonStaticPortForwards))
809
810
a.instConfig.PortForwards = nonStaticPortForwards
811
return staticPortForwards
812
}
813
814
func isGuestAgentSocketAccessible(ctx context.Context, client *guestagentclient.GuestAgentClient) bool {
815
_, err := client.Info(ctx)
816
return err == nil
817
}
818
819
func (a *HostAgent) getOrCreateClient(ctx context.Context) (*guestagentclient.GuestAgentClient, error) {
820
a.clientMu.Lock()
821
defer a.clientMu.Unlock()
822
if a.client != nil && isGuestAgentSocketAccessible(ctx, a.client) {
823
return a.client, nil
824
}
825
var err error
826
a.client, err = guestagentclient.NewGuestAgentClient(a.createConnection)
827
return a.client, err
828
}
829
830
func (a *HostAgent) createConnection(ctx context.Context) (net.Conn, error) {
831
conn, _, err := a.driver.GuestAgentConn(ctx)
832
// default to forwarded sock
833
if conn == nil && err == nil {
834
var d net.Dialer
835
conn, err = d.DialContext(ctx, "unix", filepath.Join(a.instDir, filenames.GuestAgentSock))
836
}
837
return conn, err
838
}
839
840
func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestagentclient.GuestAgentClient) error {
841
info, err := client.Info(ctx)
842
if err != nil {
843
return err
844
}
845
logrus.Info("Guest agent is running")
846
a.guestAgentAliveChOnce.Do(func() {
847
close(a.guestAgentAliveCh)
848
})
849
850
logrus.Debugf("guest agent info: %+v", info)
851
852
onEvent := func(ev *guestagentapi.Event) {
853
logrus.Debugf("guest agent event: %+v", ev)
854
for _, f := range ev.Errors {
855
logrus.Warnf("received error from the guest: %q", f)
856
}
857
// History of the default value of useSSHFwd:
858
// - v0.1.0: true (effectively)
859
// - v1.0.0: false
860
// - v1.0.1: true
861
// - v1.1.0-beta.0: false
862
useSSHFwd := false
863
if envVar := os.Getenv("LIMA_SSH_PORT_FORWARDER"); envVar != "" {
864
b, err := strconv.ParseBool(envVar)
865
if err != nil {
866
logrus.WithError(err).Warnf("invalid LIMA_SSH_PORT_FORWARDER value %q", envVar)
867
} else {
868
useSSHFwd = b
869
}
870
}
871
if useSSHFwd {
872
a.portForwarder.OnEvent(ctx, ev)
873
} else {
874
dialContext := portfwd.DialContextToGRPCTunnel(client)
875
a.grpcPortForwarder.OnEvent(ctx, dialContext, ev)
876
}
877
}
878
879
if err := client.Events(ctx, onEvent); err != nil {
880
if status.Code(err) == codes.Canceled {
881
return context.Canceled
882
}
883
return err
884
}
885
return io.EOF
886
}
887
888
const (
889
verbForward = "forward"
890
verbCancel = "cancel"
891
)
892
893
func executeSSH(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, command ...string) error {
894
args := sshConfig.Args()
895
args = append(args,
896
"-p", strconv.Itoa(sshPort),
897
sshAddress,
898
"--",
899
)
900
args = append(args, command...)
901
cmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)
902
if out, err := cmd.Output(); err != nil {
903
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
904
}
905
return nil
906
}
907
908
func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string, reverse bool) error {
909
args := sshConfig.Args()
910
args = append(args,
911
"-T",
912
"-O", verb,
913
)
914
if reverse {
915
args = append(args,
916
"-R", remote+":"+local,
917
)
918
} else {
919
args = append(args,
920
"-L", local+":"+remote,
921
)
922
}
923
args = append(args,
924
"-N",
925
"-f",
926
"-p", strconv.Itoa(sshPort),
927
sshAddress,
928
"--",
929
)
930
if strings.HasPrefix(local, "/") {
931
switch verb {
932
case verbForward:
933
if reverse {
934
logrus.Infof("Forwarding %q (host) to %q (guest)", local, remote)
935
if err := executeSSH(ctx, sshConfig, sshAddress, sshPort, "rm", "-f", remote); err != nil {
936
logrus.WithError(err).Warnf("Failed to clean up %q (guest) before setting up forwarding", remote)
937
}
938
} else {
939
logrus.Infof("Forwarding %q (guest) to %q (host)", remote, local)
940
if err := os.RemoveAll(local); err != nil {
941
logrus.WithError(err).Warnf("Failed to clean up %q (host) before setting up forwarding", local)
942
}
943
}
944
if err := os.MkdirAll(filepath.Dir(local), 0o750); err != nil {
945
return fmt.Errorf("can't create directory for local socket %q: %w", local, err)
946
}
947
case verbCancel:
948
if reverse {
949
logrus.Infof("Stopping forwarding %q (host) to %q (guest)", local, remote)
950
if err := executeSSH(ctx, sshConfig, sshAddress, sshPort, "rm", "-f", remote); err != nil {
951
logrus.WithError(err).Warnf("Failed to clean up %q (guest) after stopping forwarding", remote)
952
}
953
} else {
954
logrus.Infof("Stopping forwarding %q (guest) to %q (host)", remote, local)
955
defer func() {
956
if err := os.RemoveAll(local); err != nil {
957
logrus.WithError(err).Warnf("Failed to clean up %q (host) after stopping forwarding", local)
958
}
959
}()
960
}
961
default:
962
panic(fmt.Errorf("invalid verb %q", verb))
963
}
964
}
965
cmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)
966
logrus.Debugf("Running %q", cmd)
967
if out, err := cmd.Output(); err != nil {
968
if verb == verbForward && strings.HasPrefix(local, "/") {
969
if reverse {
970
logrus.WithError(err).Warnf("Failed to set up forward from %q (host) to %q (guest)", local, remote)
971
if err := executeSSH(ctx, sshConfig, sshAddress, sshPort, "rm", "-f", remote); err != nil {
972
logrus.WithError(err).Warnf("Failed to clean up %q (guest) after forwarding failed", remote)
973
}
974
} else {
975
logrus.WithError(err).Warnf("Failed to set up forward from %q (guest) to %q (host)", remote, local)
976
if removeErr := os.RemoveAll(local); removeErr != nil {
977
logrus.WithError(removeErr).Warnf("Failed to clean up %q (host) after forwarding failed", local)
978
}
979
}
980
}
981
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
982
}
983
return nil
984
}
985
986
func (a *HostAgent) watchCloudInitProgress(ctx context.Context) {
987
exitReason := "Cloud-init monitoring completed successfully"
988
var cmd *exec.Cmd
989
990
defer func() {
991
a.emitCloudInitProgressEvent(context.Background(), &events.CloudInitProgress{
992
Active: false,
993
Completed: true,
994
LogLine: exitReason,
995
})
996
logrus.Debug("Cloud-init progress monitoring completed")
997
}()
998
999
logrus.Debug("Starting cloud-init progress monitoring")
1000
1001
a.emitCloudInitProgressEvent(ctx, &events.CloudInitProgress{
1002
Active: true,
1003
})
1004
1005
sshAddress, sshPort := a.sshAddressPort()
1006
args := a.sshConfig.Args()
1007
args = append(args,
1008
"-p", strconv.Itoa(sshPort),
1009
sshAddress,
1010
"sh", "-c",
1011
`"if command -v systemctl >/dev/null 2>&1 && systemctl is-enabled -q cloud-init-main.service; then
1012
sudo journalctl -u cloud-init-main.service -b -S @0 -o cat -f
1013
else
1014
sudo tail -n +$(sudo awk '
1015
BEGIN{b=1; e=1}
1016
/^Cloud-init.* finished/{e=NR}
1017
/.*/{if(NR>e){b=e+1}}
1018
END{print b}
1019
' /var/log/cloud-init-output.log) -f /var/log/cloud-init-output.log
1020
fi"`,
1021
)
1022
1023
cmd = exec.CommandContext(ctx, a.sshConfig.Binary(), args...)
1024
stdout, err := cmd.StdoutPipe()
1025
if err != nil {
1026
logrus.WithError(err).Warn("Failed to create stdout pipe for cloud-init monitoring")
1027
exitReason = "Failed to create stdout pipe for cloud-init monitoring"
1028
return
1029
}
1030
1031
if err := cmd.Start(); err != nil {
1032
logrus.WithError(err).Warn("Failed to start cloud-init monitoring command")
1033
exitReason = "Failed to start cloud-init monitoring command"
1034
return
1035
}
1036
1037
scanner := bufio.NewScanner(stdout)
1038
cloudInitMainServiceStarted := false
1039
cloudInitFinished := false
1040
1041
for scanner.Scan() {
1042
line := scanner.Text()
1043
if strings.TrimSpace(line) == "" {
1044
continue
1045
}
1046
1047
if !cloudInitMainServiceStarted {
1048
if isStartedCloudInitMainService(line) {
1049
logrus.Debug("cloud-init-main.service started detected via log pattern")
1050
cloudInitMainServiceStarted = true
1051
} else if !cloudInitFinished {
1052
if isCloudInitFinished(line) {
1053
logrus.Debug("Cloud-init completion detected via log pattern")
1054
cloudInitFinished = true
1055
}
1056
}
1057
} else if !cloudInitFinished && isDeactivatedCloudInitMainService(line) {
1058
logrus.Debug("cloud-init-main.service deactivated detected via log pattern")
1059
cloudInitFinished = true
1060
}
1061
1062
a.emitCloudInitProgressEvent(ctx, &events.CloudInitProgress{
1063
Active: !cloudInitFinished,
1064
LogLine: line,
1065
Completed: cloudInitFinished,
1066
})
1067
1068
if cloudInitFinished {
1069
logrus.Debug("Breaking from cloud-init monitoring loop - completion detected")
1070
if cmd.Process != nil {
1071
logrus.Debug("Killing cloud-init monitoring process after completion")
1072
if err := cmd.Process.Kill(); err != nil {
1073
logrus.WithError(err).Debug("Failed to kill cloud-init monitoring process")
1074
}
1075
}
1076
break
1077
}
1078
}
1079
1080
if err := cmd.Wait(); err != nil {
1081
if ctx.Err() == context.DeadlineExceeded {
1082
logrus.Warn("Cloud-init monitoring timed out after 10 minutes")
1083
exitReason = "Cloud-init monitoring timed out after 10 minutes"
1084
return
1085
}
1086
logrus.WithError(err).Debug("SSH command finished (expected when cloud-init completes)")
1087
}
1088
1089
if !cloudInitFinished {
1090
logrus.Debug("Connection dropped, checking for any remaining cloud-init logs")
1091
1092
finalArgs := a.sshConfig.Args()
1093
finalArgs = append(finalArgs,
1094
"-p", strconv.Itoa(sshPort),
1095
sshAddress,
1096
"sudo", "tail", "-n", "20", "/var/log/cloud-init-output.log",
1097
)
1098
1099
finalCmd := exec.CommandContext(ctx, a.sshConfig.Binary(), finalArgs...)
1100
if finalOutput, err := finalCmd.Output(); err == nil {
1101
for line := range strings.SplitSeq(string(finalOutput), "\n") {
1102
if strings.TrimSpace(line) != "" {
1103
if !cloudInitFinished {
1104
cloudInitFinished = isCloudInitFinished(line)
1105
}
1106
1107
a.emitCloudInitProgressEvent(ctx, &events.CloudInitProgress{
1108
Active: !cloudInitFinished,
1109
LogLine: line,
1110
Completed: cloudInitFinished,
1111
})
1112
}
1113
}
1114
}
1115
}
1116
}
1117
1118
func isCloudInitFinished(line string) bool {
1119
line = strings.ToLower(strings.TrimSpace(line))
1120
return strings.Contains(line, "cloud-init") && strings.Contains(line, "finished")
1121
}
1122
1123
func isStartedCloudInitMainService(line string) bool {
1124
line = strings.ToLower(strings.TrimSpace(line))
1125
return strings.HasPrefix(line, "started cloud-init-main.service")
1126
}
1127
1128
func isDeactivatedCloudInitMainService(line string) bool {
1129
line = strings.ToLower(strings.TrimSpace(line))
1130
// Deactivated event lines end with a line reporting consumed CPU time, etc.
1131
return strings.HasPrefix(line, "cloud-init-main.service: consumed")
1132
}
1133
1134
func copyToHost(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote string) error {
1135
args := sshConfig.Args()
1136
args = append(args,
1137
"-p", strconv.Itoa(sshPort),
1138
sshAddress,
1139
"--",
1140
)
1141
args = append(args,
1142
"sudo",
1143
"cat",
1144
remote,
1145
)
1146
logrus.Infof("Copying config from %s to %s", remote, local)
1147
if err := os.MkdirAll(filepath.Dir(local), 0o700); err != nil {
1148
return fmt.Errorf("can't create directory for local file %q: %w", local, err)
1149
}
1150
cmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)
1151
out, err := cmd.Output()
1152
if err != nil {
1153
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
1154
}
1155
if err := os.WriteFile(local, out, 0o600); err != nil {
1156
return fmt.Errorf("can't write to local file %q: %w", local, err)
1157
}
1158
return nil
1159
}
1160
1161