Files
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

145 lines
3.1 KiB
Go

//go:build linux
package target
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
func findSocketInodes(port int, listenersOnly bool) (map[string]bool, error) {
inodes := make(map[string]bool)
type procNetFile struct {
path string
isTCP bool
}
files := []procNetFile{
{"/proc/net/tcp", true},
{"/proc/net/tcp6", true},
{"/proc/net/udp", false},
{"/proc/net/udp6", false},
}
targetHex := fmt.Sprintf("%04X", port)
for _, f := range files {
data, err := os.ReadFile(f.path)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
for _, line := range lines[1:] {
fields := strings.Fields(line)
if len(fields) < 10 {
continue
}
localParts := strings.Split(fields[1], ":")
if len(localParts) != 2 {
continue
}
remoteParts := strings.Split(fields[2], ":")
state := fields[3]
if f.isTCP {
if listenersOnly {
// 0A = TCP_LISTEN — only report actual listeners
if state != "0A" {
continue
}
}
// In fallback mode, accept any TCP state so connected sockets
// (ESTABLISHED, TIME_WAIT, etc.) are also surfaced.
} else {
// UDP is connectionless; state 07 (CLOSE) means the socket is
// bound and ready to receive. Also accept 01 (ESTABLISHED) for
// connected UDP sockets.
if state != "07" && state != "01" {
continue
}
}
matches := localParts[1] == targetHex
if !listenersOnly && !matches && len(remoteParts) == 2 {
// Match either side of the connection so a process with an
// outbound connection to :port is also found.
matches = remoteParts[1] == targetHex
}
if matches {
inodes[fields[9]] = true
}
}
}
if len(inodes) == 0 {
if listenersOnly {
return nil, fmt.Errorf("no process listening on port %d", port)
}
return nil, fmt.Errorf("no process bound to or connected on port %d", port)
}
return inodes, nil
}
func ResolvePort(port int) ([]int, error) {
inodes, err := findSocketInodes(port, true)
if err != nil {
fallbackInodes, fallbackErr := findSocketInodes(port, false)
if fallbackErr != nil {
return nil, err
}
inodes = fallbackInodes
}
// collect all owning pids so callers can handle multi-owner sockets.
pidSet := make(map[int]bool)
procEntries, _ := os.ReadDir("/proc")
for _, entry := range procEntries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
fdDir := filepath.Join("/proc", entry.Name(), "fd")
fds, err := os.ReadDir(fdDir)
if err != nil {
continue
}
for _, fd := range fds {
link, err := os.Readlink(filepath.Join(fdDir, fd.Name()))
if err != nil {
continue
}
if rest, ok := strings.CutPrefix(link, "socket:["); ok {
inode, ok := strings.CutSuffix(rest, "]")
if ok && inodes[inode] {
pidSet[pid] = true
break
}
}
}
}
result := make([]int, 0, len(pidSet))
for pid := range pidSet {
if len(pidSet) > 1 && pid == 1 {
continue
}
result = append(result, pid)
}
sort.Ints(result)
if len(result) == 0 {
return nil, ErrSocketOwnerUnknown
}
return result, nil
}