Skip to content

Commit 987290b

Browse files
authored
Routing: process supports macOS as well (#6447)
#6434 (comment)
1 parent d7fa207 commit 987290b

4 files changed

Lines changed: 660 additions & 1 deletion

File tree

common/net/find_process_darwin.go

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
//go:build darwin
2+
3+
package net
4+
5+
import (
6+
"bytes"
7+
"net"
8+
"net/netip"
9+
"path/filepath"
10+
"strings"
11+
"syscall"
12+
"unsafe"
13+
14+
"golang.org/x/sys/unix"
15+
16+
"github.com/xtls/xray-core/common/errors"
17+
)
18+
19+
const (
20+
darwinProcPIDListFDs = 1
21+
darwinProcPIDFDSocketInfo = 3
22+
darwinProcFDTypeSocket = 2
23+
darwinProcFDInfoSize = 8
24+
darwinSocketFDInfoSize = 792
25+
darwinSocketFDInfoPSIOff = 24
26+
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
27+
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
28+
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
29+
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
30+
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
31+
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
32+
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
33+
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
34+
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
35+
darwinInSockInfoSize = 80
36+
darwinInSockInfoIPv4 = 0x1
37+
darwinInSockInfoIPv6 = 0x2
38+
darwinSockInfoIN = 1
39+
darwinSockInfoTCP = 2
40+
)
41+
42+
type darwinSocketMatchLevel int
43+
44+
const (
45+
darwinSocketNoMatch darwinSocketMatchLevel = iota
46+
darwinSocketPortMatch
47+
darwinSocketRemoteMatch
48+
darwinSocketLocalMatch
49+
darwinSocketExactMatch
50+
)
51+
52+
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
53+
isLocal, err := IsLocal(net.ParseIP(srcIP))
54+
if err != nil {
55+
return 0, "", "", errors.New("failed to determine if address is local: ", err)
56+
}
57+
if !isLocal {
58+
return 0, "", "", ErrNotLocal
59+
}
60+
if network != "tcp" && network != "udp" {
61+
panic("Unsupported network type for process lookup.")
62+
}
63+
64+
srcAddr, err := netip.ParseAddr(srcIP)
65+
if err != nil {
66+
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
67+
}
68+
srcAddr = srcAddr.Unmap()
69+
70+
var dstAddr netip.Addr
71+
hasDstAddr := false
72+
if destIP != "" && destPort != 0 {
73+
dstAddr, err = netip.ParseAddr(destIP)
74+
if err != nil {
75+
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
76+
}
77+
dstAddr = dstAddr.Unmap()
78+
hasDstAddr = true
79+
}
80+
81+
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
82+
if err != nil {
83+
return 0, "", "", errors.New("failed to list processes").Base(err)
84+
}
85+
86+
var bestPID int32
87+
bestLevel := darwinSocketNoMatch
88+
ambiguousBest := false
89+
90+
for _, process := range processes {
91+
pid := process.Proc.P_pid
92+
if pid <= 0 {
93+
continue
94+
}
95+
96+
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
97+
if err != nil || matchLevel == darwinSocketNoMatch {
98+
continue
99+
}
100+
if matchLevel == darwinSocketExactMatch {
101+
bestPID = pid
102+
bestLevel = matchLevel
103+
ambiguousBest = false
104+
break
105+
}
106+
if matchLevel > bestLevel {
107+
bestPID = pid
108+
bestLevel = matchLevel
109+
ambiguousBest = false
110+
continue
111+
}
112+
if matchLevel == bestLevel {
113+
ambiguousBest = true
114+
}
115+
}
116+
117+
if bestLevel == darwinSocketNoMatch {
118+
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
119+
}
120+
if ambiguousBest {
121+
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
122+
}
123+
124+
absPath, err := darwinProcessPath(bestPID)
125+
if err != nil {
126+
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
127+
}
128+
129+
absPath = filepath.ToSlash(absPath)
130+
return int(bestPID), filepath.Base(absPath), absPath, nil
131+
}
132+
133+
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
134+
fds, err := darwinProcessFDs(pid)
135+
if err != nil {
136+
return darwinSocketNoMatch, err
137+
}
138+
139+
bestLevel := darwinSocketNoMatch
140+
info := make([]byte, darwinSocketFDInfoSize)
141+
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
142+
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
143+
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
144+
if fdType != darwinProcFDTypeSocket {
145+
continue
146+
}
147+
148+
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
149+
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
150+
continue
151+
}
152+
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
153+
if level == darwinSocketExactMatch {
154+
return level, nil
155+
}
156+
if level > bestLevel {
157+
bestLevel = level
158+
}
159+
}
160+
161+
return bestLevel, nil
162+
}
163+
164+
func darwinProcessFDs(pid int32) ([]byte, error) {
165+
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
166+
if err != nil {
167+
return nil, err
168+
}
169+
if n <= 0 {
170+
return nil, nil
171+
}
172+
173+
buf := make([]byte, n)
174+
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
175+
if err != nil {
176+
return nil, err
177+
}
178+
return buf[:n], nil
179+
}
180+
181+
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
182+
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
183+
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
184+
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
185+
186+
switch network {
187+
case "tcp":
188+
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
189+
return darwinSocketNoMatch
190+
}
191+
case "udp":
192+
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
193+
return darwinSocketNoMatch
194+
}
195+
default:
196+
return darwinSocketNoMatch
197+
}
198+
199+
vflag := info[darwinInSockInfoVFlagOff]
200+
if srcAddr.Is4() {
201+
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
202+
return darwinSocketNoMatch
203+
}
204+
} else {
205+
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
206+
return darwinSocketNoMatch
207+
}
208+
}
209+
210+
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
211+
if !darwinPortMatches(localPort, srcPort) {
212+
return darwinSocketNoMatch
213+
}
214+
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
215+
216+
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
217+
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
218+
219+
if !hasDstAddr {
220+
if localAddrMatches {
221+
return darwinSocketExactMatch
222+
}
223+
return darwinSocketNoMatch
224+
}
225+
226+
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
227+
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
228+
return darwinSocketExactMatch
229+
}
230+
switch {
231+
case localAddrMatches && remoteMatches:
232+
return darwinSocketExactMatch
233+
case localAddrMatches:
234+
return darwinSocketLocalMatch
235+
case remoteMatches:
236+
return darwinSocketRemoteMatch
237+
default:
238+
return darwinSocketPortMatch
239+
}
240+
}
241+
242+
func darwinPortMatches(value int32, port uint16) bool {
243+
raw := uint16(value)
244+
return raw == port || darwinNtohs(raw) == port
245+
}
246+
247+
func darwinNtohs(value uint16) uint16 {
248+
return value<<8 | value>>8
249+
}
250+
251+
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
252+
if addr.Is4() {
253+
ip := addr.As4()
254+
return bytes.Equal(raw[12:16], ip[:])
255+
}
256+
ip := addr.As16()
257+
return bytes.Equal(raw, ip[:])
258+
}
259+
260+
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
261+
if darwinAddrMatches(raw, addr) {
262+
return true
263+
}
264+
if addr.Is4() {
265+
return darwinBytesAreZero(raw[12:16])
266+
}
267+
return darwinBytesAreZero(raw)
268+
}
269+
270+
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
271+
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
272+
}
273+
274+
func darwinBytesAreZero(raw []byte) bool {
275+
for _, value := range raw {
276+
if value != 0 {
277+
return false
278+
}
279+
}
280+
return true
281+
}
282+
283+
func darwinReadNativeUint32(b []byte) uint32 {
284+
return *(*uint32)(unsafe.Pointer(&b[0]))
285+
}
286+
287+
func darwinProcessPath(pid int32) (string, error) {
288+
buf := make([]byte, unix.PathMax)
289+
n, err := darwinProcPIDPath(pid, buf)
290+
if err != nil {
291+
return "", err
292+
}
293+
if n <= 0 {
294+
return "", errors.New("empty process path")
295+
}
296+
return strings.TrimRight(string(buf[:n]), "\x00"), nil
297+
}
298+
299+
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
300+
var ptr unsafe.Pointer
301+
if len(buf) > 0 {
302+
ptr = unsafe.Pointer(&buf[0])
303+
}
304+
305+
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
306+
if errno != 0 {
307+
return 0, errno
308+
}
309+
return int(r0), nil
310+
}
311+
312+
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
313+
var ptr unsafe.Pointer
314+
if len(buf) > 0 {
315+
ptr = unsafe.Pointer(&buf[0])
316+
}
317+
318+
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
319+
if errno != 0 {
320+
return 0, errno
321+
}
322+
return int(r0), nil
323+
}
324+
325+
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
326+
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
327+
if errno != 0 {
328+
return 0, errno
329+
}
330+
return int(r0), nil
331+
}
332+
333+
var libc_proc_pidinfo_trampoline_addr uintptr
334+
335+
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
336+
337+
var libc_proc_pidfdinfo_trampoline_addr uintptr
338+
339+
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
340+
341+
var libc_proc_pidpath_trampoline_addr uintptr
342+
343+
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
344+
345+
// Implemented in the runtime package (runtime/sys_darwin.go).
346+
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
347+
348+
//go:linkname syscall_syscall6 syscall.syscall6

common/net/find_process_darwin.s

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//go:build darwin
2+
3+
#include "textflag.h"
4+
5+
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
6+
JMP libc_proc_pidinfo(SB)
7+
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
8+
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
9+
10+
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
11+
JMP libc_proc_pidfdinfo(SB)
12+
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
13+
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
14+
15+
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
16+
JMP libc_proc_pidpath(SB)
17+
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
18+
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)

0 commit comments

Comments
 (0)