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 "encoding/json"
8 "errors"
9 "io"
10 "net"
11 "os"
12 "path/filepath"
13 "sync/atomic"
14 "testing"
15 "time"
16)
17
18func TestFilesSeedSocketStartupDisconnectAndShutdown(t *testing.T) {
19 t.Parallel()
20 for _, canceled := range []bool{false, true} {
21 t.Run(map[bool]string{true: "canceled", false: "ready"}[canceled], func(t *testing.T) {
22 ctx, cancel := context.WithCancel(t.Context())
23 defer cancel()
24 ready := make(chan struct{})
25 dir := t.TempDir()
26 if err := os.Chmod(dir, 0700); err != nil {
27 t.Fatal(err)
28 }
29 path := filepath.Join(dir, "seed.sock")
30 entered, joined := make(chan struct{}), make(chan struct{})
31 var calls atomic.Int32
32 run := func(ctx context.Context, host string, out io.Writer) error {
33 calls.Add(1)
34 close(entered)
35 <-ctx.Done()
36 close(joined)
37 return ctx.Err()
38 }
39 result := make(chan *filesSeedServer, 1)
40 go func() {
41 s, _ := startFilesSeedSocket(ctx, ready, path, func(context.Context, *net.UnixConn) error { return nil }, run)
42 result <- s
43 }()
44 select {
45 case <-result:
46 t.Fatal("bound before startup")
47 case <-time.After(20 * time.Millisecond):
48 }
49 if _, err := os.Lstat(path); !os.IsNotExist(err) {
50 t.Fatal("early socket", err)
51 }
52 if canceled {
53 cancel()
54 }
55 close(ready)
56 s := <-result
57 if canceled {
58 if s != nil {
59 t.Fatal("late socket")
60 }
61 return
62 }
63 if s == nil {
64 t.Fatal("no socket")
65 }
66 c, err := net.Dial("unix", path)
67 if err != nil {
68 t.Fatal(err)
69 }
70 if err := json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed", Host: recoveryHost}); err != nil {
71 t.Fatal(err)
72 }
73 <-entered
74 c.Close()
75 select {
76 case <-joined:
77 case <-time.After(time.Second):
78 t.Fatal("disconnect detached request")
79 }
80 cancel()
81 join, cancelJoin := context.WithTimeout(context.Background(), time.Second)
82 defer cancelJoin()
83 if err := s.close(join); err != nil {
84 t.Fatal(err)
85 }
86 if calls.Load() != 1 {
87 t.Fatal(calls.Load())
88 }
89 if _, err := net.Dial("unix", path); err == nil {
90 t.Fatal("listener survived")
91 }
92 })
93 }
94}
95
96func TestFilesSeedSocketRefusesMalformedAndUntrusted(t *testing.T) {
97 for _, body := range []string{`{"verb":"seed","host":"../bad"}`, `{"verb":"repair"}`, `{"verb":"seed","pid":1}`, `{"verb":"seed","host":"a","host":"b"}`} {
98 t.Run(body, func(t *testing.T) {
99 ready := make(chan struct{})
100 close(ready)
101 dir := t.TempDir()
102 if err := os.Chmod(dir, 0700); err != nil {
103 t.Fatal(err)
104 }
105 path := filepath.Join(dir, "seed.sock")
106 s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { t.Error("malformed request executed"); return nil })
107 if err != nil {
108 t.Fatal(err)
109 }
110 defer s.close(t.Context())
111 c, err := net.Dial("unix", path)
112 if err != nil {
113 t.Fatal(err)
114 }
115 defer c.Close()
116 c.SetDeadline(time.Now().Add(time.Second))
117 io.WriteString(c, body+"\n")
118 var r filesSeedReply
119 if json.NewDecoder(c).Decode(&r) == nil && r.Code != nil && *r.Code == 0 {
120 t.Fatal("accepted")
121 }
122 })
123 }
124 ready := make(chan struct{})
125 close(ready)
126 dir := t.TempDir()
127 if err := os.Chmod(dir, 0700); err != nil {
128 t.Fatal(err)
129 }
130 path := filepath.Join(dir, "seed.sock")
131 s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return errors.New("refused") }, func(context.Context, string, io.Writer) error { t.Error("untrusted request executed"); return nil })
132 if err != nil {
133 t.Fatal(err)
134 }
135 defer s.close(t.Context())
136 c, err := net.Dial("unix", path)
137 if err != nil {
138 t.Fatal(err)
139 }
140 defer c.Close()
141 json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"})
142 var r filesSeedReply
143 if json.NewDecoder(c).Decode(&r) == nil && r.Code != nil && *r.Code == 0 {
144 t.Fatal("untrusted accepted")
145 }
146 if _, err := startFilesSeedSocket(t.Context(), ready, path, nil, nil); err == nil {
147 t.Fatal("replaced live socket")
148 }
149}
150
151func TestFilesSeedSocketStaleRecoveryAndBoundedJoin(t *testing.T) {
152 t.Parallel()
153 dir := t.TempDir()
154 os.Chmod(dir, 0700)
155 path := filepath.Join(dir, "seed.sock")
156 old, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
157 if err != nil {
158 t.Fatal(err)
159 }
160 old.SetUnlinkOnClose(false)
161 os.Chmod(path, 0600)
162 old.Close()
163 ready := make(chan struct{})
164 close(ready)
165 entered, release := make(chan struct{}), make(chan struct{})
166 s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { close(entered); <-release; return nil })
167 if err != nil {
168 t.Fatal(err)
169 }
170 var closed atomic.Bool
171 s.finalize = func(context.Context) error { closed.Store(true); return nil }
172 c, err := net.Dial("unix", path)
173 if err != nil {
174 t.Fatal(err)
175 }
176 defer c.Close()
177 json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"})
178 <-entered
179 if err := removeStaleFilesSeedSocket(t.Context(), path); err == nil {
180 t.Fatal("live listener removed")
181 }
182 deadline, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond)
183 defer cancel()
184 if err := s.close(deadline); !errors.Is(err, context.DeadlineExceeded) || closed.Load() {
185 t.Fatal("unjoined runtime allowed close", err)
186 }
187 close(release)
188 if err := s.close(t.Context()); err != nil || !closed.Load() {
189 t.Fatal("joined runtime not finalized", err)
190 }
191}
192
193func TestFilesSeedSocketWaitsConfigureAndStart(t *testing.T) {
194 for _, phase := range []string{"configure", "start", "ready"} {
195 t.Run(phase, func(t *testing.T) {
196 ctx, cancel := context.WithCancel(t.Context())
197 defer cancel()
198 dir := t.TempDir()
199 os.Chmod(dir, 0700)
200 path := filepath.Join(dir, "seed.sock")
201 configured, allowConfigure, starting, allowStart, ready := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{})
202 startup := make(chan struct{})
203 go func() {
204 defer close(startup)
205 <-(activatorStart{Claims: true, Configure: func(context.Context) { close(configured); <-allowConfigure }, Start: func() { close(starting); <-allowStart }, Ready: func() { close(ready) }}).Run(ctx)
206 }()
207 result := make(chan *filesSeedServer, 1)
208 go func() {
209 s, _ := startFilesSeedSocket(ctx, ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { return nil })
210 result <- s
211 }()
212 <-configured
213 if _, err := os.Lstat(path); !os.IsNotExist(err) {
214 t.Fatal("socket during Configure")
215 }
216 if phase == "configure" {
217 cancel()
218 }
219 close(allowConfigure)
220 if phase != "configure" {
221 <-starting
222 if _, err := os.Lstat(path); !os.IsNotExist(err) {
223 t.Fatal("socket during Start")
224 }
225 if phase == "start" {
226 cancel()
227 }
228 }
229 close(allowStart)
230 <-startup
231 s := <-result
232 if phase != "ready" {
233 if s != nil {
234 t.Fatal("socket after canceled startup")
235 }
236 return
237 }
238 if s == nil {
239 t.Fatal("no socket after readiness")
240 }
241 if err := s.close(t.Context()); err != nil {
242 t.Fatal(err)
243 }
244 })
245 }
246}
247