178 lines
4.4 KiB
Go
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)
|
|
}
|