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 switch {
371 case bytes.Equal(line, []byte(stopLine)):
372 // Ack before delivering: the owner may Close, and so drop this
373 // connection, as soon as it sees the request. The ack means the stop
374 // was requested, not that the capture file is finalized.
375 io.WriteString(c, okLine)
376 // Coalescing, non-blocking.
377 select {
378 case s.reqs <- RequestStop:
379 default:
380 }
381 case bytes.Equal(line, []byte(statusLine)):
382 io.WriteString(c, recLine)
383 }
384}
385
386// readLine reads one newline-terminated request of at most maxRequest bytes.
387func readLine(r io.Reader) ([]byte, bool) {
388 var buf [maxRequest]byte
389 b := buf[:]
390 n := 0
391 for n < len(b) {
392 m, err := r.Read(b[n:])
393 n += m
394 if i := bytes.IndexByte(b[:n], '\n'); i >= 0 {
395 // The request is exactly one line; trailing bytes are junk.
396 return b[:i+1], i+1 == n
397 }
398 if err != nil {
399 return nil, false
400 }
401 }
402 return nil, false
403}
404
405func peerUID(c *net.UnixConn) (uint32, error) {
406 rc, err := c.SyscallConn()
407 if err != nil {
408 return 0, err
409 }
410 var uc *unix.Ucred
411 var serr error
412 if err := rc.Control(func(fd uintptr) {
413 uc, serr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
414 }); err != nil {
415 return 0, err
416 }
417 if serr != nil {
418 return 0, serr
419 }
420 return uc.Uid, nil
421}
422
423// Stop asks the active recording to stop. It returns false with a nil error
424// when no recording is active. A true result means the request was accepted,
425// not that the capture has been finalized.
426func Stop(ctx context.Context) (bool, error) {
427 rd, err := runtimeDir()
428 if err != nil {
429 return false, err
430 }
431 return exchange(ctx, rd, stopLine, okLine)
432}
433
434// Query reports whether a recording is active.
435func Query(ctx context.Context) (bool, error) {
436 rd, err := runtimeDir()
437 if err != nil {
438 return false, err
439 }
440 return exchange(ctx, rd, statusLine, recLine)
441}
442
443func exchange(ctx context.Context, runtime, req, want string) (bool, error) {
444 euid := uint32(os.Geteuid())
445 dirFD, err := openSecureDir(runtime, false, euid)
446 if err != nil {
447 if errors.Is(err, unix.ENOENT) {
448 return false, nil
449 }
450 return false, err
451 }
452 defer unix.Close(dirFD)
453 var st unix.Stat_t
454 if err := unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
455 if err == unix.ENOENT {
456 return false, nil
457 }
458 return false, fmt.Errorf("control: stat socket: %w", err)
459 }
460 if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Uid != euid || st.Mode&0o077 != 0 {
461 return false, fmt.Errorf("%w: %s", ErrInsecure, sockName)
462 }
463
464 ctx, cancel := context.WithTimeout(ctx, ioTimeout)
465 defer cancel()
466 conn, err := dialRetry(ctx, dirPath(dirFD))
467 if err != nil {
468 // A dead owner leaves the socket behind; only the next server, which
469 // holds the lock, removes it.
470 if errors.Is(err, unix.ECONNREFUSED) || errors.Is(err, unix.ENOENT) {
471 return false, nil
472 }
473 return false, err
474 }
475 defer conn.Close()
476 uc := conn.(*net.UnixConn)
477 stop := context.AfterFunc(ctx, func() { uc.Close() })
478 defer stop()
479 if dl, ok := ctx.Deadline(); ok {
480 uc.SetDeadline(dl)
481 }
482 if uid, err := peerUID(uc); err != nil {
483 return false, fmt.Errorf("control: peer credentials: %w", err)
484 } else if uid != euid {
485 return false, fmt.Errorf("%w: socket served by another user", ErrInsecure)
486 }
487 dev, ino := uint64(st.Dev), uint64(st.Ino)
488 if _, err := io.WriteString(uc, req); err != nil {
489 return false, droppedErr(ctx, dirFD, dev, ino, err)
490 }
491 var buf [len(recLine)]byte // the longest reply
492 n, err := io.ReadFull(uc, buf[:len(want)])
493 if n == 0 && err != nil {
494 return false, droppedErr(ctx, dirFD, dev, ino, err)
495 }
496 if err != nil {
497 return false, ctxErr(ctx, err)
498 }
499 if string(buf[:n]) != want {
500 return false, ErrProtocol
501 }
502 return true, nil
503}
504
505// dialRetry connects, retrying while the server's small accept backlog is
506// full (EAGAIN) until ctx ends.
507func dialRetry(ctx context.Context, path string) (net.Conn, error) {
508 var d net.Dialer
509 for {
510 conn, err := d.DialContext(ctx, "unix", path)
511 if !errors.Is(err, unix.EAGAIN) {
512 return conn, err
513 }
514 t := time.NewTimer(5 * time.Millisecond)
515 select {
516 case <-t.C:
517 case <-ctx.Done():
518 t.Stop()
519 return nil, ctx.Err()
520 }
521 }
522}
523
524// droppedErr handles a connection closed before any reply. Close drops open
525// connections, then removes the socket, so an owner shutting down is seen
526// as the socket inode going away: that is no recording, not an error. A
527// server that dropped us and keeps serving yields the original error once
528// ctx ends; a cancelled caller gets context.Canceled.
529func droppedErr(ctx context.Context, dirFD int, dev, ino uint64, err error) error {
530 if !errors.Is(err, io.EOF) && !errors.Is(err, unix.ECONNRESET) && !errors.Is(err, unix.EPIPE) {
531 return ctxErr(ctx, err)
532 }
533 for !socketGone(dirFD, dev, ino) {
534 t := time.NewTimer(5 * time.Millisecond)
535 select {
536 case <-t.C:
537 case <-ctx.Done():
538 t.Stop()
539 if errors.Is(ctx.Err(), context.Canceled) {
540 return ctx.Err()
541 }
542 return fmt.Errorf("control: server closed the connection: %w", err)
543 }
544 }
545 return nil
546}
547
548func socketGone(dirFD int, dev, ino uint64) bool {
549 var st unix.Stat_t
550 err := unix.Fstatat(dirFD, sockName, &st, unix.AT_SYMLINK_NOFOLLOW)
551 return err == unix.ENOENT || err == nil && (uint64(st.Dev) != dev || uint64(st.Ino) != ino)
552}
553
554func ctxErr(ctx context.Context, err error) error {
555 if cerr := ctx.Err(); cerr != nil {
556 return cerr
557 }
558 return err
559}