procproxy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- procproxy-0.1.0.dist-info/METADATA +489 -0
- procproxy-0.1.0.dist-info/RECORD +45 -0
- procproxy-0.1.0.dist-info/WHEEL +5 -0
- procproxy-0.1.0.dist-info/entry_points.txt +2 -0
- procproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
- procproxy-0.1.0.dist-info/top_level.txt +1 -0
- proxycore/__init__.py +7 -0
- proxycore/__main__.py +65 -0
- proxycore/api.py +226 -0
- proxycore/backend/__init__.py +49 -0
- proxycore/backend/base.py +159 -0
- proxycore/backend/null.py +51 -0
- proxycore/backend/windivert/__init__.py +657 -0
- proxycore/backend/windivert/filter.py +123 -0
- proxycore/backend/windivert/pidmap.py +289 -0
- proxycore/backend/windivert/pkt.py +424 -0
- proxycore/canproxy.py +229 -0
- proxycore/checkroute.py +247 -0
- proxycore/cli.py +605 -0
- proxycore/config.py +1010 -0
- proxycore/dns/__init__.py +46 -0
- proxycore/dns/cache.py +209 -0
- proxycore/dns/modes.py +451 -0
- proxycore/dns/udp.py +606 -0
- proxycore/forward.py +572 -0
- proxycore/ipc.py +637 -0
- proxycore/log.py +298 -0
- proxycore/pathsetup.py +112 -0
- proxycore/probe.py +168 -0
- proxycore/proc.py +342 -0
- proxycore/rules.py +730 -0
- proxycore/runtime.py +654 -0
- proxycore/service/__init__.py +536 -0
- proxycore/service/__main__.py +124 -0
- proxycore/tunnel/__init__.py +8 -0
- proxycore/tunnel/relay.py +393 -0
- proxycore/tunnel/sessions.py +315 -0
- proxycore/tunnel/sink.py +325 -0
- proxycore/types.py +165 -0
- proxycore/upstream/__init__.py +52 -0
- proxycore/upstream/base.py +150 -0
- proxycore/upstream/chain.py +92 -0
- proxycore/upstream/http_connect.py +148 -0
- proxycore/upstream/socks4.py +106 -0
- proxycore/upstream/socks5.py +266 -0
proxycore/proc.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""pid → 名称/路径/会话/用户(跨平台)。
|
|
2
|
+
|
|
3
|
+
字段约定:
|
|
4
|
+
* `name` 不带路径("chrome.exe");
|
|
5
|
+
* `path` 可能为空(受保护进程,如 Windows 的 System/PID 4、带保护的 EDR 进程);
|
|
6
|
+
* `session_id` Windows 为会话 ID,Linux/macOS 为 0;
|
|
7
|
+
* `user` 可空。
|
|
8
|
+
|
|
9
|
+
红线:平台差异只允许出现在本文件与
|
|
10
|
+
`backend/` 内部,核心层不得出现 `if windows` 分支。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .types import ProcInfo
|
|
22
|
+
|
|
23
|
+
IS_WINDOWS = os.name == "nt"
|
|
24
|
+
IS_LINUX = sys.platform.startswith("linux")
|
|
25
|
+
|
|
26
|
+
# 受保护/系统进程的兜底名称(取不到路径时至少要有个名字,否则规则无法按名称匹配)
|
|
27
|
+
_WELL_KNOWN_NAMES = {0: "unknown", 4: "System"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------ 公共 ---
|
|
31
|
+
def current_pid() -> int:
|
|
32
|
+
return os.getpid()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_alive(pid: int) -> bool:
|
|
36
|
+
"""进程是否存活(用于粘性表清理)。"""
|
|
37
|
+
if pid <= 0:
|
|
38
|
+
return False
|
|
39
|
+
if IS_WINDOWS:
|
|
40
|
+
return _win_is_alive(pid)
|
|
41
|
+
try:
|
|
42
|
+
os.kill(pid, 0)
|
|
43
|
+
except ProcessLookupError:
|
|
44
|
+
return False
|
|
45
|
+
except PermissionError:
|
|
46
|
+
return True
|
|
47
|
+
except OSError:
|
|
48
|
+
return False
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_proc_info(pid: int, *, path: str | None = None, name: str | None = None) -> ProcInfo:
|
|
53
|
+
"""查询进程信息。任何字段查询失败都退化为空串,绝不抛异常。
|
|
54
|
+
|
|
55
|
+
`path`/`name` 可显式传入(后端已经拿到元数据时避免重复查询,契约要求
|
|
56
|
+
"进程身份以驱动上报为准,用户态回传的 PID 只用于对账")。
|
|
57
|
+
"""
|
|
58
|
+
if pid <= 0:
|
|
59
|
+
return ProcInfo(pid=pid, name=name or _WELL_KNOWN_NAMES.get(pid, "unknown"), path=path or "")
|
|
60
|
+
|
|
61
|
+
resolved_name, resolved_path, session_id, user = "", "", 0, ""
|
|
62
|
+
if path is not None:
|
|
63
|
+
resolved_path = path
|
|
64
|
+
resolved_name = name if name is not None else basename_of(path)
|
|
65
|
+
elif IS_WINDOWS:
|
|
66
|
+
resolved_path, resolved_name = _win_name_path(pid)
|
|
67
|
+
session_id = _win_session_id(pid)
|
|
68
|
+
user = _win_user(pid)
|
|
69
|
+
else:
|
|
70
|
+
resolved_path, resolved_name = _posix_name_path(pid)
|
|
71
|
+
session_id = 0
|
|
72
|
+
user = _posix_user(pid)
|
|
73
|
+
|
|
74
|
+
if name is not None and resolved_name != name:
|
|
75
|
+
resolved_name = name
|
|
76
|
+
if not resolved_name:
|
|
77
|
+
resolved_name = basename_of(resolved_path) or _WELL_KNOWN_NAMES.get(pid, "unknown")
|
|
78
|
+
return ProcInfo(pid=pid, name=resolved_name, path=resolved_path, session_id=session_id, user=user)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def basename_of(path: str) -> str:
|
|
82
|
+
"""从路径取进程名(Windows 反斜杠与 POSIX 斜杠都支持)。"""
|
|
83
|
+
if not path:
|
|
84
|
+
return ""
|
|
85
|
+
# 不能用 os.path.basename:跨平台(Linux 上带 `\\` 的 Windows 路径会整段返回)
|
|
86
|
+
normalized = path.replace("\\", "/").rstrip("/")
|
|
87
|
+
return normalized.rsplit("/", 1)[-1] if normalized else ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def self_pids() -> set[int]:
|
|
91
|
+
"""环路防护第一重:自身进程 PID 白名单(服务 + 拦截器 + 上游代理进程)。
|
|
92
|
+
|
|
93
|
+
上游代理进程如果跑在本机,会把它的 PID 也交给 `GuardSet` 一起白名单。
|
|
94
|
+
"""
|
|
95
|
+
pids = {os.getpid()}
|
|
96
|
+
if hasattr(os, "getppid"):
|
|
97
|
+
with contextlib.suppress(OSError): # pragma: no cover - 平台差异
|
|
98
|
+
pids.add(os.getppid())
|
|
99
|
+
return pids
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def iter_pids() -> list[int]:
|
|
103
|
+
"""枚举本机 PID(粘性表清理、`cli status` 用)。"""
|
|
104
|
+
if IS_WINDOWS:
|
|
105
|
+
return _win_list_pids()
|
|
106
|
+
out: list[int] = []
|
|
107
|
+
for entry in os.listdir("/proc"):
|
|
108
|
+
if entry.isdigit():
|
|
109
|
+
out.append(int(entry))
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------ Windows ------
|
|
114
|
+
def _win_kernel32() -> Any:
|
|
115
|
+
"""kernel32 + 显式签名。
|
|
116
|
+
|
|
117
|
+
64 位下必须声明 `argtypes`/`restype`,否则 ctypes 会把 HANDLE 当 32 位
|
|
118
|
+
并有符号扩展,导致 `OverflowError: int too long to convert`。
|
|
119
|
+
"""
|
|
120
|
+
import ctypes
|
|
121
|
+
from ctypes import wintypes
|
|
122
|
+
|
|
123
|
+
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
124
|
+
k32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
125
|
+
k32.OpenProcess.restype = wintypes.HANDLE
|
|
126
|
+
k32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
127
|
+
k32.CloseHandle.restype = wintypes.BOOL
|
|
128
|
+
k32.QueryFullProcessImageNameW.argtypes = [
|
|
129
|
+
wintypes.HANDLE,
|
|
130
|
+
wintypes.DWORD,
|
|
131
|
+
wintypes.LPWSTR,
|
|
132
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
133
|
+
]
|
|
134
|
+
k32.QueryFullProcessImageNameW.restype = wintypes.BOOL
|
|
135
|
+
k32.ProcessIdToSessionId.argtypes = [wintypes.DWORD, ctypes.POINTER(wintypes.DWORD)]
|
|
136
|
+
k32.ProcessIdToSessionId.restype = wintypes.BOOL
|
|
137
|
+
k32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
|
|
138
|
+
k32.GetExitCodeProcess.restype = wintypes.BOOL
|
|
139
|
+
return k32
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _win_psapi() -> Any:
|
|
143
|
+
"""`EnumProcesses` 在 psapi.dll(部分版本也在 kernel32 转发)。"""
|
|
144
|
+
import ctypes
|
|
145
|
+
from ctypes import wintypes
|
|
146
|
+
|
|
147
|
+
psapi = ctypes.WinDLL("psapi", use_last_error=True)
|
|
148
|
+
psapi.EnumProcesses.argtypes = [ctypes.POINTER(wintypes.DWORD), wintypes.DWORD, ctypes.POINTER(wintypes.DWORD)]
|
|
149
|
+
psapi.EnumProcesses.restype = wintypes.BOOL
|
|
150
|
+
return psapi
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _win_name_path(pid: int) -> tuple[str, str]:
|
|
154
|
+
import ctypes
|
|
155
|
+
from ctypes import wintypes
|
|
156
|
+
|
|
157
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
158
|
+
k32 = _win_kernel32()
|
|
159
|
+
handle = k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
160
|
+
if not handle:
|
|
161
|
+
# 受保护进程:路径为空(拿不到路径时的降级场景)
|
|
162
|
+
return "", _WELL_KNOWN_NAMES.get(pid, "")
|
|
163
|
+
try:
|
|
164
|
+
buf = ctypes.create_unicode_buffer(32768)
|
|
165
|
+
size = wintypes.DWORD(len(buf))
|
|
166
|
+
ok = k32.QueryFullProcessImageNameW(handle, 0, buf, ctypes.byref(size))
|
|
167
|
+
if not ok:
|
|
168
|
+
return "", _WELL_KNOWN_NAMES.get(pid, "")
|
|
169
|
+
full = buf.value
|
|
170
|
+
return full, basename_of(full)
|
|
171
|
+
finally:
|
|
172
|
+
k32.CloseHandle(handle)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _win_session_id(pid: int) -> int:
|
|
176
|
+
import ctypes
|
|
177
|
+
from ctypes import wintypes
|
|
178
|
+
|
|
179
|
+
k32 = _win_kernel32()
|
|
180
|
+
handle = k32.OpenProcess(0x0400, False, pid) # PROCESS_QUERY_INFORMATION
|
|
181
|
+
if not handle:
|
|
182
|
+
return 0
|
|
183
|
+
try:
|
|
184
|
+
sid = wintypes.DWORD(0)
|
|
185
|
+
if not k32.ProcessIdToSessionId(wintypes.DWORD(pid), ctypes.byref(sid)):
|
|
186
|
+
return 0
|
|
187
|
+
return int(sid.value)
|
|
188
|
+
finally:
|
|
189
|
+
k32.CloseHandle(handle)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _win_advapi() -> Any:
|
|
193
|
+
"""advapi32 + 显式签名(SID 指针必须按 64 位传,否则 OverflowError)。"""
|
|
194
|
+
import ctypes
|
|
195
|
+
from ctypes import wintypes
|
|
196
|
+
|
|
197
|
+
advapi = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
198
|
+
advapi.OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)]
|
|
199
|
+
advapi.OpenProcessToken.restype = wintypes.BOOL
|
|
200
|
+
advapi.GetTokenInformation.argtypes = [
|
|
201
|
+
wintypes.HANDLE,
|
|
202
|
+
ctypes.c_int,
|
|
203
|
+
wintypes.LPVOID,
|
|
204
|
+
wintypes.DWORD,
|
|
205
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
206
|
+
]
|
|
207
|
+
advapi.GetTokenInformation.restype = wintypes.BOOL
|
|
208
|
+
advapi.LookupAccountSidW.argtypes = [
|
|
209
|
+
wintypes.LPCWSTR,
|
|
210
|
+
wintypes.LPVOID,
|
|
211
|
+
wintypes.LPWSTR,
|
|
212
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
213
|
+
wintypes.LPWSTR,
|
|
214
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
215
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
216
|
+
]
|
|
217
|
+
advapi.LookupAccountSidW.restype = wintypes.BOOL
|
|
218
|
+
return advapi
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _win_user(pid: int) -> str:
|
|
222
|
+
import ctypes
|
|
223
|
+
from ctypes import wintypes
|
|
224
|
+
|
|
225
|
+
advapi = _win_advapi()
|
|
226
|
+
k32 = _win_kernel32()
|
|
227
|
+
TOKEN_QUERY = 0x0008
|
|
228
|
+
TokenUser = 1
|
|
229
|
+
handle = k32.OpenProcess(0x0400, False, pid) # PROCESS_QUERY_INFORMATION
|
|
230
|
+
if not handle:
|
|
231
|
+
return ""
|
|
232
|
+
try:
|
|
233
|
+
token = wintypes.HANDLE()
|
|
234
|
+
if not advapi.OpenProcessToken(handle, TOKEN_QUERY, ctypes.byref(token)):
|
|
235
|
+
return ""
|
|
236
|
+
try:
|
|
237
|
+
size = wintypes.DWORD(0)
|
|
238
|
+
advapi.GetTokenInformation(token, TokenUser, None, 0, ctypes.byref(size))
|
|
239
|
+
if size.value == 0:
|
|
240
|
+
return ""
|
|
241
|
+
buf = ctypes.create_string_buffer(size.value)
|
|
242
|
+
if not advapi.GetTokenInformation(token, TokenUser, buf, size.value, ctypes.byref(size)):
|
|
243
|
+
return ""
|
|
244
|
+
# TOKEN_USER.Sid 是缓冲区的首指针(64 位下必须按 c_void_p 读出)
|
|
245
|
+
sid_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_void_p))[0]
|
|
246
|
+
if not sid_ptr:
|
|
247
|
+
return ""
|
|
248
|
+
return _win_sid_to_string(advapi, sid_ptr)
|
|
249
|
+
finally:
|
|
250
|
+
k32.CloseHandle(token)
|
|
251
|
+
finally:
|
|
252
|
+
k32.CloseHandle(handle)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _win_sid_to_string(advapi: Any, sid_ptr: Any) -> str:
|
|
256
|
+
import ctypes
|
|
257
|
+
from ctypes import wintypes
|
|
258
|
+
|
|
259
|
+
name = ctypes.create_unicode_buffer(256)
|
|
260
|
+
domain = ctypes.create_unicode_buffer(256)
|
|
261
|
+
name_len = wintypes.DWORD(256)
|
|
262
|
+
domain_len = wintypes.DWORD(256)
|
|
263
|
+
use = wintypes.DWORD(0)
|
|
264
|
+
ok = advapi.LookupAccountSidW(
|
|
265
|
+
None,
|
|
266
|
+
ctypes.c_void_p(sid_ptr),
|
|
267
|
+
name,
|
|
268
|
+
ctypes.byref(name_len),
|
|
269
|
+
domain,
|
|
270
|
+
ctypes.byref(domain_len),
|
|
271
|
+
ctypes.byref(use),
|
|
272
|
+
)
|
|
273
|
+
if ok:
|
|
274
|
+
return f"{domain.value}\\{name.value}" if domain.value else name.value
|
|
275
|
+
return ""
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _win_is_alive(pid: int) -> bool:
|
|
279
|
+
import ctypes
|
|
280
|
+
from ctypes import wintypes
|
|
281
|
+
|
|
282
|
+
k32 = _win_kernel32()
|
|
283
|
+
handle = k32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
284
|
+
if not handle:
|
|
285
|
+
# 可能是权限不足而非进程不存在;用是否 ERROR_INVALID_PARAMETER 粗判
|
|
286
|
+
return ctypes.get_last_error() != 87
|
|
287
|
+
try:
|
|
288
|
+
code = wintypes.DWORD(0)
|
|
289
|
+
if k32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
290
|
+
return code.value == 259 # STILL_ACTIVE
|
|
291
|
+
return True
|
|
292
|
+
finally:
|
|
293
|
+
k32.CloseHandle(handle)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _win_list_pids() -> list[int]:
|
|
297
|
+
import ctypes
|
|
298
|
+
from ctypes import wintypes
|
|
299
|
+
|
|
300
|
+
psapi = _win_psapi()
|
|
301
|
+
count = 4096
|
|
302
|
+
while True:
|
|
303
|
+
buf = (wintypes.DWORD * count)()
|
|
304
|
+
needed = wintypes.DWORD(0)
|
|
305
|
+
if not psapi.EnumProcesses(buf, ctypes.sizeof(buf), ctypes.byref(needed)):
|
|
306
|
+
return []
|
|
307
|
+
n = needed.value // ctypes.sizeof(wintypes.DWORD)
|
|
308
|
+
if n < count:
|
|
309
|
+
return [int(buf[i]) for i in range(n) if buf[i]]
|
|
310
|
+
count *= 2
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ------------------------------------------------------------- POSIX ------
|
|
314
|
+
def _posix_name_path(pid: int) -> tuple[str, str]:
|
|
315
|
+
# Linux:/proc/<pid>/exe 是最准的;退化为 /proc/<pid>/comm
|
|
316
|
+
exe_link = f"/proc/{pid}/exe"
|
|
317
|
+
try:
|
|
318
|
+
raw = os.readlink(exe_link)
|
|
319
|
+
if raw.endswith(" (deleted)"):
|
|
320
|
+
raw = raw[: -len(" (deleted)")]
|
|
321
|
+
return raw, basename_of(raw)
|
|
322
|
+
except OSError:
|
|
323
|
+
pass
|
|
324
|
+
try:
|
|
325
|
+
name = Path(f"/proc/{pid}/comm").read_text(encoding="utf-8").strip()
|
|
326
|
+
return "", name
|
|
327
|
+
except OSError:
|
|
328
|
+
return "", ""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _posix_user(pid: int) -> str:
|
|
332
|
+
try:
|
|
333
|
+
st = os.stat(f"/proc/{pid}")
|
|
334
|
+
except OSError:
|
|
335
|
+
return ""
|
|
336
|
+
lookup = getattr(__import__("pwd"), "getpwuid", None) if IS_LINUX else None
|
|
337
|
+
if lookup is None: # pragma: no cover - 非 Linux
|
|
338
|
+
return str(st.st_uid)
|
|
339
|
+
try:
|
|
340
|
+
return str(lookup(st.st_uid).pw_name)
|
|
341
|
+
except KeyError: # pragma: no cover - uid 无对应用户名
|
|
342
|
+
return str(st.st_uid)
|