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
@@ -0,0 +1,247 @@
1
+ """核对某进程的连接到底走了哪条路。
2
+
3
+ 两类用法:
4
+
5
+ 1. 离线核对(不需要管理员),用会话日志 + 配置复算:
6
+ ```
7
+ python -m proxycore check-route --target-pid curl.exe --expect proxied \\
8
+ --sessions %TEMP%\\sessions.jsonl --config samples/profiles/dev.yaml
9
+ ```
10
+ 它回答的是"按当前规则,curl.exe 连 1.1.1.1:443 应该被代理吗",
11
+ 以及"日志里 curl.exe 的会话是否真的走了上游"。
12
+
13
+ 2. 在线核对(需要管理员),查本机 TCP 表,看目标进程的连接是不是连到了 sink:
14
+ ```
15
+ python -m proxycore check-route --pid-of curl.exe --expect proxied --probe
16
+ ```
17
+
18
+ 退出码 0 = 符合预期;1 = 不符合;2 = 参数/环境问题。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import socket
26
+ import sys
27
+ from collections import Counter
28
+ from pathlib import Path
29
+
30
+ from proxycore.backend.windivert.pidmap import PortPidTable, is_supported
31
+ from proxycore.config import Config, ConfigError
32
+ from proxycore.proc import get_proc_info, iter_pids
33
+ from proxycore.rules import GuardSet, decide
34
+ from proxycore.types import Action, ConnRequest, Endpoint, ProcInfo, Proto
35
+
36
+ # 合成 PID:仅用于"进程没在跑时只算规则",见 replay_rule 的说明
37
+ _SYNTHETIC_PID = 999_999
38
+
39
+
40
+ def load_sessions(path: Path) -> list[dict]:
41
+ """读会话日志里的 `session_end` 行。"""
42
+ if not path.exists():
43
+ return []
44
+ out: list[dict] = []
45
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
46
+ line = line.strip()
47
+ if not line:
48
+ continue
49
+ try:
50
+ payload = json.loads(line)
51
+ except json.JSONDecodeError:
52
+ continue
53
+ if isinstance(payload, dict) and payload.get("event") == "session_end":
54
+ out.append(payload)
55
+ return out
56
+
57
+
58
+ def evaluate_sessions(sessions: list[dict], target: str, *, dst: str = "", dport: int = 0) -> str:
59
+ """返回 `proxied` / `direct` / `failed` / `none`。"""
60
+ matches = [
61
+ s
62
+ for s in sessions
63
+ if target.lower() in str(s.get("proc", "")).lower()
64
+ and (not dport or str(dport) in str(s.get("dst", "")))
65
+ and (not dst or dst in str(s.get("dst", "")))
66
+ ]
67
+ if not matches:
68
+ return "none"
69
+ last = matches[-1]
70
+ if last.get("upstream"):
71
+ return "proxied"
72
+ if str(last.get("state")) == "failed":
73
+ return "failed"
74
+ return "direct"
75
+
76
+
77
+ def resolve_target_pid(pattern: str) -> int:
78
+ """按进程名(或路径片段)查 PID;先精确后包含,找不到返回 0。"""
79
+ pattern = pattern.lower()
80
+ stem = pattern[:-4] if pattern.endswith(".exe") else pattern
81
+ partial = 0
82
+ for pid in iter_pids():
83
+ info = get_proc_info(pid)
84
+ name = info.name.lower()
85
+ if not name:
86
+ continue
87
+ if name in (pattern, stem):
88
+ return pid
89
+ if partial == 0 and (pattern in name or name == f"{stem}.exe"):
90
+ partial = pid
91
+ return partial
92
+
93
+
94
+ def replay_rule(config: Config, proc_name: str, dst: str, dport: int, proto: str) -> tuple[Action, str]:
95
+ """按当前规则复算"这条连接会被怎么处理"。
96
+
97
+ 返回 `(动作, 说明)`。
98
+
99
+ 关于"进程没在跑"这个坑(真机踩过):
100
+ 规则引擎的第一道分支是"PID 未知 → 按 `service.on_unknown_pid` 处置",它在规则之前,
101
+ 所以拿 pid=0 去复算,得到的永远是那个策略(常见 `direct`),
102
+ 看起来就像"规则没生效"。这会让"验证配置"这件事直接失效。
103
+ 因此:进程没找到时,用一个合成 PID 只算规则本身,并把这一点打印出来。
104
+ 真机上 PID 查不到时实际走哪条路,由 `on_unknown_pid` 决定(下面会一并列出)。
105
+ """
106
+ pid = resolve_target_pid(proc_name)
107
+ note = ""
108
+ if pid:
109
+ found = get_proc_info(pid)
110
+ info = ProcInfo(pid=pid, name=found.name or proc_name, path=found.path)
111
+ else:
112
+ # 合成 PID:只为了跳过"未知 PID"分支,让规则本身被真正评估
113
+ info = ProcInfo(pid=_SYNTHETIC_PID, name=proc_name, path="")
114
+ note = (
115
+ f"进程 {proc_name!r} 当前未运行:这里用合成 PID={_SYNTHETIC_PID} 只做规则复算;"
116
+ f"真机上若 PID 查不到,会按 service.on_unknown_pid={config.service.on_unknown_pid!r} 处置"
117
+ )
118
+ conn = ConnRequest(
119
+ proc=info,
120
+ proto=Proto.UDP if proto == "udp" else Proto.TCP,
121
+ src=Endpoint("127.0.0.1", 40000),
122
+ dst=Endpoint(dst, dport, is_v6=":" in dst),
123
+ )
124
+ guards = GuardSet(
125
+ self_pids=set(),
126
+ upstream_endpoints=config.profile.upstream_endpoints(),
127
+ on_unknown_pid=config.service.on_unknown_pid,
128
+ )
129
+ return decide(conn, config.profile, None, guards).action, note
130
+
131
+
132
+ def live_connections(pid: int | None, sink_port: int) -> tuple[list[str], str]:
133
+ """查本机 TCP 表;返回 `(命中行, 说明)`。"""
134
+ if not is_supported():
135
+ return [], "在线核对仅 Windows 支持"
136
+ try:
137
+ table = PortPidTable(ttl_s=0.0)
138
+ rows = table.all_rows()
139
+ except OSError as exc:
140
+ return [], f"读取 TCP 表失败:{exc}"
141
+ if not rows:
142
+ return [], "TCP 表为空或权限不足(需要管理员)"
143
+
144
+ hits: list[str] = []
145
+ for row in rows:
146
+ if (pid and row.pid != pid) or row.pid == 0:
147
+ continue
148
+ target = f"{row.remote_addr}:{row.remote_port}"
149
+ marker = "SINK" if row.remote_port == sink_port else "remote"
150
+ hits.append(f"pid={row.pid} local={row.local_addr}:{row.local_port} -> {target} [{marker}]")
151
+ return hits, f"共 {len(rows)} 行,命中 {len(hits)} 行"
152
+
153
+
154
+ def main(argv: list[str] | None = None) -> int:
155
+ ap = argparse.ArgumentParser(prog="procproxy check-route", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
156
+ ap.add_argument("--config", default="samples/profiles/dev.yaml")
157
+ ap.add_argument("--target-pid", default="", help="进程名/通配(如 curl.exe)")
158
+ ap.add_argument("--pid-of", default="", help="同 --target-pid,按名称查 PID")
159
+ ap.add_argument("--expect", choices=["proxied", "direct", "blocked"], required=True)
160
+ ap.add_argument("--proc", default="", help="规则复算用的进程名(默认取 --target-pid)")
161
+ ap.add_argument("--dst", default="1.1.1.1")
162
+ ap.add_argument("--port", type=int, default=443)
163
+ ap.add_argument("--proto", choices=["tcp", "udp"], default="tcp")
164
+ ap.add_argument("--sessions", default="")
165
+ ap.add_argument("--probe", action="store_true")
166
+ ap.add_argument(
167
+ "--live-required",
168
+ action="store_true",
169
+ help="要求在线核对(TCP 表出现连到 sink 的连接)也通过;默认只报告不判定",
170
+ )
171
+ args = ap.parse_args(argv)
172
+
173
+ target = args.target_pid or args.pid_of
174
+ if not target:
175
+ print("需要 --target-pid 或 --pid-of", file=sys.stderr)
176
+ return 2
177
+ proc_name = args.proc or target
178
+
179
+ try:
180
+ config = Config.load(args.config)
181
+ except ConfigError as exc:
182
+ print(f"配置无效:{exc}", file=sys.stderr)
183
+ return 2
184
+
185
+ print(f"# check_route target={target} expect={args.expect} config={args.config}")
186
+ print(f"# 规则复算:{proc_name} -> {args.dst}:{args.port}/{args.proto}")
187
+
188
+ actual_action, replay_note = replay_rule(config, proc_name, args.dst, args.port, args.proto)
189
+ expected_action = {"proxied": Action.PROXY, "direct": Action.DIRECT, "blocked": Action.BLOCK}[args.expect]
190
+ rule_ok = actual_action is expected_action
191
+ print(
192
+ f"规则复算结果: {actual_action.value} -> {'OK' if rule_ok else 'MISMATCH'}(期望 {expected_action.value})"
193
+ )
194
+ if replay_note:
195
+ print(f" [!] {replay_note}")
196
+
197
+ session_verdict = "none"
198
+ if args.sessions:
199
+ sessions = load_sessions(Path(args.sessions))
200
+ session_verdict = evaluate_sessions(sessions, target, dst=args.dst, dport=args.port)
201
+ counts = Counter(str(s.get("proc", "")) for s in sessions)
202
+ print(f"会话日志: {len(sessions)} 条;进程分布 top5 = {counts.most_common(5)}")
203
+ print(f"目标进程最近会话: {session_verdict}")
204
+
205
+ pid = resolve_target_pid(target)
206
+ print(f"PID: {pid or '(未找到)'}")
207
+ live_ok = True
208
+ if args.probe or is_supported():
209
+ sink = config.service.sink
210
+ hits, note = live_connections(pid or None, sink.port)
211
+ print(f"在线 TCP 表: {note}")
212
+ for line in hits[:20]:
213
+ print(f" {line}")
214
+ if pid and hits:
215
+ proxied = any("SINK" in line for line in hits)
216
+ live_ok = proxied == (args.expect == "proxied")
217
+ print(f"在线核对: {'OK' if live_ok else 'MISMATCH'}(期望 {args.expect})")
218
+ if not live_ok and not args.live_required:
219
+ print(" (未使用 --live-required,故不据此判失败;ProcProxy 可能没在跑)")
220
+ live_ok = True
221
+
222
+ if args.probe:
223
+ probe_sink(config)
224
+
225
+ if not rule_ok:
226
+ return 1
227
+ if args.sessions and args.expect == "proxied" and session_verdict not in {"none", "proxied"}:
228
+ return 1
229
+ return 0 if live_ok else 1
230
+
231
+
232
+ def probe_sink(config: Config) -> None:
233
+ sink = config.service.sink
234
+ try:
235
+ with socket.create_connection((sink.host, sink.port), timeout=1.0):
236
+ print(f"sink 探测: {sink.host}:{sink.port} 正在监听")
237
+ except OSError as exc:
238
+ print(f"sink 探测: {sink.host}:{sink.port} 不可达({exc})→ ProcProxy 可能没在跑")
239
+ for alias, upstream in config.profile.upstreams.items():
240
+ try:
241
+ with socket.create_connection((upstream.host, upstream.port), timeout=1.0):
242
+ print(f"上游探测: {alias} ({upstream.host}:{upstream.port}) 可达")
243
+ except OSError as exc:
244
+ print(f"上游探测: {alias} ({upstream.host}:{upstream.port}) 不可达({exc})")
245
+
246
+ if __name__ == "__main__": # pragma: no cover - 也支持以模块方式直接运行
247
+ raise SystemExit(main())