From 4ef3432c0d00a1b653d1e8ea244debc8258dad83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Sep 2026 15:51:35 +0800 Subject: [PATCH] Fix Linux process search matching the wrong socket --- common/process/searcher_linux.go | 4 +- common/process/socket_diag_linux.go | 150 +++++++++++++++++++---- common/process/socket_diag_linux_test.go | 108 ++++++++++++++++ 3 files changed, 234 insertions(+), 28 deletions(-) create mode 100644 common/process/socket_diag_linux_test.go diff --git a/common/process/searcher_linux.go b/common/process/searcher_linux.go index d226c0e4..573899c2 100644 --- a/common/process/searcher_linux.go +++ b/common/process/searcher_linux.go @@ -101,6 +101,8 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou } func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + source = netip.AddrPortFrom(source.Addr().Unmap(), source.Port()) + destination = netip.AddrPortFrom(destination.Addr().Unmap(), destination.Port()) family, protocol, err := socketDiagSettings(network, source) if err != nil { return 0, 0, err @@ -118,7 +120,7 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr return 0, 0, err } } - return querySocketDiagOnce(family, protocol, source) + return dumpSocketDiag(family, protocol, source, destination) } func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) { diff --git a/common/process/socket_diag_linux.go b/common/process/socket_diag_linux.go index 0adb3d13..b4769b85 100644 --- a/common/process/socket_diag_linux.go +++ b/common/process/socket_diag_linux.go @@ -9,6 +9,7 @@ import ( "os" "sync" "syscall" + "time" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" @@ -19,6 +20,7 @@ const ( sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData socketDiagResponseMinSize = 72 socketDiagByFamily = 20 + socketDiagTimeout = 100 * time.Millisecond ) type socketDiagConn struct { @@ -28,6 +30,13 @@ type socketDiagConn struct { fd int } +type socketDiagEntry struct { + source netip.AddrPort + destination netip.AddrPort + uid uint32 + inode uint32 +} + func socketDiagConnIndex(family, protocol uint8) int { index := 0 if protocol == syscall.IPPROTO_UDP { @@ -83,15 +92,6 @@ func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort return 0, 0, err } -func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) { - fd, err := openSocketDiag() - if err != nil { - return 0, 0, E.Cause(err, "dial netlink") - } - defer syscall.Close(fd) - return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) -} - func (c *socketDiagConn) ensureOpenLocked() error { if c.fd != -1 { return nil @@ -109,12 +109,12 @@ func openSocketDiag() (int, error) { if err != nil { return -1, err } - timeout := &syscall.Timeval{Usec: 100} - if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil { + timeout := syscall.NsecToTimeval(socketDiagTimeout.Nanoseconds()) + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &timeout); err != nil { syscall.Close(fd) return -1, err } - if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil { + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil { syscall.Close(fd) return -1, err } @@ -196,36 +196,132 @@ func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) { if err != nil { return 0, 0, E.Cause(err, "parse netlink message") } - return unpackSocketDiagMessages(messages) -} - -func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) { for _, message := range messages { switch message.Header.Type { - case syscall.NLMSG_DONE: - continue case syscall.NLMSG_ERROR: err = unpackSocketDiagError(&message) if err != nil { return 0, 0, err } case socketDiagByFamily: - inode, uid = unpackSocketDiagResponse(&message) - if inode != 0 || uid != 0 { - return inode, uid, nil + entry, valid := unpackSocketDiagEntry(&message) + if valid && (entry.inode != 0 || entry.uid != 0) { + return entry.inode, entry.uid, nil } } } return 0, 0, ErrNotFound } -func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { - if len(msg.Data) < socketDiagResponseMinSize { - return 0, 0 +// The dump only filters by port (inet_diag_dump_icsk, udp_dump), so every socket +// in the namespace with the requested local port is returned regardless of address. +// A dual-stack socket carrying a v4-mapped address is only listed under AF_INET6. +func dumpSocketDiag(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + families := []uint8{family} + if family == syscall.AF_INET { + families = append(families, syscall.AF_INET6) } - uid = binary.NativeEndian.Uint32(msg.Data[64:68]) - inode = binary.NativeEndian.Uint32(msg.Data[68:72]) - return inode, uid + for _, dumpFamily := range families { + inode, uid, err = dumpSocketDiagFamily(dumpFamily, protocol, source, destination) + if err == nil || !errors.Is(err, ErrNotFound) { + return inode, uid, err + } + } + return 0, 0, ErrNotFound +} + +func dumpSocketDiagFamily(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + fd, err := openSocketDiag() + if err != nil { + return 0, 0, E.Cause(err, "dial netlink") + } + defer syscall.Close(fd) + _, err = syscall.Write(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) + if err != nil { + return 0, 0, E.Cause(err, "write netlink request") + } + var ( + localMatch socketDiagEntry + hasLocalMatch bool + wildcardMatch socketDiagEntry + hasWildcardMatch bool + buffer = make([]byte, 64<<10) + n int + messages []syscall.NetlinkMessage + ) + for { + n, err = syscall.Read(fd, buffer) + if err != nil { + return 0, 0, E.Cause(err, "read netlink response") + } + messages, err = syscall.ParseNetlinkMessage(buffer[:n]) + if err != nil { + return 0, 0, E.Cause(err, "parse netlink message") + } + if len(messages) == 0 { + return 0, 0, E.New("empty netlink response") + } + for _, message := range messages { + switch message.Header.Type { + case syscall.NLMSG_DONE: + if hasLocalMatch { + return localMatch.inode, localMatch.uid, nil + } + if hasWildcardMatch { + return wildcardMatch.inode, wildcardMatch.uid, nil + } + return 0, 0, ErrNotFound + case syscall.NLMSG_ERROR: + err = unpackSocketDiagError(&message) + if err != nil { + return 0, 0, err + } + case socketDiagByFamily: + entry, valid := unpackSocketDiagEntry(&message) + if !valid || (entry.inode == 0 && entry.uid == 0) || entry.source.Port() != source.Port() { + continue + } + if entry.source.Addr() == source.Addr() && (!destination.IsValid() || entry.destination == destination) { + return entry.inode, entry.uid, nil + } + if protocol != syscall.IPPROTO_UDP { + continue + } + if !hasLocalMatch && entry.source.Addr() == source.Addr() { + hasLocalMatch = true + localMatch = entry + } + if !hasWildcardMatch && entry.source.Addr().IsUnspecified() { + hasWildcardMatch = true + wildcardMatch = entry + } + } + } + } +} + +func unpackSocketDiagEntry(msg *syscall.NetlinkMessage) (socketDiagEntry, bool) { + if len(msg.Data) < socketDiagResponseMinSize { + return socketDiagEntry{}, false + } + data := msg.Data + var sourceAddr, destinationAddr netip.Addr + switch data[0] { + case syscall.AF_INET: + sourceAddr = netip.AddrFrom4([4]byte(data[8:12])) + destinationAddr = netip.AddrFrom4([4]byte(data[24:28])) + case syscall.AF_INET6: + sourceAddr = netip.AddrFrom16([16]byte(data[8:24])).Unmap() + destinationAddr = netip.AddrFrom16([16]byte(data[24:40])).Unmap() + default: + return socketDiagEntry{}, false + } + return socketDiagEntry{ + source: netip.AddrPortFrom(sourceAddr, binary.BigEndian.Uint16(data[4:6])), + destination: netip.AddrPortFrom(destinationAddr, binary.BigEndian.Uint16(data[6:8])), + uid: binary.NativeEndian.Uint32(data[64:68]), + inode: binary.NativeEndian.Uint32(data[68:72]), + }, true } func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { diff --git a/common/process/socket_diag_linux_test.go b/common/process/socket_diag_linux_test.go new file mode 100644 index 00000000..1aec6f20 --- /dev/null +++ b/common/process/socket_diag_linux_test.go @@ -0,0 +1,108 @@ +//go:build linux + +package process + +import ( + "context" + "net" + "net/netip" + "os" + "syscall" + "testing" + + "github.com/sagernet/sing-box/log" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func socketInode(t *testing.T, conn syscall.Conn) uint32 { + rawConn, err := conn.SyscallConn() + require.NoError(t, err) + var inode uint32 + err = rawConn.Control(func(fd uintptr) { + var stat syscall.Stat_t + require.NoError(t, syscall.Fstat(int(fd), &stat)) + inode = uint32(stat.Ino) + }) + require.NoError(t, err) + return inode +} + +func TestDumpSocketDiagTCP(t *testing.T) { + t.Parallel() + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + first, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2)}}).Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer first.Close() + second, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(M.AddrPortFromNet(first.LocalAddr()).Port())}}).Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer second.Close() + destination := M.AddrPortFromNet(listener.Addr()) + for _, conn := range []net.Conn{second, first} { + inode, uid, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), destination) + require.NoError(t, err) + require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode) + require.Equal(t, uint32(os.Getuid()), uid) + inode, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{}) + require.NoError(t, err) + require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode) + } + _, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(first.LocalAddr()).Port()), destination) + require.ErrorIs(t, err, ErrNotFound) +} + +func TestDumpSocketDiagUDP(t *testing.T) { + t.Parallel() + first, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 2)}) + require.NoError(t, err) + defer first.Close() + port := M.AddrPortFromNet(first.LocalAddr()).Port() + second, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(port)}) + require.NoError(t, err) + defer second.Close() + for _, conn := range []*net.UDPConn{second, first} { + inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{}) + require.NoError(t, err) + require.Equal(t, socketInode(t, conn), inode) + } + _, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), port), netip.AddrPort{}) + require.ErrorIs(t, err, ErrNotFound) + + wildcard, err := net.ListenUDP("udp4", &net.UDPAddr{}) + require.NoError(t, err) + defer wildcard.Close() + inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(wildcard.LocalAddr()).Port()), netip.AddrPort{}) + require.NoError(t, err) + require.Equal(t, socketInode(t, wildcard), inode) +} + +func TestLinuxSearcherFindProcessInfo(t *testing.T) { + t.Parallel() + searcher, err := NewSearcher(Config{Logger: log.NewNOPFactory().NewLogger("test")}) + require.NoError(t, err) + defer searcher.Close() + executable, err := os.Executable() + require.NoError(t, err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + tcpConn, err := net.Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer tcpConn.Close() + info, err := searcher.FindProcessInfo(context.Background(), N.NetworkTCP, M.AddrPortFromNet(tcpConn.LocalAddr()), M.AddrPortFromNet(listener.Addr())) + require.NoError(t, err) + require.Equal(t, executable, info.ProcessPath) + require.Equal(t, int32(os.Getuid()), info.UserId) + + udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{}) + require.NoError(t, err) + defer udpConn.Close() + info, err = searcher.FindProcessInfo(context.Background(), N.NetworkUDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), M.AddrPortFromNet(udpConn.LocalAddr()).Port()), netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 53)) + require.NoError(t, err) + require.Equal(t, executable, info.ProcessPath) +}