Skip to content

Process Searcher ​

The process searcher identifies which local process owns a network connection, enabling process-based routing rules (process_name, process_path, package_name).

Source: common/process/

Interface ​

go
type Searcher interface {
    FindProcessInfo(ctx context.Context, network string, source netip.AddrPort,
        destination netip.AddrPort) (*adapter.ConnectionOwner, error)
}

type Config struct {
    Logger         log.ContextLogger
    PackageManager tun.PackageManager  // Android only
}

type ConnectionOwner struct {
    ProcessID          uint32
    UserId             int32
    UserName           string
    ProcessPath        string
    AndroidPackageName string
}

After the searcher returns a ConnectionOwner, the framework enriches it by looking up the Unix username from the UID:

go
func FindProcessInfo(searcher Searcher, ctx, network, source, destination) (*ConnectionOwner, error) {
    info, err := searcher.FindProcessInfo(ctx, network, source, destination)
    if info.UserId != -1 {
        osUser, _ := user.LookupId(F.ToString(info.UserId))
        if osUser != nil {
            info.UserName = osUser.Username
        }
    }
    return info, nil
}

Linux Implementation ​

File: searcher_linux.go, searcher_linux_shared.goBuild constraint: linux && !android

Architecture ​

The Linux searcher uses a two-step process:

  1. Netlink socket diagnosis -- find the socket inode and UID for a given connection
  2. Procfs search -- scan /proc to find which process owns that socket inode
go
func resolveSocketByNetlink(network string, source, destination netip.AddrPort) (inode, uid uint32, err error)

This sends a SOCK_DIAG_BY_FAMILY netlink message to the kernel:

go
const sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48

// Request structure (72 bytes):
// [0:4]   nlmsg_len    (native endian)
// [4:6]   nlmsg_type   = socketDiagByFamily (20)
// [6:8]   nlmsg_flags  = NLM_F_REQUEST | NLM_F_DUMP
// [8:12]  nlmsg_seq    = 0
// [12:16] nlmsg_pid    = 0
// [16]    sdiag_family = AF_INET or AF_INET6
// [17]    sdiag_protocol = IPPROTO_TCP or IPPROTO_UDP
// [18:20] pad          = 0
// [20:24] idiag_states = 0xFFFFFFFF (all states)
// [24:26] source_port  (big-endian)
// [26:28] dest_port    = 0
// [28:44] source_addr  (16 bytes, padded)
// [44:60] dest_addr    = IPv6 zero
// [60:64] idiag_if     = 0
// [64:72] idiag_cookie = 0xFFFFFFFFFFFFFFFF

The response contains UID at offset [64:68] and inode at offset [68:72] (both native endian).

go
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error)

This scans /proc/[pid]/fd/ for a symlink matching socket:[inode]:

  1. List all /proc/ entries that are numeric (PIDs)
  2. Filter by UID match (from stat.Uid)
  3. For each PID, enumerate /proc/[pid]/fd/
  4. Readlink each fd entry, compare with socket:[inode]
  5. When found, readlink /proc/[pid]/exe to get the process path

Darwin (macOS) Implementation ​

File: searcher_darwin.go

Architecture ​

macOS uses sysctl to read the kernel's PCB (Protocol Control Block) list, then matches by source port and IP address.

go
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
    var spath string
    switch network {
    case "tcp": spath = "net.inet.tcp.pcblist_n"
    case "udp": spath = "net.inet.udp.pcblist_n"
    }
    value, err := unix.SysctlRaw(spath)
    // ...
}

PCB List Parsing ​

The sysctl returns a packed array of xinpcb_n + xsocket_n structures. Key offsets:

// Structure sizes depend on macOS version:
//   darwin >= 22 (Ventura+): 408 bytes per item
//   darwin < 22:             384 bytes per item
// TCP adds 208 bytes for xtcpcb_n

// Within each item (offsets from item start):
//   inp + 18:20  = source port (big-endian uint16)
//   inp + 44     = inp_vflag (0x1 = IPv4, 0x2 = IPv6)
//   inp + 64:80  = IPv6 address (or IPv4 in last 4 bytes)
//   so  + 68:72  = so_last_pid (native-endian uint32)

After finding the matching PID, it calls proc_info syscall to get the process path:

