procproxy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- procproxy-0.1.0.dist-info/METADATA +489 -0
- procproxy-0.1.0.dist-info/RECORD +45 -0
- procproxy-0.1.0.dist-info/WHEEL +5 -0
- procproxy-0.1.0.dist-info/entry_points.txt +2 -0
- procproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
- procproxy-0.1.0.dist-info/top_level.txt +1 -0
- proxycore/__init__.py +7 -0
- proxycore/__main__.py +65 -0
- proxycore/api.py +226 -0
- proxycore/backend/__init__.py +49 -0
- proxycore/backend/base.py +159 -0
- proxycore/backend/null.py +51 -0
- proxycore/backend/windivert/__init__.py +657 -0
- proxycore/backend/windivert/filter.py +123 -0
- proxycore/backend/windivert/pidmap.py +289 -0
- proxycore/backend/windivert/pkt.py +424 -0
- proxycore/canproxy.py +229 -0
- proxycore/checkroute.py +247 -0
- proxycore/cli.py +605 -0
- proxycore/config.py +1010 -0
- proxycore/dns/__init__.py +46 -0
- proxycore/dns/cache.py +209 -0
- proxycore/dns/modes.py +451 -0
- proxycore/dns/udp.py +606 -0
- proxycore/forward.py +572 -0
- proxycore/ipc.py +637 -0
- proxycore/log.py +298 -0
- proxycore/pathsetup.py +112 -0
- proxycore/probe.py +168 -0
- proxycore/proc.py +342 -0
- proxycore/rules.py +730 -0
- proxycore/runtime.py +654 -0
- proxycore/service/__init__.py +536 -0
- proxycore/service/__main__.py +124 -0
- proxycore/tunnel/__init__.py +8 -0
- proxycore/tunnel/relay.py +393 -0
- proxycore/tunnel/sessions.py +315 -0
- proxycore/tunnel/sink.py +325 -0
- proxycore/types.py +165 -0
- proxycore/upstream/__init__.py +52 -0
- proxycore/upstream/base.py +150 -0
- proxycore/upstream/chain.py +92 -0
- proxycore/upstream/http_connect.py +148 -0
- proxycore/upstream/socks4.py +106 -0
- proxycore/upstream/socks5.py +266 -0
proxycore/api.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""编程式启动接口:在 Python 里直接起 ProcProxy(不需要 CLI / 不需要 PowerShell)。
|
|
2
|
+
|
|
3
|
+
三种用法:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# ① 最简:起服务,拿到句柄,自己用
|
|
7
|
+
from proxycore.api import start_proxy
|
|
8
|
+
|
|
9
|
+
with start_proxy("samples/profiles/dev.yaml", backend="null") as ppx:
|
|
10
|
+
print(ppx.status()) # 后端/规则/会话统计
|
|
11
|
+
print(ppx.sessions()) # 活跃会话
|
|
12
|
+
print(ppx.proxy.of("127.0.0.1:8081")) # sink 地址
|
|
13
|
+
|
|
14
|
+
# ② 只想验证转发链路(不做抓包):把 sink 收到的连接转发到固定目标
|
|
15
|
+
from proxycore.api import start_proxy
|
|
16
|
+
with start_proxy("samples/profiles/smoke.yaml", backend="null",
|
|
17
|
+
sink_test_target="127.0.0.1:8080", upstream="local-mock") as ppx:
|
|
18
|
+
...
|
|
19
|
+
# 此时 curl -x http://127.0.0.1:19080 http://any/ 会被转发到 127.0.0.1:8080
|
|
20
|
+
|
|
21
|
+
# ③ 内嵌到自己的程序里:自己喂连接、自己看决策
|
|
22
|
+
from proxycore.api import make_runtime
|
|
23
|
+
rt = make_runtime("samples/profiles/dev.yaml", backend="null")
|
|
24
|
+
decision = rt.decide(conn_request) # 纯决策,不抓包
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`start_proxy()` 返回的 `ProxyHandle` 支持 `with`(退出时自动 stop),
|
|
28
|
+
也可以手动 `handle.stop()`。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from .config import Config, ConfigError, ConfigManager
|
|
38
|
+
from .log import EventLogger, build_logger, get_logger
|
|
39
|
+
from .runtime import Runtime
|
|
40
|
+
from .types import ConnRequest, Decision
|
|
41
|
+
|
|
42
|
+
LOG = EventLogger(get_logger("proxycore.api"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProxyStartError(RuntimeError):
|
|
46
|
+
"""启动失败(配置非法 / 后端不可用 / sink 端口占用)。"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _coerce_config(config: Config | str | os.PathLike[str], *, check_sink: bool = False) -> Config:
|
|
50
|
+
if isinstance(config, Config):
|
|
51
|
+
return config
|
|
52
|
+
return load_config(os.fspath(config), check_sink=check_sink)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_config(path: str | os.PathLike[str], *, check_sink: bool = False) -> Config:
|
|
56
|
+
"""读配置(含 V1–V10 校验)。失败抛 `ProxyStartError`。"""
|
|
57
|
+
try:
|
|
58
|
+
return Config.load(path, check_sink=check_sink)
|
|
59
|
+
except ConfigError as exc:
|
|
60
|
+
raise ProxyStartError(f"配置无效:[{exc.code}] {exc.message}") from exc
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def make_runtime(
|
|
64
|
+
config: Config | str | os.PathLike[str],
|
|
65
|
+
*,
|
|
66
|
+
backend: str = "",
|
|
67
|
+
log_level: str = "INFO",
|
|
68
|
+
log_jsonl: str = "",
|
|
69
|
+
) -> Runtime:
|
|
70
|
+
"""只装配 Runtime(不 start),便于在测试/嵌入场景里自己控制生命周期。"""
|
|
71
|
+
config = _coerce_config(config)
|
|
72
|
+
if backend:
|
|
73
|
+
config.service.backend = backend
|
|
74
|
+
if log_level:
|
|
75
|
+
config.service.log_level = log_level
|
|
76
|
+
if log_jsonl:
|
|
77
|
+
config.service.log_jsonl = log_jsonl
|
|
78
|
+
|
|
79
|
+
build_logger("proxycore", level=config.service.log_level, jsonl_path=config.service.log_jsonl or None)
|
|
80
|
+
|
|
81
|
+
from .backend import BackendUnavailable, make_backend
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
backend_obj = make_backend(config.service.backend, config=config)
|
|
85
|
+
except BackendUnavailable as exc:
|
|
86
|
+
# 绝不静默直连,启动失败就明确失败
|
|
87
|
+
raise ProxyStartError(
|
|
88
|
+
f"后端不可用({getattr(exc, 'code', 'E_BACKEND_UNAVAILABLE')}):{exc}"
|
|
89
|
+
) from exc
|
|
90
|
+
|
|
91
|
+
return Runtime(ConfigManager(config), backend_obj)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ProxyHandle:
|
|
96
|
+
"""已启动的 ProcProxy 句柄。"""
|
|
97
|
+
|
|
98
|
+
runtime: Runtime
|
|
99
|
+
sink: str
|
|
100
|
+
backend: str
|
|
101
|
+
_stopped: bool = False
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------ 控制 -----
|
|
104
|
+
def stop(self, *, reason: str = "api-stop") -> None:
|
|
105
|
+
if self._stopped:
|
|
106
|
+
return
|
|
107
|
+
self.runtime.stop(reason=reason)
|
|
108
|
+
self._stopped = True
|
|
109
|
+
|
|
110
|
+
def __enter__(self) -> ProxyHandle:
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
114
|
+
self.stop()
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def running(self) -> bool:
|
|
118
|
+
return not self._stopped
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------ 查询 -----
|
|
121
|
+
def status(self) -> dict[str, Any]:
|
|
122
|
+
snapshot = self.runtime.snapshot()
|
|
123
|
+
return {
|
|
124
|
+
"name": self.backend, # 后端名("null" | "windivert")
|
|
125
|
+
"backend": snapshot["backend"], # 后端计数字典
|
|
126
|
+
"rules": snapshot["rules"],
|
|
127
|
+
"sessions": snapshot["sessions"],
|
|
128
|
+
"breaker": snapshot["breaker"],
|
|
129
|
+
"dns": snapshot["dns"],
|
|
130
|
+
"sink": self.sink,
|
|
131
|
+
"running": self.running,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def sessions(self, limit: int = 50) -> list[dict[str, Any]]:
|
|
135
|
+
return [s.to_dict() for s in self.runtime.sessions.all()][-limit:]
|
|
136
|
+
|
|
137
|
+
def active_sessions(self) -> int:
|
|
138
|
+
return self.runtime.sessions.count()
|
|
139
|
+
|
|
140
|
+
def reload(self, path: str = "") -> int:
|
|
141
|
+
"""热重载配置,返回新的 generation(失败抛 `ProxyStartError`)。"""
|
|
142
|
+
target = path or self.runtime.config.path
|
|
143
|
+
try:
|
|
144
|
+
self.runtime.manager.reload(target)
|
|
145
|
+
except ConfigError as exc:
|
|
146
|
+
raise ProxyStartError(f"热重载被拒绝:[{exc.code}] {exc.message}") from exc
|
|
147
|
+
# 与文件监视器走同一个收口(DNS/UDP 策略 + 守卫 + 粘性迁移),
|
|
148
|
+
# 否则这条入口会悄悄少刷新几样东西。
|
|
149
|
+
self.runtime.apply_profile()
|
|
150
|
+
return self.runtime.manager.generation
|
|
151
|
+
|
|
152
|
+
def decide(self, conn: ConnRequest) -> Decision:
|
|
153
|
+
"""直接问规则引擎"这条连接怎么处理"(不抓包、不改道)。"""
|
|
154
|
+
return self.runtime.decide(conn)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def profile(self) -> Any:
|
|
158
|
+
return self.runtime.config.profile
|
|
159
|
+
|
|
160
|
+
def __repr__(self) -> str:
|
|
161
|
+
return f"<ProxyHandle backend={self.backend} sink={self.sink} running={self.running}>"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def start_proxy(
|
|
165
|
+
config: Config | str | os.PathLike[str] = "samples/profiles/dev.yaml",
|
|
166
|
+
*,
|
|
167
|
+
backend: str = "",
|
|
168
|
+
sink_test_target: str = "",
|
|
169
|
+
upstream: str = "",
|
|
170
|
+
log_level: str = "INFO",
|
|
171
|
+
log_jsonl: str = "",
|
|
172
|
+
) -> ProxyHandle:
|
|
173
|
+
"""启动 ProcProxy 数据面(sink 监听 + 后端 + 规则 + 会话表)。
|
|
174
|
+
|
|
175
|
+
* 返回的 `ProxyHandle` 支持 `with`,退出会自动 stop;
|
|
176
|
+
* `backend=""` 用配置里的 backend;`backend="null"` 用于联调(不抓包);
|
|
177
|
+
* `sink_test_target` 非空 = 测试模式:sink 把所有连接转发到该固定目标;
|
|
178
|
+
* 后端不可用(WinDivert 不可加载/非管理员)→ 抛 `ProxyStartError`,不会静默直连。
|
|
179
|
+
"""
|
|
180
|
+
config_obj = _coerce_config(config)
|
|
181
|
+
runtime = make_runtime(
|
|
182
|
+
config_obj,
|
|
183
|
+
backend=backend,
|
|
184
|
+
log_level=log_level,
|
|
185
|
+
log_jsonl=log_jsonl,
|
|
186
|
+
)
|
|
187
|
+
try:
|
|
188
|
+
runtime.start(sink_test_target=sink_test_target, upstream_alias=upstream)
|
|
189
|
+
except Exception as exc: # 启动失败要保证不留半开的监听
|
|
190
|
+
runtime.stop(reason="start-failed")
|
|
191
|
+
raise ProxyStartError(f"启动失败:{exc}") from exc
|
|
192
|
+
|
|
193
|
+
sink_cfg = runtime.config.service.sink
|
|
194
|
+
LOG.info(
|
|
195
|
+
"api_started",
|
|
196
|
+
backend=runtime.backend.name,
|
|
197
|
+
sink=f"{sink_cfg.host}:{sink_cfg.port}",
|
|
198
|
+
test_target=sink_test_target or "-",
|
|
199
|
+
)
|
|
200
|
+
return ProxyHandle(
|
|
201
|
+
runtime=runtime,
|
|
202
|
+
sink=f"{sink_cfg.host}:{sink_cfg.port}",
|
|
203
|
+
backend=runtime.backend.name,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def run_forever(config: Config | str | os.PathLike[str] = "samples/profiles/dev.yaml", **kwargs: Any) -> int:
|
|
208
|
+
"""启动并阻塞(等价于 `cli run`),Ctrl+C 退出。返回进程退出码。"""
|
|
209
|
+
handle = start_proxy(config, **kwargs)
|
|
210
|
+
try:
|
|
211
|
+
handle.runtime.serve_forever(reload_watch=True)
|
|
212
|
+
except KeyboardInterrupt:
|
|
213
|
+
print("\n收到 Ctrl+C,正在停止…")
|
|
214
|
+
finally:
|
|
215
|
+
handle.stop(reason="signal")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
__all__ = [
|
|
220
|
+
"ProxyHandle",
|
|
221
|
+
"ProxyStartError",
|
|
222
|
+
"load_config",
|
|
223
|
+
"make_runtime",
|
|
224
|
+
"run_forever",
|
|
225
|
+
"start_proxy",
|
|
226
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""后端工厂。
|
|
2
|
+
|
|
3
|
+
当前只包含两条实现:
|
|
4
|
+
|
|
5
|
+
* `windivert`:路线 A,用户态 WinDivert(pydivert 自带已签名的 `WinDivert64.sys`);
|
|
6
|
+
* `null`:联调用的空后端:不抓包、不改道,只提供 `decide()` 与状态输出。
|
|
7
|
+
|
|
8
|
+
硬性约束:后端不可用时必须明确报错退出,禁止静默直连
|
|
9
|
+
(用户会以为在走代理,实际是把真实 IP 漏出去了)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .base import (
|
|
17
|
+
Backend,
|
|
18
|
+
BackendUnavailable,
|
|
19
|
+
OriginalDstUnknown,
|
|
20
|
+
RedirectFailed,
|
|
21
|
+
)
|
|
22
|
+
from .null import NullBackend
|
|
23
|
+
|
|
24
|
+
SUPPORTED_BACKENDS = ("null", "windivert")
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"SUPPORTED_BACKENDS",
|
|
28
|
+
"Backend",
|
|
29
|
+
"BackendUnavailable",
|
|
30
|
+
"NullBackend",
|
|
31
|
+
"OriginalDstUnknown",
|
|
32
|
+
"RedirectFailed",
|
|
33
|
+
"make_backend",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_backend(name: str, *, config: Any = None, **kwargs: Any) -> Backend:
|
|
38
|
+
"""按名字构造后端;不可用时抛 `BackendUnavailable`(绝不静默返回直连实现)。"""
|
|
39
|
+
backend = (name or "null").strip().lower()
|
|
40
|
+
|
|
41
|
+
if backend == "null":
|
|
42
|
+
return NullBackend()
|
|
43
|
+
|
|
44
|
+
if backend == "windivert":
|
|
45
|
+
from .windivert import WinDivertBackend
|
|
46
|
+
|
|
47
|
+
return WinDivertBackend(config=config, **kwargs)
|
|
48
|
+
|
|
49
|
+
raise BackendUnavailable(f"未知后端 {name!r}(可选:{', '.join(SUPPORTED_BACKENDS)})")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Backend 抽象(硬约束:改道失败不得静默直连)。
|
|
2
|
+
|
|
3
|
+
各后端实现必须满足的语义:
|
|
4
|
+
* `redirect()` 返回 `False` = 改道失败 → 核心必须记录并关闭该连接
|
|
5
|
+
(对 TCP 表示丢弃 SYN → 客户端超时;禁止"失败了就直连");
|
|
6
|
+
* `lookup_original()` 必须能区分"没有重定向记录"(返回 None)与"查询出错"(抛异常);
|
|
7
|
+
* 所有后端必须支持热停:`stop()` 后 2 秒内不再回调 `decide`。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any, Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from ..types import Action, ConnRequest, Decision, Endpoint
|
|
17
|
+
|
|
18
|
+
# 后端起停的硬性时限
|
|
19
|
+
STOP_QUIESCE_SECONDS = 2.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BackendUnavailable(RuntimeError):
|
|
23
|
+
"""后端不可用(对应错误码 `E_BACKEND_UNAVAILABLE`)。"""
|
|
24
|
+
|
|
25
|
+
code = "E_BACKEND_UNAVAILABLE"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OriginalDstUnknown(LookupError):
|
|
29
|
+
"""无法恢复原始目标(对应错误码 `E_ORIGINAL_DST_UNKNOWN`)。
|
|
30
|
+
|
|
31
|
+
`lookup_original()` 的契约:没有记录 → 返回 None;查询出错 → 抛本异常。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
code = "E_ORIGINAL_DST_UNKNOWN"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RedirectFailed(RuntimeError):
|
|
38
|
+
"""改道失败(对应错误码 `E_REDIRECT_FAILED`)。"""
|
|
39
|
+
|
|
40
|
+
code = "E_REDIRECT_FAILED"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@runtime_checkable
|
|
44
|
+
class Backend(Protocol):
|
|
45
|
+
name: str # "windivert" | "null"
|
|
46
|
+
supports_udp: bool
|
|
47
|
+
supports_pid_native: bool # True=后端自身能给出可靠 PID;False=需查表
|
|
48
|
+
counts_decisions: bool # 决策类计数归谁记;`Runtime.start()` 会置 False(见 BaseBackend)
|
|
49
|
+
|
|
50
|
+
def start(self) -> None:
|
|
51
|
+
"""打开句柄 / 注册过滤器。失败抛 BackendUnavailable。"""
|
|
52
|
+
|
|
53
|
+
def stop(self) -> None:
|
|
54
|
+
"""卸载过滤器、销毁句柄、等待在途请求结束。幂等。"""
|
|
55
|
+
|
|
56
|
+
def set_decider(self, decide: Callable[[ConnRequest], Decision]) -> None:
|
|
57
|
+
"""核心把决策函数注册给后端。后端在捕获到连接时调用。"""
|
|
58
|
+
|
|
59
|
+
def redirect(self, conn: ConnRequest, sink: Endpoint) -> bool:
|
|
60
|
+
"""把这条连接(conn)改道到 sink。
|
|
61
|
+
|
|
62
|
+
WinDivert 后端:改写报文头并回注。
|
|
63
|
+
必须在 `set_decider` 的返回之前完成(同步语义)。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def lookup_original(self, client_socket: object, backend_ctx: int) -> Endpoint | None:
|
|
67
|
+
"""sink accept 之后调用:恢复原始目标。
|
|
68
|
+
|
|
69
|
+
WinDivert 后端:用 `backend_ctx`(=客户端源端口) 查本地表。
|
|
70
|
+
路线A: 用 `backend_ctx`(=客户端源端口) 查本地表
|
|
71
|
+
返回 None 时 sink 必须关闭该连接并记 ERROR 日志。
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def stats(self) -> dict[str, Any]:
|
|
75
|
+
"""`{"captured": n, "redirected": n, "active_flows": n, "active_redirects": n}`"""
|
|
76
|
+
|
|
77
|
+
def on_udp(self, handler: Callable[[ConnRequest, bytes], Action]) -> None:
|
|
78
|
+
"""可选:UDP 数据报到达(不支持的后端抛 NotImplementedError)。"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class BaseBackend:
|
|
82
|
+
"""后端公共基类:统一计数、决策回调登记、线程安全停止。
|
|
83
|
+
|
|
84
|
+
不做平台相关的事情;平台后端继承它并实现 `start/stop/redirect/lookup_original`。
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
name: str = "base"
|
|
88
|
+
supports_udp: bool = False
|
|
89
|
+
supports_pid_native: bool = False
|
|
90
|
+
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
self._decider: Callable[[ConnRequest], Decision] | None = None
|
|
93
|
+
self._udp_handler: Callable[[ConnRequest, bytes], Action] | None = None
|
|
94
|
+
self._stopped = True
|
|
95
|
+
self._lock = threading.RLock()
|
|
96
|
+
self._counters: dict[str, int] = {"captured": 0, "redirected": 0, "direct": 0, "blocked": 0, "failed": 0}
|
|
97
|
+
self._active_flows = 0
|
|
98
|
+
self._active_redirects = 0
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------ 生命周期 --
|
|
101
|
+
def start(self) -> None:
|
|
102
|
+
self._stopped = False
|
|
103
|
+
|
|
104
|
+
def stop(self) -> None:
|
|
105
|
+
self._stopped = True
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def running(self) -> bool:
|
|
109
|
+
return not self._stopped
|
|
110
|
+
|
|
111
|
+
# --------------------------------------------------------------- 决策 ---
|
|
112
|
+
def set_decider(self, decide: Callable[[ConnRequest], Decision]) -> None:
|
|
113
|
+
self._decider = decide
|
|
114
|
+
|
|
115
|
+
def decide(self, conn: ConnRequest) -> Decision:
|
|
116
|
+
"""后端在捕获到连接时调用(`redirect()` 必须在返回前完成)。"""
|
|
117
|
+
if self._decider is None: # pragma: no cover - 启动顺序错误
|
|
118
|
+
raise RedirectFailed("set_decider() 尚未注册决策函数")
|
|
119
|
+
return self._decider(conn)
|
|
120
|
+
|
|
121
|
+
def on_udp(self, handler: Callable[[ConnRequest, bytes], Action]) -> None:
|
|
122
|
+
if not self.supports_udp:
|
|
123
|
+
raise NotImplementedError(f"{self.name} 后端不支持 UDP")
|
|
124
|
+
self._udp_handler = handler
|
|
125
|
+
|
|
126
|
+
# --------------------------------------------------------------- 计数 ---
|
|
127
|
+
#: 决策类计数(`captured`/`direct`/`blocked`)由谁来记。
|
|
128
|
+
#:
|
|
129
|
+
#: 后端可以脱离 `Runtime` 独立使用(单测、手工投喂联调),那时它得自己记。
|
|
130
|
+
#: 但一旦核心通过 `set_decider()` 接管,`Runtime.decide()` 自己就会记这三个键。
|
|
131
|
+
#: 后端再记一遍就是双倍(实测:喂 1 个包,`captured` 变 2)。
|
|
132
|
+
#: `Runtime.start()` 会把它置 `False`,那是这个关系的建立点,一眼能看见。
|
|
133
|
+
#:
|
|
134
|
+
#: 区分原则:决策计数归核心,传输计数(`redirected`/`failed`/`injected`)
|
|
135
|
+
#: 归后端:后者只有后端知道(改道是否真的发出去了)。
|
|
136
|
+
counts_decisions: bool = True
|
|
137
|
+
|
|
138
|
+
def count(self, key: str, delta: int = 1) -> None:
|
|
139
|
+
with self._lock:
|
|
140
|
+
self._counters[key] = self._counters.get(key, 0) + delta
|
|
141
|
+
|
|
142
|
+
def count_decision(self, key: str, delta: int = 1) -> None:
|
|
143
|
+
"""记一个决策类计数;核心接管后是空操作(见 `counts_decisions`)。"""
|
|
144
|
+
if self.counts_decisions:
|
|
145
|
+
self.count(key, delta)
|
|
146
|
+
|
|
147
|
+
def set_active(self, flows: int | None = None, redirects: int | None = None) -> None:
|
|
148
|
+
with self._lock:
|
|
149
|
+
if flows is not None:
|
|
150
|
+
self._active_flows = flows
|
|
151
|
+
if redirects is not None:
|
|
152
|
+
self._active_redirects = redirects
|
|
153
|
+
|
|
154
|
+
def stats(self) -> dict[str, Any]:
|
|
155
|
+
with self._lock:
|
|
156
|
+
out = dict(self._counters)
|
|
157
|
+
out["active_flows"] = self._active_flows
|
|
158
|
+
out["active_redirects"] = self._active_redirects
|
|
159
|
+
return out
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""`null` 后端:不做任何抓包/改道,只提供手工把连接送进 sink 的能力。
|
|
2
|
+
|
|
3
|
+
用途:
|
|
4
|
+
* 联调与测试(`--backend null --sink-test-target host:port`);
|
|
5
|
+
* L2 后端契约一致性测试的基准实现。
|
|
6
|
+
|
|
7
|
+
它不会代理任何真实流量,这是刻意的,避免"看起来在走代理其实没走"。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ..types import ConnRequest, Endpoint
|
|
13
|
+
from .base import BaseBackend
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NullBackend(BaseBackend):
|
|
17
|
+
name = "null"
|
|
18
|
+
supports_udp = True
|
|
19
|
+
supports_pid_native = False
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._origins: dict[int, Endpoint] = {}
|
|
24
|
+
self._next_id = 1
|
|
25
|
+
self._sink: Endpoint | None = None
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------ 生命周期 --
|
|
28
|
+
def start(self) -> None:
|
|
29
|
+
super().start()
|
|
30
|
+
|
|
31
|
+
def stop(self) -> None:
|
|
32
|
+
super().stop()
|
|
33
|
+
self._origins.clear()
|
|
34
|
+
|
|
35
|
+
# --------------------------------------------------------------- 改道 ---
|
|
36
|
+
def redirect(self, conn: ConnRequest, sink: Endpoint) -> bool:
|
|
37
|
+
if not self.running:
|
|
38
|
+
self.count("failed")
|
|
39
|
+
return False
|
|
40
|
+
self._sink = sink
|
|
41
|
+
# 先记账再"发包"
|
|
42
|
+
self._origins[conn.backend_id or (self._next_id)] = conn.dst
|
|
43
|
+
self._next_id += 1
|
|
44
|
+
self.count("redirected")
|
|
45
|
+
self.set_active(redirects=len(self._origins))
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
def lookup_original(self, client_socket: object, backend_ctx: int) -> Endpoint | None:
|
|
49
|
+
return self._origins.get(backend_ctx)
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------- 手工投喂(测试)--
|