1// SPDX-License-Identifier: MIT
2
3package guest
4
5import (
6 "context"
7 "fmt"
8 "io"
9 "net"
10 "path/filepath"
11 "strconv"
12 "strings"
13 "time"
14
15 "golang.org/x/sys/unix"
16
17 "bnema.dev/ermite-runner/internal/sandbox"
18)
19
20// Target is where the executor connects back to: "unix:/path" (container) or
21// "vsock:PORT" (vm; the host is CID 2).
22type Target struct {
23 Network string // "unix" or "vsock"
24 Addr string // socket path, or the vsock port
25}
26
27// ParseTarget parses "unix:/path" or "vsock:port".
28func ParseTarget(s string) (Target, error) {
29 kind, rest, ok := strings.Cut(s, ":")
30 if !ok || rest == "" {
31 return Target{}, fmt.Errorf("connect target %q: want unix:PATH or vsock:PORT", s)
32 }
33 switch kind {
34 case "unix":
35 return Target{Network: "unix", Addr: rest}, nil
36 case "vsock":
37 if p, err := strconv.ParseUint(rest, 10, 32); err != nil || p == 0 {
38 return Target{}, fmt.Errorf("connect target %q: bad vsock port", s)
39 }
40 return Target{Network: "vsock", Addr: rest}, nil
41 }
42 return Target{}, fmt.Errorf("connect target %q: unknown kind %q", s, kind)
43}
44
45func (t Target) String() string { return t.Network + ":" + t.Addr }
46
47// CacheTarget is where the cache streams of the sandbox connect: the host
48// channel of the executor (t) has a sibling for the cache, on the same
49// sandbox-private path. A VM uses its own vsock port (sandbox.CacheVsockPort);
50// a container uses sandbox.CacheSocket next to the executor socket.
51func (t Target) CacheTarget() Target {
52 if t.Network == "vsock" {
53 return Target{Network: "vsock", Addr: strconv.Itoa(sandbox.CacheVsockPort)}
54 }
55 return Target{Network: "unix", Addr: filepath.Join(filepath.Dir(t.Addr), sandbox.CacheSocket)}
56}
57
58// DialStream opens one new connection to the target, without retrying: the
59// cache relay opens one per accepted TCP connection and a failure closes that
60// connection (the client retries).
61func (t Target) DialStream(ctx context.Context) (io.ReadWriteCloser, error) {
62 if t.Network == "unix" {
63 var d net.Dialer
64 return d.DialContext(ctx, "unix", t.Addr)
65 }
66 c, err := dialVsock(t.Addr)
67 if err != nil {
68 return nil, err
69 }
70 return c, nil
71}
72
73// Dial connects to the host, retrying until ctx ends or timeout elapses: the
74// host listens before the sandbox starts, but a vsock device may take a moment.
75func (t Target) Dial(ctx context.Context, timeout time.Duration) (io.ReadWriteCloser, error) {
76 ctx, cancel := context.WithTimeout(ctx, timeout)
77 defer cancel()
78 var last error
79 for {
80 var c io.ReadWriteCloser
81 var err error
82 if t.Network == "unix" {
83 var d net.Dialer
84 c, err = d.DialContext(ctx, "unix", t.Addr)
85 } else {
86 var vc *fdConn
87 if vc, err = dialVsock(t.Addr); err == nil {
88 c = vc
89 }
90 }
91 if err == nil {
92 return c, nil
93 }
94 last = err
95 select {
96 case <-ctx.Done():
97 return nil, fmt.Errorf("connect to %s: %w (last error: %v)", t, ctx.Err(), last)
98 case <-time.After(100 * time.Millisecond):
99 }
100 }
101}
102
103// vsockHostCID is the well-known CID of the host.
104const vsockHostCID = 2
105
106func dialVsock(port string) (*fdConn, error) {
107 p, _ := strconv.ParseUint(port, 10, 32)
108 fd, err := unix.Socket(unix.AF_VSOCK, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
109 if err != nil {
110 return nil, err
111 }
112 // A blocking connect is fine: a Firecracker vsock either connects to the
113 // host listener at once or fails.
114 if err := unix.Connect(fd, &unix.SockaddrVM{CID: vsockHostCID, Port: uint32(p)}); err != nil {
115 _ = unix.Close(fd)
116 return nil, err
117 }
118 return newFDConn(fd)
119}