//go:build linux package host import ( "context" "encoding/json" "errors" "io" "net" "os" "path/filepath" "sync/atomic" "testing" "time" ) func TestFilesSeedSocketStartupDisconnectAndShutdown(t *testing.T) { t.Parallel() for _, canceled := range []bool{false, true} { t.Run(map[bool]string{true: "canceled", false: "ready"}[canceled], func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() ready := make(chan struct{}) dir := t.TempDir() if err := os.Chmod(dir, 0700); err != nil { t.Fatal(err) } path := filepath.Join(dir, "seed.sock") entered, joined := make(chan struct{}), make(chan struct{}) var calls atomic.Int32 run := func(ctx context.Context, host string, out io.Writer) error { calls.Add(1) close(entered) <-ctx.Done() close(joined) return ctx.Err() } result := make(chan *filesSeedServer, 1) go func() { s, _ := startFilesSeedSocket(ctx, ready, path, func(context.Context, *net.UnixConn) error { return nil }, run) result <- s }() select { case <-result: t.Fatal("bound before startup") case <-time.After(20 * time.Millisecond): } if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Fatal("early socket", err) } if canceled { cancel() } close(ready) s := <-result if canceled { if s != nil { t.Fatal("late socket") } return } if s == nil { t.Fatal("no socket") } c, err := net.Dial("unix", path) if err != nil { t.Fatal(err) } if err := json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed", Host: recoveryHost}); err != nil { t.Fatal(err) } <-entered c.Close() select { case <-joined: case <-time.After(time.Second): t.Fatal("disconnect detached request") } cancel() join, cancelJoin := context.WithTimeout(context.Background(), time.Second) defer cancelJoin() if err := s.close(join); err != nil { t.Fatal(err) } if calls.Load() != 1 { t.Fatal(calls.Load()) } if _, err := net.Dial("unix", path); err == nil { t.Fatal("listener survived") } }) } } func TestFilesSeedSocketRefusesMalformedAndUntrusted(t *testing.T) { for _, body := range []string{`{"verb":"seed","host":"../bad"}`, `{"verb":"repair"}`, `{"verb":"seed","pid":1}`, `{"verb":"seed","host":"a","host":"b"}`} { t.Run(body, func(t *testing.T) { ready := make(chan struct{}) close(ready) dir := t.TempDir() if err := os.Chmod(dir, 0700); err != nil { t.Fatal(err) } path := filepath.Join(dir, "seed.sock") 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 }) if err != nil { t.Fatal(err) } defer s.close(t.Context()) c, err := net.Dial("unix", path) if err != nil { t.Fatal(err) } defer c.Close() c.SetDeadline(time.Now().Add(time.Second)) io.WriteString(c, body+"\n") var r filesSeedReply if json.NewDecoder(c).Decode(&r) == nil && r.Code != nil && *r.Code == 0 { t.Fatal("accepted") } }) } ready := make(chan struct{}) close(ready) dir := t.TempDir() if err := os.Chmod(dir, 0700); err != nil { t.Fatal(err) } path := filepath.Join(dir, "seed.sock") 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 }) if err != nil { t.Fatal(err) } defer s.close(t.Context()) c, err := net.Dial("unix", path) if err != nil { t.Fatal(err) } defer c.Close() json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"}) var r filesSeedReply if json.NewDecoder(c).Decode(&r) == nil && r.Code != nil && *r.Code == 0 { t.Fatal("untrusted accepted") } if _, err := startFilesSeedSocket(t.Context(), ready, path, nil, nil); err == nil { t.Fatal("replaced live socket") } } func TestFilesSeedSocketStaleRecoveryAndBoundedJoin(t *testing.T) { t.Parallel() dir := t.TempDir() os.Chmod(dir, 0700) path := filepath.Join(dir, "seed.sock") old, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"}) if err != nil { t.Fatal(err) } old.SetUnlinkOnClose(false) os.Chmod(path, 0600) old.Close() ready := make(chan struct{}) close(ready) entered, release := make(chan struct{}), make(chan struct{}) 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 }) if err != nil { t.Fatal(err) } var closed atomic.Bool s.finalize = func(context.Context) error { closed.Store(true); return nil } c, err := net.Dial("unix", path) if err != nil { t.Fatal(err) } defer c.Close() json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"}) <-entered if err := removeStaleFilesSeedSocket(t.Context(), path); err == nil { t.Fatal("live listener removed") } deadline, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond) defer cancel() if err := s.close(deadline); !errors.Is(err, context.DeadlineExceeded) || closed.Load() { t.Fatal("unjoined runtime allowed close", err) } close(release) if err := s.close(t.Context()); err != nil || !closed.Load() { t.Fatal("joined runtime not finalized", err) } } func TestFilesSeedSocketWaitsConfigureAndStart(t *testing.T) { for _, phase := range []string{"configure", "start", "ready"} { t.Run(phase, func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() dir := t.TempDir() os.Chmod(dir, 0700) path := filepath.Join(dir, "seed.sock") configured, allowConfigure, starting, allowStart, ready := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{}) startup := make(chan struct{}) go func() { defer close(startup) <-(activatorStart{Claims: true, Configure: func(context.Context) { close(configured); <-allowConfigure }, Start: func() { close(starting); <-allowStart }, Ready: func() { close(ready) }}).Run(ctx) }() result := make(chan *filesSeedServer, 1) go func() { s, _ := startFilesSeedSocket(ctx, ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { return nil }) result <- s }() <-configured if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Fatal("socket during Configure") } if phase == "configure" { cancel() } close(allowConfigure) if phase != "configure" { <-starting if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Fatal("socket during Start") } if phase == "start" { cancel() } } close(allowStart) <-startup s := <-result if phase != "ready" { if s != nil { t.Fatal("socket after canceled startup") } return } if s == nil { t.Fatal("no socket after readiness") } if err := s.close(t.Context()); err != nil { t.Fatal(err) } }) } }