procproxy 0.1.2__tar.gz → 0.1.4__tar.gz
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.4/MANIFEST.in +2 -0
- {procproxy-0.1.2/src/procproxy.egg-info → procproxy-0.1.4}/PKG-INFO +57 -8
- {procproxy-0.1.2 → procproxy-0.1.4}/README.md +56 -7
- procproxy-0.1.4/docs/00-/346/236/266/346/236/204/344/270/216/351/230/205/350/257/273/350/267/257/347/272/277.md +152 -0
- procproxy-0.1.4/docs/07-/350/247/204/345/210/231/345/274/225/346/223/216/344/270/216Profile/346/240/274/345/274/217.md +197 -0
- procproxy-0.1.4/docs/11-/350/260/203/350/257/225/346/216/222/351/224/231/346/211/213/345/206/214.md +129 -0
- procproxy-0.1.4/docs/14-/346/212/212/350/277/233/347/250/213/346/265/201/351/207/217/350/275/254/345/217/221/347/273/231/346/212/223/345/214/205/345/267/245/345/205/267.md +306 -0
- procproxy-0.1.4/docs/15-/347/274/226/347/250/213/346/216/245/345/217/243/344/270/216SDK.md +902 -0
- procproxy-0.1.4/examples/proxy_process.py +139 -0
- procproxy-0.1.4/examples/verify_sdk.py +153 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/pyproject.toml +1 -1
- {procproxy-0.1.2 → procproxy-0.1.4/src/procproxy.egg-info}/PKG-INFO +57 -8
- {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/SOURCES.txt +9 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/__init__.py +1 -1
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/api.py +30 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/cli.py +26 -30
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/config.py +2 -0
- procproxy-0.1.4/src/proxycore/config_init.py +85 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/LICENSE +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/setup.cfg +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/dependency_links.txt +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/entry_points.txt +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/requires.txt +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/top_level.txt +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/__main__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/__init__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/base.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/null.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/__init__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/filter.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/pidmap.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/pkt.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/canproxy.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/checkroute.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/config_source.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/data/default.yaml +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/__init__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/cache.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/modes.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/udp.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/forward.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/ipc.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/log.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/pathsetup.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/probe.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/proc.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/rules.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/runtime.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/service/__init__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/service/__main__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/__init__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/relay.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/sessions.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/sink.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/types.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/__init__.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/base.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/chain.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/http_connect.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/socks4.py +0 -0
- {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/socks5.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: procproxy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: ProcProxy —— 进程级代理:按进程决定哪条连接走哪个上游代理
|
|
5
5
|
Author: Longersking
|
|
6
6
|
License-Expression: MIT
|
|
@@ -74,7 +74,7 @@ python procproxy.py doctor | rules | sessions | ... # 子命令(等价于 pyt
|
|
|
74
74
|
pip install procproxy
|
|
75
75
|
procproxy init # 生成 ~/.procproxy/config.yaml
|
|
76
76
|
procproxy doctor # 环境自检
|
|
77
|
-
procproxy forward WeChat.exe --port 8848 # 真实转发(需管理员)
|
|
77
|
+
procproxy forward --process WeChat.exe --port 8848 # 真实转发(需管理员)
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
安装后直接运行 `procproxy` 会显示首次使用指引,不依赖任何样例文件。
|
|
@@ -250,7 +250,7 @@ python procproxy.py Weixin.exe WeChatAppEx.exe --port 8848
|
|
|
250
250
|
⚠️ 只有 ① 没有 ② → 端口填错了(**要填抓包工具的 MITM 监听口,不是它的 Web 管理口**)。
|
|
251
251
|
① ② 都正常但内容看不懂 → 那是 TLS,不是转发的问题。
|
|
252
252
|
|
|
253
|
-
完整指南(含排错与参数表)见 [`docs/14-把进程流量转发给抓包工具.md`](docs/14
|
|
253
|
+
完整指南(含排错与参数表)见 [`docs/14-把进程流量转发给抓包工具.md`](https://github.com/Longersking/procproxy/blob/master/docs/14-%E6%8A%8A%E8%BF%9B%E7%A8%8B%E6%B5%81%E9%87%8F%E8%BD%AC%E5%8F%91%E7%BB%99%E6%8A%93%E5%8C%85%E5%B7%A5%E5%85%B7.md)。
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
@@ -340,14 +340,16 @@ rules: # 自上而下,**首个命中生
|
|
|
340
340
|
7. 都没命中 → profile.default_action
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
-
完整语义见 [`docs/07-规则引擎与Profile格式.md`](docs/07
|
|
343
|
+
完整语义见 [`docs/07-规则引擎与Profile格式.md`](https://github.com/Longersking/procproxy/blob/master/docs/07-%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E%E4%B8%8EProfile%E6%A0%BC%E5%BC%8F.md)。
|
|
344
344
|
|
|
345
345
|
---
|
|
346
346
|
|
|
347
347
|
## 编程接口
|
|
348
348
|
|
|
349
349
|
`proxycore.api` 是与 CLI 共用同一套配置、规则引擎和会话表的 SDK。
|
|
350
|
-
|
|
350
|
+
**以下全部只用 `import proxycore`,不需要运行 `procproxy` 命令。**
|
|
351
|
+
完整教程见 [`docs/15-编程接口与SDK.md`](https://github.com/Longersking/procproxy/blob/master/docs/15-%E7%BC%96%E7%A8%8B%E6%8E%A5%E5%8F%A3%E4%B8%8ESDK.md),
|
|
352
|
+
可执行契约见 [`examples/verify_sdk.py`](https://github.com/Longersking/procproxy/blob/master/examples/verify_sdk.py)。
|
|
351
353
|
|
|
352
354
|
### 30 秒:零配置启动
|
|
353
355
|
|
|
@@ -373,6 +375,52 @@ with start_proxy() as ppx: # 不需要任何配置文件
|
|
|
373
375
|
状态查询和联调;要真实抓包请用 `backend="windivert"`(需管理员)或
|
|
374
376
|
`procproxy init` 生成自己的配置。
|
|
375
377
|
|
|
378
|
+
### 初始化:生成一份可编辑配置
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from proxycore.api import init_config
|
|
382
|
+
|
|
383
|
+
path = init_config(
|
|
384
|
+
"D:/cfg/procproxy.yaml", # 省略则用 PROCPROXY_CONFIG 或 ~/.procproxy/config.yaml
|
|
385
|
+
backend="windivert", # 真抓包需管理员;联调用 "null"
|
|
386
|
+
sink_port=19080,
|
|
387
|
+
)
|
|
388
|
+
print(path) # 绝对路径;等价于 procproxy init
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
生成的是最小模板,`upstreams` / `rules` 仍为空,需要按下面的案例补上。
|
|
392
|
+
|
|
393
|
+
### 代理一个进程(最小完整配置)
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
version: 1
|
|
397
|
+
service:
|
|
398
|
+
sink: { host: 127.0.0.1, port: 19080 }
|
|
399
|
+
backend: windivert # 真抓包;联调改 null
|
|
400
|
+
profile:
|
|
401
|
+
name: chrome-via-socks
|
|
402
|
+
default_action: direct
|
|
403
|
+
dns_mode: remote
|
|
404
|
+
upstreams:
|
|
405
|
+
- { alias: socks, kind: socks5, host: 127.0.0.1, port: 1080, auth: none }
|
|
406
|
+
rules:
|
|
407
|
+
- { id: r-private, action: direct, targets: "*",
|
|
408
|
+
dst_ip: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8" }
|
|
409
|
+
- { id: r-chrome, action: proxy, upstream: socks,
|
|
410
|
+
targets: "chrome.exe", dst_port: "*" }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
from proxycore.api import start_proxy
|
|
415
|
+
|
|
416
|
+
with start_proxy("D:/cfg/chrome.yaml") as ppx: # 管理员终端
|
|
417
|
+
print(ppx.status())
|
|
418
|
+
print(ppx.sessions(limit=20))
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
规则自上而下、首个命中生效;启动后只影响**新建立**的连接。
|
|
422
|
+
完整步骤、验证方法与常见坑见 [`docs/15-编程接口与SDK.md`](https://github.com/Longersking/procproxy/blob/master/docs/15-%E7%BC%96%E7%A8%8B%E6%8E%A5%E5%8F%A3%E4%B8%8ESDK.md)。
|
|
423
|
+
|
|
376
424
|
### 显式配置与测试转发
|
|
377
425
|
|
|
378
426
|
```python
|
|
@@ -540,8 +588,9 @@ src/proxycore/ 核心包
|
|
|
540
588
|
├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
|
|
541
589
|
└── service/ ipc.py Windows 服务 + 命名管道
|
|
542
590
|
samples/ mock 服务 + 示例配置
|
|
591
|
+
examples/ 纯 Python 示例(proxy_process.py / verify_sdk.py)
|
|
543
592
|
docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
|
|
544
|
-
tests/ 测试套件(
|
|
593
|
+
tests/ 测试套件(624 passed / 4 skipped)
|
|
545
594
|
└── tools/ 验证脚本(run_demo / run_e2e / bench / …)
|
|
546
595
|
```
|
|
547
596
|
|
|
@@ -562,7 +611,7 @@ tests/ 测试套件(619 passed / 4 skipped)
|
|
|
562
611
|
|
|
563
612
|
## 排错
|
|
564
613
|
|
|
565
|
-
见 [`docs/11-调试排错手册.md`](docs/11
|
|
614
|
+
见 [`docs/11-调试排错手册.md`](https://github.com/Longersking/procproxy/blob/master/docs/11-%E8%B0%83%E8%AF%95%E6%8E%92%E9%94%99%E6%89%8B%E5%86%8C.md)(症状 → 定位 → 解决)。
|
|
566
615
|
|
|
567
616
|
最常见的一条:**`[WinError 5] 拒绝访问` = 不是管理员**。
|
|
568
617
|
|
|
@@ -572,7 +621,7 @@ tests/ 测试套件(619 passed / 4 skipped)
|
|
|
572
621
|
|
|
573
622
|
```bash
|
|
574
623
|
pip install -e ".[dev]"
|
|
575
|
-
python -m pytest -q tests/ #
|
|
624
|
+
python -m pytest -q tests/ # 624 passed / 4 skipped(非管理员 Windows)
|
|
576
625
|
python -m ruff check src tests
|
|
577
626
|
python -m mypy src/proxycore
|
|
578
627
|
```
|
|
@@ -35,7 +35,7 @@ python procproxy.py doctor | rules | sessions | ... # 子命令(等价于 pyt
|
|
|
35
35
|
pip install procproxy
|
|
36
36
|
procproxy init # 生成 ~/.procproxy/config.yaml
|
|
37
37
|
procproxy doctor # 环境自检
|
|
38
|
-
procproxy forward WeChat.exe --port 8848 # 真实转发(需管理员)
|
|
38
|
+
procproxy forward --process WeChat.exe --port 8848 # 真实转发(需管理员)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
安装后直接运行 `procproxy` 会显示首次使用指引,不依赖任何样例文件。
|
|
@@ -211,7 +211,7 @@ python procproxy.py Weixin.exe WeChatAppEx.exe --port 8848
|
|
|
211
211
|
⚠️ 只有 ① 没有 ② → 端口填错了(**要填抓包工具的 MITM 监听口,不是它的 Web 管理口**)。
|
|
212
212
|
① ② 都正常但内容看不懂 → 那是 TLS,不是转发的问题。
|
|
213
213
|
|
|
214
|
-
完整指南(含排错与参数表)见 [`docs/14-把进程流量转发给抓包工具.md`](docs/14
|
|
214
|
+
完整指南(含排错与参数表)见 [`docs/14-把进程流量转发给抓包工具.md`](https://github.com/Longersking/procproxy/blob/master/docs/14-%E6%8A%8A%E8%BF%9B%E7%A8%8B%E6%B5%81%E9%87%8F%E8%BD%AC%E5%8F%91%E7%BB%99%E6%8A%93%E5%8C%85%E5%B7%A5%E5%85%B7.md)。
|
|
215
215
|
|
|
216
216
|
---
|
|
217
217
|
|
|
@@ -301,14 +301,16 @@ rules: # 自上而下,**首个命中生
|
|
|
301
301
|
7. 都没命中 → profile.default_action
|
|
302
302
|
```
|
|
303
303
|
|
|
304
|
-
完整语义见 [`docs/07-规则引擎与Profile格式.md`](docs/07
|
|
304
|
+
完整语义见 [`docs/07-规则引擎与Profile格式.md`](https://github.com/Longersking/procproxy/blob/master/docs/07-%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E%E4%B8%8EProfile%E6%A0%BC%E5%BC%8F.md)。
|
|
305
305
|
|
|
306
306
|
---
|
|
307
307
|
|
|
308
308
|
## 编程接口
|
|
309
309
|
|
|
310
310
|
`proxycore.api` 是与 CLI 共用同一套配置、规则引擎和会话表的 SDK。
|
|
311
|
-
|
|
311
|
+
**以下全部只用 `import proxycore`,不需要运行 `procproxy` 命令。**
|
|
312
|
+
完整教程见 [`docs/15-编程接口与SDK.md`](https://github.com/Longersking/procproxy/blob/master/docs/15-%E7%BC%96%E7%A8%8B%E6%8E%A5%E5%8F%A3%E4%B8%8ESDK.md),
|
|
313
|
+
可执行契约见 [`examples/verify_sdk.py`](https://github.com/Longersking/procproxy/blob/master/examples/verify_sdk.py)。
|
|
312
314
|
|
|
313
315
|
### 30 秒:零配置启动
|
|
314
316
|
|
|
@@ -334,6 +336,52 @@ with start_proxy() as ppx: # 不需要任何配置文件
|
|
|
334
336
|
状态查询和联调;要真实抓包请用 `backend="windivert"`(需管理员)或
|
|
335
337
|
`procproxy init` 生成自己的配置。
|
|
336
338
|
|
|
339
|
+
### 初始化:生成一份可编辑配置
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from proxycore.api import init_config
|
|
343
|
+
|
|
344
|
+
path = init_config(
|
|
345
|
+
"D:/cfg/procproxy.yaml", # 省略则用 PROCPROXY_CONFIG 或 ~/.procproxy/config.yaml
|
|
346
|
+
backend="windivert", # 真抓包需管理员;联调用 "null"
|
|
347
|
+
sink_port=19080,
|
|
348
|
+
)
|
|
349
|
+
print(path) # 绝对路径;等价于 procproxy init
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
生成的是最小模板,`upstreams` / `rules` 仍为空,需要按下面的案例补上。
|
|
353
|
+
|
|
354
|
+
### 代理一个进程(最小完整配置)
|
|
355
|
+
|
|
356
|
+
```yaml
|
|
357
|
+
version: 1
|
|
358
|
+
service:
|
|
359
|
+
sink: { host: 127.0.0.1, port: 19080 }
|
|
360
|
+
backend: windivert # 真抓包;联调改 null
|
|
361
|
+
profile:
|
|
362
|
+
name: chrome-via-socks
|
|
363
|
+
default_action: direct
|
|
364
|
+
dns_mode: remote
|
|
365
|
+
upstreams:
|
|
366
|
+
- { alias: socks, kind: socks5, host: 127.0.0.1, port: 1080, auth: none }
|
|
367
|
+
rules:
|
|
368
|
+
- { id: r-private, action: direct, targets: "*",
|
|
369
|
+
dst_ip: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8" }
|
|
370
|
+
- { id: r-chrome, action: proxy, upstream: socks,
|
|
371
|
+
targets: "chrome.exe", dst_port: "*" }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
from proxycore.api import start_proxy
|
|
376
|
+
|
|
377
|
+
with start_proxy("D:/cfg/chrome.yaml") as ppx: # 管理员终端
|
|
378
|
+
print(ppx.status())
|
|
379
|
+
print(ppx.sessions(limit=20))
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
规则自上而下、首个命中生效;启动后只影响**新建立**的连接。
|
|
383
|
+
完整步骤、验证方法与常见坑见 [`docs/15-编程接口与SDK.md`](https://github.com/Longersking/procproxy/blob/master/docs/15-%E7%BC%96%E7%A8%8B%E6%8E%A5%E5%8F%A3%E4%B8%8ESDK.md)。
|
|
384
|
+
|
|
337
385
|
### 显式配置与测试转发
|
|
338
386
|
|
|
339
387
|
```python
|
|
@@ -501,8 +549,9 @@ src/proxycore/ 核心包
|
|
|
501
549
|
├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
|
|
502
550
|
└── service/ ipc.py Windows 服务 + 命名管道
|
|
503
551
|
samples/ mock 服务 + 示例配置
|
|
552
|
+
examples/ 纯 Python 示例(proxy_process.py / verify_sdk.py)
|
|
504
553
|
docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
|
|
505
|
-
tests/ 测试套件(
|
|
554
|
+
tests/ 测试套件(624 passed / 4 skipped)
|
|
506
555
|
└── tools/ 验证脚本(run_demo / run_e2e / bench / …)
|
|
507
556
|
```
|
|
508
557
|
|
|
@@ -523,7 +572,7 @@ tests/ 测试套件(619 passed / 4 skipped)
|
|
|
523
572
|
|
|
524
573
|
## 排错
|
|
525
574
|
|
|
526
|
-
见 [`docs/11-调试排错手册.md`](docs/11
|
|
575
|
+
见 [`docs/11-调试排错手册.md`](https://github.com/Longersking/procproxy/blob/master/docs/11-%E8%B0%83%E8%AF%95%E6%8E%92%E9%94%99%E6%89%8B%E5%86%8C.md)(症状 → 定位 → 解决)。
|
|
527
576
|
|
|
528
577
|
最常见的一条:**`[WinError 5] 拒绝访问` = 不是管理员**。
|
|
529
578
|
|
|
@@ -533,7 +582,7 @@ tests/ 测试套件(619 passed / 4 skipped)
|
|
|
533
582
|
|
|
534
583
|
```bash
|
|
535
584
|
pip install -e ".[dev]"
|
|
536
|
-
python -m pytest -q tests/ #
|
|
585
|
+
python -m pytest -q tests/ # 624 passed / 4 skipped(非管理员 Windows)
|
|
537
586
|
python -m ruff check src tests
|
|
538
587
|
python -m mypy src/proxycore
|
|
539
588
|
```
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# 00 · 架构与阅读路线
|
|
2
|
+
|
|
3
|
+
> 这份文档写给**想读源码的人**。
|
|
4
|
+
> 只用工具看 `README.md`;想程序化调用看 [`15-编程接口与SDK.md`](15-编程接口与SDK.md) 和 `AGENTS.md`。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 一句话
|
|
9
|
+
|
|
10
|
+
ProcProxy 只做一件事:认出某个进程发出的 TCP 连接,把它**改道**到本机的一个
|
|
11
|
+
**sink** 端口,再由 sink 经你指定的**上游代理**转发出去。目标进程全程不知道。
|
|
12
|
+
|
|
13
|
+
用比喻说:保安认出这是微信寄的信,把收件地址改成"本小区代收点"(sink),
|
|
14
|
+
代收点再按你的吩咐让快递公司(上游代理)寄出去。回信时保安把信封改回原样。
|
|
15
|
+
不拆信(不解密)、不问微信要密码(不注入 DLL)。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. 五个概念
|
|
20
|
+
|
|
21
|
+
| 术语 | 是什么 | 代码位置 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| **sink** | 本机的一个监听端口(默认 19080)。被改道的连接都落到这里 | `tunnel/sink.py` |
|
|
24
|
+
| **改道** | 改写报文的目的地址到 sink,再重新注入网络栈 | `backend/windivert/` |
|
|
25
|
+
| **回注** | 把改写后的包"送回去"。必须同时翻方向标志,否则内核会丢弃 | `windivert/__init__.py:415` |
|
|
26
|
+
| **上游** | 你指定的代理出口(SOCKS5 / SOCKS4 / HTTP CONNECT) | `upstream/` |
|
|
27
|
+
| **路线 A** | 本仓库唯一的数据面方案:用户态 WinDivert。历史上的内核方案(路线 B)已移除 | `backend/windivert/` |
|
|
28
|
+
|
|
29
|
+
两个容易混的:
|
|
30
|
+
|
|
31
|
+
* **`backend_id`**:路线 A 里它就是**客户端源端口**。sink 靠它反查原始目标。
|
|
32
|
+
* **环路熔断**:拦截器自己连上游时会被自己再抓一遍,所以有一套守卫 + 熔断防止无限循环。
|
|
33
|
+
规则匹配顺序里的 G1–G4 就是这四道守卫。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. 一条连接的完整路径
|
|
38
|
+
|
|
39
|
+
按代码的**实际调用顺序**排,不是按目录顺序。
|
|
40
|
+
|
|
41
|
+
### 第一段:抓包与决策(WinDivert 捕获线程)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
WinDivertBackend._capture_loop backend/windivert/__init__.py:292
|
|
45
|
+
└─ _handle_packet backend/windivert/__init__.py:316
|
|
46
|
+
├─ 解析 IP/TCP → 构造 ConnRequest backend/windivert/__init__.py:485
|
|
47
|
+
├─ self.decide(conn) backend/windivert/__init__.py:492
|
|
48
|
+
│ └─ BaseBackend.decide backend/base.py:117
|
|
49
|
+
│ └─ Runtime.decide runtime.py:208
|
|
50
|
+
│ ├─ rules.decide runtime.py:224 → rules.py:550
|
|
51
|
+
│ └─ record_decision runtime.py:240 → sessions.py:282
|
|
52
|
+
│ └─ put_pending sessions.py:77
|
|
53
|
+
├─ BLOCK → 不回注(丢弃) backend/windivert/__init__.py:495
|
|
54
|
+
├─ DIRECT → 原样放行 backend/windivert/__init__.py:502
|
|
55
|
+
└─ PROXY → 先记账,再改道 backend/windivert/__init__.py:510
|
|
56
|
+
└─ _redirect_to_sink backend/windivert/__init__.py:415
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**为什么"先记账再改道"**:sink 收到连接时只能看到客户端源端口,必须靠这张表
|
|
60
|
+
反查原始目标。顺序反了,在 TIME_WAIT 撞车时会查不到。
|
|
61
|
+
|
|
62
|
+
### 第二段:sink 落地与转发(sink 工作线程)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Sink._accept_loop tunnel/sink.py:149
|
|
66
|
+
└─ _accept_one tunnel/sink.py:163
|
|
67
|
+
└─ _handle_client tunnel/sink.py:175
|
|
68
|
+
├─ backend.lookup_original tunnel/sink.py:186
|
|
69
|
+
│ 用 backend_id 反查原始目标(查不到就关闭,不猜、不直连)
|
|
70
|
+
├─ sessions.pop_pending tunnel/sink.py:204
|
|
71
|
+
├─ _make_dialer tunnel/sink.py:212 / :300
|
|
72
|
+
├─ dial_tcp_sync tunnel/sink.py:238 → upstream/base.py:55
|
|
73
|
+
└─ relay_bidirectional tunnel/sink.py:253 → tunnel/relay.py:93
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 反向路径
|
|
77
|
+
|
|
78
|
+
sink 的应答要从 `127.0.0.1:<sink>` 改回"来自原始目标",否则客户端三次握手完不成。
|
|
79
|
+
这段在 `_reflect_sink_reply`(`backend/windivert/__init__.py:356`),
|
|
80
|
+
由同一个 `_handle_packet` 按"源端口 == sink 端口"分流进入。
|
|
81
|
+
|
|
82
|
+
> 注意:`BaseBackend.redirect()`(`backend/base.py:59`)是**后端契约里的方法**,
|
|
83
|
+
> 只有契约测试与压测工具直接调用。生产路径是在 `_handle_packet` 里内联完成的。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. 规则匹配顺序(不可调换)
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
1. 未知 PID → 按 on_unknown_pid 兜底
|
|
91
|
+
2. 守卫 G1 → 回环 / 私网 / 链路本地 → DIRECT
|
|
92
|
+
3. 守卫 G2 → 自身进程 → DIRECT
|
|
93
|
+
4. 守卫 G3 → 上游 / 本地 DNS 端点 → DIRECT
|
|
94
|
+
5. 守卫 G4 → 同 (pid, dst) 熔断 → DIRECT + 告警
|
|
95
|
+
6. 规则表 → 自上而下,首个命中生效
|
|
96
|
+
7. 都没命中 → profile.default_action
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
守卫是**硬编码、不可配置**的,优先于任何用户规则。
|
|
100
|
+
完整语义见 [`07-规则引擎与Profile格式.md`](07-规则引擎与Profile格式.md)。
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 5. 推荐阅读顺序
|
|
105
|
+
|
|
106
|
+
不要按目录读。按这个顺序,每一步都能独立跑通验证。
|
|
107
|
+
|
|
108
|
+
| 顺序 | 文件 | 为什么先读它 |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| 1 | `types.py` | 164 行、零逻辑,全是数据结构。搞懂 `ConnRequest` / `Decision` 就够了 |
|
|
111
|
+
| 2 | `rules.py:550` 的 `decide()` | **纯函数**,不做 IO。整个项目的"大脑" |
|
|
112
|
+
| 3 | `tunnel/sink.py:175` 的 `_handle_client()` | 88 行主流程,看懂"代收点怎么转寄" |
|
|
113
|
+
| 4 | `tunnel/relay.py:93` | 只是搬字节,先看大框架 |
|
|
114
|
+
| 5 | `upstream/` | 协议客户端,和本项目其余部分耦合最低 |
|
|
115
|
+
| 6 | `backend/windivert/` | **最后看**。最难的一层,前面都懂了再回来 |
|
|
116
|
+
|
|
117
|
+
配套的可执行材料:
|
|
118
|
+
|
|
119
|
+
* `tests/unit/test_rules.py` —— `docs/07` 那 35 条规则矩阵的可执行版,按编号读;
|
|
120
|
+
* `tests/tools/run_e2e.py` —— 2 秒跑通 sink → 上游 → 目标;
|
|
121
|
+
* `tests/tools/api_demo.py` —— 三种编程式用法(完整说明见 `docs/15`);
|
|
122
|
+
* `learn_project.md` —— 一份 7 天读源码计划(按上面的顺序展开)。
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 6. 三十秒上手
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e .
|
|
130
|
+
python procproxy.py # 环境自检
|
|
131
|
+
python tests/tools/run_demo.py # 15 组免管理员自检
|
|
132
|
+
python tests/tools/run_e2e.py # 端到端链路
|
|
133
|
+
python procproxy.py curl.exe --port 7890 --check-only # 真用法的预演
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
真改道需要管理员:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python procproxy.py curl.exe --port 7890
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 7. 边界(读代码前必须知道)
|
|
145
|
+
|
|
146
|
+
* **只抓 TCP**。过滤器以 `tcp` 开头,UDP/QUIC 不进用户态,`udp_policy` 在路线 A 下空转。
|
|
147
|
+
* **`dst_name` 规则永不命中**。抓到连接时应用早已 `getaddrinfo` 完成,只拿得到 IP。
|
|
148
|
+
* **不改道就失败**。查不到原始目标一律关闭连接,绝不回落直连(那是一次真实 IP 泄漏)。
|
|
149
|
+
* **上游端点必须写 IP 字面量**。环路守卫按 `(host, port)` 字面量比对,写域名护栏就失效。
|
|
150
|
+
|
|
151
|
+
排错见 [`11-调试排错手册.md`](11-调试排错手册.md);
|
|
152
|
+
抓包工具场景见 [`14-把进程流量转发给抓包工具.md`](14-把进程流量转发给抓包工具.md)。
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# 07 · 规则引擎与 Profile 格式
|
|
2
|
+
|
|
3
|
+
## 1. 规则语义(严格,实现必须与本文一致)
|
|
4
|
+
|
|
5
|
+
### 1.1 匹配顺序(不可调整)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
1. profile.rules 里 enabled=true 的规则,自上而下
|
|
9
|
+
2. 首个命中的所有条件都满足的规则生效 → 返回其 action/upstream
|
|
10
|
+
3. 未命中 → profile.default_action(默认 direct)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**前置守卫(在进入规则匹配前执行,优先于任何用户规则)**:
|
|
14
|
+
|
|
15
|
+
| 顺序 | 守卫 | 结果 | 理由 |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| G1 | `dst` 属于回环/私网/链路本地,且未开 `keep_private_proxied` | DIRECT | 防止把内网流量送出去 + 防止环路 |
|
|
18
|
+
| G2 | `pid ∈ self_pids`(服务自身、拦截器、上游代理进程) | DIRECT | 环路防护第一重 |
|
|
19
|
+
| G3 | `dst ∈ upstream_endpoints ∪ local_dns_servers` | DIRECT | 环路防护第二重(上游地址变了要热更新此集合) |
|
|
20
|
+
| G4 | 熔断:`(pid, dst)` 在 `window_s` 内命中次数 > `threshold` | DIRECT + 告警 | 环路防护第三重 |
|
|
21
|
+
|
|
22
|
+
> 守卫要**可观测**:每条连接日志都带 `reason`:`guard-private` / `self-pid` / `upstream-endpoint` / `loop-breaker` / `rule:<id>` / `default`。
|
|
23
|
+
|
|
24
|
+
### 1.2 字段语义
|
|
25
|
+
|
|
26
|
+
| 字段 | 语法 | 说明 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `targets` | `curl.exe`、`fire*.exe`、`C:\path\app.exe`、多值 `a.exe;b.exe` | 大小写不敏感;`*`/`?` 通配;含 `\` 或 `/` 时按**完整路径**匹配,否则按**文件名**匹配 |
|
|
29
|
+
| `dst_ip` | `1.2.3.4`、`10.0.0.0/8`、多值逗号分隔 | IPv4/IPv6 CIDR |
|
|
30
|
+
| `dst_port` | `443`、`80,443`、`1000-2000`、`*` | 端口段含边界 |
|
|
31
|
+
| `dst_name` | `*.corp.example`、`example.com` | **仅在域名已知时匹配**(见契约文档第 3 节的语义边界表) |
|
|
32
|
+
| `proto` | `tcp`、`udp`、`*` | 默认 `*` |
|
|
33
|
+
| `action` | `proxy`、`direct`、`block` | `proxy` 必须给 `upstream` |
|
|
34
|
+
| `keep_private_proxied` | `true/false` | 显式代理私网(默认 false) |
|
|
35
|
+
|
|
36
|
+
### 1.3 交互示例(顺序敏感,必须按此顺序理解)
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
rules:
|
|
40
|
+
# ① 例外优先:公司内网即使规则②命中也直连
|
|
41
|
+
- {id: r1, action: direct, targets: "*", dst_ip: "10.20.0.0/16"}
|
|
42
|
+
# ② 浏览器走日本出口
|
|
43
|
+
- {id: r2, action: proxy, upstream: jp-http, targets: "chrome.exe;msedge.exe", dst_port: "443,80"}
|
|
44
|
+
# ③ curl 走 SOCKS5
|
|
45
|
+
- {id: r3, action: proxy, upstream: office-socks, targets: "curl.exe", dst_port: "*"}
|
|
46
|
+
# ④ 内网域名绕过
|
|
47
|
+
- {id: r4, action: direct, targets: "*", dst_name: "*.internal.corp"}
|
|
48
|
+
# ⑤ 默认拒绝所有 exe 的 53/UDP(防 DNS 泄漏,除非配置了代理 DNS)
|
|
49
|
+
- {id: r5, action: block, targets: "*.exe", dst_port: "53", proto: udp}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> 关键点:**"例外"必须写在"通用规则"之前**。这是与旧版工具兼容的行为约定:
|
|
53
|
+
> 规则按从上到下的顺序处理,可通过上下移动调整顺序。
|
|
54
|
+
> 规则编辑器必须支持上下移动,且拖动后自动重编号。
|
|
55
|
+
|
|
56
|
+
### 1.4 粘性(`sticky_per_pid`)
|
|
57
|
+
|
|
58
|
+
- `true`(默认):同一 PID 的所有连接使用首次选定的上游(兼容旧版工具的行为约定)。
|
|
59
|
+
实现:`sessions.sticky[pid] = upstream_alias`,PID 退出(进程句柄失效)后清理。
|
|
60
|
+
- `false`:每条连接独立按规则选上游。
|
|
61
|
+
- 注意:粘性只影响**上游选择**,不影响规则匹配本身;规则命中改变时,粘性以**规则 id**
|
|
62
|
+
为键更新(`sticky[(pid, rule_id)] = upstream`),避免"规则改了但粘性还指向旧代理"。
|
|
63
|
+
|
|
64
|
+
## 2. 旧版 Profile 字段映射(手工迁移)
|
|
65
|
+
|
|
66
|
+
旧版工具的 Profile 使用 `.ppx`(v3)/`.prx`(v2)私有格式;本项目不解析该格式,
|
|
67
|
+
只提供字段映射表,供用户手工迁移:
|
|
68
|
+
|
|
69
|
+
| 旧版工具概念 | 本项目字段 | 备注 |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Applications(进程) | `targets` | 支持 `fire*.exe` 通配语法(旧版工具使用相同语法) |
|
|
72
|
+
| Target Hosts / Ports | `dst_ip` / `dst_port` | 旧版工具支持域名 → 对应 `dst_name`(注意其近似性) |
|
|
73
|
+
| Action: Proxy/Direct/Block | `action` | 一一对应 |
|
|
74
|
+
| Proxies(代理列表) | `upstreams[]` | 含类型/地址/认证/链式 |
|
|
75
|
+
| Name Resolution | `dns_mode` | `local` = 本地 DNS 解析;`remote` = 通过上游远端解析 |
|
|
76
|
+
| Services and Other Users | `handle_services` / `handle_other_users` | 尚未接入数据面:设为 `true` 会在校验期报 `profile_option_unsupported` |
|
|
77
|
+
|
|
78
|
+
**导入器(未实现)**:本项目不提供 `.ppx/.prx` 自动解析;如需迁移,请按上表手工配置。
|
|
79
|
+
|
|
80
|
+
## 3. 配置校验规则(`config.py` 必须实现,且每条都要有单测)
|
|
81
|
+
|
|
82
|
+
| # | 校验 | 错误码 |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| V1 | `version == 1` | `config_version_unsupported` |
|
|
85
|
+
| V2 | `action=proxy` 必须有 `upstream` 且别名存在 | `rule_upstream_missing` |
|
|
86
|
+
| V3 | `chain` 不得成环,跳数 ≤ 8 | `upstream_chain_cycle` |
|
|
87
|
+
| V4 | `dst_ip` 必须是合法 IP/CIDR;`dst_port` 段须 `lo<=hi` | `rule_match_invalid` |
|
|
88
|
+
| V5 | `targets` 不能为空;长度 ≤ 1024 | `rule_targets_invalid` |
|
|
89
|
+
| V6 | `sink.port` ∈ 1024–65535 且未被占用 | `sink_port_unavailable` |
|
|
90
|
+
| V7 | `loop_breaker.threshold > 0 && window_s > 0` | `loop_breaker_invalid` |
|
|
91
|
+
| V8 | `dns_mode ∈ {local, remote, bypass}`;`bypass` 时 `dns_bypass` 不能为空 | `dns_mode_invalid` |
|
|
92
|
+
| V9 | 规则 id 唯一 | `rule_id_duplicate` |
|
|
93
|
+
| V10 | 未知字段 → 警告(不报错,便于前向兼容) | `config_unknown_field` |
|
|
94
|
+
|
|
95
|
+
### 3.1 热重载(原子)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
def reload(path) -> Profile:
|
|
99
|
+
doc = yaml.safe_load(open(path))
|
|
100
|
+
new = Profile.from_dict(doc) # 校验失败抛 ConfigError
|
|
101
|
+
# 校验通过才替换
|
|
102
|
+
with lock:
|
|
103
|
+
old, CURRENT = CURRENT, new
|
|
104
|
+
log.info("config_reloaded", rules=len(new.rules), from_rules=len(old.rules))
|
|
105
|
+
return new
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
要求:
|
|
109
|
+
- 重载**不得**断开已有会话;
|
|
110
|
+
- 已建立会话的粘性保留(按 `rule_id` 与 `pid` 双键);
|
|
111
|
+
- 若 `sink.port` 变化 → 需要重建监听(本期直接禁止热改,要求重启服务,并给出明确提示);
|
|
112
|
+
- 若 `backend` 变化 → 明确报错要求重启。
|
|
113
|
+
|
|
114
|
+
## 4. 规则测试矩阵
|
|
115
|
+
|
|
116
|
+
| # | 输入(进程 / 目标 / 端口 / 协议) | 期望 | 覆盖点 |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| 1 | `curl.exe` / `1.1.1.1` / 443 / tcp | proxy(office-socks) | 基础命中 |
|
|
119
|
+
| 2 | `CURL.EXE` / ... | proxy | 大小写不敏感 |
|
|
120
|
+
| 3 | `curl.exe` / `1.1.1.1` / 22 | direct | 端口不匹配 |
|
|
121
|
+
| 4 | `curl2.exe` / ... | direct | 通配不误命中(无 `*`) |
|
|
122
|
+
| 5 | `firefox.exe` / `1.1.1.1` / 443 | proxy(jp-http) | 通配命中 `fire*.exe` |
|
|
123
|
+
| 6 | 任一进程 / `127.0.0.1` / 80 | direct | 守卫 G1 回环 |
|
|
124
|
+
| 7 | 任一进程 / `192.168.1.5` / 80 | direct | 守卫 G1 私网 |
|
|
125
|
+
| 8 | 服务自身进程 / `8.8.8.8` / 443 | direct | 守卫 G2 |
|
|
126
|
+
| 9 | 任一进程 / `<上游代理IP>` / 1080 | direct | 守卫 G3 |
|
|
127
|
+
| 10 | `a.exe` / `8.8.8.8` / 53 / udp | block | 规则⑤ |
|
|
128
|
+
| 11 | `a.exe` / `8.8.8.8` / 53 / tcp | direct(默认) | 协议区分 |
|
|
129
|
+
| 12 | `x.exe` / `10.20.1.1` / 443 | direct | 规则①先于② |
|
|
130
|
+
| 13 | 同 PID 二次连接不同目标 / 443 | 同上游 | 粘性 |
|
|
131
|
+
| 14 | 同 PID 规则变更后再连接 | 用**新规则**的上游 | 粘性按 rule_id 键 |
|
|
132
|
+
| 15 | 私网 + `keep_private_proxied=true` | proxy | 显式覆盖守卫 G1 |
|
|
133
|
+
| 16 | 同 (pid,dst) 5s 内 60 次 | 第 51 次起 direct + 告警 | 熔断 |
|
|
134
|
+
| 17 | `dst_name` 已知(从 DNS 缓存)匹配 `*.corp.example` | direct | 域名规则 |
|
|
135
|
+
| 18 | `dst_name` 未知(只有 IP) | 不匹配域名规则 | 近似性边界 |
|
|
136
|
+
| 19 | 端口 `1000-2000` 命中 1500 | proxy | 端口段 |
|
|
137
|
+
| 20 | 端口段非法 `2000-1000` | 加载失败 | V4 |
|
|
138
|
+
| 21 | 空 `targets` | 加载失败 | V5 |
|
|
139
|
+
| 22 | `action=proxy` 缺 upstream | 加载失败 | V2 |
|
|
140
|
+
| 23 | 上游链 A→B→A | 加载失败 | V3 |
|
|
141
|
+
| 24 | 规则 id 重复 | 加载失败 | V9 |
|
|
142
|
+
| 25 | `dns_mode=bypass` 但 bypass 列表为空 | 加载失败 | V8 |
|
|
143
|
+
| 26 | IPv6 目标 `2001:db8::1` + `dst_ip: 2001:db8::/32` | proxy | v6 匹配 |
|
|
144
|
+
| 27 | Unix 路径风格 targets(Linux 后端) `/usr/bin/curl` | proxy | 路径匹配 |
|
|
145
|
+
| 28 | 受保护进程(path 为空)但名称可读 | 按名称规则命中 | 缺路径降级 |
|
|
146
|
+
| 29 | 未知 PID(`pid=0`)+ `on_unknown_pid=proxy` | proxy | 未知处置策略 |
|
|
147
|
+
| 30 | 未知 PID + `on_unknown_pid=direct`(默认) | direct + WARN | 默认安全 |
|
|
148
|
+
| 31 | 规则 `enabled: false` | 跳过该规则 | 启停开关 |
|
|
149
|
+
| 32 | 前向兼容:含未知字段 `future: 1` | 加载成功 + 警告 | V10 |
|
|
150
|
+
| 33 | **决策路径抛异常** + `on_decide_error=block`(默认) | `block` + ERROR 日志 | 契约第 2 节:禁止静默直连 |
|
|
151
|
+
| 34 | 决策路径抛异常 + `on_decide_error=direct` | `direct`(用户显式选择) | 可配置,非写死 |
|
|
152
|
+
| 35 | 决策路径抛异常 + `on_decide_error=proxy` | 第一条 `action=proxy` 启用规则的上游;**一条都没有 → `block`** | 没有上游就没法建连,不能退回直连 |
|
|
153
|
+
|
|
154
|
+
## 5. 规则引擎实现骨架(可直接抄)
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
def decide(conn: ConnRequest, profile: Profile, sessions, guards) -> Decision:
|
|
158
|
+
# G1 私网/回环
|
|
159
|
+
if not guards.allow_private and is_private(conn.dst.addr):
|
|
160
|
+
return Decision(Action.DIRECT, reason="guard-private")
|
|
161
|
+
# G2 自身进程
|
|
162
|
+
if conn.proc.pid in guards.self_pids:
|
|
163
|
+
return Decision(Action.DIRECT, reason="self-pid")
|
|
164
|
+
# G3 上游/DNS 端点
|
|
165
|
+
if (conn.dst.addr, conn.dst.port) in guards.upstream_endpoints:
|
|
166
|
+
return Decision(Action.DIRECT, reason="upstream-endpoint")
|
|
167
|
+
# G4 熔断
|
|
168
|
+
if sessions.breaker.tripped(conn.proc.pid, conn.dst):
|
|
169
|
+
return Decision(Action.DIRECT, reason="loop-breaker")
|
|
170
|
+
|
|
171
|
+
name = guards.name_for_ip(conn.dst.addr) # DNS 缓存,可能为 None
|
|
172
|
+
for r in profile.rules:
|
|
173
|
+
if not r.enabled:
|
|
174
|
+
continue
|
|
175
|
+
if not match_proc(r.targets, conn.proc):
|
|
176
|
+
continue
|
|
177
|
+
if not match_proto(r.proto, conn.proto):
|
|
178
|
+
continue
|
|
179
|
+
if not match_port(r.dst_port, conn.dst.port):
|
|
180
|
+
continue
|
|
181
|
+
if not match_ip(r.dst_ip, conn.dst.addr):
|
|
182
|
+
continue
|
|
183
|
+
if r.dst_name != "*":
|
|
184
|
+
if name is None:
|
|
185
|
+
continue # 域名未知 → 该规则不适用(不做近似猜测)
|
|
186
|
+
if not wildcard(r.dst_name, name):
|
|
187
|
+
continue
|
|
188
|
+
# 命中
|
|
189
|
+
up = r.upstream
|
|
190
|
+
if profile.sticky_per_pid and r.action is Action.PROXY:
|
|
191
|
+
up = sessions.sticky_upstream(conn.proc.pid, r.id, default=r.upstream)
|
|
192
|
+
return Decision(r.action, rule_id=r.id, upstream=up, reason=f"rule:{r.id}")
|
|
193
|
+
return Decision(profile.default_action, reason="default")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**性能要求**:`decide()` 必须 O(规则数),单次 < 20µs(1000 条规则时 < 100µs);
|
|
197
|
+
禁止在 `decide()` 里做 DNS 解析、日志格式化(用惰性参数)、文件读取。
|