Smail/backend/services/adminreset/transport_unix.go
smail 37bb937c40
Some checks are pending
Security Checks / security (push) Waiting to run
加固管理员恢复边界并补充三库实机与安全复扫验收
2026-09-21 04:21:14 +08:00

178 lines
4.4 KiB
Go

//go:build linux
package adminreset
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"syscall"
"time"
"golang.org/x/sys/unix"
)
const DefaultControlPath = "/run/smail/admin-control.sock"
type UnixServer struct {
listener net.Listener
path string
allowed map[uint32]bool
}
func ListenUnix(path string, allowedUIDs []uint32) (*UnixServer, error) {
if path == "" {
path = DefaultControlPath
}
parent := filepath.Dir(path)
if err := os.MkdirAll(parent, 0750); err != nil {
return nil, fmt.Errorf("create control directory: %w", err)
}
if err := validateControlDirectory(parent); err != nil {
return nil, err
}
if err := os.Chmod(parent, 0750); err != nil {
return nil, fmt.Errorf("set control directory permissions: %w", err)
}
if info, err := os.Lstat(path); err == nil {
if info.Mode()&os.ModeSymlink != 0 || info.Mode()&os.ModeSocket == 0 {
return nil, fmt.Errorf("control path is not a socket")
}
if err := validateControlSocketOwner(info); err != nil {
return nil, err
}
if conn, dialErr := net.DialTimeout("unix", path, 100*time.Millisecond); dialErr == nil {
_ = conn.Close()
return nil, fmt.Errorf("control socket is already active")
}
if err := os.Remove(path); err != nil {
return nil, fmt.Errorf("remove stale control socket: %w", err)
}
} else if !os.IsNotExist(err) {
return nil, err
}
listener, err := net.Listen("unix", path)
if err != nil {
return nil, fmt.Errorf("listen control socket: %w", err)
}
if err := os.Chmod(path, 0600); err != nil {
_ = listener.Close()
_ = os.Remove(path)
return nil, fmt.Errorf("set control socket permissions: %w", err)
}
allowed := make(map[uint32]bool, len(allowedUIDs))
for _, uid := range allowedUIDs {
allowed[uid] = true
}
return &UnixServer{listener: listener, path: path, allowed: allowed}, nil
}
func validateControlDirectory(path string) error {
info, err := os.Lstat(path)
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return fmt.Errorf("control directory is not secure")
}
if info.Mode().Perm()&0o022 != 0 {
return fmt.Errorf("control directory must not be writable by group or other users")
}
return validateControlOwner(info, "control directory")
}
func validateControlSocketOwner(info os.FileInfo) error {
return validateControlOwner(info, "control socket")
}
func validateControlOwner(info os.FileInfo, resource string) error {
stat, ok := info.Sys().(*syscall.Stat_t)
uid := os.Geteuid()
if !ok || uid < 0 || uint64(stat.Uid) != uint64(uid) {
return fmt.Errorf("%s owner does not match service identity", resource)
}
return nil
}
func (s *UnixServer) Serve(ctx context.Context, handler ResetHandler, guard *SessionGuard) error {
if s == nil || s.listener == nil || handler == nil || guard == nil {
return ErrInvalidRequest
}
for {
conn, err := s.listener.Accept()
if err != nil {
select {
case <-ctx.Done():
return nil
default:
}
return err
}
go s.handle(conn, handler, guard)
}
}
func (s *UnixServer) handle(conn net.Conn, handler ResetHandler, guard *SessionGuard) {
defer conn.Close()
if err := s.authorize(conn); err != nil {
_ = EncodeResponse(conn, Response{Code: "UNAUTHORIZED", Message: "本机调用者身份不受信任"})
return
}
handleResetConnection(conn, conn.SetDeadline, handler, guard)
}
func (s *UnixServer) authorize(conn net.Conn) error {
unixConn, ok := conn.(*net.UnixConn)
if !ok {
return ErrUnauthorizedPeer
}
var uid uint32
var controlErr error
rawConn, err := unixConn.SyscallConn()
if err != nil {
return err
}
if err := rawConn.Control(func(fd uintptr) {
ucred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
if err != nil {
controlErr = err
return
}
uid = ucred.Uid
}); err != nil {
return err
}
if controlErr != nil {
return controlErr
}
if len(s.allowed) == 0 {
return ErrUnauthorizedPeer
}
if !s.allowed[uid] {
return syscall.EPERM
}
return nil
}
func (s *UnixServer) Close() error {
if s == nil {
return nil
}
if s.listener != nil {
_ = s.listener.Close()
}
if s.path != "" {
if info, err := os.Lstat(s.path); err == nil && info.Mode()&os.ModeSymlink == 0 && info.Mode()&os.ModeSocket != 0 {
return os.Remove(s.path)
}
}
return nil
}
func DialControl(ctx context.Context, path string) (net.Conn, error) {
if path == "" {
path = DefaultControlPath
}
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", path)
}