This repository has no description
1package microvm
2
3import (
4 "context"
5 "crypto/sha256"
6 _ "embed"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "log/slog"
12 "net"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "strconv"
17 "strings"
18 "sync"
19 "time"
20
21 "github.com/digitalocean/go-qemu/qmp"
22 "github.com/google/uuid"
23)
24
25const (
26 defaultQMPTimeout = 10 * time.Second
27 outerSlirpCIDR = "10.0.2.0/24"
28 innerSlirpNet = "10.0.3.0/24"
29 innerSlirpHost = "10.0.3.2"
30 innerSlirpDNS = "10.0.3.3"
31 innerSlirpDHCP = "10.0.3.15"
32 netnsTapName = "tap0"
33 netnsMTU = "65520"
34)
35
36type QEMUConfig struct {
37 Image ImageSpec
38 BootTimeout time.Duration
39 CID uint32
40 EnableKVM bool
41 QEMULogPath string
42 QMPPath string
43 SerialLogPath string
44 WorkDir string
45 VolumePaths map[string]string
46 VolumeBaseName string
47 Cgroup CgroupLimits
48 Dev bool
49}
50
51type QEMUVMHandle struct {
52 cid uint32
53 Process *os.Process
54 qemuLogPath string
55 QMPMon *qmp.SocketMonitor
56 QMPPath string
57 serialLogPath string
58 workDir string
59
60 qmpSocketPath string
61
62 cmd *exec.Cmd
63 done chan struct{}
64 qemuLogFile *os.File
65 cgroup *CgroupHandle
66 slirpCmd *exec.Cmd
67 slirpExit *os.File
68 waitErr error
69 waitErrMu sync.Mutex
70}
71
72type qemuRunner struct{}
73
74func (qemuRunner) Validate(spec ImageSpec, enableKVM bool) error {
75 if _, err := exec.LookPath(spec.RunnerCmd()); err != nil {
76 return fmt.Errorf("required host command %q not found in PATH: %w", spec.RunnerCmd(), err)
77 }
78 if _, err := os.Stat("/dev/vhost-vsock"); err != nil {
79 return fmt.Errorf("microvm requires /dev/vhost-vsock for vhost-vsock-device: %w", err)
80 }
81 if enableKVM {
82 if _, err := os.Stat("/dev/kvm"); err != nil {
83 return fmt.Errorf("microvm KVM was requested but /dev/kvm is not accessible: %w", err)
84 }
85 }
86 if len(spec.NetworkInterfaces) > 0 {
87 if _, err := os.Stat("/dev/net/tun"); err != nil {
88 return fmt.Errorf("microvm slirp4netns networking requires /dev/net/tun: %w", err)
89 }
90 for _, cmd := range []string{"ip", "mount", "slirp4netns", "unshare"} {
91 if _, err := exec.LookPath(cmd); err != nil {
92 return fmt.Errorf("required host command %q not found in PATH: %w", cmd, err)
93 }
94 }
95 }
96 return nil
97}
98
99func (qemuRunner) Start(ctx context.Context, cfg VMConfig, volumePaths map[string]string, logger *slog.Logger) (VMHandle, error) {
100 bootTimeout := cfg.BootTimeout
101 if bootTimeout == 0 {
102 bootTimeout = 10 * time.Second
103 }
104 return StartQEMU(ctx, QEMUConfig{
105 Image: cfg.Image,
106 BootTimeout: bootTimeout,
107 CID: cfg.CID,
108 EnableKVM: cfg.EnableKVM,
109 WorkDir: cfg.WorkDir,
110 VolumePaths: volumePaths,
111 Cgroup: cfg.Cgroup,
112 Dev: cfg.Dev,
113 }, logger)
114}
115
116// we hash the workDir to get a deterministic qmpSock path
117// since they are AF_UNIX sockets, they are bound by a 108 char long path limit...
118func qmpSocketPath(workDir string) string {
119 base := filepath.Dir(workDir)
120 if workDir == "" {
121 base = os.TempDir()
122 }
123 sum := sha256.Sum256([]byte(workDir))
124 return filepath.Join(base, hex.EncodeToString(sum[:8])+".qmp.sock")
125}
126
127// the child sees ExtraFiles at fd 3 and up (stdio owns 0-2), and the
128// qmp socket is our only entry
129const qemuExtraFD = 3
130
131func listenQMP(path string) (*net.UnixListener, error) {
132 _ = os.Remove(path)
133 l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
134 if err != nil {
135 return nil, fmt.Errorf("listen qmp socket: %w", err)
136 }
137 if err := os.Chmod(path, 0o600); err != nil {
138 _ = l.Close()
139 return nil, fmt.Errorf("chmod qmp socket: %w", err)
140 }
141 return l, nil
142}
143
144func StartQEMU(ctx context.Context, cfg QEMUConfig, logger *slog.Logger) (VMHandle, error) {
145 if logger == nil {
146 logger = slog.Default()
147 }
148
149 workDir := cfg.WorkDir
150
151 handle := &QEMUVMHandle{
152 workDir: workDir,
153 }
154
155 var ok bool
156 defer func() {
157 if !ok {
158 if detail := VMCrashLog(handle); detail != "" {
159 logger.Error("microVM failed to start", "cid", handle.cid, "detail", detail)
160 }
161 _ = handle.Close()
162 }
163 }()
164
165 cid := cfg.CID
166 if cid == 0 {
167 var err error
168 cid, err = AllocateCID()
169 if err != nil {
170 return nil, err
171 }
172 }
173 if cid < minGuestCID {
174 return nil, fmt.Errorf("guest CID must be >= %d", minGuestCID)
175 }
176 handle.cid = cid
177
178 volumePaths := cfg.VolumePaths
179
180 qemuLogPath := cfg.QEMULogPath
181 if qemuLogPath == "" {
182 qemuLogPath = filepath.Join(workDir, "qemu.log")
183 }
184 qemuLogFile, err := createParentedFile(qemuLogPath)
185 if err != nil {
186 return nil, err
187 }
188 handle.qemuLogPath = qemuLogPath
189 handle.qemuLogFile = qemuLogFile
190
191 serialLogPath := cfg.SerialLogPath
192 if serialLogPath == "" {
193 serialLogPath = filepath.Join(workDir, "serial.log")
194 }
195 if err := os.MkdirAll(filepath.Dir(serialLogPath), 0o755); err != nil {
196 return nil, fmt.Errorf("create serial log directory: %w", err)
197 }
198 handle.serialLogPath = serialLogPath
199
200 qmpPath := cfg.QMPPath
201 if qmpPath == "" {
202 qmpPath = qmpSocketPath(workDir)
203 handle.qmpSocketPath = qmpPath
204 }
205 handle.QMPPath = qmpPath
206
207 // qmp is full control of a tenant vm, so we bind it at 0600 and hand
208 // qemu the inherited fd. a qemu-bound socket would sit at umask perms
209 // in a shared dir, so if a job ever escaped its vm it could read and
210 // drive every other job's vm too
211 qmpListener, err := listenQMP(qmpPath)
212 if err != nil {
213 return nil, err
214 }
215 defer qmpListener.Close()
216 qmpFile, err := qmpListener.File()
217 if err != nil {
218 return nil, fmt.Errorf("get qmp socket file: %w", err)
219 }
220 defer qmpFile.Close()
221
222 qemuCmd := cfg.Image.RunnerCmd()
223 qemuBinary, err := exec.LookPath(qemuCmd)
224 if err != nil {
225 return nil, fmt.Errorf("%s command not found in PATH: %w", qemuCmd, err)
226 }
227
228 args, err := qemuArgs(qemuArgsConfig{
229 Image: cfg.Image,
230 CID: cid,
231 EnableKVM: cfg.EnableKVM,
232 SerialLogPath: serialLogPath,
233 VolumePaths: volumePaths,
234 })
235 if err != nil {
236 return nil, err
237 }
238
239 cmd, slirpNet, err := qemuCommand(ctx, qemuBinary, args, cfg.Image, workDir, cfg.Dev, qmpFile)
240 if err != nil {
241 return nil, err
242 }
243 cmd.Env = append(os.Environ(), "TMPDIR="+workDir)
244 cmd.Stdout = qemuLogFile
245 cmd.Stderr = qemuLogFile
246
247 cgroup, err := prepareCgroup(cfg.Cgroup, logger)
248 if err != nil {
249 return nil, err
250 }
251 handle.cgroup = cgroup
252
253 logger.Info("starting qemu microvm", "cid", cid, "workDir", workDir, "serialLog", serialLogPath, "qmp", qmpPath)
254 if err := cmd.Start(); err != nil {
255 return nil, fmt.Errorf("starting qemu: %w", err)
256 }
257 handle.cmd = cmd
258 handle.Process = cmd.Process
259 handle.done = make(chan struct{})
260 go func() {
261 err := cmd.Wait()
262 handle.waitErrMu.Lock()
263 handle.waitErr = err
264 handle.waitErrMu.Unlock()
265 close(handle.done)
266 }()
267
268 if err := cgroup.AddProcess(cmd.Process.Pid, logger); err != nil {
269 return nil, err
270 }
271
272 if slirpNet != nil {
273 handle.slirpCmd, handle.slirpExit, err = slirpNet.Start(ctx, qemuLogFile, logger)
274 if err != nil {
275 return nil, err
276 }
277 if handle.slirpCmd != nil && handle.slirpCmd.Process != nil {
278 if err := cgroup.AddProcess(handle.slirpCmd.Process.Pid, logger); err != nil {
279 return nil, err
280 }
281 }
282 }
283
284 qmpTimeout := cfg.BootTimeout
285 if qmpTimeout == 0 {
286 qmpTimeout = defaultQMPTimeout
287 }
288 qmpCtx, cancelQMP := context.WithTimeout(ctx, qmpTimeout)
289 defer cancelQMP()
290 if err := handle.waitForQMP(qmpCtx, qmpTimeout); err != nil {
291 return nil, err
292 }
293
294 status, err := handle.waitForQMPRunning(qmpCtx, qmpTimeout)
295 if err != nil {
296 return nil, err
297 }
298 logger.Info("qemu microvm running", "cid", cid, "status", status)
299
300 ok = true
301 return handle, nil
302}
303
304func (h *QEMUVMHandle) Wait() error {
305 if h == nil || h.done == nil {
306 return nil
307 }
308 <-h.done
309 h.waitErrMu.Lock()
310 defer h.waitErrMu.Unlock()
311 return h.waitErr
312}
313
314func (h *QEMUVMHandle) WaitContext(ctx context.Context) error {
315 if h == nil || h.done == nil {
316 return nil
317 }
318 select {
319 case <-h.done:
320 h.waitErrMu.Lock()
321 defer h.waitErrMu.Unlock()
322 return h.waitErr
323 case <-ctx.Done():
324 return ctx.Err()
325 }
326}
327
328func (h *QEMUVMHandle) Kill() error {
329 if h == nil || h.Process == nil {
330 return nil
331 }
332 return h.Process.Kill()
333}
334
335func (h *QEMUVMHandle) Shutdown(ctx context.Context) error {
336 if h == nil {
337 return nil
338 }
339 if h.QMPMon != nil {
340 if err := h.QMPSystemPowerdown(); err != nil {
341 // dead qmp socket means qemu exited concurrently so we wait
342 select {
343 case <-h.done:
344 return nil
345 case <-ctx.Done():
346 return err
347 }
348 }
349 }
350 if h.done == nil {
351 return nil
352 }
353 select {
354 case <-h.done:
355 return h.Wait()
356 case <-ctx.Done():
357 _ = h.Kill()
358 _ = h.Wait()
359 return ctx.Err()
360 }
361}
362
363func (h *QEMUVMHandle) Close() error {
364 if h == nil {
365 return nil
366 }
367
368 var closeErr error
369 if h.QMPMon != nil {
370 closeErr = errors.Join(closeErr, h.QMPMon.Disconnect())
371 h.QMPMon = nil
372 }
373 if h.Process != nil {
374 _ = h.Process.Kill()
375 _ = h.Wait()
376 }
377 if h.slirpExit != nil {
378 _ = h.slirpExit.Close()
379 h.slirpExit = nil
380 }
381 if h.slirpCmd != nil && h.slirpCmd.Process != nil {
382 _ = h.slirpCmd.Process.Kill()
383 _ = h.slirpCmd.Wait()
384 h.slirpCmd = nil
385 }
386 if h.qemuLogFile != nil {
387 closeErr = errors.Join(closeErr, h.qemuLogFile.Close())
388 h.qemuLogFile = nil
389 }
390 if h.cgroup != nil {
391 closeErr = errors.Join(closeErr, h.cgroup.Close())
392 h.cgroup = nil
393 }
394 if h.qmpSocketPath != "" {
395 if err := os.Remove(h.qmpSocketPath); err != nil && !os.IsNotExist(err) {
396 closeErr = errors.Join(closeErr, err)
397 }
398 h.qmpSocketPath = ""
399 }
400 return closeErr
401}
402
403func (h *QEMUVMHandle) QMPRun(command qmp.Command) ([]byte, error) {
404 if h == nil || h.QMPMon == nil {
405 return nil, fmt.Errorf("qmp monitor is not connected")
406 }
407 data, err := json.Marshal(command)
408 if err != nil {
409 return nil, err
410 }
411 return h.QMPMon.Run(data)
412}
413
414func (h *QEMUVMHandle) QMPQueryStatus() (string, error) {
415 raw, err := h.QMPRun(qmp.Command{Execute: "query-status"})
416 if err != nil {
417 return "", fmt.Errorf("qmp query-status failed: %w", err)
418 }
419
420 var resp struct {
421 Return *struct {
422 Status string `json:"status"`
423 } `json:"return"`
424 }
425 if err := json.Unmarshal(raw, &resp); err != nil {
426 return "", fmt.Errorf("qmp query-status parse %q: %w", raw, err)
427 }
428 if resp.Return == nil {
429 return "", fmt.Errorf("qmp query-status missing return: %s", raw)
430 }
431 if resp.Return.Status == "" {
432 return "", fmt.Errorf("qmp query-status missing return.status: %s", raw)
433 }
434 return resp.Return.Status, nil
435}
436
437func (h *QEMUVMHandle) waitForQMPRunning(ctx context.Context, timeout time.Duration) (string, error) {
438 statusCtx, cancel := context.WithTimeout(ctx, timeout)
439 defer cancel()
440
441 ticker := time.NewTicker(25 * time.Millisecond)
442 defer ticker.Stop()
443
444 var lastStatus string
445 var lastErr error
446
447 for {
448 status, err := h.QMPQueryStatus()
449 if err != nil {
450 lastErr = err
451 } else {
452 lastErr = nil
453 lastStatus = status
454
455 switch status {
456 case "running":
457 return status, nil
458 case "shutdown", "internal-error", "io-error", "guest-panicked":
459 return "", fmt.Errorf("qemu guest entered unhealthy state before running (status: %s)", status)
460 }
461 }
462
463 select {
464 case <-statusCtx.Done():
465 if lastErr != nil {
466 return "", fmt.Errorf("qemu guest did not reach running state (last status: %s): %w", lastStatus, errors.Join(statusCtx.Err(), lastErr))
467 }
468 return "", fmt.Errorf("qemu guest did not reach running state (last status: %s): %w", lastStatus, statusCtx.Err())
469 case <-h.done:
470 return "", fmt.Errorf("qemu exited before reaching running state: %w", h.Wait())
471 case <-ticker.C:
472 }
473 }
474}
475
476func (h *QEMUVMHandle) QMPSystemPowerdown() error {
477 _, err := h.QMPRun(qmp.Command{Execute: "system_powerdown"})
478 return err
479}
480
481func (h *QEMUVMHandle) Logs() VMLogs {
482 if h == nil {
483 return VMLogs{}
484 }
485 return VMLogs{
486 Serial: h.serialLogPath,
487 Extra: map[string]string{
488 "qemu": h.qemuLogPath,
489 },
490 }
491}
492
493func (h *QEMUVMHandle) CID() uint32 {
494 if h == nil {
495 return 0
496 }
497 return h.cid
498}
499
500func (h *QEMUVMHandle) WorkDir() string {
501 if h == nil {
502 return ""
503 }
504 return h.workDir
505}
506
507func (h *QEMUVMHandle) OOMKilled() bool {
508 if h == nil {
509 return false
510 }
511 return h.cgroup.OOMKilled()
512}
513
514func (h *QEMUVMHandle) waitForQMP(ctx context.Context, timeout time.Duration) error {
515 qmpCtx, cancel := context.WithTimeout(ctx, timeout)
516 defer cancel()
517
518 var lastErr error
519 for {
520 mon, err := qmp.NewSocketMonitor("unix", h.QMPPath, 2*time.Second)
521 if err == nil {
522 if err = mon.Connect(); err == nil {
523 h.QMPMon = mon
524 return nil
525 }
526 _ = mon.Disconnect()
527 }
528 lastErr = err
529
530 select {
531 case <-qmpCtx.Done():
532 return fmt.Errorf("qmp connect timeout: %w", lastErr)
533 case <-h.done:
534 return fmt.Errorf("qemu exited before qmp was ready: %w", h.Wait())
535 case <-time.After(25 * time.Millisecond):
536 }
537 }
538}
539
540func qemuCommand(
541 ctx context.Context,
542 qemuBinary string,
543 args []string,
544 spec ImageSpec,
545 workDir string,
546 dev bool,
547 qmpFile *os.File,
548) (*exec.Cmd, *slirpNamespace, error) {
549 if len(spec.NetworkInterfaces) == 0 {
550 cmd := exec.CommandContext(ctx, qemuBinary, args...)
551 cmd.ExtraFiles = []*os.File{qmpFile}
552 return cmd, nil, nil
553 }
554
555 ipPath, err := exec.LookPath("ip")
556 if err != nil {
557 return nil, nil, fmt.Errorf("ip command not found in PATH: %w", err)
558 }
559 mountPath, err := exec.LookPath("mount")
560 if err != nil {
561 return nil, nil, fmt.Errorf("mount command not found in PATH: %w", err)
562 }
563 unsharePath, err := exec.LookPath("unshare")
564 if err != nil {
565 return nil, nil, fmt.Errorf("unshare command not found in PATH: %w", err)
566 }
567
568 pidFile, resolvPath, wrapperPath, err := prepareQEMUNetnsFiles(workDir, dev)
569 if err != nil {
570 return nil, nil, err
571 }
572
573 cmdArgs := append([]string{
574 "--user",
575 "--map-root-user",
576 "--net",
577 "--mount",
578 "--propagation", "private",
579 "--",
580 wrapperPath,
581 pidFile,
582 ipPath,
583 mountPath,
584 resolvPath,
585 qemuBinary,
586 }, args...)
587
588 cmd := exec.CommandContext(ctx, unsharePath, cmdArgs...)
589 cmd.ExtraFiles = []*os.File{qmpFile}
590
591 return cmd, &slirpNamespace{
592 spec: spec,
593 pidFile: pidFile,
594 dev: dev,
595 }, nil
596}
597
598func prepareQEMUNetnsFiles(workDir string, dev bool) (pidFile, resolvPath, wrapperPath string, err error) {
599 pidFile = filepath.Join(workDir, "qemu-netns.pid")
600 resolvPath = filepath.Join(workDir, "qemu-netns-resolv.conf")
601 wrapperPath = filepath.Join(workDir, "qemu-netns-wrapper")
602
603 // the guest resolves through shuttle on 127.0.0.1. keep qemu's slirp DNS
604 // pointed at an unroutable local resolver inside this network namespace so
605 // direct guest queries to 10.0.3.3 don't bypass the shuttle dns policy.
606 if err := os.WriteFile(resolvPath, []byte("nameserver 127.0.0.1\n"), 0o644); err != nil {
607 return "", "", "", fmt.Errorf("write qemu network namespace resolv.conf: %w", err)
608 }
609
610 if err := writeNetnsWrapper(wrapperPath, dev); err != nil {
611 return "", "", "", fmt.Errorf("write qemu network namespace wrapper: %w", err)
612 }
613
614 return pidFile, resolvPath, wrapperPath, nil
615}
616
617type qemuArgsConfig struct {
618 Image ImageSpec
619 CID uint32
620 EnableKVM bool
621 SerialLogPath string
622 VolumePaths map[string]string
623}
624
625func qemuArgs(cfg qemuArgsConfig) ([]string, error) {
626 uuid := uuid.New()
627
628 b := newArgBuilder(64)
629
630 addQEMUMachineArgs(&b, cfg, uuid)
631 addQEMUStoreArgs(&b, cfg)
632
633 if cfg.EnableKVM {
634 addQEMUKVMArgs(&b, cfg.Image)
635 }
636
637 if err := addQEMUVolumeArgs(&b, cfg); err != nil {
638 return nil, err
639 }
640
641 if err := addQEMUNetworkArgs(&b, cfg.Image.NetworkInterfaces); err != nil {
642 return nil, err
643 }
644
645 b.Optf("-device", "vhost-vsock-device,guest-cid=%d", cfg.CID)
646
647 if len(cfg.Image.RunnerConfig.ExtraArgs) > 0 {
648 b.Add(cfg.Image.RunnerConfig.ExtraArgs...)
649 }
650
651 return b.Args(), nil
652}
653
654func addQEMUMachineArgs(b *argBuilder, cfg qemuArgsConfig, uuid uuid.UUID) {
655 if cfg.Image.RunnerConfig.Machine != "" {
656 b.Opt("-M", cfg.Image.RunnerConfig.Machine)
657 }
658 b.Optf("-m", "%dM", cfg.Image.MemoryMiB)
659 b.Opt("-smp", strconv.Itoa(cfg.Image.VCPUs))
660
661 b.Add(
662 "-nodefaults",
663 "-no-user-config",
664 "-no-reboot",
665 )
666
667 b.Opt("-kernel", cfg.Image.Kernel)
668 b.Opt("-initrd", cfg.Image.Initrd)
669
670 b.Opt("-device", "virtio-rng-device")
671
672 b.Optf("-smbios", "type=1,uuid=%s", uuid)
673 b.Opt("-serial", "file:"+cfg.SerialLogPath)
674
675 // use virtio console if requsted. this is faster than the serial UART logging
676 // because serial has a higher cost when being accesssed. we still have to
677 // support serial itself for early kernel boot but thats OK.
678 if cfg.Image.RunnerConfig.Console == "hvc0" {
679 b.Optf("-chardev", "file,id=virtiocon0,path=%s,append=on", cfg.SerialLogPath)
680 b.Add("-device", "virtio-serial-device")
681 b.Opt("-device", "virtconsole,chardev=virtiocon0")
682 }
683 b.Opt("-display", "none")
684 b.Opt("-monitor", "none")
685 b.Opt("-append", cfg.Image.BootArgs)
686
687 b.Opt("-sandbox", "on")
688 b.Optf("-chardev", "socket,id=qmp0,fd=%d,server=on,wait=off", qemuExtraFD)
689 b.Opt("-mon", "chardev=qmp0,mode=control")
690}
691
692func addQEMUStoreArgs(b *argBuilder, cfg qemuArgsConfig) {
693 drive := newOptionBuilder(8)
694 drive.KV("id", "store")
695 drive.KV("format", "raw")
696 drive.Add("read-only=on")
697 drive.KV("file", cfg.Image.StoreDisk)
698 drive.Add("if=none")
699 drive.Add("aio=io_uring")
700
701 b.Opt("-drive", drive.String())
702 b.Opt("-device", "virtio-blk-device,drive=store")
703}
704
705func addQEMUKVMArgs(b *argBuilder, image ImageSpec) {
706 b.Flag("-enable-kvm")
707 if image.RunnerConfig.CPU != "" {
708 b.Opt("-cpu", image.RunnerConfig.CPU)
709 }
710 b.Opt("-device", "i8042")
711}
712
713func addQEMUVolumeArgs(b *argBuilder, cfg qemuArgsConfig) error {
714 for index, volume := range cfg.Image.Volumes {
715 path := cfg.VolumePaths[volume.Image]
716 if path == "" {
717 return fmt.Errorf("missing prepared path for volume %q", volume.Image)
718 }
719
720 driveID := fmt.Sprintf("volume%d", index)
721
722 drive := newOptionBuilder(10)
723 drive.KV("id", driveID)
724 drive.KV("format", "raw")
725 drive.Add("read-only=off")
726 drive.KV("file", path)
727 drive.Add("if=none")
728 drive.Add("aio=io_uring")
729 drive.Add("discard=unmap")
730 drive.Add("cache=none")
731
732 b.Opt("-drive", drive.String())
733 b.Optf("-device", "virtio-blk-device,drive=%s", driveID)
734 }
735
736 return nil
737}
738
739func addQEMUNetworkArgs(b *argBuilder, interfaces []NetworkInterface) error {
740 for _, networkInterface := range interfaces {
741 if networkInterface.Type != "slirp4netns" {
742 return fmt.Errorf("unsupported microvm network interface type %q", networkInterface.Type)
743 }
744
745 netdevOpts := newOptionBuilder(6)
746 netdevOpts.Add("user")
747 netdevOpts.KV("id", networkInterface.ID)
748 netdevOpts.KV("net", innerSlirpNet)
749 netdevOpts.KV("host", innerSlirpHost)
750 netdevOpts.KV("dns", innerSlirpDNS)
751 netdevOpts.KV("dhcpstart", innerSlirpDHCP)
752
753 b.Opt("-netdev", netdevOpts.String())
754 b.Optf(
755 "-device", "virtio-net-device,netdev=%s,mac=%s",
756 networkInterface.ID, networkInterface.MAC,
757 )
758 }
759
760 return nil
761}
762
763func waitForPIDFile(ctx context.Context, path string) (string, error) {
764 waitCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
765 defer cancel()
766
767 ticker := time.NewTicker(25 * time.Millisecond)
768 defer ticker.Stop()
769
770 for {
771 data, err := os.ReadFile(path)
772 if err == nil {
773 pid := strings.TrimSpace(string(data))
774 if pid != "" {
775 return pid, nil
776 }
777 } else if !errors.Is(err, os.ErrNotExist) {
778 return "", fmt.Errorf("read qemu network namespace pid: %w", err)
779 }
780
781 select {
782 case <-waitCtx.Done():
783 return "", fmt.Errorf("waiting for qemu network namespace pid: %w", waitCtx.Err())
784 case <-ticker.C:
785 }
786 }
787}