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/checkroute.py
ADDED
|
@@ -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())
|