1// SPDX-License-Identifier: MIT
2
3// Package sandboxtest has helpers shared by the tests of the sandbox drivers.
4package sandboxtest
5
6import (
7 "context"
8 "encoding/binary"
9 "io"
10 "net"
11 "net/http"
12 "net/netip"
13 "os"
14 "path/filepath"
15 "sync"
16 "testing"
17 "time"
18
19 "bnema.dev/ermite-runner/internal/cachesvc"
20 "bnema.dev/ermite-runner/internal/cmdrun"
21 "bnema.dev/ermite-runner/internal/netpolicy"
22 "bnema.dev/ermite-runner/internal/sandbox"
23)
24
25// NoMountRunner runs real commands except mount and umount, which succeed
26// without doing anything: loop mounts need root, and the tests that use it
27// replace them by plain directories (mkfs still runs).
28type NoMountRunner struct{}
29
30// Run implements cmdrun.Runner.
31func (NoMountRunner) Run(ctx context.Context, in io.Reader, name string, args ...string) ([]byte, error) {
32 if name == "mount" || name == "umount" {
33 return nil, nil
34 }
35 return cmdrun.Exec{}.Run(ctx, in, name, args...)
36}
37
38// WriteStaticELF writes a minimal statically linked ELF64 file (one PT_LOAD, no
39// PT_INTERP): enough for sandbox.CheckStatic, never executed.
40func WriteStaticELF(t testing.TB, path string) {
41 t.Helper()
42 var b [64 + 56]byte
43 copy(b[:], "\x7fELF")
44 b[4], b[5], b[6] = 2, 1, 1 // 64-bit, little endian, version
45 binary.LittleEndian.PutUint16(b[16:], 2) // ET_EXEC
46 binary.LittleEndian.PutUint16(b[18:], 62) // x86-64
47 binary.LittleEndian.PutUint32(b[20:], 1)
48 binary.LittleEndian.PutUint64(b[32:], 64) // e_phoff
49 binary.LittleEndian.PutUint16(b[52:], 64) // e_ehsize
50 binary.LittleEndian.PutUint16(b[54:], 56) // e_phentsize
51 binary.LittleEndian.PutUint16(b[56:], 1) // e_phnum
52 binary.LittleEndian.PutUint32(b[64:], 1) // PT_LOAD
53 if err := os.WriteFile(path, b[:], 0o755); err != nil {
54 t.Fatal(err)
55 }
56}
57
58// Plumbing is a netsetup.Plumbing that records what is attached.
59type Plumbing struct {
60 mu sync.Mutex
61 Attached map[string]netpolicy.Guest
62 Events []string
63 Resets int
64 FailNext error
65}
66
67// NewPlumbing returns an empty Plumbing.
68func NewPlumbing() *Plumbing { return &Plumbing{Attached: map[string]netpolicy.Guest{}} }
69
70// EnableForwarding implements netsetup.Plumbing.
71func (p *Plumbing) EnableForwarding() error { return nil }
72
73// Attach implements netsetup.Plumbing.
74func (p *Plumbing) Attach(_ context.Context, g netpolicy.Guest) error {
75 p.mu.Lock()
76 defer p.mu.Unlock()
77 if p.FailNext != nil {
78 err := p.FailNext
79 p.FailNext = nil
80 return err
81 }
82 p.Attached[g.Iface] = g
83 p.Events = append(p.Events, "attach "+g.Iface)
84 return nil
85}
86
87// Detach implements netsetup.Plumbing.
88func (p *Plumbing) Detach(_ context.Context, iface string, _ netip.Addr) error {
89 p.mu.Lock()
90 defer p.mu.Unlock()
91 delete(p.Attached, iface)
92 p.Events = append(p.Events, "detach "+iface)
93 return nil
94}
95
96// Reset implements netsetup.Plumbing.
97func (p *Plumbing) Reset(context.Context) error {
98 p.mu.Lock()
99 defer p.mu.Unlock()
100 p.Resets++
101 p.Attached = map[string]netpolicy.Guest{}
102 return nil
103}
104
105// Close implements netsetup.Plumbing.
106func (p *Plumbing) Close() {}
107
108// Live returns the number of attached guests.
109func (p *Plumbing) Live() int {
110 p.mu.Lock()
111 defer p.mu.Unlock()
112 return len(p.Attached)
113}
114
115// Externals is a fixed, replaceable sandbox.ExternalsSource.
116type Externals struct {
117 mu sync.Mutex
118 set []sandbox.External
119}
120
121// Set replaces the set the drivers see.
122func (e *Externals) Set(set ...sandbox.External) { e.mu.Lock(); e.set = set; e.mu.Unlock() }
123
124// Externals implements sandbox.ExternalsSource.
125func (e *Externals) Externals() []sandbox.External {
126 e.mu.Lock()
127 defer e.mu.Unlock()
128 return append([]sandbox.External(nil), e.set...)
129}
130
131// NewCache starts a cache service in a temporary directory and closes it with
132// the test.
133func NewCache(t testing.TB) *cachesvc.Service {
134 t.Helper()
135 svc, err := cachesvc.New(cachesvc.Config{Dir: filepath.Join(t.TempDir(), "cache")})
136 if err != nil {
137 t.Fatal(err)
138 }
139 t.Cleanup(func() { _ = svc.CloseAll() })
140 return svc
141}
142
143// CacheLookup sends a cache lookup with token over the Unix socket sock, as the
144// guest relay delivers it, and returns the HTTP status; the error is that of
145// the connection (the socket is closed or gone).
146func CacheLookup(sock, token string) (int, error) {
147 cl := &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{
148 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
149 var d net.Dialer
150 return d.DialContext(ctx, "unix", sock)
151 },
152 DisableKeepAlives: true,
153 }}
154 req, err := http.NewRequest("GET", "http://cache"+cachesvc.APIPath+"cache?keys=k&version=v", nil)
155 if err != nil {
156 return 0, err
157 }
158 req.Header.Set("Authorization", "Bearer "+token)
159 resp, err := cl.Do(req)
160 if err != nil {
161 return 0, err
162 }
163 _ = resp.Body.Close()
164 return resp.StatusCode, nil
165}
166
167// CacheToken is a token of the service for a job that may read and write main.
168func CacheToken(svc *cachesvc.Service, job string) string {
169 ref := "refs/heads/main"
170 return svc.Token(job, cachesvc.Scope{Repository: "bnema/app", WriteRef: ref, ReadRefs: []string{ref}}, time.Hour)
171}
172
173// MakeExternal creates a host directory holding bin/node with content and
174// returns it as an external called name.
175func MakeExternal(t testing.TB, name, id, content string) sandbox.External {
176 t.Helper()
177 dir := filepath.Join(t.TempDir(), name)
178 if err := os.MkdirAll(filepath.Join(dir, "bin"), 0o755); err != nil {
179 t.Fatal(err)
180 }
181 if err := os.WriteFile(filepath.Join(dir, "bin", "node"), []byte(content), 0o755); err != nil {
182 t.Fatal(err)
183 }
184 if err := os.Symlink("bin/node", filepath.Join(dir, "nodejs")); err != nil {
185 t.Fatal(err)
186 }
187 return sandbox.External{Name: name, ID: id, Dir: dir}
188}