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 "bufio"
7 "bytes"
8 "context"
9 "encoding/json"
10 "errors"
11 "io"
12 "net"
13 "os"
14 "path/filepath"
15 "sync"
16 "time"
17
18 "github.com/carlosframework/platform/internal/activator"
19 "github.com/carlosframework/platform/internal/controlsock"
20 "github.com/carlosframework/platform/internal/hostbox"
21 "github.com/carlosframework/platform/internal/hostclaim"
22 "golang.org/x/sys/unix"
23)
24
25const filesSeedSocket = "/run/carlos/files-seed.sock"
26
27type filesSeedRequest struct {
28 Verb string `json:"verb"`
29 Host string `json:"host,omitempty"`
30}
31type filesSeedReply struct {
32 Text string `json:"text,omitempty"`
33 Code *int `json:"code,omitempty"`
34}
35type filesSeedServer struct {
36 listener *net.UnixListener
37 cancel context.CancelFunc
38 done chan struct{}
39 handlers sync.WaitGroup
40 finalize func(context.Context) error
41}
42type filesSeedStream struct {
43 conn net.Conn
44 encoder *json.Encoder
45}
46
47func (s filesSeedStream) Write(p []byte) (int, error) {
48 s.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
49 if err := s.encoder.Encode(filesSeedReply{Text: string(p)}); err != nil {
50 return 0, err
51 }
52 return len(p), nil
53}
54
55func startFilesSeedSocket(ctx context.Context, ready <-chan struct{}, path string, auth func(context.Context, *net.UnixConn) error, run func(context.Context, string, io.Writer) error) (*filesSeedServer, error) {
56 select {
57 case <-ctx.Done():
58 return nil, ctx.Err()
59 case <-ready:
60 }
61 if err := ctx.Err(); err != nil {
62 return nil, err
63 }
64 if auth == nil || run == nil {
65 return nil, hostbox.ErrUnsupported
66 }
67 parent, err := os.Lstat(filepath.Dir(path))
68 if err != nil {
69 return nil, err
70 }
71 var dir unix.Stat_t
72 if err := unix.Lstat(filepath.Dir(path), &dir); err != nil {
73 return nil, err
74 }
75 if !parent.IsDir() || parent.Mode()&os.ModeSymlink != 0 || parent.Mode().Perm()&0022 != 0 || int(dir.Uid) != os.Geteuid() {
76 return nil, ErrClaimStopIdentity
77 }
78 if err := removeStaleFilesSeedSocket(ctx, path); err != nil {
79 return nil, err
80 }
81 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
82 if err != nil {
83 return nil, err
84 }
85 if err := os.Chmod(path, 0600); err != nil {
86 listener.Close()
87 return nil, err
88 }
89 if err := ctx.Err(); err != nil {
90 listener.Close()
91 return nil, err
92 }
93 live, cancel := context.WithCancel(ctx)
94 live, finish := activator.WithCleanupBudget(live, 30*time.Second)
95 s := &filesSeedServer{listener: listener, cancel: cancel, done: make(chan struct{})}
96 stop := context.AfterFunc(live, func() { listener.Close() })
97 go func() {
98 defer close(s.done)
99 defer finish()
100 defer stop()
101 defer listener.Close()
102 busy := make(chan struct{}, 1)
103 for {
104 conn, err := listener.AcceptUnix()
105 if err != nil {
106 break
107 }
108 select {
109 case busy <- struct{}{}:
110 default:
111 conn.Close()
112 continue
113 }
114 s.handlers.Add(1)
115 go func() {
116 defer s.handlers.Done()
117 defer func() { <-busy }()
118 defer conn.Close()
119 serveFilesSeed(live, conn, auth, run)
120 }()
121 }
122 s.handlers.Wait()
123 }()
124 return s, nil
125}
126
127func (s *filesSeedServer) close(ctx context.Context) error {
128 if s == nil {
129 return nil
130 }
131 s.cancel()
132 s.listener.Close()
133 select {
134 case <-s.done:
135 if s.finalize != nil {
136 return s.finalize(ctx)
137 }
138 return nil
139 case <-ctx.Done():
140 return ctx.Err()
141 }
142}
143
144func serveFilesSeed(parent context.Context, conn *net.UnixConn, auth func(context.Context, *net.UnixConn) error, run func(context.Context, string, io.Writer) error) {
145 ctx, cancel := context.WithCancel(parent)
146 defer cancel()
147 ctx, finish := activator.WithCleanupBudget(ctx, 30*time.Second)
148 defer finish()
149 stop := context.AfterFunc(ctx, func() { conn.Close() })
150 defer stop()
151 conn.SetReadDeadline(time.Now().Add(5 * time.Second))
152 if err := auth(ctx, conn); err != nil {
153 return
154 }
155 reader := bufio.NewReaderSize(conn, 1024)
156 line, err := reader.ReadSlice('\n')
157 if err != nil || len(line) > 1024 {
158 return
159 }
160 var q filesSeedRequest
161 decoder := json.NewDecoder(bytes.NewReader(line))
162 decoder.DisallowUnknownFields()
163 if err := decoder.Decode(&q); err != nil {
164 return
165 }
166 // Canonical encoding also rejects duplicate fields and trailing JSON.
167 canonical, _ := json.Marshal(q)
168 var compact bytes.Buffer
169 if json.Compact(&compact, line) != nil || !bytes.Equal(compact.Bytes(), canonical) {
170 return
171 }
172 if q.Verb != "seed" {
173 return
174 }
175 if q.Host != "" {
176 if _, err := hostclaim.Key(q.Host); err != nil {
177 return
178 }
179 }
180 if err := auth(ctx, conn); err != nil {
181 return
182 }
183 conn.SetReadDeadline(time.Time{})
184 monitor := make(chan struct{})
185 go func() { defer close(monitor); _, _ = reader.ReadByte(); cancel() }()
186 stream := filesSeedStream{conn: conn, encoder: json.NewEncoder(conn)}
187 result := run(ctx, q.Host, stream)
188 code := filesSeedExitCode(result)
189 if result != nil {
190 _, _ = stream.Write([]byte(result.Error() + "\n"))
191 }
192 conn.SetWriteDeadline(time.Now().Add(time.Second))
193 _ = stream.encoder.Encode(filesSeedReply{Code: &code})
194 conn.Close()
195 <-monitor
196}
197
198func filesSeedClient(ctx context.Context, host string, out io.Writer, uid int) error {
199 var st unix.Stat_t
200 if err := unix.Lstat(filesSeedSocket, &st); err != nil {
201 return errors.Join(errFilesSeedDelegation, err)
202 }
203 if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Mode&0777 != 0600 || st.Uid != uint32(uid) {
204 return ErrClaimStopIdentity
205 }
206 conn, err := (&net.Dialer{}).DialContext(ctx, "unix", filesSeedSocket)
207 if err != nil {
208 return errors.Join(errFilesSeedDelegation, err)
209 }
210 defer conn.Close()
211 stop := context.AfterFunc(ctx, func() { conn.Close() })
212 defer stop()
213 if err := authenticateFilesSeedPeer(ctx, conn.(*net.UnixConn), uid, true); err != nil {
214 return err
215 }
216 var after unix.Stat_t
217 if err := unix.Lstat(filesSeedSocket, &after); err != nil || st.Dev != after.Dev || st.Ino != after.Ino {
218 return ErrClaimStopIdentity
219 }
220 if err := json.NewEncoder(conn).Encode(filesSeedRequest{Verb: "seed", Host: host}); err != nil {
221 return err
222 }
223 reader := bufio.NewReaderSize(conn, 64*1024)
224 for {
225 line, err := reader.ReadSlice('\n')
226 if err != nil {
227 return err
228 }
229 var reply filesSeedReply
230 if err := json.Unmarshal(line, &reply); err != nil {
231 return err
232 }
233 if reply.Text != "" {
234 if _, err := io.WriteString(out, reply.Text); err != nil {
235 return err
236 }
237 }
238 if reply.Code != nil {
239 switch *reply.Code {
240 case 0:
241 return nil
242 case 2:
243 return filesSeedIncompleteError{hostbox.ErrChanged}
244 default:
245 return hostbox.ErrChanged
246 }
247 }
248 }
249}
250
251func startAgentFilesSeed(ctx context.Context, ready <-chan struct{}, svc *activator.Service) (*filesSeedServer, error) {
252 entry, _ := ctx.Value(claimEntryKey{}).(claimEntry)
253 resources := entry.Resources
254 source, ok := productionClaimFilesSource().(*movesFilesSource)
255 if !ok || entry.Assembly == nil || svc == nil || resources.Manager == nil {
256 return nil, hostbox.ErrUnsupported
257 }
258 if err := NewClaimRoleRootClient(controlsock.Path).KindCapability(ctx, "files-seed"); err != nil {
259 return nil, err
260 }
261 executor := &filesSeedExecutor{assembly: entry.Assembly, service: svc, registry: resources.Registry, source: source}
262 server, err := startFilesSeedSocket(ctx, ready, filesSeedSocket, func(ctx context.Context, c *net.UnixConn) error {
263 return authenticateFilesSeedPeer(ctx, c, os.Geteuid(), false)
264 }, executor.run)
265 if server != nil {
266 server.finalize = executor.close
267 }
268 return server, err
269}
270
271// Recovery removes only an unchanged, service-owned socket which the kernel
272// refused to connect to. A live or uncertain listener always wins.
273func removeStaleFilesSeedSocket(ctx context.Context, path string) error {
274 var before unix.Stat_t
275 err := unix.Lstat(path, &before)
276 if errors.Is(err, unix.ENOENT) {
277 return nil
278 }
279 if err != nil {
280 return err
281 }
282 if before.Mode&unix.S_IFMT != unix.S_IFSOCK || before.Mode&0777 != 0600 || int(before.Uid) != os.Geteuid() {
283 return ErrClaimStopIdentity
284 }
285 conn, err := (&net.Dialer{Timeout: time.Second}).DialContext(ctx, "unix", path)
286 if err == nil {
287 conn.Close()
288 return hostbox.ErrChanged
289 }
290 if !errors.Is(err, unix.ECONNREFUSED) {
291 return err
292 }
293 var after unix.Stat_t
294 if err := unix.Lstat(path, &after); err != nil {
295 return err
296 }
297 if before.Dev != after.Dev || before.Ino != after.Ino || before.Ctim != after.Ctim {
298 return ErrClaimStopIdentity
299 }
300 return os.Remove(path)
301}
302