procproxy 0.1.3__tar.gz → 0.1.4.1__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.1/MANIFEST.in +2 -0
- {procproxy-0.1.3/src/procproxy.egg-info → procproxy-0.1.4.1}/PKG-INFO +7 -4
- {procproxy-0.1.3 → procproxy-0.1.4.1}/README.md +6 -3
- procproxy-0.1.4.1/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.1/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.1/docs/11-/350/260/203/350/257/225/346/216/222/351/224/231/346/211/213/345/206/214.md +139 -0
- procproxy-0.1.4.1/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.1/docs/15-/347/274/226/347/250/213/346/216/245/345/217/243/344/270/216SDK.md +902 -0
- procproxy-0.1.4.1/examples/proxy_process.py +139 -0
- procproxy-0.1.4.1/examples/verify_sdk.py +153 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/pyproject.toml +1 -1
- {procproxy-0.1.3 → procproxy-0.1.4.1/src/procproxy.egg-info}/PKG-INFO +7 -4
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/procproxy.egg-info/SOURCES.txt +8 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/__init__.py +1 -1
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/windivert/__init__.py +17 -15
- {procproxy-0.1.3 → procproxy-0.1.4.1}/LICENSE +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/setup.cfg +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/procproxy.egg-info/dependency_links.txt +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/procproxy.egg-info/entry_points.txt +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/procproxy.egg-info/requires.txt +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/procproxy.egg-info/top_level.txt +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/__main__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/api.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/__init__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/base.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/null.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/windivert/filter.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/windivert/pidmap.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/backend/windivert/pkt.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/canproxy.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/checkroute.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/cli.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/config.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/config_init.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/config_source.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/data/default.yaml +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/dns/__init__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/dns/cache.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/dns/modes.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/dns/udp.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/forward.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/ipc.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/log.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/pathsetup.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/probe.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/proc.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/rules.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/runtime.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/service/__init__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/service/__main__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/tunnel/__init__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/tunnel/relay.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/tunnel/sessions.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/tunnel/sink.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/types.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/upstream/__init__.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/upstream/base.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/upstream/chain.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/upstream/http_connect.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/src/proxycore/upstream/socks4.py +0 -0
- {procproxy-0.1.3 → procproxy-0.1.4.1}/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.1
|
|
4
4
|
Summary: ProcProxy —— 进程级代理:按进程决定哪条连接走哪个上游代理
|
|
5
5
|
Author: Longersking
|
|
6
6
|
License-Expression: MIT
|
|
@@ -347,7 +347,9 @@ rules: # 自上而下,**首个命中生
|
|
|
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
|
|
|
@@ -586,8 +588,9 @@ src/proxycore/ 核心包
|
|
|
586
588
|
├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
|
|
587
589
|
└── service/ ipc.py Windows 服务 + 命名管道
|
|
588
590
|
samples/ mock 服务 + 示例配置
|
|
591
|
+
examples/ 纯 Python 示例(proxy_process.py / verify_sdk.py)
|
|
589
592
|
docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
|
|
590
|
-
tests/ 测试套件(
|
|
593
|
+
tests/ 测试套件(625 passed / 4 skipped)
|
|
591
594
|
└── tools/ 验证脚本(run_demo / run_e2e / bench / …)
|
|
592
595
|
```
|
|
593
596
|
|
|
@@ -618,7 +621,7 @@ tests/ 测试套件(624 passed / 4 skipped)
|
|
|
618
621
|
|
|
619
622
|
```bash
|
|
620
623
|
pip install -e ".[dev]"
|
|
621
|
-
python -m pytest -q tests/ #
|
|
624
|
+
python -m pytest -q tests/ # 625 passed / 4 skipped(非管理员 Windows)
|
|
622
625
|
python -m ruff check src tests
|
|
623
626
|
python -m mypy src/proxycore
|
|
624
627
|
```
|
|
@@ -308,7 +308,9 @@ rules: # 自上而下,**首个命中生
|
|
|
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
|
|
|
@@ -547,8 +549,9 @@ src/proxycore/ 核心包
|
|
|
547
549
|
├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
|
|
548
550
|
└── service/ ipc.py Windows 服务 + 命名管道
|
|
549
551
|
samples/ mock 服务 + 示例配置
|
|
552
|
+
examples/ 纯 Python 示例(proxy_process.py / verify_sdk.py)
|
|
550
553
|
docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
|
|
551
|
-
tests/ 测试套件(
|
|
554
|
+
tests/ 测试套件(625 passed / 4 skipped)
|
|
552
555
|
└── tools/ 验证脚本(run_demo / run_e2e / bench / …)
|
|
553
556
|
```
|
|
554
557
|
|
|
@@ -579,7 +582,7 @@ tests/ 测试套件(624 passed / 4 skipped)
|
|
|
579
582
|
|
|
580
583
|
```bash
|
|
581
584
|
pip install -e ".[dev]"
|
|
582
|
-
python -m pytest -q tests/ #
|
|
585
|
+
python -m pytest -q tests/ # 625 passed / 4 skipped(非管理员 Windows)
|
|
583
586
|
python -m ruff check src tests
|
|
584
587
|
python -m mypy src/proxycore
|
|
585
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 解析、日志格式化(用惰性参数)、文件读取。
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# 11 · 调试排错手册
|
|
2
|
+
|
|
3
|
+
> 用法:按"症状"查表 → 按"定位"执行命令 → 按"解决"修复。
|
|
4
|
+
> 本手册面向 **WinDivert(用户态)后端**。`backend=null` 只做联调,不抓包、不改道。
|
|
5
|
+
|
|
6
|
+
## 1. 快速诊断命令(先跑这些)
|
|
7
|
+
|
|
8
|
+
```powershell
|
|
9
|
+
python procproxy.py doctor
|
|
10
|
+
sc query WinDivert # WinDivert 内核驱动是否在跑
|
|
11
|
+
pktmon start --etw -c --comp nics -f %TEMP%\net.etl # 抓包(Win10 1809+)
|
|
12
|
+
Get-NetTCPConnection -State SynSent | Select-Object -First 20
|
|
13
|
+
tasklist /FI "PID eq <pid>"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 2. 症状 → 定位 → 解决
|
|
17
|
+
|
|
18
|
+
### 2.1 目标程序连接卡死 / 一直超时(最常见)
|
|
19
|
+
|
|
20
|
+
| 可能原因 | 定位 | 解决 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| **改道成功但回注失败** | 日志里每条 `redirect ... rule=...` 后面紧跟 `ERROR windivert_packet_failed`(或 `redirect_inject_failed`)。特征:**改道日志有、上游 0 条 CONNECT、`sessions.jsonl` 没有 `session_end`** | 已修:`_send_rewritten()` 不能把 `bytes` 赋给 `packet.raw` —— pydivert 3.x 的 `Packet.raw` 必须是 `memoryview`(内部走 `self.raw.obj`)。正确做法是 `pydivert.Packet(raw, wd_addr=packet.wd_addr)` 建新包再 `send()`。回归测试见 `tests/unit/test_windivert.py` |
|
|
23
|
+
| **改写报文没重算校验和** | Wireshark 抓 `127.0.0.1:19080`,看 SYN 的 IP/TCP checksum 是否 `unverified` | 调 `packet.recalculate_checksums()` |
|
|
24
|
+
| **不是管理员** | 启动日志出现 `[E_BACKEND_UNAVAILABLE] 打开 WinDivert 失败:[WinError 5] 拒绝访问` | 用**管理员**终端重开。这是最常见的"根本没开始工作" |
|
|
25
|
+
| sink 没在监听 / 端口被占用 | `netstat -ano \| findstr :19080` | 换端口(配置里改 `service.sink.port`) |
|
|
26
|
+
| 原始目标没查到 → 连接被我们关闭 | 日志搜 `E_ORIGINAL_DST_UNKNOWN` | 检查会话表写入时机(必须**先记账再发包**);检查 TIME_WAIT 撞车 |
|
|
27
|
+
| WinDivert 过滤器把改写后的包又抓了一遍 | 日志里同一连接出现两次 REDIRECT | 过滤器已排除 sink 端口与回环;若仍出现,检查 `upstream_ips_from_config` 是否正确注入了上游 IP |
|
|
28
|
+
| 目标进程走了 IPv6 而 v6 句柄没起来 | 日志搜 `sink_ipv6_unavailable`;或 `Get-NetTCPConnection` 看对端是不是 `::` 地址 | v6 走**第二个句柄 + `ipv6.*` 过滤器**。若 v6 sink 绑定失败会打 ERROR(不静默降级);`[::1]:<port>` 被占用是常见原因 |
|
|
29
|
+
| 上游代理握手卡住 | 手动 `curl -x socks5://<up> ...` 测试 | 上游问题;核对认证方式 |
|
|
30
|
+
|
|
31
|
+
### 2.2 全部流量都不通了(危险,优先恢复)
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
python procproxy.py stop --panic # 立刻停掉数据面(恢复直连)
|
|
35
|
+
sc stop WinDivert # 极端情况:直接卸掉 WinDivert 内核驱动
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| 原因 | 定位 | 解决 |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| 我们的上游流量被自己代理(环路) | 日志出现大量 `upstream-endpoint` 缺失 + 连接暴涨 | 检查守卫 G3(上游端点集合);确认过滤器的排除列表里有上游 IP;熔断器会在 5s/50 次时兜底 |
|
|
41
|
+
| 服务崩了但连接仍被拦 | 进程已退出,网络仍不通 | 正常不会发生:WinDivert 句柄随进程关闭自动释放。若真卡住,`sc stop WinDivert` |
|
|
42
|
+
| 目标程序连不上,但 `direct` 的进程正常 | `sessions.jsonl` 里有 `state=failed` + `error=E_UPSTREAM_DOWN` | 上游不可达/认证失败。**设计如此:改道失败不会回落到直连**(避免真实 IP 泄漏)——要么修上游,要么把规则改成 `direct` |
|
|
43
|
+
|
|
44
|
+
### 2.3 只有部分进程被代理
|
|
45
|
+
|
|
46
|
+
| 原因 | 定位 | 解决 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 进程路径为空(受保护进程)导致路径规则不匹配 | 日志 `proc_path=""` | 规则里用**名称**而非路径 |
|
|
49
|
+
| 规则顺序问题(通用规则在前) | `cli rules --trace` 打印命中链路 | 把例外规则上移(首个命中生效) |
|
|
50
|
+
| `unknown_pid` 偏高 | `cli status` 看 `unknown_pid` 比例 | 本后端靠 `GetExtendedTcpTable` 查 PID,SYN 刚发出时可能还查不到(竞态)。比例 > 0.5% 时把 `service.on_unknown_pid` 从 `direct` 改成 `block`(宁可这条失败也不漏真实 IP) |
|
|
51
|
+
| 私网规则把目标"吃掉"了 | `cli rules --trace` 看 G1 是否为 `true` | 回环/私网**默认直连**;要代理私网必须给规则加 `keep_private_proxied: true` |
|
|
52
|
+
| 进程名写错(少了 `.exe`) | `procproxy <进程名> --check-only` 会打印形似的进程名 | 写完整 exe 名,或用 `*` 通配 |
|
|
53
|
+
| 被 EDR / 杀软拦在更早的层 | Wireshark 里看不到该进程的任何外发连接 | 换一台机器验证;或在 EDR 里放行 WinDivert |
|
|
54
|
+
|
|
55
|
+
### 2.4 UDP / QUIC 异常
|
|
56
|
+
|
|
57
|
+
> **前提**:WinDivert 后端**完全不捕获 UDP**(两个句柄的过滤器都以 `tcp` 开头)。
|
|
58
|
+
> 所以 `udp_policy` 在本后端**整体空转** —— 既不做 proxy,也不做 block。
|
|
59
|
+
|
|
60
|
+
| 症状 | 原因 | 解决 |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| 想让 QUIC 回落 TCP,但 `by_port: {443: block}` 不生效 | 本后端看不到 UDP 报文 | 这是**能力缺口,不是配置错**。要么在应用侧禁用 QUIC,要么换一个能捕获 UDP 的内核方案 |
|
|
63
|
+
| 视频会议/游戏正常,但你以为它走了代理 | `udp_policy.default` 配了 `proxy` | 实际直连出去了(会泄漏真实 IP)。`cli run` 启动时会打 `udp_unsupported_backend` 明确告知 |
|
|
64
|
+
| DNS 查询被代理后解析失败 | `dns_local_transport=proxy` 尚未实现(校验期直接拒绝) | 改 `direct`,并把 DNS 服务器加入守卫白名单 |
|
|
65
|
+
|
|
66
|
+
### 2.5 WinDivert 相关
|
|
67
|
+
|
|
68
|
+
| 症状 | 原因 | 解决 |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `python procproxy.py run` 报 `[WinError 5] 拒绝访问` | 不是管理员 | 用管理员终端(右键 → 以管理员身份运行) |
|
|
71
|
+
| `[WinError 2] 系统找不到指定的文件` / 打不开 | `WinDivert64.sys` 缺失或未随 pydivert 安装 | 重装 `pip install --force-reinstall pydivert`;确认 `site-packages\pydivert\windivert_dll\WinDivert64.sys` 存在 |
|
|
72
|
+
| 打开失败但确实是管理员 | EDR/杀软拦截了 WinDivert,或已有别的 WinDivert 实例 | `sc query WinDivert` 看状态;关掉占用它的程序;在 EDR 里放行 |
|
|
73
|
+
| 换新版本的 `.sys` 时提示文件被占用 | 已加载的内核驱动会锁住自己的映像 | `sc stop WinDivert` 后重试;不行就重启 |
|
|
74
|
+
|
|
75
|
+
### 2.6 服务与会话
|
|
76
|
+
|
|
77
|
+
| 症状 | 解决 |
|
|
78
|
+
|---|---|
|
|
79
|
+
| 服务启动成功但代理不生效 | 服务跑在 Session 0;检查配置里的 Profile 是否覆盖了目标进程所在的会话 |
|
|
80
|
+
| GUI / CLI 连不上服务 | 命名管道 ACL 只允许 SYSTEM + Administrators;确认调用方身份 |
|
|
81
|
+
| 重启后规则丢失 | 配置路径错误(应为 `%ProgramData%\ProcProxy\`);确认服务读的是 `ProgramData` 而非用户目录 |
|
|
82
|
+
| `cli status` 读不到状态 | `state.json` 在 `%ProgramData%\ProcProxy\state.json`(可用 `PROCPROXY_RUNTIME_DIR` 覆盖) |
|
|
83
|
+
|
|
84
|
+
### 2.7 真后端"一条会话都没有",但目标程序却连上了(本机地址陷阱)
|
|
85
|
+
|
|
86
|
+
| 症状 | 定位与解决 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `curl` 成功拿到响应、日志里 0 条 `session_end`、上游 0 条 CONNECT | 目标地址是**本机自己的地址**(回环或本机网卡 IP)。Windows 对"发给自己"的连接走回环捷径,包不经过 IP 层外发路径,WinDivert 看不到;同时回环/自身流量按设计被守卫排除。**这不是改道失败**。 |
|
|
89
|
+
| 怎么确认 | `python tests/tools/real_machine_check.py` 会提前识别并拒绝:它把 `--target-host` 与本机地址集合比对,命中就直接退出并打印替代方案。 |
|
|
90
|
+
| 正确做法 | ① 用远端目标验证:`python tests/tools/real_machine_check.py --target-host www.baidu.com --target-port 80 --no-hosts`;② 要看**自己写的程序**的流量:把上游指向抓包工具(见 `docs/14`);③ 目标服务本身就在本机 → 网络层拦截拿不到,改为让程序直接走代理(`HTTP_PROXY` / `--proxy`)。 |
|
|
91
|
+
|
|
92
|
+
### 2.8 配置读取失败(`config_not_found` / `config_unreadable` / `config_parse_invalid`)
|
|
93
|
+
|
|
94
|
+
| 症状 | 定位与解决 |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `config_not_found`:`配置文件不存在: ...` | 显式路径或 `PROCPROXY_CONFIG` 不会回退。检查路径拼写、`~`/环境变量展开、当前工作目录;安装后没有用户配置时,CLI 会自动使用内置 `default.yaml`,API 也一样。 |
|
|
97
|
+
| `config_unreadable` | 文件存在但读不了:权限不足、路径是目录、被其他进程独占。检查 ACL 与路径类型。 |
|
|
98
|
+
| `config_parse_invalid`:`YAML 解析失败` / `根节点必须是映射` / `不是有效的 UTF-8` | 用 `python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1], encoding='utf-8'))" <file>` 定位语法错误;Windows 记事本另存的文件请确认编码为 UTF-8。 |
|
|
99
|
+
| 分不清读的是哪份配置 | CLI:`procproxy doctor` 的 `config` 行会显示路径与 `source=`;API:打印 `handle.config_path` / `handle.config_source`。来源优先级:显式 → `PROCPROXY_CONFIG` → `~/.procproxy/config.yaml` → 仓库样例/内置。 |
|
|
100
|
+
|
|
101
|
+
> `version != 1` 仍然报 `config_version_unsupported`,与读取错误分开。
|
|
102
|
+
|
|
103
|
+
### 2.9 升级 pydivert 4.0.0 后,改道成功但连接卡在 SYN_SENT
|
|
104
|
+
|
|
105
|
+
| 项 | 内容 |
|
|
106
|
+
|---|---|
|
|
107
|
+
| 症状 | 日志有 `redirect`,但应用一直 SYN_SENT、curl 超时、sink/Yakit 收不到包;上游 0 条 CONNECT |
|
|
108
|
+
| 根因 | pydivert 4.0.0 的 `send()` 默认调用 `recalculate_checksums()`,后者在 Windows 上先执行 `_populate_wd_addr()`,按**构造期缓存**重建 `WINDIVERT_ADDRESS`。旧实现先构造 `Packet` 再直接改 `wd_addr.Loopback/IfIdx`,重建时被清零 → 回环 SYN 被内核丢弃。3.1.3 不做这次重建,所以 3.x 正常 |
|
|
109
|
+
| 修复 | 0.1.4.1 起:`Loopback` / `IfIdx` / `direction` 通过 `Packet(...)` 构造参数传入,并且 `send(..., recalculate_checksum=False)`(校验和已由 `pkt.py` 算好);同时兼容 3.x / 4.x |
|
|
110
|
+
| 快速确认 | `python -c "import pydivert; print(pydivert.__version__)"`;4.x 且有上述症状就升级 proxycore |
|
|
111
|
+
| 临时绕过 | 不要长期 monkey patch;可在验证阶段把 pydivert 固定到 `3.1.3`,或直接使用含修复的 proxycore |
|
|
112
|
+
|
|
113
|
+
## 3. 抓包与验证技巧
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
# 只抓被改道的连接
|
|
117
|
+
pktmon filter add -p 19080 -t tcp
|
|
118
|
+
pktmon start --etw ; pktmon stop ; pktmon etl2pcap %TEMP%\net.etl -o %TEMP%\net.pcap
|
|
119
|
+
|
|
120
|
+
# 验证"应用侧看到的对端没变"(这是重定向方案的关键性质)
|
|
121
|
+
python -m proxycore check-route --proc chrome.exe --expect getpeername-original
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 4. 日志级别与埋点约定
|
|
125
|
+
|
|
126
|
+
| 级别 | 内容 | 何时开 |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `ERROR` | 会话失败、后端不可用、熔断 | 常开 |
|
|
129
|
+
| `INFO` | 每次会话结束一行 JSONL | 常开 |
|
|
130
|
+
| `DEBUG` | 每条连接的决策链路(命中规则 id、守卫原因) | 排错 |
|
|
131
|
+
| `TRACE` | 报文级(仅长度/方向/五元组,**不含 payload**) | 深度排错,默认关闭 |
|
|
132
|
+
|
|
133
|
+
必须有的计数器(`cli status` 输出,用于判断"是否在正常工作"):
|
|
134
|
+
`captured / redirected / direct / blocked / failed / unknown_pid / active_sessions / loop_breaker_trips`。
|
|
135
|
+
|
|
136
|
+
## 5. 两条"最后手段"
|
|
137
|
+
|
|
138
|
+
1. `python procproxy.py stop --panic` —— 立刻停掉数据面,恢复直连。
|
|
139
|
+
2. `sc stop WinDivert` —— 直接从内核层卸掉 WinDivert。句柄随进程退出也会自动释放,这一步只在极端情况用。
|