Metadata-Version: 2.4
Name: procproxy
Version: 0.1.4.2
Summary: ProcProxy —— 进程级代理：按进程决定哪条连接走哪个上游代理
Author: Longersking
License-Expression: MIT
Project-URL: Homepage, https://github.com/Longersking/procproxy
Project-URL: Repository, https://github.com/Longersking/procproxy
Project-URL: Issues, https://github.com/Longersking/procproxy/issues
Keywords: proxy,transparent-proxy,process,windivert,windows,socks5
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Win32 (MS Windows)
Classifier: Intended Audience :: Developers
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: Proxy Servers
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Provides-Extra: windivert
Requires-Dist: pydivert>=3.0; extra == "windivert"
Requires-Dist: psutil>=5.9; extra == "windivert"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Provides-Extra: release
Requires-Dist: build>=1.2; extra == "release"
Requires-Dist: twine>=5; extra == "release"
Dynamic: license-file

# ProcProxy

**按「哪个进程」决定「哪条连接走哪个上游代理」** —— 对目标程序零侵入：
不注入 DLL、不改目标进程内存、不解密流量。

Windows · 基于 [WinDivert](https://github.com/basil00/WinDivert)（用户态 + 已签名的内核驱动）。

---

## 只有一个入口：`procproxy.py`

```bash
python procproxy.py          # ← 不知道从哪下手就跑这个：报环境状态 + 最常用的命令
```

它认识三种输入：

```bash
python procproxy.py                                  # 无参数：环境状态 + 命令清单
python procproxy.py WeChat.exe --port 8848           # 进程名 → 把它的流量转发出去（主要用法）
python procproxy.py doctor | rules | sessions | ...  # 子命令（等价于 python -m proxycore <子命令>）
```

其余一切都在这三种之下，不需要你去找"该运行哪个文件"。

---

## 安装

需要 **Python 3.10+**。

### 从 PyPI 安装（推荐）

```bash
pip install procproxy
procproxy init          # 生成 ~/.procproxy/config.yaml
procproxy doctor        # 环境自检
procproxy forward --process WeChat.exe --port 8848   # 真实转发（需管理员）
```

安装后直接运行 `procproxy` 会显示首次使用指引，不依赖任何样例文件。
Python SDK 也一样：`start_proxy()` 不传配置即可启动（内置 `backend=null`），
不需要先跑 `init`；`init` 只用于生成可编辑配置。

非管理员：WinDivert 抓包必须管理员；可以用 `--backend null` 做转发链路联调，
或在服务已启动、且配置了 `service.ipc_allow_group` 时读取状态。

### 源码开发安装

```bash
pip install -e ".[dev]"
# 或者不装（零依赖方式，往 site-packages 写一个 .pth，让 python -m proxycore 全局可用）
python procproxy.py install-path
```

数据面依赖（要用真抓包就必需）：

```bash
pip install "procproxy[windivert]"
```

> `pydivert` 自带**已签名**的 `WinDivert64.sys` —— 所以**不需要**开测试签名、不需要自己签驱动。
> 但**运行时需要管理员权限**（内核驱动只能由管理员加载）。

先自检一下：

```bash
python procproxy.py
```

```
ProcProxy —— 按进程选路的本机代理
====================================================================

  配置文件   D:\tools\proxypy-public\samples\profiles\dev.yaml
            OK（3 条规则 / 1 个上游）
  数据面依赖 已安装（pydivert）
  管理员权限 否 —— 真抓包改道需要管理员终端
...
```

---

## 让非管理员也能用（可选）

**默认不需要改**：不开这个功能时，IPC 命名管道只允许管理员 + SYSTEM，与以前完全一致。

**为什么需要它**：真拦截要开 WinDivert 设备，而那个设备只允许管理员打开
（实测：驱动已经是 `RUNNING`，非管理员仍然 `WinError 5` —— 卡的是设备的安全描述符，
不是"没装驱动"）。所以**服务必须由管理员启动**，但"谁来用它"是可以放开的：

```
管理员执行一次                    普通用户之后可以
─────────────────────────────    ────────────────────────────────
1. 建一个组（例如 ProcProxy Users）
2. 配置里写 service.ipc_allow_group
3. 装并以管理员启动服务      ───▶  python procproxy.py status / sessions / rules
```

配置：

```yaml
service:
  ipc_allow_group: "ProcProxy Users"   # 空 = 只有管理员（默认）
```

然后在**管理员终端**：

```powershell
# 建组（一次性）
net localgroup "ProcProxy Users" /add
net localgroup "ProcProxy Users" alice /add

# 装服务（会先校验组名能不能解析，写错了当场报）
python -m proxycore.service install --config samples\profiles\dev.yaml
```

之后 `alice`（**不需要管理员**）就能：

```bash
python procproxy.py status
python procproxy.py sessions --follow
python procproxy.py rules
```

### 权限边界（重要）

**被放行的组只能读，不能改**。这条不是靠 ACL，而是服务侧**按命令**判的：

| 命令 | 放行组 | 为什么 |
|---|---|---|
| `status` / `sessions` / `rules` | ✅ 可读 | 只是看 |
| `reload`（换规则） | ❌ **需管理员令牌** | 能把**别人的**流量导到另一个上游 |
| `stop` / `panic` | ❌ **需管理员令牌** | 能让共享的服务停摆 |

原因：管道 ACL 只能**整体**授权 —— 要让人能发命令，就必须同时给他写权限。
所以"能不能改规则"表达不到 ACL 上，只能由服务侧再判一次（见 `ipc.ADMIN_ONLY_COMMANDS`）。
被放行的组拿到的也只是 `GRGW`（读+写），**不是 `GA`** —— `GA` 含 `WRITE_DAC`，
对方能反过来改管道自己的 ACL。

**仍然需要管理员的**：建组、装服务、启动服务。这三件事永远要有特权。

> ⚠️ **本机无法自测的部分**：跨用户"低权用户能连上管道"这条，需要一台装了服务 +
> 有第二个用户账户的机器才能验。本仓库只对**可离线验证的部分**做了断言
> （SDDL 拼装、按命令的权限判定、组名解析），其余如实标注、不伪装通过。

---

## 快速开始

### 1) 先看它真的能跑（不需要管理员）

```bash
python tests/tools/run_demo.py     # 15 组自检案例
python tests/tools/run_e2e.py      # 只跑端到端那条链路，约 2 秒
```

`run_e2e.py` 会起 mock 上游 + mock 目标 + ProcProxy（`backend=null`），断言
"sink → 上游代理 → 目标"这条链路。**注意 `backend=null` 不抓包、不改道**。

### 2) 真正改道一个进程（需要管理员）

```bash
python procproxy.py curl.exe --port 7890
```

它会自动：① 探测那个端口是不是"代理监听"；② 生成配置；③ 启动数据面。
加 `--check-only` 则只自检 + 生成配置 —— **不启动、不改任何流量、不需要管理员**：

```bash
python procproxy.py curl.exe --port 7890 --check-only
```

另一个窗口验证（`--noproxy '*'` **必须加**，否则 curl 会走系统 `http_proxy`，测的就不是它了）：

```bash
curl --noproxy '*' http://example.com/ -v
python procproxy.py sessions --follow
```

---

## 转发单个进程的流量到抓包工具（Yakit / Burp / Charles）

这是最常用的场景：**看某个程序到底发了什么**。

```bash
# 第 1 步：自检（不动任何流量、不需要管理员）
python procproxy.py Weixin.exe --port 8848 --check-only

# 第 2 步：真跑（管理员终端）
python procproxy.py Weixin.exe --port 8848

# 第 3 步：操作那个程序（刷朋友圈、点开文章），然后 Ctrl+C
```

一次给多个进程名（也支持 `'a.exe;b.exe'` 或 `*` 通配符）：

```bash
python procproxy.py Weixin.exe WeChatAppEx.exe --port 8848
```

**怎么算成功（三层证据）**：

| # | 看哪里 | 成功的样子 |
|---|---|---|
| ① | ProcProxy 终端 | `session_end ... proc=Weixin.exe rule=r-watch upstream=capture` |
| ② | Yakit 的「MITM 交互式劫持」历史 | 出现对应的 `CONNECT 目标:端口` |
| ③ | 状态行 | `走代理 N 条` 随你的操作增长 |

⚠️ 只有 ① 没有 ② → 端口填错了（**要填抓包工具的 MITM 监听口，不是它的 Web 管理口**）。
① ② 都正常但内容看不懂 → 那是 TLS，不是转发的问题。

完整指南（含排错与参数表）见 [`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)。

---

## 子命令

```bash
python procproxy.py doctor                            # 环境自检
python procproxy.py rules                             # 打印规则表
python procproxy.py rules --trace --proc curl.exe --dst 1.1.1.1 --port 443
python procproxy.py config --json                     # 打印生效配置
python procproxy.py status                            # 运行时状态
python procproxy.py sessions --follow                 # 跟会话
python procproxy.py run                               # 前台跑数据面（需管理员）
python procproxy.py stop [--panic]                    # 停

# 下面这几个自带完整参数，用 --help 看：
python procproxy.py forward --help                    # 转发进程（上面的 <进程名> 就是它的简写）
python procproxy.py can-proxy --help                  # 一句话：现在能不能转发
python procproxy.py check-route --help                # 核对某进程走了哪条路
python procproxy.py probe-upstream --help             # 探测某端口是不是可用的上游代理
python procproxy.py install-path --help               # 让 python -m proxycore 全局可用
```

`--trace` 有个坑值得知道：`--pid` 默认是 `cli` 自己的 PID，所以 `G2 self-pid` 几乎永远是 `true`。
想看真实判定，显式传 `--pid <目标进程真实PID>`。

也可以完全不用 `procproxy.py`，直接 `python -m proxycore <同样的子命令>`。

### 当 Windows 服务

```powershell
python -m proxycore.service install --config samples\profiles\dev.yaml   # 管理员
sc query procproxysvc
```

---

## 配置文件

```yaml
version: 1

service:
  sink: { host: 127.0.0.1, port: 19080 }   # 本地落地端口
  backend: windivert                        # windivert | null
  on_unknown_pid: direct                    # 查不到 PID 时：direct | proxy | block
  on_decide_error: block                    # 决策抛异常时：默认 block（不放行）
  loop_breaker: { window_s: 5, threshold: 50 }

profile:
  name: dev
  default_action: direct                    # 未命中任何规则时
  sticky_per_pid: true                      # 同 PID 固定用同一个上游
  dns_mode: remote                          # remote | local | bypass

upstreams:
  - alias: local-mock
    kind: socks5                            # socks5 | socks4 | http | https
    host: 127.0.0.1
    port: 1080
    # 口令不落明文：password_ref: "dpapi:alias" / "env:VAR" / "file:path"
  # 多跳：chain: [hop-b, hop-c]

rules:                                      # 自上而下，**首个命中生效**
  - id: r1-private
    action: direct
    targets: "*"
    dst_ip: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8"
  - id: r2-curl
    action: proxy
    upstream: local-mock
    targets: "curl.exe;curl"
  - id: r3-default
    action: direct
    targets: "*"
```

**规则匹配顺序**（顺序不可改，改了就变语义）：

```
1. 未知 PID     → 按 on_unknown_pid 兜底
2. 守卫 G1      → 回环/私网/链路本地 → DIRECT
3. 守卫 G2      → 自身进程 → DIRECT
4. 守卫 G3      → 上游 / 本地 DNS 端点 → DIRECT
5. 守卫 G4      → 同 (pid,dst) 熔断 → DIRECT + 告警
6. 规则表       → 自上而下，首个命中生效
7. 都没命中     → profile.default_action
```

完整语义见 [`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)。

---

## 编程接口

`proxycore.api` 是与 CLI 共用同一套配置、规则引擎和会话表的 SDK。
**以下全部只用 `import proxycore`，不需要运行 `procproxy` 命令。**
完整教程见 [`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)，
可执行契约见 [`examples/verify_sdk.py`](https://github.com/Longersking/procproxy/blob/master/examples/verify_sdk.py)。

### 30 秒：零配置启动

```python
from proxycore.api import start_proxy

with start_proxy() as ppx:          # 不需要任何配置文件
    print(ppx.config_source)        # "packaged"
    print(ppx.config_path)          # .../proxycore/data/default.yaml
    print(ppx.status())             # 后端 / 规则 / 会话 / UDP / 配置来源
```

不传配置时的来源优先级：

| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | `start_proxy(path)` | 显式路径（也接受内存中的 `Config` 对象） |
| 2 | `PROCPROXY_CONFIG` | 指向不存在文件会直接报错，不回退 |
| 3 | `~/.procproxy/config.yaml` | `procproxy init` 生成的用户配置 |
| 4 | 包内 `data/default.yaml` | `backend=null`，不抓包、不改道 |

内置配置**不会**监听 sink，也**不会**创建任何文件。它适合装配、规则决策、
状态查询和联调；要真实抓包请用 `backend="windivert"`（需管理员）或
`procproxy init` 生成自己的配置。

### 初始化：生成一份可编辑配置

```python
from proxycore.api import init_config

path = init_config(
    "D:/cfg/procproxy.yaml",   # 省略则用 PROCPROXY_CONFIG 或 ~/.procproxy/config.yaml
    backend="windivert",       # 真抓包需管理员；联调用 "null"
    sink_port=19080,
)
print(path)                    # 绝对路径；等价于 procproxy init
```

生成的是最小模板，`upstreams` / `rules` 仍为空，需要按下面的案例补上。

### 代理一个进程（最小完整配置）

```yaml
version: 1
service:
  sink: { host: 127.0.0.1, port: 19080 }
  backend: windivert            # 真抓包；联调改 null
profile:
  name: chrome-via-socks
  default_action: direct
  dns_mode: remote
upstreams:
  - { alias: socks, kind: socks5, host: 127.0.0.1, port: 1080, auth: none }
rules:
  - { id: r-private, action: direct, targets: "*",
      dst_ip: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8" }
  - { id: r-chrome, action: proxy, upstream: socks,
      targets: "chrome.exe", dst_port: "*" }
```

```python
from proxycore.api import start_proxy

with start_proxy("D:/cfg/chrome.yaml") as ppx:   # 管理员终端
    print(ppx.status())
    print(ppx.sessions(limit=20))
```

规则自上而下、首个命中生效；启动后只影响**新建立**的连接。
完整步骤、验证方法与常见坑见 [`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)。

### 显式配置与测试转发

```python
from proxycore.api import start_proxy

# 显式路径 + 真实抓包（需要管理员）
with start_proxy(r"D:\cfg\procproxy.yaml", backend="windivert") as ppx:
    print(ppx.sink)                 # sink 监听地址
    print(ppx.sessions(limit=20))

# 联调模式：backend=null + 把 sink 收到的连接固定转发到目标
with start_proxy(backend="null", sink_test_target="127.0.0.1:8080",
                 upstream="local-mock") as ppx:
    ...
    # curl -x http://127.0.0.1:19080 http://any/ → 127.0.0.1:8080
```

### 在代码里构建配置

可以从字典构造 `Config`，也可以先落盘再按路径加载：

```python
from proxycore.config import Config, atomic_write
from proxycore.api import start_proxy

doc = {
    "version": 1,
    "service": {"sink": {"host": "127.0.0.1", "port": 19080}, "backend": "null"},
    "profile": {
        "name": "myapp",
        "default_action": "direct",
        "dns_mode": "remote",
        "rules": [
            {"id": "r1", "action": "proxy", "upstream": "office",
             "targets": "chrome.exe;msedge.exe", "dst_port": "443,80"},
        ],
    },
    "upstreams": [
        {"alias": "office", "kind": "socks5", "host": "127.0.0.1", "port": 1080},
    ],
}

cfg = Config.from_dict(doc)
cfg.validate()                       # from_dict 只解析，不校验
atomic_write(cfg, "myconfig.yaml")   # 临时文件 → 校验 → 原子替换

with start_proxy("myconfig.yaml") as ppx:
    ...
```

> `Config.from_dict()` 不会解析 `password_ref`；需要凭据时请用
> `Config.load()` / `start_proxy(path)`，或自己给 `upstream.password` 赋值。

### 只做决策，不启动数据面

```python
from proxycore.api import make_runtime

runtime = make_runtime()             # 默认用内置配置，backend=null
decision = runtime.decide(conn_request)
print(decision.action, decision.reason)
```

### 生命周期与句柄

```python
ppx = start_proxy()
try:
    print(ppx.status())
finally:
    ppx.stop()                       # 幂等；with 退出时也会自动调用
```

| 成员 | 说明 |
|---|---|
| `status()` | 后端、规则、会话、熔断、DNS、UDP、sink、`config_path`、`config_source` |
| `sessions(limit=50)` / `active_sessions()` | 会话明细 / 活跃数 |
| `reload(path="")` | 热重载；不传路径则重读当前文件。`sink.port`/`backend` 变更需重启 |
| `decide(conn)` | 只问规则引擎，不抓包、不改道 |
| `profile` | 当前生效的 `Profile` |
| `config_path` / `config_source` | 当前配置的绝对路径与来源 |
| `running` | 是否仍在运行 |

`start_proxy()` 会启动后台维护线程；`run_forever()` 会阻塞到 Ctrl+C。
请在主线程使用，并在退出时 `stop()`（或使用 `with`）。

### 后台常驻（父进程可退出）

```python
from proxycore.api import get_status, start_proxy, stop_proxy

bg = start_proxy("D:/cfg/chrome.yaml", background=True)
print(bg.pid, bg.config_path, bg.runtime_dir)
# 父进程可以退出；独立子进程继续运行

print(get_status(bg.runtime_dir))   # 跨进程读 state.json
print(stop_proxy(bg.runtime_dir))   # 写 control.json 并等待退出
```

- 状态目录默认 `~/.procproxy-app`（可用 `runtime_dir=` 或 `PROCPROXY_RUNTIME_DIR` 覆盖）
- 文件协议：`state.json`（数据面写）、`control.json`（应用写 `stop`/`panic`）
- `start_proxy(background=True, replace=True)` 可替换同目录下已在跑的数据面
- 子进程继承父进程权限；`backend="windivert"` 仍需管理员启动

### 错误处理

配置 / 后端 / 启动失败统一抛 `ProxyStartError`，`code` 是稳定的机器可读标识：

```python
from proxycore.api import ProxyStartError, start_proxy

try:
    start_proxy("missing.yaml")
except ProxyStartError as exc:
    print(exc.code)                  # config_not_found
```

| code | 含义 |
|---|---|
| `config_not_found` | 配置文件不存在（显式路径 / 环境变量不会回退） |
| `config_unreadable` | 文件存在但读不了（权限、目录等） |
| `config_parse_invalid` | YAML 语法错误 / 根节点非映射 / 非 UTF-8 |
| `config_version_unsupported` | `version != 1` |
| `rule_upstream_missing` 等 | 规则或上游校验失败（完整列表见文档） |
| `E_BACKEND_UNAVAILABLE` | 后端不可用（未知后端、WinDivert 不可加载等） |
| `already_running` | 后台模式同一 runtime_dir 已有存活数据面 |
| `background_start_failed` / `background_start_timeout` | 后台子进程启动失败 / 超时 |
| `runtime_dir_unavailable` | 后台状态目录无法创建或写入 |

`api.load_config()` 同样抛 `ProxyStartError`，不是 `ConfigError`。

### 环境变量

| 变量 | 作用 |
|---|---|
| `PROCPROXY_CONFIG` | 配置文件路径；显式路径之后、用户配置之前生效 |
| `PROCPROXY_RUNTIME_DIR` | 运行时状态/日志目录（CLI 与服务使用） |

跑 `python tests/tools/api_demo.py` 看完整演示。

---

## 工作原理（一页）

```
应用 connect()
   │
   ▼
① Backend（WinDivert）把出向 TCP 包捞进用户态，并查表得出 PID
   │  ConnRequest{proc, proto, src, dst, backend_id=客户端源端口}
   ▼
② rules.decide()（纯函数）→ DIRECT / BLOCK / PROXY
   │  Decision{action, rule_id, upstream, reason}
   ▼
③ 先记账（SessionTable），再改道 —— 顺序不能反
   │  改写目的地址到 127.0.0.1:<sink>，并标记为回环、按出向回注
   ▼
④ Sink accept → 用「客户端源端口」反查回原始目标 → 选上游 → dial → relay
   │
   ▼
⑤ UpstreamDialer（SOCKS5 / SOCKS4 / HTTP CONNECT / 跳链）
```

反向路径同样要改：sink 的回应改写回"来自原始目标"，否则三次握手完不成。
这两个方向 + 注入方式（源也要改成回环、按出向注入）是这条路线最容易踩的坑。

**分层**：`backend` 只管"捞进来 + 改道"，`rules` 只管"该不该走代理"（纯函数、可单测），
`sink/relay` 只管"搬字节"，`upstream` 只管"怎么跟代理服务器说话"。

---

## 目录

```
procproxy.py            唯一入口（薄壳：挂 sys.path + 转发给 cli）
AGENTS.md               接口文档（给 AI / 程序化调用：SDK 用法 + 硬约束 + 禁区）
src/proxycore/          核心包
  ├── rules.py          规则引擎（纯函数）
  ├── runtime.py        装配：后端 + 规则 + 会话表 + sink
  ├── backend/windivert 路线 A：WinDivert 抓包 / 改写 / 回注
  ├── tunnel/           sink + relay（本地落地与双向转发）
  ├── upstream/         SOCKS5 / SOCKS4 / HTTP CONNECT / 跳链
  ├── dns/              DNS 三模式与 UDP
  ├── config_source.py  配置来源解析（显式 / env / 用户 / 样例 / 内置）
  ├── forward.py        ← procproxy.py <进程名> 的实现
  ├── canproxy.py / checkroute.py / probe.py / pathsetup.py  ← 其余子命令
  └── service/ ipc.py   Windows 服务 + 命名管道
samples/                mock 服务 + 示例配置
examples/               纯 Python 示例（proxy_process.py / verify_sdk.py）
docs/                   文档（00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK）
tests/                  测试套件（633 passed / 4 skipped）
  └── tools/            验证脚本（run_demo / run_e2e / bench / …）
```

---

## 边界与已知限制

| 限制 | 说明 |
|---|---|
| **不捕获 UDP** | 过滤器以 `tcp` 开头。`udp_policy`（含 `443: block` 拦 QUIC）在本后端**整体空转** |
| **`dst_name` 规则永不命中** | 抓包式后端只拿得到 IP，拿不到域名 |
| **不解密** | HTTPS 是密文；要明文请转发给抓包工具 |
| **需要管理员** | WinDivert 内核驱动只能由管理员加载 |
| **上游端点必须写 IP 字面量** | 环路守卫按 `(host, port)` **字面量**比对；写 `localhost` 护栏就形同虚设 |
| **`unknown_pid`** | 本后端靠 `GetExtendedTcpTable` 查 PID，SYN 刚发出时可能查不到（竞态）。比例偏高时把 `on_unknown_pid` 改成 `block` |

---

## 排错

见 [`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)（症状 → 定位 → 解决）。

最常见的一条：**`[WinError 5] 拒绝访问` = 不是管理员**。

---

## 开发

```bash
pip install -e ".[dev]"
python -m pytest -q tests/                     # 633 passed / 4 skipped（非管理员 Windows）
python -m ruff check src tests
python -m mypy src/proxycore
```

验证脚本住在 `tests/tools/`（它们设计成独立可跑）：

```bash
python tests/tools/run_demo.py                 # 15 组自检
python tests/tools/bench.py connect             # 连接延迟
python tests/tools/real_machine_check.py        # 真机改道（需管理员）
```

---

## 第三方组件与许可证

本项目自身以 **MIT** 许可证发布，全文见 [`LICENSE`](LICENSE)。

第三方组件：

| 组件 | 许可证 | 说明 |
|---|---|---|
| [pydivert](https://github.com/ffalcinelli/pydivert) | **LGPLv3+ / GPLv2+**（双许可） | 见下方说明 |
| [WinDivert](https://github.com/basil00/WinDivert) | **LGPLv3** | 已签名内核驱动打包在 pydivert 的 wheel 内，由后者随附 |
| [PyYAML](https://pyyaml.org/) | MIT | 必需依赖 |
| [psutil](https://github.com/giampaolo/psutil) | BSD-3-Clause | `windivert` 可选依赖 |
| [pywin32](https://github.com/mhammond/pywin32) | PSF | Windows 服务包装，可选 |
| [keyring](https://github.com/jaraco/keyring) | MIT | `password_ref: "keyring:..."` 用，可选 |

### 关于 pydivert 的 LGPL

**默认情况下不触发任何 LGPL 义务**：本项目不打包、不内联 pydivert，
只声明依赖；用户从 PyPI 自行安装，分发者不是本项目。
LGPL 的义务绑定在「分发该库」这个动作上。

**打包成 exe 分发时**才需要满足三条：随附 LGPLv3 全文、提供源码获取途径、
允许用户替换该库。（pydivert 的 `dist-info/licenses/` 内已带全文。）

注：LGPL 不传染本项目自身的代码 —— 这正是它与 GPL 的区别。

### 使用须知

* 本工具会**拦截并转发本机网络流量**，需要管理员权限与内核驱动。
* **仅在你自有或已获授权的设备与网络上使用。**
* 使用代理可能违反你所在组织的网络策略或服务条款 —— 由使用者自行确认授权。
* 默认**不上报任何遥测**。默认不代理回环/私网。

### 独立实现与商标声明

本项目是独立的进程级代理实现，与 Proxifier 的权利人无任何关联，未获其授权或背书。
`Proxifier` 是其权利人的商标，本项目仅在兼容性/迁移说明中作指称使用。
本项目不包含、不解析 Proxifier 的私有文件格式或代码，也不提供任何绕过授权/DRM 的功能。
使用者应自行确认所在司法辖区的法律与软件许可协议（EULA）约束。

### AI 辅助开发说明

本项目在 AI 编程助手辅助下开发；需求、设计、取舍、测试与发布由人类作者负责。
代码来源审查未发现从第三方闭源产品复制的内容；AI 辅助与“复制反编译代码”是两件事。
AI 输出在部分司法辖区可能不具备可版权性，或与训练数据中的代码存在相似之处；
本项目按 MIT 许可证提供，不作额外保证，使用者应自行评估适用性与合规性。
