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.
Files changed (45) hide show
  1. procproxy-0.1.0.dist-info/METADATA +489 -0
  2. procproxy-0.1.0.dist-info/RECORD +45 -0
  3. procproxy-0.1.0.dist-info/WHEEL +5 -0
  4. procproxy-0.1.0.dist-info/entry_points.txt +2 -0
  5. procproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. procproxy-0.1.0.dist-info/top_level.txt +1 -0
  7. proxycore/__init__.py +7 -0
  8. proxycore/__main__.py +65 -0
  9. proxycore/api.py +226 -0
  10. proxycore/backend/__init__.py +49 -0
  11. proxycore/backend/base.py +159 -0
  12. proxycore/backend/null.py +51 -0
  13. proxycore/backend/windivert/__init__.py +657 -0
  14. proxycore/backend/windivert/filter.py +123 -0
  15. proxycore/backend/windivert/pidmap.py +289 -0
  16. proxycore/backend/windivert/pkt.py +424 -0
  17. proxycore/canproxy.py +229 -0
  18. proxycore/checkroute.py +247 -0
  19. proxycore/cli.py +605 -0
  20. proxycore/config.py +1010 -0
  21. proxycore/dns/__init__.py +46 -0
  22. proxycore/dns/cache.py +209 -0
  23. proxycore/dns/modes.py +451 -0
  24. proxycore/dns/udp.py +606 -0
  25. proxycore/forward.py +572 -0
  26. proxycore/ipc.py +637 -0
  27. proxycore/log.py +298 -0
  28. proxycore/pathsetup.py +112 -0
  29. proxycore/probe.py +168 -0
  30. proxycore/proc.py +342 -0
  31. proxycore/rules.py +730 -0
  32. proxycore/runtime.py +654 -0
  33. proxycore/service/__init__.py +536 -0
  34. proxycore/service/__main__.py +124 -0
  35. proxycore/tunnel/__init__.py +8 -0
  36. proxycore/tunnel/relay.py +393 -0
  37. proxycore/tunnel/sessions.py +315 -0
  38. proxycore/tunnel/sink.py +325 -0
  39. proxycore/types.py +165 -0
  40. proxycore/upstream/__init__.py +52 -0
  41. proxycore/upstream/base.py +150 -0
  42. proxycore/upstream/chain.py +92 -0
  43. proxycore/upstream/http_connect.py +148 -0
  44. proxycore/upstream/socks4.py +106 -0
  45. 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)