boxdd: Fix linux permission

This commit is contained in:
世界
2026-07-19 17:30:59 +08:00
parent 77d9c07d2f
commit 552994b6ca
22 changed files with 747 additions and 214 deletions
@@ -0,0 +1,102 @@
//go:build linux
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"math"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
"github.com/godbus/dbus/v5"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
policyKitService = "org.freedesktop.PolicyKit1"
policyKitAuthorityPath = dbus.ObjectPath("/org/freedesktop/PolicyKit1/Authority")
policyKitAuthorityInterface = "org.freedesktop.PolicyKit1.Authority"
policyKitTakeOverAction = "io.nekohasekai.sfl.take-over-service"
policyKitAllowUserInteraction = uint32(1)
)
type policyKitSubject struct {
Kind string
Details map[string]dbus.Variant
}
type policyKitAuthorizationResult struct {
Authorized bool
Challenge bool
Details map[string]string
}
func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
if listenAddress != "" {
return nil
}
userID, err := strconv.ParseUint(identity.UserID, 10, 32)
if err != nil || userID > math.MaxInt32 {
return status.Error(codes.Unauthenticated, "daemon peer has an invalid Linux user ID")
}
if identity.ProcessID == 0 || identity.ProcessStartTime == 0 {
return status.Error(codes.Unauthenticated, "daemon peer has an invalid Linux process identity")
}
cancellationContent := make([]byte, 16)
_, err = rand.Read(cancellationContent)
if err != nil {
return E.Cause(err, "create PolicyKit cancellation ID")
}
cancellationID := "sing-box-" + hex.EncodeToString(cancellationContent)
connection, err := dbus.ConnectSystemBus()
if err != nil {
return E.Cause(err, "connect to system bus")
}
defer connection.Close()
authority := connection.Object(policyKitService, policyKitAuthorityPath)
subject := policyKitSubject{
Kind: "unix-process",
Details: map[string]dbus.Variant{
"pid": dbus.MakeVariant(identity.ProcessID),
"start-time": dbus.MakeVariant(identity.ProcessStartTime),
"uid": dbus.MakeVariant(int32(userID)),
},
}
resultChannel := make(chan *dbus.Call, 1)
authority.Go(
policyKitAuthorityInterface+".CheckAuthorization",
0,
resultChannel,
subject,
policyKitTakeOverAction,
map[string]string{},
policyKitAllowUserInteraction,
cancellationID,
)
select {
case call := <-resultChannel:
if call.Err != nil {
return E.Cause(call.Err, "check PolicyKit authorization")
}
var result policyKitAuthorizationResult
err = call.Store(&result)
if err != nil {
return E.Cause(err, "read PolicyKit authorization result")
}
if !result.Authorized {
return status.Error(codes.PermissionDenied, "take over authorization was denied")
}
return nil
case <-ctx.Done():
_ = authority.Call(
policyKitAuthorityInterface+".CancelCheckAuthorization",
0,
cancellationID,
).Err
return status.Error(codes.Canceled, "take over authorization was canceled")
}
}
@@ -0,0 +1,9 @@
//go:build !linux
package main
import "context"
func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
return nil
}
+61
View File
@@ -1,8 +1,12 @@
package main
import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
@@ -27,8 +31,65 @@ var commandServiceRestart = &cobra.Command{
},
}
var commandServiceSetInsecureMode = &cobra.Command{
Use: "set-insecure-mode <enabled>",
Short: "Set whether configurations may use privileges unrelated to networking",
Args: cobra.ExactArgs(1),
Run: func(command *cobra.Command, args []string) {
err := serviceSetInsecureMode(args[0])
if err != nil {
log.Fatal(E.Cause(err, "set insecure mode"))
}
},
}
func addPlatformServiceCommands() {
commandService.AddCommand(commandServiceRestart)
commandService.AddCommand(commandServiceSetInsecureMode)
}
func serviceSetInsecureMode(value string) error {
enabled, err := strconv.ParseBool(value)
if err != nil {
return E.Cause(err, "parse value")
}
if os.Geteuid() != 0 {
return E.New("setting insecure mode requires an elevated process")
}
directory, err := filepath.Abs(commandServiceFlagWorkingDirectory)
if err != nil {
return E.Cause(err, "resolve working directory")
}
err = validateProtectedLinuxDirectory(directory)
if err != nil {
return E.Cause(err, "validate working directory")
}
return saveSecuritySettings(directory, securitySettings{InsecureModeEnabled: enabled})
}
func validateProtectedLinuxDirectory(directory string) error {
currentPath := directory
for {
info, err := os.Lstat(currentPath)
if err != nil {
return err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return E.New("protected path is not a directory: ", currentPath)
}
fileStatus, loaded := info.Sys().(*syscall.Stat_t)
if !loaded || fileStatus.Uid != 0 {
return E.New("protected path is not owned by root: ", currentPath)
}
if info.Mode().Perm()&0o022 != 0 {
return E.New("protected path is writable by non-root users: ", currentPath)
}
parentPath := filepath.Dir(currentPath)
if parentPath == currentPath {
return nil
}
currentPath = parentPath
}
}
func runSystemctl(arguments ...string) error {
+48 -17
View File
@@ -149,51 +149,78 @@ func (s *desktopService) TakeOverService(ctx context.Context, empty *emptypb.Emp
return nil, err
}
s.daemon.lifecycleAccess.Lock()
defer s.daemon.lifecycleAccess.Unlock()
if s.daemon.closed {
s.daemon.lifecycleAccess.Unlock()
return nil, os.ErrClosed
}
ownerUserID, err := loadOwner()
if err != nil && !os.IsNotExist(err) {
s.daemon.lifecycleAccess.Unlock()
return nil, err
}
if ownerUserID == identity.UserID {
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID, identity.SessionID)
if ownerUserID == "" || ownerUserID == identity.UserID {
err = s.takeOverServiceLocked(identity, ownerUserID)
s.daemon.lifecycleAccess.Unlock()
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
if ownerUserID != "" {
err = s.daemon.stopServiceLocked(ownerUserID)
s.daemon.lifecycleAccess.Unlock()
err = authorizeTakeOver(ctx, identity)
if err != nil {
return nil, err
}
s.daemon.lifecycleAccess.Lock()
defer s.daemon.lifecycleAccess.Unlock()
if s.daemon.closed {
return nil, os.ErrClosed
}
ownerUserID, err = loadOwner()
if err != nil && !os.IsNotExist(err) {
return nil, err
}
err = s.takeOverServiceLocked(identity, ownerUserID)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (s *desktopService) takeOverServiceLocked(identity peerIdentity, ownerUserID string) error {
if ownerUserID == identity.UserID {
err := s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
return err
}
return saveOwner(identity.UserID, identity.SessionID)
}
if ownerUserID != "" {
err := s.daemon.stopServiceLocked(ownerUserID)
if err != nil {
return err
}
if s.daemon.platform != nil {
err = s.daemon.platform.ReleaseOwner()
if err != nil {
return nil, err
return err
}
}
}
err = s.daemon.configureWorkingDirectoryLocked(userWorkingDirectory(identity.UserID))
err := s.daemon.configureWorkingDirectoryLocked(userWorkingDirectory(identity.UserID))
if err != nil {
return nil, err
return err
}
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
return err
}
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
return err
}
s.daemon.disconnectPeerConnectionsExcept(identity.UserID)
return &emptypb.Empty{}, nil
return nil
}
func (s *desktopService) GetSecuritySettings(ctx context.Context, empty *emptypb.Empty) (*SecuritySettings, error) {
@@ -211,7 +238,7 @@ func (s *desktopService) GetSecuritySettings(ctx context.Context, empty *emptypb
}
func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *SetInsecureModeEnabledRequest) (*emptypb.Empty, error) {
_, err := peerIdentityFromContext(ctx)
identity, err := peerIdentityFromContext(ctx)
if err != nil {
return nil, err
}
@@ -226,6 +253,10 @@ func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *Se
if s.daemon.closed {
return nil, os.ErrClosed
}
err = authorizeDisableInsecureMode(identity)
if err != nil {
return nil, err
}
wasEnabled := s.daemon.insecureModeEnabled()
err = saveSecuritySettings(workingDirectory, securitySettings{InsecureModeEnabled: false})
if err != nil {
@@ -0,0 +1,24 @@
//go:build linux
package main
import (
"os"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func authorizeDisableInsecureMode(identity peerIdentity) error {
ownerUserID, err := loadOwner()
if err != nil {
if os.IsNotExist(err) {
return status.Error(codes.PermissionDenied, "the service has no owner")
}
return err
}
if ownerUserID != identity.UserID {
return status.Error(codes.PermissionDenied, "the service is owned by another user")
}
return nil
}
@@ -0,0 +1,7 @@
//go:build !linux
package main
func authorizeDisableInsecureMode(identity peerIdentity) error {
return nil
}
+47
View File
@@ -0,0 +1,47 @@
//go:build linux
package main
import (
"context"
"os"
"path/filepath"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
)
func registerSecurityPolicy(ctx context.Context, daemon *Daemon) {
service.MustRegister[adapter.SecurityPolicy](ctx, &daemonSecurityPolicy{daemon})
service.MustRegister[filemanager.Manager](ctx, &restrictedFileManager{daemon})
}
func insecureModeAvailable() bool {
return true
}
func insecureModePlatformName() string {
return "Linux"
}
func loadSecuritySettings(directory string) (securitySettings, error) {
content, err := os.ReadFile(filepath.Join(directory, securitySettingsFileName))
if err != nil {
return securitySettings{}, err
}
settings, err := json.UnmarshalExtended[securitySettings](content)
if err != nil {
return securitySettings{}, err
}
return settings, nil
}
func (d *Daemon) insecureModeEnabled() bool {
settings, err := loadSecuritySettings(workingDirectory)
if err != nil {
return false
}
return settings.InsecureModeEnabled
}
@@ -0,0 +1,9 @@
//go:build linux
package main
import "path/filepath"
func normalizeRestrictedPath(path string) string {
return filepath.Clean(path)
}
@@ -0,0 +1,12 @@
//go:build windows
package main
import (
"path/filepath"
"strings"
)
func normalizeRestrictedPath(path string) string {
return strings.ToLower(filepath.Clean(path))
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !linux
package main
@@ -0,0 +1,176 @@
//go:build windows || linux
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/experimental/locale"
E "github.com/sagernet/sing/common/exceptions"
)
func insecureFeatureError(feature string) error {
return E.New(fmt.Sprintf(locale.Current().InsecureFeatureMessage, feature, insecureModePlatformName()))
}
type daemonSecurityPolicy struct {
daemon *Daemon
}
func (p *daemonSecurityPolicy) CheckFeature(feature string) error {
if p.daemon.insecureModeEnabled() {
return nil
}
return insecureFeatureError(feature)
}
type restrictedFileManager struct {
daemon *Daemon
}
func (m *restrictedFileManager) BasePath(name string) string {
if filepath.IsAbs(name) {
return name
}
currentDirectory, err := os.Getwd()
if err != nil {
return name
}
return filepath.Join(currentDirectory, name)
}
func (m *restrictedFileManager) TempPath() string {
currentDirectory, err := os.Getwd()
if err != nil {
return "."
}
return currentDirectory
}
func (m *restrictedFileManager) checkPath(name string) (string, error) {
path, err := filepath.Abs(m.BasePath(name))
if err != nil {
return "", err
}
if m.daemon.insecureModeEnabled() {
return path, nil
}
currentDirectory, err := os.Getwd()
if err != nil {
return "", err
}
normalizedRoot := normalizeRestrictedPath(currentDirectory)
normalizedPath := normalizeRestrictedPath(path)
if normalizedPath != normalizedRoot && !strings.HasPrefix(normalizedPath, normalizedRoot+string(filepath.Separator)) {
return "", E.New(fmt.Sprintf(locale.Current().ExternalPathFeature, path, insecureModePlatformName()))
}
existingPath := path
for {
_, err = os.Lstat(existingPath)
if err == nil {
break
}
if !os.IsNotExist(err) {
return "", err
}
parentPath := filepath.Dir(existingPath)
if parentPath == existingPath {
return "", err
}
existingPath = parentPath
}
resolvedRoot, err := filepath.EvalSymlinks(currentDirectory)
if err != nil {
return "", err
}
resolvedExistingPath, err := filepath.EvalSymlinks(existingPath)
if err != nil {
return "", err
}
remainingPath, err := filepath.Rel(existingPath, path)
if err != nil {
return "", err
}
resolvedPath := filepath.Join(resolvedExistingPath, remainingPath)
normalizedResolvedRoot := normalizeRestrictedPath(resolvedRoot)
normalizedResolvedPath := normalizeRestrictedPath(resolvedPath)
if normalizedResolvedPath != normalizedResolvedRoot && !strings.HasPrefix(normalizedResolvedPath, normalizedResolvedRoot+string(filepath.Separator)) {
return "", E.New(fmt.Sprintf(locale.Current().ExternalPathFeature, path, insecureModePlatformName()))
}
return path, nil
}
func (m *restrictedFileManager) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
path, err := m.checkPath(name)
if err != nil {
return nil, err
}
return os.OpenFile(path, flag, perm)
}
func (m *restrictedFileManager) Create(name string) (*os.File, error) {
path, err := m.checkPath(name)
if err != nil {
return nil, err
}
return os.Create(path)
}
func (m *restrictedFileManager) CreateTemp(pattern string) (*os.File, error) {
currentDirectory, err := os.Getwd()
if err != nil {
return nil, err
}
return os.CreateTemp(currentDirectory, pattern)
}
func (m *restrictedFileManager) Chown(path string) error {
return nil
}
func (m *restrictedFileManager) Mkdir(path string, perm os.FileMode) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.Mkdir(checkedPath, perm)
}
func (m *restrictedFileManager) MkdirAll(path string, perm os.FileMode) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.MkdirAll(checkedPath, perm)
}
func (m *restrictedFileManager) Remove(path string) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.Remove(checkedPath)
}
func (m *restrictedFileManager) RemoveAll(path string) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.RemoveAll(checkedPath)
}
func (m *restrictedFileManager) Rename(oldPath string, newPath string) error {
checkedOldPath, err := m.checkPath(oldPath)
if err != nil {
return err
}
checkedNewPath, err := m.checkPath(newPath)
if err != nil {
return err
}
return os.Rename(checkedOldPath, checkedNewPath)
}
+4 -167
View File
@@ -4,14 +4,10 @@ package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/locale"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
@@ -26,6 +22,10 @@ func insecureModeAvailable() bool {
return true
}
func insecureModePlatformName() string {
return "Windows"
}
func loadSecuritySettings(directory string) (securitySettings, error) {
content, err := os.ReadFile(filepath.Join(directory, securitySettingsFileName))
if err != nil {
@@ -45,166 +45,3 @@ func (d *Daemon) insecureModeEnabled() bool {
}
return settings.InsecureModeEnabled
}
func insecureFeatureError(feature string) error {
return E.New(fmt.Sprintf(locale.Current().InsecureFeatureMessage, feature))
}
type daemonSecurityPolicy struct {
daemon *Daemon
}
func (p *daemonSecurityPolicy) CheckFeature(feature string) error {
if p.daemon.insecureModeEnabled() {
return nil
}
return insecureFeatureError(feature)
}
type restrictedFileManager struct {
daemon *Daemon
}
func (m *restrictedFileManager) BasePath(name string) string {
if filepath.IsAbs(name) {
return name
}
currentDirectory, err := os.Getwd()
if err != nil {
return name
}
return filepath.Join(currentDirectory, name)
}
func (m *restrictedFileManager) TempPath() string {
currentDirectory, err := os.Getwd()
if err != nil {
return "."
}
return currentDirectory
}
func (m *restrictedFileManager) checkPath(name string) (string, error) {
path, err := filepath.Abs(m.BasePath(name))
if err != nil {
return "", err
}
if m.daemon.insecureModeEnabled() {
return path, nil
}
currentDirectory, err := os.Getwd()
if err != nil {
return "", err
}
normalizedRoot := strings.ToLower(filepath.Clean(currentDirectory))
normalizedPath := strings.ToLower(filepath.Clean(path))
if normalizedPath != normalizedRoot && !strings.HasPrefix(normalizedPath, normalizedRoot+string(filepath.Separator)) {
return "", E.New(fmt.Sprintf(locale.Current().ExternalPathFeature, path))
}
existingPath := path
for {
_, err = os.Lstat(existingPath)
if err == nil {
break
}
if !os.IsNotExist(err) {
return "", err
}
parentPath := filepath.Dir(existingPath)
if parentPath == existingPath {
return "", err
}
existingPath = parentPath
}
resolvedRoot, err := filepath.EvalSymlinks(currentDirectory)
if err != nil {
return "", err
}
resolvedExistingPath, err := filepath.EvalSymlinks(existingPath)
if err != nil {
return "", err
}
remainingPath, err := filepath.Rel(existingPath, path)
if err != nil {
return "", err
}
resolvedPath := filepath.Join(resolvedExistingPath, remainingPath)
normalizedResolvedRoot := strings.ToLower(filepath.Clean(resolvedRoot))
normalizedResolvedPath := strings.ToLower(filepath.Clean(resolvedPath))
if normalizedResolvedPath != normalizedResolvedRoot && !strings.HasPrefix(normalizedResolvedPath, normalizedResolvedRoot+string(filepath.Separator)) {
return "", E.New(fmt.Sprintf(locale.Current().ExternalPathFeature, path))
}
return path, nil
}
func (m *restrictedFileManager) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
path, err := m.checkPath(name)
if err != nil {
return nil, err
}
return os.OpenFile(path, flag, perm)
}
func (m *restrictedFileManager) Create(name string) (*os.File, error) {
path, err := m.checkPath(name)
if err != nil {
return nil, err
}
return os.Create(path)
}
func (m *restrictedFileManager) CreateTemp(pattern string) (*os.File, error) {
currentDirectory, err := os.Getwd()
if err != nil {
return nil, err
}
return os.CreateTemp(currentDirectory, pattern)
}
func (m *restrictedFileManager) Chown(path string) error {
return nil
}
func (m *restrictedFileManager) Mkdir(path string, perm os.FileMode) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.Mkdir(checkedPath, perm)
}
func (m *restrictedFileManager) MkdirAll(path string, perm os.FileMode) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.MkdirAll(checkedPath, perm)
}
func (m *restrictedFileManager) Remove(path string) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.Remove(checkedPath)
}
func (m *restrictedFileManager) RemoveAll(path string) error {
checkedPath, err := m.checkPath(path)
if err != nil {
return err
}
return os.RemoveAll(checkedPath)
}
func (m *restrictedFileManager) Rename(oldPath string, newPath string) error {
checkedOldPath, err := m.checkPath(oldPath)
if err != nil {
return err
}
checkedNewPath, err := m.checkPath(newPath)
if err != nil {
return err
}
return os.Rename(checkedOldPath, checkedNewPath)
}
+19 -3
View File
@@ -11,9 +11,10 @@ import (
)
type peerIdentity struct {
UserID string
ProcessID uint32
SessionID uint32
UserID string
ProcessID uint32
ProcessStartTime uint64
SessionID uint32
}
type peerAuthInfo struct {
@@ -43,3 +44,18 @@ type peerConnection interface {
net.Conn
peerConnectionIdentity() peerIdentity
}
func (d *Daemon) registerPeerConnection(connection peerConnection) {
d.peerAccess.Lock()
defer d.peerAccess.Unlock()
if d.peerConnections == nil {
d.peerConnections = make(map[peerConnection]peerIdentity)
}
d.peerConnections[connection] = connection.peerConnectionIdentity()
}
func (d *Daemon) unregisterPeerConnection(connection peerConnection) {
d.peerAccess.Lock()
defer d.peerAccess.Unlock()
delete(d.peerConnections, connection)
}
+155
View File
@@ -0,0 +1,155 @@
//go:build linux
package main
import (
"bytes"
"context"
"net"
"os"
"strconv"
"strings"
"sync"
"syscall"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/unix"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type linuxTransportCredentials struct {
daemon *Daemon
}
type linuxAuthenticatedConnection struct {
net.Conn
daemon *Daemon
identity peerIdentity
close sync.Once
closeError error
}
func platformServerOptions(daemon *Daemon) ([]grpc.ServerOption, error) {
if listenAddress != "" {
return nil, nil
}
return []grpc.ServerOption{grpc.Creds(&linuxTransportCredentials{daemon: daemon})}, nil
}
func platformFallbackPeerIdentity(ctx context.Context) (peerIdentity, error) {
if listenAddress != "" {
return peerIdentity{UserID: "local"}, nil
}
return peerIdentity{}, E.New("missing Linux peer authentication")
}
func (c *linuxTransportCredentials) ClientHandshake(ctx context.Context, authority string, rawConnection net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, nil, E.New("Linux local process credentials do not support client handshakes")
}
func (c *linuxTransportCredentials) ServerHandshake(rawConnection net.Conn) (net.Conn, credentials.AuthInfo, error) {
identity, err := linuxPeerIdentity(rawConnection)
if err != nil {
return nil, nil, err
}
connection := &linuxAuthenticatedConnection{
Conn: rawConnection,
daemon: c.daemon,
identity: identity,
}
c.daemon.registerPeerConnection(connection)
authenticationInformation := &peerAuthInfo{
CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity},
identity: identity,
}
return connection, authenticationInformation, nil
}
func (c *linuxTransportCredentials) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{
SecurityProtocol: "linux-local-process",
SecurityVersion: "1",
}
}
func (c *linuxTransportCredentials) Clone() credentials.TransportCredentials {
return &linuxTransportCredentials{daemon: c.daemon}
}
func (c *linuxTransportCredentials) OverrideServerName(serverNameOverride string) error {
return nil
}
func linuxPeerIdentity(connection net.Conn) (peerIdentity, error) {
syscallConnection, loaded := connection.(syscall.Conn)
if !loaded {
return peerIdentity{}, E.New("daemon endpoint does not expose a syscall connection")
}
rawConnection, err := syscallConnection.SyscallConn()
if err != nil {
return peerIdentity{}, E.Cause(err, "access daemon endpoint")
}
var peerCredentials *unix.Ucred
var credentialError error
err = rawConnection.Control(func(fileDescriptor uintptr) {
peerCredentials, credentialError = unix.GetsockoptUcred(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_PEERCRED)
})
if err != nil {
return peerIdentity{}, E.Cause(err, "inspect daemon endpoint")
}
if credentialError != nil {
return peerIdentity{}, E.Cause(credentialError, "identify daemon peer")
}
if peerCredentials == nil || peerCredentials.Pid <= 0 {
return peerIdentity{}, E.New("daemon peer has invalid credentials")
}
processID := uint32(peerCredentials.Pid)
processStartTime, err := linuxProcessStartTime(processID)
if err != nil {
return peerIdentity{}, E.Cause(err, "identify daemon peer process")
}
return peerIdentity{
UserID: strconv.FormatUint(uint64(peerCredentials.Uid), 10),
ProcessID: processID,
ProcessStartTime: processStartTime,
}, nil
}
func linuxProcessStartTime(processID uint32) (uint64, error) {
content, err := os.ReadFile("/proc/" + strconv.FormatUint(uint64(processID), 10) + "/stat")
if err != nil {
return 0, err
}
commandEnd := bytes.LastIndexByte(content, ')')
if commandEnd < 0 {
return 0, E.New("invalid process stat")
}
fields := strings.Fields(string(content[commandEnd+1:]))
if len(fields) <= 19 {
return 0, E.New("incomplete process stat")
}
startTime, err := strconv.ParseUint(fields[19], 10, 64)
if err != nil {
return 0, E.Cause(err, "parse process start time")
}
return startTime, nil
}
func (c *linuxAuthenticatedConnection) peerConnectionIdentity() peerIdentity {
return c.identity
}
func (c *linuxAuthenticatedConnection) Close() error {
c.close.Do(func() {
c.daemon.unregisterPeerConnection(c)
c.closeError = c.Conn.Close()
})
return c.closeError
}
var (
_ credentials.TransportCredentials = (*linuxTransportCredentials)(nil)
_ peerConnection = (*linuxAuthenticatedConnection)(nil)
)
+62
View File
@@ -0,0 +1,62 @@
//go:build linux
package main
import (
"net"
"os"
"path/filepath"
"strconv"
"testing"
)
func TestLinuxPeerAuthentication(t *testing.T) {
socketPath := filepath.Join(t.TempDir(), "daemon.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
clientConnection, err := net.Dial("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer clientConnection.Close()
serverConnection, err := listener.Accept()
if err != nil {
t.Fatal(err)
}
daemon := &Daemon{}
authenticatedConnection, authenticationInformation, err := (&linuxTransportCredentials{daemon: daemon}).ServerHandshake(serverConnection)
if err != nil {
t.Fatal(err)
}
authentication, loaded := authenticationInformation.(*peerAuthInfo)
if !loaded {
t.Fatal("missing peer authentication information")
}
identity := authentication.identity
if identity.UserID != strconv.Itoa(os.Getuid()) {
t.Fatalf("unexpected peer user ID: %s", identity.UserID)
}
if identity.ProcessID != uint32(os.Getpid()) {
t.Fatalf("unexpected peer process ID: %d", identity.ProcessID)
}
expectedStartTime, err := linuxProcessStartTime(identity.ProcessID)
if err != nil {
t.Fatal(err)
}
if identity.ProcessStartTime != expectedStartTime {
t.Fatalf("unexpected peer process start time: %d", identity.ProcessStartTime)
}
if len(daemon.peerConnections) != 1 {
t.Fatalf("unexpected authenticated connection count: %d", len(daemon.peerConnections))
}
err = authenticatedConnection.Close()
if err != nil {
t.Fatal(err)
}
if len(daemon.peerConnections) != 0 {
t.Fatalf("authenticated connection was not removed: %d", len(daemon.peerConnections))
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !linux
package main
-15
View File
@@ -547,21 +547,6 @@ func (d *Daemon) duplicatePeerImpersonationToken(identity peerIdentity) (windows
return 0, E.New("authenticated application connection is no longer available")
}
func (d *Daemon) registerPeerConnection(connection peerConnection) {
d.peerAccess.Lock()
defer d.peerAccess.Unlock()
if d.peerConnections == nil {
d.peerConnections = make(map[peerConnection]peerIdentity)
}
d.peerConnections[connection] = connection.peerConnectionIdentity()
}
func (d *Daemon) unregisterPeerConnection(connection peerConnection) {
d.peerAccess.Lock()
defer d.peerAccess.Unlock()
delete(d.peerConnections, connection)
}
func (c *windowsAuthenticatedConnection) Close() error {
c.close.Do(func() {
c.daemon.unregisterPeerConnection(c)
+2 -2
View File
@@ -50,8 +50,8 @@ var defaultLocale = &Locale{
Locale: "en",
DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s. Please check the documentation for migration.",
DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.",
InsecureFeatureMessage: "%s is considered insecure in the graphical client for sing-box on Windows. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
ExternalPathFeature: "Access to %s (outside of the working directory) is considered insecure in the graphical client for sing-box on Windows. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
InsecureFeatureMessage: "%s is considered insecure in the graphical client for sing-box on %s. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
ExternalPathFeature: "Access to %s (outside of the working directory) is considered insecure in the graphical client for sing-box on %s. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
TailscaleInitializing: "Initializing",
TailscaleInUse: "In use by another user",
TailscaleNeedsLogin: "Needs login",
+2 -2
View File
@@ -5,8 +5,8 @@ func init() {
Locale: "fa",
DeprecatedMessage: "%s از sing-box %s منسوخ شده است و در sing-box %s حذف خواهد شد؛ لطفاً راهنمای مهاجرت را ببینید.",
DeprecatedMessageNoLink: "%s از sing-box %s منسوخ شده است و در sing-box %s حذف خواهد شد.",
InsecureFeatureMessage: "%s در کلاینت گرافیکی sing-box برای Windows ناامن تلقی می\u200cشود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.",
ExternalPathFeature: "دسترسی به %s (خارج از پوشهٔ کاری) در کلاینت گرافیکی sing-box برای Windows ناامن تلقی می\u200cشود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.",
InsecureFeatureMessage: "%s در کلاینت گرافیکی sing-box برای %s ناامن تلقی می\u200cشود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.",
ExternalPathFeature: "دسترسی به %s (خارج از پوشهٔ کاری) در کلاینت گرافیکی sing-box برای %s ناامن تلقی می\u200cشود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.",
TailscaleInitializing: "در حال راه\u200cاندازی",
TailscaleInUse: "در حال استفاده توسط کاربر دیگری",
TailscaleNeedsLogin: "نیاز به ورود",
+2 -2
View File
@@ -5,8 +5,8 @@ func init() {
Locale: "ru",
DeprecatedMessage: "Использование %s устарело в sing-box %s, и эта возможность будет удалена в sing-box %s. Ознакомьтесь с руководством по миграции.",
DeprecatedMessageNoLink: "Использование %s устарело в sing-box %s, и эта возможность будет удалена в sing-box %s.",
InsecureFeatureMessage: "%s считается небезопасным в графическом клиенте sing-box для Windows. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.",
ExternalPathFeature: "Доступ к %s (за пределами рабочего каталога) считается небезопасным в графическом клиенте sing-box для Windows. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.",
InsecureFeatureMessage: "%s считается небезопасным в графическом клиенте sing-box для %s. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.",
ExternalPathFeature: "Доступ к %s (за пределами рабочего каталога) считается небезопасным в графическом клиенте sing-box для %s. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.",
TailscaleInitializing: "Инициализация",
TailscaleInUse: "Используется другим пользователем",
TailscaleNeedsLogin: "Требуется вход",
+2 -2
View File
@@ -7,8 +7,8 @@ func init() {
Locale: "zh-Hans",
DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers,
DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers,
InsecureFeatureMessage: "%s 在 sing-box 的 Windows 图形客户端中被视为不安全。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。",
ExternalPathFeature: "访问 %s(位于工作目录之外)在 sing-box 的 Windows 图形客户端中是不安全的。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。",
InsecureFeatureMessage: "%s 在 sing-box 的 %s 图形客户端中被视为不安全。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。",
ExternalPathFeature: "访问 %s(位于工作目录之外)在 sing-box 的 %s 图形客户端中是不安全的。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。",
TailscaleInitializing: "正在初始化",
TailscaleInUse: "正由其他用户使用",
TailscaleNeedsLogin: "需要登录",
+2 -2
View File
@@ -5,8 +5,8 @@ func init() {
Locale: "zh-Hant",
DeprecatedMessage: "%s 已在 sing-box %s 中棄用,且將在 sing-box %s 中移除,請參閱遷移指南。",
DeprecatedMessageNoLink: "%s 已在 sing-box %s 中棄用,且將在 sing-box %s 中移除。",
InsecureFeatureMessage: "%s 在 sing-box 的 Windows 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。",
ExternalPathFeature: "存取 %s(位於工作目錄之外)在 sing-box 的 Windows 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。",
InsecureFeatureMessage: "%s 在 sing-box 的 %s 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。",
ExternalPathFeature: "存取 %s(位於工作目錄之外)在 sing-box 的 %s 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。",
TailscaleInitializing: "正在初始化",
TailscaleInUse: "正由其他使用者使用",
TailscaleNeedsLogin: "需要登入",