From 0b850d6b56a5d6799f5b13e11bd36b30dbab003f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 10 Aug 2026 08:32:22 +0800 Subject: [PATCH] Fix Android process finder --- common/process/searcher_android.go | 55 ------- common/process/searcher_linux.go | 142 +++++++++++++++++- common/process/searcher_linux_shared_test.go | 60 -------- ...r_linux_shared.go => socket_diag_linux.go} | 135 ----------------- 4 files changed, 136 insertions(+), 256 deletions(-) delete mode 100644 common/process/searcher_android.go delete mode 100644 common/process/searcher_linux_shared_test.go rename common/process/{searcher_linux_shared.go => socket_diag_linux.go} (65%) diff --git a/common/process/searcher_android.go b/common/process/searcher_android.go deleted file mode 100644 index 6b845eb5..00000000 --- a/common/process/searcher_android.go +++ /dev/null @@ -1,55 +0,0 @@ -package process - -import ( - "context" - "net/netip" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" -) - -var _ Searcher = (*androidSearcher)(nil) - -type androidSearcher struct { - packageManager tun.PackageManager -} - -func NewSearcher(config Config) (Searcher, error) { - if config.PackageManager == nil { - return nil, E.New("missing package manager") - } - return &androidSearcher{config.PackageManager}, nil -} - -func (s *androidSearcher) ResetCache() { -} - -func (s *androidSearcher) Close() error { - return nil -} - -func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { - family, protocol, err := socketDiagSettings(network, source) - if err != nil { - return nil, err - } - _, uid, err := querySocketDiagOnce(family, protocol, source) - if err != nil { - return nil, err - } - appID := uid % 100000 - var packageNames []string - if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded { - packageNames = append(packageNames, sharedPackage) - } - if packages, loaded := s.packageManager.PackagesByID(appID); loaded { - packageNames = append(packageNames, packages...) - } - packageNames = common.Uniq(packageNames) - return &adapter.ConnectionOwner{ - UserId: int32(uid), - AndroidPackageNames: packageNames, - }, nil -} diff --git a/common/process/searcher_linux.go b/common/process/searcher_linux.go index 1d94ea72..d226c0e4 100644 --- a/common/process/searcher_linux.go +++ b/common/process/searcher_linux.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build linux package process @@ -6,37 +6,59 @@ import ( "context" "errors" "net/netip" + "os" + "path/filepath" + "strings" "syscall" "time" + "unicode" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" ) +const pathProc = "/proc" + var _ Searcher = (*linuxSearcher)(nil) type linuxSearcher struct { logger log.ContextLogger + packageManager tun.PackageManager diagConns [4]*socketDiagConn - processPathCache *uidProcessPathCache + processPathCache *freelru.Cache[uint32, *uidProcessPaths] +} + +type uidProcessPaths struct { + entries map[uint32]string } func NewSearcher(config Config) (Searcher, error) { + processPathCache := common.Must1(freelru.New[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32, true)) + processPathCache.SetLifetime(time.Second) searcher := &linuxSearcher{ logger: config.Logger, - processPathCache: newUIDProcessPathCache(time.Second), + packageManager: config.PackageManager, + processPathCache: processPathCache, } for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} { for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} { - searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol) + searcher.diagConns[socketDiagConnIndex(family, protocol)] = &socketDiagConn{ + family: family, + protocol: protocol, + fd: -1, + } } } return searcher, nil } func (s *linuxSearcher) ResetCache() { - s.processPathCache.cache.Purge() + s.processPathCache.Purge() } func (s *linuxSearcher) Close() error { @@ -58,12 +80,23 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou processInfo := &adapter.ConnectionOwner{ UserId: int32(uid), } - processPath, err := s.processPathCache.findProcessPath(inode, uid) + processPath, err := s.findProcessPath(inode, uid) if err != nil { s.logger.DebugContext(ctx, "find process path: ", err) } else { processInfo.ProcessPath = processPath } + if s.packageManager != nil { + appID := uid % 100000 + var packageNames []string + if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded { + packageNames = append(packageNames, sharedPackage) + } + if packages, loaded := s.packageManager.PackagesByID(appID); loaded { + packageNames = append(packageNames, packages...) + } + processInfo.AndroidPackageNames = common.Uniq(packageNames) + } return processInfo, nil } @@ -87,3 +120,100 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr } return querySocketDiagOnce(family, protocol, source) } + +func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) { + if cached, ok := s.processPathCache.Get(uid); ok { + if processPath, found := cached.entries[targetInode]; found { + return processPath, nil + } + } + processPaths, err := buildProcessPathsByUID(uid) + if err != nil { + return "", err + } + s.processPathCache.Add(uid, &uidProcessPaths{entries: processPaths}) + processPath, found := processPaths[targetInode] + if !found { + return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found") + } + return processPath, nil +} + +func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) { + files, err := os.ReadDir(pathProc) + if err != nil { + return nil, err + } + buffer := make([]byte, syscall.PathMax) + processPaths := make(map[uint32]string) + for _, file := range files { + if !file.IsDir() || !isPid(file.Name()) { + continue + } + info, err := file.Info() + if err != nil { + if isIgnorableProcError(err) { + continue + } + return nil, err + } + if info.Sys().(*syscall.Stat_t).Uid != uid { + continue + } + processPath := filepath.Join(pathProc, file.Name()) + fdPath := filepath.Join(processPath, "fd") + exePath, err := os.Readlink(filepath.Join(processPath, "exe")) + if err != nil { + if isIgnorableProcError(err) { + continue + } + return nil, err + } + fds, err := os.ReadDir(fdPath) + if err != nil { + continue + } + for _, fd := range fds { + n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer) + if err != nil { + continue + } + inode, ok := parseSocketInode(buffer[:n]) + if !ok { + continue + } + if _, loaded := processPaths[inode]; !loaded { + processPaths[inode] = exePath + } + } + } + return processPaths, nil +} + +func isIgnorableProcError(err error) bool { + return os.IsNotExist(err) || os.IsPermission(err) +} + +func parseSocketInode(link []byte) (uint32, bool) { + const socketPrefix = "socket:[" + if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' { + return 0, false + } + var inode uint64 + for _, char := range link[len(socketPrefix) : len(link)-1] { + if char < '0' || char > '9' { + return 0, false + } + inode = inode*10 + uint64(char-'0') + if inode > uint64(^uint32(0)) { + return 0, false + } + } + return uint32(inode), true +} + +func isPid(s string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !unicode.IsDigit(r) + }) == -1 +} diff --git a/common/process/searcher_linux_shared_test.go b/common/process/searcher_linux_shared_test.go deleted file mode 100644 index 1befff4e..00000000 --- a/common/process/searcher_linux_shared_test.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build linux - -package process - -import ( - "net" - "net/netip" - "os" - "syscall" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestQuerySocketDiagUDPExact(t *testing.T) { - t.Parallel() - server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) - require.NoError(t, err) - defer server.Close() - - client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr)) - require.NoError(t, err) - defer client.Close() - - err = client.SetDeadline(time.Now().Add(time.Second)) - require.NoError(t, err) - _, err = client.Write([]byte{0}) - require.NoError(t, err) - - err = server.SetReadDeadline(time.Now().Add(time.Second)) - require.NoError(t, err) - buffer := make([]byte, 1) - _, _, err = server.ReadFromUDP(buffer) - require.NoError(t, err) - - source := addrPortFromUDPAddr(t, client.LocalAddr()) - destination := addrPortFromUDPAddr(t, client.RemoteAddr()) - - fd, err := openSocketDiag() - require.NoError(t, err) - defer syscall.Close(fd) - - inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false)) - require.NoError(t, err) - require.NotZero(t, inode) - require.EqualValues(t, os.Getuid(), uid) -} - -func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort { - t.Helper() - - udpAddr, ok := addr.(*net.UDPAddr) - require.True(t, ok) - - ip, ok := netip.AddrFromSlice(udpAddr.IP) - require.True(t, ok) - - return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port)) -} diff --git a/common/process/searcher_linux_shared.go b/common/process/socket_diag_linux.go similarity index 65% rename from common/process/searcher_linux_shared.go rename to common/process/socket_diag_linux.go index faf2bbe9..0adb3d13 100644 --- a/common/process/searcher_linux_shared.go +++ b/common/process/socket_diag_linux.go @@ -1,6 +1,5 @@ //go:build linux -//nolint:unused package process import ( @@ -8,18 +7,11 @@ import ( "errors" "net/netip" "os" - "path/filepath" - "strings" "sync" "syscall" - "time" - "unicode" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/contrab/freelru" - "github.com/sagernet/sing/contrab/maphash" ) const ( @@ -27,7 +19,6 @@ const ( sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData socketDiagResponseMinSize = 72 socketDiagByFamily = 20 - pathProc = "/proc" ) type socketDiagConn struct { @@ -37,22 +28,6 @@ type socketDiagConn struct { fd int } -type uidProcessPathCache struct { - cache *freelru.Cache[uint32, *uidProcessPaths] -} - -type uidProcessPaths struct { - entries map[uint32]string -} - -func newSocketDiagConn(family, protocol uint8) *socketDiagConn { - return &socketDiagConn{ - family: family, - protocol: protocol, - fd: -1, - } -} - func socketDiagConnIndex(family, protocol uint8) int { index := 0 if protocol == syscall.IPPROTO_UDP { @@ -84,30 +59,6 @@ func socketDiagSettings(network string, source netip.AddrPort) (family, protocol return family, protocol, nil } -func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache { - cache := common.Must1(freelru.New[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32, true)) - cache.SetLifetime(ttl) - return &uidProcessPathCache{cache: cache} -} - -func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) { - if cached, ok := c.cache.Get(uid); ok { - if processPath, found := cached.entries[targetInode]; found { - return processPath, nil - } - } - processPaths, err := buildProcessPathByUIDCache(uid) - if err != nil { - return "", err - } - c.cache.Add(uid, &uidProcessPaths{entries: processPaths}) - processPath, found := processPaths[targetInode] - if !found { - return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found") - } - return processPath, nil -} - func (c *socketDiagConn) Close() error { c.access.Lock() defer c.access.Unlock() @@ -127,9 +78,6 @@ func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort if err == nil || errors.Is(err, ErrNotFound) { return inode, uid, err } - if !shouldRetrySocketDiag(err) { - return 0, 0, err - } _ = c.closeLocked() } return 0, 0, err @@ -299,86 +247,3 @@ func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { return E.New("netlink message: ", sysErr) } } - -func shouldRetrySocketDiag(err error) bool { - return err != nil && !errors.Is(err, ErrNotFound) -} - -func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) { - files, err := os.ReadDir(pathProc) - if err != nil { - return nil, err - } - buffer := make([]byte, syscall.PathMax) - processPaths := make(map[uint32]string) - for _, file := range files { - if !file.IsDir() || !isPid(file.Name()) { - continue - } - info, err := file.Info() - if err != nil { - if isIgnorableProcError(err) { - continue - } - return nil, err - } - if info.Sys().(*syscall.Stat_t).Uid != uid { - continue - } - processPath := filepath.Join(pathProc, file.Name()) - fdPath := filepath.Join(processPath, "fd") - exePath, err := os.Readlink(filepath.Join(processPath, "exe")) - if err != nil { - if isIgnorableProcError(err) { - continue - } - return nil, err - } - fds, err := os.ReadDir(fdPath) - if err != nil { - continue - } - for _, fd := range fds { - n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer) - if err != nil { - continue - } - inode, ok := parseSocketInode(buffer[:n]) - if !ok { - continue - } - if _, loaded := processPaths[inode]; !loaded { - processPaths[inode] = exePath - } - } - } - return processPaths, nil -} - -func isIgnorableProcError(err error) bool { - return os.IsNotExist(err) || os.IsPermission(err) -} - -func parseSocketInode(link []byte) (uint32, bool) { - const socketPrefix = "socket:[" - if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' { - return 0, false - } - var inode uint64 - for _, char := range link[len(socketPrefix) : len(link)-1] { - if char < '0' || char > '9' { - return 0, false - } - inode = inode*10 + uint64(char-'0') - if inode > uint64(^uint32(0)) { - return 0, false - } - } - return uint32(inode), true -} - -func isPid(s string) bool { - return strings.IndexFunc(s, func(r rune) bool { - return !unicode.IsDigit(r) - }) == -1 -}