1// Package control implements the private recording control socket.
2//
3// A recording process calls Listen to own $XDG_RUNTIME_DIR/nefercap/record.sock
4// (mode 0600, inside a 0700 directory owned by the caller). Another process
5// asks it to stop or reports its state with Stop and Query. The wire protocol
6// is two request lines, "stop\n" and "status\n", answered with "ok\n" and
7// "recording\n". No path, pid or file name is ever sent.
8//
9// Ownership is enforced by an flock(2) on a private lock file held for the
10// server's lifetime, so a leftover socket is removed only by the process that
11// holds the lock and clients never unlink anything. Peers are checked with
12// SO_PEERCRED and must run as the same user; this is not general Unix
13// authentication, and processes of the same user are trusted.
14package control
15
16import (
17 "bytes"
18 "context"
19 "errors"
20 "fmt"
21 "io"
22 "net"
23 "os"
24 "path/filepath"
25 "strconv"
26 "strings"
27 "sync"
28 "time"
29
30 "golang.org/x/sys/unix"
31)
32
33const (
34 dirName = "nefercap"
35 sockName = "record.sock"
36 lockName = "record.lock"
37 maxPath = 4096 // longest accepted runtime directory
38 maxRequest = 64
39 connLimit = 4
40 ioTimeout = time.Second
41 backlog = connLimit
42
43 stopLine = "stop\n"
44 statusLine = "status\n"
45 okLine = "ok\n"
46 recLine = "recording\n"
47)
48
49var (
50 // ErrAlreadyRecording means another live process owns the control socket.
51 ErrAlreadyRecording = errors.New("control: a recording is already active")
52 // ErrNoRuntimeDir means XDG_RUNTIME_DIR is unset or unusable.
53 ErrNoRuntimeDir = errors.New("control: XDG_RUNTIME_DIR is not an absolute path")
54 // ErrInsecure means a control path has the wrong type, owner or mode.
55 ErrInsecure = errors.New("control: insecure control path")
56 // ErrProtocol means the peer answered with something unexpected.
57 ErrProtocol = errors.New("control: protocol error")
58)
59
60// Request is a control request delivered to the recording owner. Status
61// requests are answered by the server itself and are never delivered.
62type Request uint8
63
64// RequestStop asks the recording owner to cancel and finalize.
65const RequestStop Request = iota + 1
66
67// Server is the listening side owned by the recording process.
68type Server struct {
69 ln *net.UnixListener
70 dirFD int
71 lockFD int
72 dev uint64
73 ino uint64
74 peerUID uint32
75
76 reqs chan Request
77 sem chan struct{}
78 done chan struct{}
79 wg sync.WaitGroup
80
81 mu sync.Mutex
82 closed bool
83 conns map[net.Conn]struct{}
84
85 once sync.Once
86 closeErr error
87}
88
89// cleanRuntimeDir validates a raw runtime directory string.
90func cleanRuntimeDir(rd string) (string, error) {
91 if !filepath.IsAbs(rd) || len(rd) > maxPath || strings.IndexByte(rd, 0) >= 0 {
92 return "", ErrNoRuntimeDir
93 }
94 return filepath.Clean(rd), nil
95}
96
97func runtimeDir() (string, error) { return cleanRuntimeDir(os.Getenv("XDG_RUNTIME_DIR")) }
98
99// openSecureDir opens runtime/nefercap without following symlinks and checks
100// that it is a directory owned by uid with mode exactly 0700. All later
101// operations are relative to the returned descriptor.
102func openSecureDir(runtime string, create bool, uid uint32) (int, error) {
103 dir := filepath.Join(runtime, dirName)
104 if create {
105 err := unix.Mkdir(dir, 0o700)
106 if err == nil {
107 err = unix.Chmod(dir, 0o700) // independent of the umask
108 }
109 if err != nil && err != unix.EEXIST {
110 return -1, fmt.Errorf("control: create %s: %w", dirName, err)
111 }
112 }
113 fd, err := unix.Open(dir, unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
114 if err != nil {
115 if err == unix.ELOOP || err == unix.ENOTDIR {
116 return -1, fmt.Errorf("%w: %s is not a directory", ErrInsecure, dirName)
117 }
118 return -1, fmt.Errorf("control: open %s: %w", dirName, err)
119 }
120 var st unix.Stat_t
121 if err := unix.Fstat(fd, &st); err != nil {
122 unix.Close(fd)
123 return -1, fmt.Errorf("control: stat %s: %w", dirName, err)
124 }
125 if st.Mode&unix.S_IFMT != unix.S_IFDIR || st.Uid != uid || st.Mode&0o777 != 0o700 {
126 unix.Close(fd)
127 return -1, fmt.Errorf("%w: %s must be a 0700 directory owned by the current user", ErrInsecure, dirName)
128 }
129 return fd, nil
130}
131
132// Listen becomes the single control server. It returns ErrAlreadyRecording
133// when another live process holds the lock. The caller must Close the server.
134func Listen() (*Server, error) {
135 rd, err := runtimeDir()
136 if err != nil {
137 return nil, err
138 }
139 return listen(rd, uint32(os.Geteuid()))
140}
141
142// listen is Listen with an explicit accepted peer uid, so tests can exercise
143// the foreign-peer rejection with real sockets.
144func listen(runtime string, peerUID uint32) (s *Server, err error) {
145 if runtime, err = cleanRuntimeDir(runtime); err != nil {
146 return nil, err
147 }
148 euid := uint32(os.Geteuid())
149 dirFD, err := openSecureDir(runtime, true, euid)
150 if err != nil {
151 return nil, err
152 }
153 lockFD := -1
154 sockFD := -1
155 bound := false
156 var dev, ino uint64
157 defer func() {
158 if err == nil {
159 return
160 }
161 if sockFD >= 0 {
162 unix.Close(sockFD)
163 }
164 if bound && sameInode(dirFD, dev, ino) {
165 unix.Unlinkat(dirFD, sockName, 0)
166 }
167 if lockFD >= 0 {
168 unix.Close(lockFD)
169 }
170 unix.Close(dirFD)
171 }()
172
173 lockFD, err = unix.Openat(dirFD, lockName, unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600)
174 if err != nil {
175 return nil, fmt.Errorf("control: open lock: %w", err)
176 }
177 var st unix.Stat_t
178 if err = unix.Fstat(lockFD, &st); err != nil {
179 return nil, fmt.Errorf("control: stat lock: %w", err)
180 }
181 if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Uid != euid {
182 return nil, fmt.Errorf("%w: lock file", ErrInsecure)
183 }
184 if err = unix.Fchmod(lockFD, 0o600); err != nil {
185 return nil, fmt.Errorf("control: chmod lock: %w", err)
186 }
187 if err = unix.Flock(lockFD, unix.LOCK_EX|unix.LOCK_NB); err != nil {
188 if err == unix.EWOULDBLOCK {
189 err = ErrAlreadyRecording
190 } else {
191 err = fmt.Errorf("control: lock: %w", err)
192 }
193 return nil, err
194 }
195
196 // The lock is held, so no live server owns the socket path: a leftover
197 // socket of ours is stale. Anything else is left alone.
198 if err = unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW); err == nil {
199 if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Uid != euid {
200 err = fmt.Errorf("%w: %s is not a socket owned by the current user", ErrInsecure, sockName)
201 return nil, err
202 }
203 if err = unix.Unlinkat(dirFD, sockName, 0); err != nil {
204 return nil, fmt.Errorf("control: remove stale socket: %w", err)
205 }
206 } else if err != unix.ENOENT {
207 return nil, fmt.Errorf("control: stat socket: %w", err)
208 }
209
210 sockFD, err = unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
211 if err != nil {
212 return nil, fmt.Errorf("control: socket: %w", err)
213 }
214 if err = unix.Bind(sockFD, &unix.SockaddrUnix{Name: dirPath(dirFD)}); err != nil {
215 return nil, fmt.Errorf("control: bind: %w", err)
216 }
217 bound = true
218 if err = unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
219 bound = false
220 return nil, fmt.Errorf("control: stat socket: %w", err)
221 }
222 dev, ino = uint64(st.Dev), uint64(st.Ino)
223 // Not yet listening, so nobody can connect while the mode is tightened.
224 if err = unix.Fchmodat(dirFD, sockName, 0o600, 0); err != nil {
225 return nil, fmt.Errorf("control: chmod socket: %w", err)
226 }
227 if err = unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
228 return nil, fmt.Errorf("control: stat socket: %w", err)
229 }
230 if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Uid != euid || st.Mode&0o777 != 0o600 ||
231 uint64(st.Dev) != dev || uint64(st.Ino) != ino {
232 err = fmt.Errorf("%w: created socket", ErrInsecure)
233 return nil, err
234 }
235 if err = unix.Listen(sockFD, backlog); err != nil {
236 return nil, fmt.Errorf("control: listen: %w", err)
237 }
238 f := os.NewFile(uintptr(sockFD), sockName)
239 sockFD = -1
240 l, lerr := net.FileListener(f)
241 f.Close()
242 if lerr != nil {
243 err = fmt.Errorf("control: listener: %w", lerr)
244 return nil, err
245 }
246 ln := l.(*net.UnixListener)
247 ln.SetUnlinkOnClose(false)
248
249 s = &Server{
250 ln: ln,
251 dirFD: dirFD,
252 lockFD: lockFD,
253 dev: dev,
254 ino: ino,
255 peerUID: peerUID,
256 reqs: make(chan Request, 1),
257 sem: make(chan struct{}, connLimit),
258 done: make(chan struct{}),
259 conns: make(map[net.Conn]struct{}, connLimit),
260 }
261 s.wg.Add(1)
262 go s.acceptLoop()
263 return s, nil
264}
265
266// dirPath names the verified directory through its descriptor, so bind and
267// connect cannot be redirected by swapping path components afterwards. It is
268// short, which also avoids the sun_path length limit.
269func dirPath(dirFD int) string {
270 return "/proc/self/fd/" + strconv.Itoa(dirFD) + "/" + sockName
271}
272
273func sameInode(dirFD int, dev, ino uint64) bool {
274 var st unix.Stat_t
275 if unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW) != nil {
276 return false
277 }
278 return st.Mode&unix.S_IFMT == unix.S_IFSOCK && uint64(st.Dev) == dev && uint64(st.Ino) == ino
279}
280
281// Requests delivers RequestStop. Repeated stops coalesce into one pending
282// request. The channel is never closed; select on it together with the
283// recording's own completion.
284func (s *Server) Requests() <-chan Request { return s.reqs }
285
286// Close stops serving, removes the socket only if it is still the inode this
287// server created, then releases the lock. It is idempotent and safe for
288// concurrent use.
289func (s *Server) Close() error {
290 s.once.Do(func() {
291 s.mu.Lock()
292 s.closed = true
293 close(s.done)
294 for c := range s.conns {
295 c.Close()
296 }
297 s.mu.Unlock()
298 s.closeErr = s.ln.Close()
299 s.wg.Wait()
300 if sameInode(s.dirFD, s.dev, s.ino) {
301 if err := unix.Unlinkat(s.dirFD, sockName, 0); err != nil && err != unix.ENOENT && s.closeErr == nil {
302 s.closeErr = fmt.Errorf("control: remove socket: %w", err)
303 }
304 }
305 unix.Close(s.dirFD)
306 unix.Close(s.lockFD) // releases the flock last
307 })
308 return s.closeErr
309}
310
311func (s *Server) acceptLoop() {
312 defer s.wg.Done()
313 for {
314 select {
315 case s.sem <- struct{}{}:
316 case <-s.done:
317 return
318 }
319 c, err := s.ln.AcceptUnix()
320 if err != nil {
321 <-s.sem
322 select {
323 case <-s.done:
324 return
325 default:
326 }
327 // Transient failure (for example fd exhaustion): back off.
328 t := time.NewTimer(50 * time.Millisecond)
329 select {
330 case <-t.C:
331 case <-s.done:
332 t.Stop()
333 return
334 }
335 continue
336 }
337 s.wg.Add(1)
338 go s.serve(c)
339 }
340}
341
342func (s *Server) serve(c *net.UnixConn) {
343 defer s.wg.Done()
344 defer func() { <-s.sem }()
345 s.mu.Lock()
346 if s.closed {
347 s.mu.Unlock()
348 c.Close()
349 return
350 }
351 s.conns[c] = struct{}{}
352 s.mu.Unlock()
353 defer func() {
354 s.mu.Lock()
355 delete(s.conns, c)
356 s.mu.Unlock()
357 c.Close()
358 }()
359
360 if c.SetDeadline(time.Now().Add(ioTimeout)) != nil {
361 return
362 }
363 if uid, err := peerUID(c); err != nil || uid != s.peerUID {
364 return
365 }
366 line, ok := readLine(c)
367 if !ok {
368 return
369 }
370 var reply string
371 switch {
372 case bytes.Equal(line, []byte(stopLine)):
373 // Coalescing, non-blocking: the ack means the stop was requested,
374 // not that the capture file is finalized.
375 select {
376 case s.reqs <- RequestStop:
377 default:
378 }
379 reply = okLine
380 case bytes.Equal(line, []byte(statusLine)):
381 reply = recLine
382 default:
383 return
384 }
385 io.WriteString(c, reply)
386}
387
388// readLine reads one newline-terminated request of at most maxRequest bytes.
389func readLine(r io.Reader) ([]byte, bool) {
390 var buf [maxRequest]byte
391 b := buf[:]
392 n := 0
393 for n < len(b) {
394 m, err := r.Read(b[n:])
395 n += m
396 if i := bytes.IndexByte(b[:n], '\n'); i >= 0 {
397 // The request is exactly one line; trailing bytes are junk.
398 return b[:i+1], i+1 == n
399 }
400 if err != nil {
401 return nil, false
402 }
403 }
404 return nil, false
405}
406
407func peerUID(c *net.UnixConn) (uint32, error) {
408 rc, err := c.SyscallConn()
409 if err != nil {
410 return 0, err
411 }
412 var uc *unix.Ucred
413 var serr error
414 if err := rc.Control(func(fd uintptr) {
415 uc, serr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
416 }); err != nil {
417 return 0, err
418 }
419 if serr != nil {
420 return 0, serr
421 }
422 return uc.Uid, nil
423}
424
425// Stop asks the active recording to stop. It returns false with a nil error
426// when no recording is active. A true result means the request was accepted,
427// not that the capture has been finalized.
428func Stop(ctx context.Context) (bool, error) {
429 rd, err := runtimeDir()
430 if err != nil {
431 return false, err
432 }
433 return exchange(ctx, rd, stopLine, okLine)
434}
435
436// Query reports whether a recording is active.
437func Query(ctx context.Context) (bool, error) {
438 rd, err := runtimeDir()
439 if err != nil {
440 return false, err
441 }
442 return exchange(ctx, rd, statusLine, recLine)
443}
444
445func exchange(ctx context.Context, runtime, req, want string) (bool, error) {
446 euid := uint32(os.Geteuid())
447 dirFD, err := openSecureDir(runtime, false, euid)
448 if err != nil {
449 if errors.Is(err, unix.ENOENT) {
450 return false, nil
451 }
452 return false, err
453 }
454 defer unix.Close(dirFD)
455 var st unix.Stat_t
456 if err := unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
457 if err == unix.ENOENT {
458 return false, nil
459 }
460 return false, fmt.Errorf("control: stat socket: %w", err)
461 }
462 if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Uid != euid || st.Mode&0o077 != 0 {
463 return false, fmt.Errorf("%w: %s", ErrInsecure, sockName)
464 }
465
466 ctx, cancel := context.WithTimeout(ctx, ioTimeout)
467 defer cancel()
468 conn, err := dialRetry(ctx, dirPath(dirFD))
469 if err != nil {
470 // A dead owner leaves the socket behind; only the next server, which
471 // holds the lock, removes it.
472 if errors.Is(err, unix.ECONNREFUSED) || errors.Is(err, unix.ENOENT) {
473 return false, nil
474 }
475 return false, err
476 }
477 defer conn.Close()
478 uc := conn.(*net.UnixConn)
479 stop := context.AfterFunc(ctx, func() { uc.Close() })
480 defer stop()
481 if dl, ok := ctx.Deadline(); ok {
482 uc.SetDeadline(dl)
483 }
484 if uid, err := peerUID(uc); err != nil {
485 return false, fmt.Errorf("control: peer credentials: %w", err)
486 } else if uid != euid {
487 return false, fmt.Errorf("%w: socket served by another user", ErrInsecure)
488 }
489 if _, err := io.WriteString(uc, req); err != nil {
490 return false, ctxErr(ctx, err)
491 }
492 var buf [len(recLine)]byte // the longest reply
493 n, err := io.ReadFull(uc, buf[:len(want)])
494 if err != nil {
495 return false, ctxErr(ctx, err)
496 }
497 if string(buf[:n]) != want {
498 return false, ErrProtocol
499 }
500 return true, nil
501}
502
503// dialRetry connects, retrying while the server's small accept backlog is
504// full (EAGAIN) until ctx ends.
505func dialRetry(ctx context.Context, path string) (net.Conn, error) {
506 var d net.Dialer
507 for {
508 conn, err := d.DialContext(ctx, "unix", path)
509 if !errors.Is(err, unix.EAGAIN) {
510 return conn, err
511 }
512 t := time.NewTimer(5 * time.Millisecond)
513 select {
514 case <-t.C:
515 case <-ctx.Done():
516 t.Stop()
517 return nil, ctx.Err()
518 }
519 }
520}
521
522func ctxErr(ctx context.Context, err error) error {
523 if cerr := ctx.Err(); cerr != nil {
524 return cerr
525 }
526 return err
527}