carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "database/sql"
8 "encoding/json"
9 "errors"
10 "net"
11 "os"
12 "path/filepath"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/benbjohnson/litestream"
18
19 "github.com/carlosframework/platform/internal/dbreads"
20)
21
22// dbreadTestMarkers holds marker files the root-runner tests drop to change
23// one thing about the worker. The unit can read /run outside /run/carlos.
24const dbreadTestMarkers = "/run/carlos-dbread-test"
25
26func dbreadMarker(name string) bool {
27 _, err := os.Stat(filepath.Join(dbreadTestMarkers, name))
28 return err == nil
29}
30
31// dbreadsTestWorker is this test binary acting as /opt/carlos/carlos system
32// db-worker inside the installed carlos-dbread@ unit. Without a marker it is
33// the real worker (dbreads.RunWorkerWith with RunWorker's options).
34func dbreadsTestWorker() {
35 opt := dbreads.WorkerOptions{Scratch: "/var/tmp"}
36 switch {
37 case dbreadMarker("exit-1"):
38 os.Exit(1)
39 case dbreadMarker("witness"):
40 dbreadsWitness()
41 return
42 }
43 if dbreadMarker("version-2") {
44 opt.Self = func() dbreads.HelloReply { h := dbreads.SelfView(); h.V = 2; return h }
45 }
46 if dbreadMarker("hang-restore") {
47 // A restore that ignores its context, as litestream's compactor
48 // does (spec §8.3): only the 5 s exit ends it.
49 opt.Restore = func(context.Context, litestream.ReplicaClient, string, uint64) error {
50 time.Sleep(time.Hour)
51 return nil
52 }
53 }
54 dbreads.RunWorkerWith(opt)
55}
56
57// dbreadsWitness is T3 from inside the unit: everything a member's SQL could
58// reach for if SQLite let it, tried directly. It reports each attempt's
59// error ("" for success) as one JSON object on the connection.
60func dbreadsWitness() {
61 conn, err := dbreads.StdinConn(os.Stdin, dbreads.CarlosUID)
62 if err != nil {
63 os.Exit(2)
64 }
65 defer conn.Close()
66 report := map[string]string{}
67 try := func(name string, f func() error) {
68 if err := f(); err != nil {
69 report[name] = err.Error()
70 } else {
71 report[name] = ""
72 }
73 }
74 open := func(p string) func() error {
75 return func() error {
76 f, err := os.Open(p)
77 if err == nil {
78 f.Close()
79 }
80 return err
81 }
82 }
83 dial := func(addr string) func() error {
84 return func() error {
85 c, err := net.DialTimeout("tcp", addr, 2*time.Second)
86 if err == nil {
87 c.Close()
88 }
89 return err
90 }
91 }
92 write := func(p string) func() error { return func() error { return os.WriteFile(p, []byte("x"), 0o600) } }
93 try("open /data/registry.db", open("/data/registry.db"))
94 try("open /etc/carlos/host.env", open("/etc/carlos/host.env"))
95 try("open another instance", open("/data/instances/other.example.com/other.db"))
96 try("connect the metadata service", dial("169.254.169.254:80"))
97 try("connect a TCP address", dial("1.1.1.1:443"))
98 // Network-namespace evidence that needs no socket (plan review finding
99 // 17): with RestrictAddressFamilies=AF_UNIX the TCP dials above fail
100 // even without PrivateNetwork, so they cannot witness it. This can: it
101 // "succeeds" (and so fails the test) when any interface but loopback is
102 // visible, which is the case the moment PrivateNetwork is removed.
103 try("see an interface other than loopback", func() error {
104 raw, err := os.ReadFile("/proc/self/net/dev")
105 if err != nil {
106 return nil // unobservable counts as reachable: the test fails
107 }
108 ifs := strings.Join(dbreadsParseNetDev(string(raw)), ",")
109 if ifs == "lo" {
110 return errors.New("only lo")
111 }
112 return nil
113 })
114 try("write /opt/carlos", write("/opt/carlos/witness"))
115 try("write /var/lib", write("/var/lib/witness"))
116 try("write /etc", write("/etc/witness"))
117 try("write /var/tmp", write("/var/tmp/witness")) // the positive control
118 // SQLite with the attach limit removed: the mutation. Only the sandbox
119 // stands between these statements and the box. The attached targets are
120 // real SQLite databases the runner wrote on the host, so a statement
121 // fails only because the file cannot be reached, never because it is
122 // not a database.
123 sqlTry := func(name, q string) {
124 try(name, func() error {
125 db, err := sql.Open("sqlite", "file:/var/tmp/witness.db")
126 if err != nil {
127 return err
128 }
129 defer db.Close()
130 _, err = db.Exec(q)
131 return err
132 })
133 }
134 sqlTry("attach /data/registry.db", "ATTACH DATABASE '/data/registry.db' AS r; SELECT count(*) FROM r.sqlite_master")
135 sqlTry("attach /etc/carlos/witness.db", "ATTACH DATABASE '/etc/carlos/witness.db' AS e; SELECT count(*) FROM e.sqlite_master")
136 sqlTry("vacuum into /opt/carlos", "VACUUM INTO '/opt/carlos/copy.db'")
137 sqlTry("load_extension", "SELECT load_extension('/lib/x86_64-linux-gnu/libc.so.6')")
138 json.NewEncoder(conn).Encode(report)
139}
140
141// dbreadsParseNetDev is dbreads.parseNetDev, restated: that one is
142// unexported in another package.
143func dbreadsParseNetDev(s string) []string {
144 var out []string
145 for i, line := range strings.Split(s, "\n") {
146 if i < 2 {
147 continue
148 }
149 if name, _, ok := strings.Cut(line, ":"); ok && strings.TrimSpace(name) != "" {
150 out = append(out, strings.TrimSpace(name))
151 }
152 }
153 return out
154}
155
156// dbreadsTenantWitness is this test binary as a tenant inside the installed
157// tenant unit (TestRootDBReadTenantCannotReachTheWorker): it tries every way
158// to the db-read worker a tenant running as carlos might have, and writes
159// what happened ("" for success) to its working directory.
160func dbreadsTenantWitness() {
161 report := map[string]string{"uid": strconv.Itoa(os.Getuid())}
162 for _, p := range []string{dbReadSocketPath, "/run/carlos-dbread/worker.sock"} {
163 c, err := net.DialTimeout("unix", p, 2*time.Second)
164 if err == nil {
165 c.Close()
166 report["connect "+p] = ""
167 } else {
168 report["connect "+p] = err.Error()
169 }
170 }
171 b, _ := json.Marshal(report)
172 if err := os.WriteFile("dbread-witness.json", b, 0o600); err != nil {
173 os.Exit(1)
174 }
175}
176