go
func getExecPathFromPID(pid uint32) (string, error) {
    // SYS_PROC_INFO with PROC_PIDPATHINFO
    buf := make([]byte, 1024)
    syscall.Syscall6(syscall.SYS_PROC_INFO,
        2,              // PROCCALLNUM_PIDINFO
        uintptr(pid),
        0xb,            // PROC_PIDPATHINFO
        0, uintptr(unsafe.Pointer(&buf[0])), 1024)
    return unix.ByteSliceToString(buf), nil
}

UDP Fallback ​

For UDP, if no exact source IP match is found, the implementation falls back to a matching entry with an unspecified (0.0.0.0 or :😃 source address, since UDP sockets may not be bound to a specific address.

Windows Implementation ​

File: searcher_windows.go

Architecture ​

Windows uses the IP Helper API (GetExtendedTcpTable / GetExtendedUdpTable) via the winiphlpapi package:

go
func (s *windowsSearcher) FindProcessInfo(ctx, network, source, destination) (*ConnectionOwner, error) {
    pid, err := winiphlpapi.FindPid(network, source)
    path, err := getProcessPath(pid)
    return &ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
}

Process path retrieval uses OpenProcess + QueryFullProcessImageName:

go
func getProcessPath(pid uint32) (string, error) {
    switch pid {
    case 0: return ":System Idle Process", nil
    case 4: return ":System", nil
    }
    handle, _ := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
    defer windows.CloseHandle(handle)
    buf := make([]uint16, syscall.MAX_LONG_PATH)
    windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
    return windows.UTF16ToString(buf[:size]), nil
}

Android Implementation ​

File: searcher_android.go

Architecture ​

Android reuses the Linux netlink socket diagnosis to get the UID, then maps the UID to a package name using the tun.PackageManager:

go
func (s *androidSearcher) FindProcessInfo(ctx, network, source, destination) (*ConnectionOwner, error) {
    _, uid, err := resolveSocketByNetlink(network, source, destination)
    // Android uses user ID remapping: actual UID = uid % 100000
    if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
        return &ConnectionOwner{UserId: int32(uid), AndroidPackageName: sharedPackage}, nil
    }
    if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
        return &ConnectionOwner{UserId: int32(uid), AndroidPackageName: packageName}, nil
    }
    return &ConnectionOwner{UserId: int32(uid)}, nil
}

Android's multi-user system uses UID ranges: actual_app_uid = uid % 100000. The PackageManager maps these app UIDs to package names.

Stub Implementation ​

File: searcher_stub.goBuild constraint: !linux && !windows && !darwin

Unsupported platforms return os.ErrInvalid:

go
func NewSearcher(_ Config) (Searcher, error) {
    return nil, os.ErrInvalid
}

Platform Interface Override ​

On mobile platforms (Android/iOS via libbox), the process searcher may be overridden by the platform interface. The platformInterfaceWrapper delegates to either:

  • procfs scanning (when UseProcFS() returns true on Android)
  • The platform's native FindConnectionOwner() method

How Process Info Is Used in Routing ​

When route.find_process is enabled, the router calls the process searcher for each new connection. The result populates InboundContext.ProcessInfo, which routing rules can match against:

  • process_name -- matches the executable filename (basename of ProcessPath)
  • process_path -- matches the full executable path
  • process_path_regex -- regex match on the full path
  • package_name -- matches Android package name

Rule set metadata tracks ContainsProcessRule to avoid the expensive process lookup when no rules need it.

Reimplementation Notes ​

  1. Linux: The netlink protocol is well-documented. The SOCK_DIAG_BY_FAMILY message is standard. The procfs scan is O(processes * fds) and can be slow on systems with many processes
  2. macOS: The sysctl PCB list format is not publicly documented and changes between macOS versions. The struct size detection via kern.osrelease is a fragile but necessary heuristic
  3. Windows: Requires loading iphlpapi.dll functions. The GetExtendedTcpTable/GetExtendedUdpTable functions are well-documented in MSDN
  4. Android: UID-to-package mapping requires access to the Android package manager, typically via the platform interface binding
  5. Performance: Process lookup is performed per-connection and can be expensive. The ContainsProcessRule metadata flag allows skipping it entirely when not needed