//go:build linux package instancectl import ( "context" "errors" "fmt" "log" "net" "os" "os/exec" "os/user" "strconv" "strings" "sync" "syscall" "time" "golang.org/x/sys/unix" ) // peerCred reads SO_PEERCRED from a connected unix socket. func peerCred(conn net.Conn) (*unix.Ucred, error) { uc, ok := conn.(*net.UnixConn) if !ok { return nil, errors.New("not a unix connection") } raw, err := uc.SyscallConn() if err != nil { return nil, err } var cred *unix.Ucred var cerr error if err := raw.Control(func(fd uintptr) { cred, cerr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil { return nil, err } return cred, cerr } // productionPeerOK requires the peer to be uid carlos AND the main process // of carlos-edge.service as systemd reports it (design §5.4): a flat tenant // is a child of that pid, never the pid itself, and a nested tenant cannot // reach the socket at all. MainPID is asked of systemctl and cached for one // second, because the agent restarts and the pid changes. func productionPeerOK() func(net.Conn) error { // A missing carlos user or an unparsable uid is fatal at startup, not a // permanent refusal cached into the returned closure: socket activation // restarts this service on the very next connection attempt, so a fix // to the box (the user provisioned, say) takes effect on the next // accept instead of requiring every already-running process to be // found and killed by hand (final review, plan 2, item 6). u, err := user.Lookup("carlos") if err != nil { log.Fatalf("instance-control: no carlos user: %v", err) } carlosUID, err := strconv.Atoi(u.Uid) if err != nil { log.Fatalf("instance-control: carlos uid %q: %v", u.Uid, err) } var mu sync.Mutex var mainPID int var lastErr error var at time.Time edgePID := func() (int, error) { mu.Lock() defer mu.Unlock() // Cache both a success and a failure for one second: while the // edge has no MainPID yet (a restart window), every refused // connect would otherwise re-exec systemctl under this mutex — // the negative case needs the same cache the positive one always // had (codex review fix round 2 item 5). if time.Since(at) < time.Second { if mainPID > 0 { return mainPID, nil } if lastErr != nil { return 0, lastErr } } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() out, err := exec.CommandContext(ctx, "systemctl", "show", "-p", "MainPID", "--value", edgeUnit).Output() if err != nil { mainPID, lastErr, at = 0, err, time.Now() return 0, err } pid, err := strconv.Atoi(strings.TrimSpace(string(out))) if err != nil || pid <= 0 { err = fmt.Errorf("carlos-edge.service has no MainPID") mainPID, lastErr, at = 0, err, time.Now() return 0, err } mainPID, lastErr, at = pid, nil, time.Now() return pid, nil } return func(conn net.Conn) error { cred, err := peerCred(conn) if err != nil { return err } if int(cred.Uid) != carlosUID { return fmt.Errorf("peer uid %d is not carlos", cred.Uid) } pid, err := edgePID() if err != nil { return err } if int(cred.Pid) != pid { return fmt.Errorf("peer pid %d is not the edge's main pid %d", cred.Pid, pid) } return nil } } // activatedListener returns the socket-activated listener when systemd // passed one (LISTEN_PID is us, LISTEN_FDS is 1, the fd is 3). func activatedListener() (net.Listener, bool, error) { if os.Getenv("LISTEN_PID") != strconv.Itoa(os.Getpid()) { return nil, false, nil } if os.Getenv("LISTEN_FDS") != "1" { return nil, false, fmt.Errorf("LISTEN_FDS=%q, want 1", os.Getenv("LISTEN_FDS")) } f := os.NewFile(3, "control.sock") defer f.Close() ln, err := net.FileListener(f) if err != nil { return nil, false, err } return ln, true, nil } // bindControlSocket is the hand-run fallback: bind the path with the // ownership the socket unit would give it (carlos, 0600). func bindControlSocket(path string) (net.Listener, error) { _ = os.Remove(path) ln, err := net.Listen("unix", path) if err != nil { return nil, err } u, err := user.Lookup("carlos") if err != nil { ln.Close() return nil, err } uid, _ := strconv.Atoi(u.Uid) gid, _ := strconv.Atoi(u.Gid) if err := os.Chown(path, uid, gid); err != nil { ln.Close() return nil, err } if err := os.Chmod(path, 0o600); err != nil { ln.Close() return nil, err } return ln, nil } // probeFixturesOK checks the two probe fixtures tmpfiles stages: the // secret the probe must not read, and the host /dev/shm file it must not // see. Either one missing would make its check pass vacuously. func probeFixturesOK() error { shm, err := os.Lstat("/dev/shm/carlos-probe") if err != nil { return fmt.Errorf("probe shm fixture absent: %v", err) } if sst, ok := shm.Sys().(*syscall.Stat_t); !ok || sst.Uid != 0 || !shm.Mode().IsRegular() { return errors.New("probe shm fixture: not a root-owned regular file") } fi, err := os.Stat("/etc/carlos/probe-secret") if err != nil { return fmt.Errorf("probe fixture absent: %v", err) } st, ok := fi.Sys().(*syscall.Stat_t) if !ok { return errors.New("probe fixture: no stat") } g, err := user.LookupGroup("carlos") if err != nil { return err } gid, _ := strconv.Atoi(g.Gid) if st.Uid != 0 || int(st.Gid) != gid || fi.Mode().Perm() != 0o640 { return fmt.Errorf("probe fixture is uid %d gid %d mode %o, want root:carlos 0640", st.Uid, st.Gid, fi.Mode().Perm()) } return nil } // statfsBytes is the free space an unprivileged writer can use (f_bavail, // so root's reserved blocks do not count) and the filesystem's size. func statfsBytes(path string) (avail, total uint64, err error) { var st unix.Statfs_t if err := unix.Statfs(path, &st); err != nil { return 0, 0, err } bs := uint64(st.Bsize) return st.Bavail * bs, st.Blocks * bs, nil }