package process import ( "bytes" "encoding/binary" "fmt" "net" "net/netip" "os" "unsafe" "github.com/Dreamacro/clash/common/pool" "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) type inetDiagRequest struct { Family byte Protocol byte Ext byte Pad byte States uint32 SrcPort [2]byte DstPort [2]byte Src [16]byte Dst [16]byte If uint32 Cookie [2]uint32 } type inetDiagResponse struct { Family byte State byte Timer byte ReTrans byte SrcPort [2]byte DstPort [2]byte Src [16]byte Dst [16]byte If uint32 Cookie [2]uint32 Expires uint32 RQueue uint32 WQueue uint32 UID uint32 INode uint32 } func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) { inode, uid, err := resolveSocketByNetlink(network, from, to) if err != nil { return "", err } return resolveProcessPathByProcSearch(inode, uid) } func resolveSocketByNetlink(network string, from netip.AddrPort, to netip.AddrPort) (inode uint32, uid uint32, err error) { var families []byte if from.Addr().Unmap().Is4() { families = []byte{unix.AF_INET, unix.AF_INET6} } else { families = []byte{unix.AF_INET6, unix.AF_INET} } var protocol byte switch network { case TCP: protocol = unix.IPPROTO_TCP case UDP: protocol = unix.IPPROTO_UDP default: return 0, 0, ErrInvalidNetwork } if protocol == unix.IPPROTO_UDP { // Swap from & to for udp // See also https://www.mail-archive.com/netdev@vger.kernel.org/msg248638.html from, to = to, from } for _, family := range families { inode, uid, err = resolveSocketByNetlinkExact(family, protocol, from, to, netlink.Request) if err == nil { return inode, uid, err } } return 0, 0, ErrNotFound } func resolveSocketByNetlinkExact(family byte, protocol byte, from netip.AddrPort, to netip.AddrPort, flags netlink.HeaderFlags) (inode uint32, uid uint32, err error) { request := &inetDiagRequest{ Family: family, Protocol: protocol, States: 0xffffffff, Cookie: [2]uint32{0xffffffff, 0xffffffff}, } var ( fromAddr []byte toAddr []byte ) if family == unix.AF_INET { fromAddr = net.IP(from.Addr().AsSlice()).To4() toAddr = net.IP(to.Addr().AsSlice()).To4() } else { fromAddr = net.IP(from.Addr().AsSlice()).To16() toAddr = net.IP(to.Addr().AsSlice()).To16() } copy(request.Src[:], fromAddr) copy(request.Dst[:], toAddr) binary.BigEndian.PutUint16(request.SrcPort[:], from.Port()) binary.BigEndian.PutUint16(request.DstPort[:], to.Port()) conn, err := netlink.Dial(unix.NETLINK_INET_DIAG, nil) if err != nil { return 0, 0, err } defer conn.Close() message := netlink.Message{ Header: netlink.Header{ Type: 20, // SOCK_DIAG_BY_FAMILY Flags: flags, }, Data: (*(*[unsafe.Sizeof(*request)]byte)(unsafe.Pointer(request)))[:], } messages, err := conn.Execute(message) if err != nil { return 0, 0, err } for _, msg := range messages { if len(msg.Data) < int(unsafe.Sizeof(inetDiagResponse{})) { continue } response := (*inetDiagResponse)(unsafe.Pointer(&msg.Data[0])) return response.INode, response.UID, nil } return 0, 0, ErrNotFound } func resolveProcessPathByProcSearch(inode, uid uint32) (string, error) { procDir, err := os.Open("/proc") if err != nil { return "", err } defer procDir.Close() pids, err := procDir.Readdirnames(-1) if err != nil { return "", err } expectedSocketName := fmt.Appendf(nil, "socket:[%d]", inode) pathBuffer := pool.Get(64) defer pool.Put(pathBuffer) readlinkBuffer := pool.Get(32) defer pool.Put(readlinkBuffer) copy(pathBuffer, "/proc/") for _, pid := range pids { if !isPid(pid) { continue } pathBuffer = append(pathBuffer[:len("/proc/")], pid...) stat := &unix.Stat_t{} err = unix.Stat(string(pathBuffer), stat) if err != nil { continue } else if stat.Uid != uid { continue } pathBuffer = append(pathBuffer, "/fd/"...) fdsPrefixLength := len(pathBuffer) fdDir, err := os.Open(string(pathBuffer)) if err != nil { continue } fds, err := fdDir.Readdirnames(-1) fdDir.Close() if err != nil { continue } for _, fd := range fds { pathBuffer = pathBuffer[:fdsPrefixLength] pathBuffer = append(pathBuffer, fd...) n, err := unix.Readlink(string(pathBuffer), readlinkBuffer) if err != nil { continue } if bytes.Equal(readlinkBuffer[:n], expectedSocketName) { return os.Readlink("/proc/" + pid + "/exe") } } } return "", fmt.Errorf("inode %d of uid %d not found", inode, uid) } func isPid(name string) bool { for _, c := range name { if c < '0' || c > '9' { return false } } return true }