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.
Files changed (61) hide show
  1. procproxy-0.1.4/MANIFEST.in +2 -0
  2. {procproxy-0.1.2/src/procproxy.egg-info → procproxy-0.1.4}/PKG-INFO +57 -8
  3. {procproxy-0.1.2 → procproxy-0.1.4}/README.md +56 -7
  4. 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
  5. 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
  6. 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
  7. 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
  8. procproxy-0.1.4/docs/15-/347/274/226/347/250/213/346/216/245/345/217/243/344/270/216SDK.md +902 -0
  9. procproxy-0.1.4/examples/proxy_process.py +139 -0
  10. procproxy-0.1.4/examples/verify_sdk.py +153 -0
  11. {procproxy-0.1.2 → procproxy-0.1.4}/pyproject.toml +1 -1
  12. {procproxy-0.1.2 → procproxy-0.1.4/src/procproxy.egg-info}/PKG-INFO +57 -8
  13. {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/SOURCES.txt +9 -0
  14. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/__init__.py +1 -1
  15. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/api.py +30 -0
  16. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/cli.py +26 -30
  17. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/config.py +2 -0
  18. procproxy-0.1.4/src/proxycore/config_init.py +85 -0
  19. {procproxy-0.1.2 → procproxy-0.1.4}/LICENSE +0 -0
  20. {procproxy-0.1.2 → procproxy-0.1.4}/setup.cfg +0 -0
  21. {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/dependency_links.txt +0 -0
  22. {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/entry_points.txt +0 -0
  23. {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/requires.txt +0 -0
  24. {procproxy-0.1.2 → procproxy-0.1.4}/src/procproxy.egg-info/top_level.txt +0 -0
  25. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/__main__.py +0 -0
  26. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/__init__.py +0 -0
  27. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/base.py +0 -0
  28. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/null.py +0 -0
  29. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/__init__.py +0 -0
  30. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/filter.py +0 -0
  31. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/pidmap.py +0 -0
  32. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/backend/windivert/pkt.py +0 -0
  33. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/canproxy.py +0 -0
  34. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/checkroute.py +0 -0
  35. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/config_source.py +0 -0
  36. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/data/default.yaml +0 -0
  37. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/__init__.py +0 -0
  38. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/cache.py +0 -0
  39. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/modes.py +0 -0
  40. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/dns/udp.py +0 -0
  41. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/forward.py +0 -0
  42. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/ipc.py +0 -0
  43. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/log.py +0 -0
  44. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/pathsetup.py +0 -0
  45. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/probe.py +0 -0
  46. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/proc.py +0 -0
  47. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/rules.py +0 -0
  48. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/runtime.py +0 -0
  49. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/service/__init__.py +0 -0
  50. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/service/__main__.py +0 -0
  51. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/__init__.py +0 -0
  52. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/relay.py +0 -0
  53. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/sessions.py +0 -0
  54. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/tunnel/sink.py +0 -0
  55. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/types.py +0 -0
  56. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/__init__.py +0 -0
  57. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/base.py +0 -0
  58. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/chain.py +0 -0
  59. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/http_connect.py +0 -0
  60. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/socks4.py +0 -0
  61. {procproxy-0.1.2 → procproxy-0.1.4}/src/proxycore/upstream/socks5.py +0 -0
@@ -0,0 +1,2 @@
1
+ graft docs
2
+ graft examples
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procproxy
3
- Version: 0.1.2
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-把进程流量转发给抓包工具.md)。
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-规则引擎与Profile格式.md)。
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
- 完整教程见 [`docs/15-编程接口与SDK.md`](docs/15-编程接口与SDK.md)。
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/ 测试套件(619 passed / 4 skipped)
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-调试排错手册.md)(症状 → 定位 → 解决)。
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/ # 619 passed / 4 skipped(非管理员 Windows)
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-把进程流量转发给抓包工具.md)。
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-规则引擎与Profile格式.md)。
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
- 完整教程见 [`docs/15-编程接口与SDK.md`](docs/15-编程接口与SDK.md)。
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/ 测试套件(619 passed / 4 skipped)
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-调试排错手册.md)(症状 → 定位 → 解决)。
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/ # 619 passed / 4 skipped(非管理员 Windows)
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 解析、日志格式化(用惰性参数)、文件读取。