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.
Files changed (45) hide show
  1. procproxy-0.1.0.dist-info/METADATA +489 -0
  2. procproxy-0.1.0.dist-info/RECORD +45 -0
  3. procproxy-0.1.0.dist-info/WHEEL +5 -0
  4. procproxy-0.1.0.dist-info/entry_points.txt +2 -0
  5. procproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. procproxy-0.1.0.dist-info/top_level.txt +1 -0
  7. proxycore/__init__.py +7 -0
  8. proxycore/__main__.py +65 -0
  9. proxycore/api.py +226 -0
  10. proxycore/backend/__init__.py +49 -0
  11. proxycore/backend/base.py +159 -0
  12. proxycore/backend/null.py +51 -0
  13. proxycore/backend/windivert/__init__.py +657 -0
  14. proxycore/backend/windivert/filter.py +123 -0
  15. proxycore/backend/windivert/pidmap.py +289 -0
  16. proxycore/backend/windivert/pkt.py +424 -0
  17. proxycore/canproxy.py +229 -0
  18. proxycore/checkroute.py +247 -0
  19. proxycore/cli.py +605 -0
  20. proxycore/config.py +1010 -0
  21. proxycore/dns/__init__.py +46 -0
  22. proxycore/dns/cache.py +209 -0
  23. proxycore/dns/modes.py +451 -0
  24. proxycore/dns/udp.py +606 -0
  25. proxycore/forward.py +572 -0
  26. proxycore/ipc.py +637 -0
  27. proxycore/log.py +298 -0
  28. proxycore/pathsetup.py +112 -0
  29. proxycore/probe.py +168 -0
  30. proxycore/proc.py +342 -0
  31. proxycore/rules.py +730 -0
  32. proxycore/runtime.py +654 -0
  33. proxycore/service/__init__.py +536 -0
  34. proxycore/service/__main__.py +124 -0
  35. proxycore/tunnel/__init__.py +8 -0
  36. proxycore/tunnel/relay.py +393 -0
  37. proxycore/tunnel/sessions.py +315 -0
  38. proxycore/tunnel/sink.py +325 -0
  39. proxycore/types.py +165 -0
  40. proxycore/upstream/__init__.py +52 -0
  41. proxycore/upstream/base.py +150 -0
  42. proxycore/upstream/chain.py +92 -0
  43. proxycore/upstream/http_connect.py +148 -0
  44. proxycore/upstream/socks4.py +106 -0
  45. proxycore/upstream/socks5.py +266 -0
@@ -0,0 +1,393 @@
1
+ """双向转发(事件驱动,不用 `time.sleep` 轮询)。
2
+
3
+ 实现:基于跨平台 `selectors` 的 reactor(Windows 上用 select)。
4
+
5
+ 语义要求:
6
+ * 半关闭:一端 FIN 后把另一向 `shutdown(SHUT_WR)`,在对端也 FIN 或超时前保持另一向;
7
+ * 背压:只在 `send()` 能全部写走时继续读,否则挂 写事件 等可写(不忙等);
8
+ * 计数:`bytes_up`(客户端→目标)、`bytes_down`(目标→客户端);
9
+ * 空闲与半关闭超时。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import contextlib
15
+ import selectors
16
+ import socket
17
+ import time
18
+ from dataclasses import dataclass, field
19
+
20
+ CHUNK = 65536
21
+ # 单连接单方向允许堆积的缓冲上限(背压阈值)
22
+ MAX_BUFFER = 4 * 1024 * 1024
23
+
24
+ REASON_OK = "ok"
25
+ REASON_IDLE_TIMEOUT = "idle-timeout"
26
+ REASON_HALF_CLOSE_TIMEOUT = "half-close-timeout"
27
+ REASON_CLIENT_RESET = "client-reset"
28
+ REASON_UPSTREAM_RESET = "upstream-reset"
29
+
30
+
31
+ @dataclass
32
+ class _Direction:
33
+ """一个方向的转发状态。"""
34
+
35
+ src: socket.socket
36
+ dst: socket.socket
37
+ is_up: bool
38
+ buffer: bytearray = field(default_factory=bytearray)
39
+ src_closed: bool = False
40
+ dst_shutdown: bool = False
41
+ done: bool = False
42
+ rst: bool = False
43
+ bytes_moved: int = 0
44
+
45
+ def flush(self) -> bool:
46
+ """尽力写走缓冲;返回 True 表示缓冲已空。"""
47
+ while self.buffer:
48
+ try:
49
+ sent = self.dst.send(self.buffer)
50
+ except BlockingIOError:
51
+ return False
52
+ except OSError:
53
+ raise
54
+ if sent <= 0: # pragma: no cover - 对端已关闭
55
+ return False
56
+ del self.buffer[:sent]
57
+ self.bytes_moved += sent
58
+ return True
59
+
60
+
61
+ @dataclass
62
+ class _Watch:
63
+ """selector 的注册单位:按 socket 注册,不能按方向注册。
64
+
65
+ socketpair 是双工的:一个 socket 既是"某条方向的读端",又是"另一条方向的写端"。
66
+ 旧实现把 `EVENT_WRITE` 挂在 `direction.src` 上,而数据是往 `direction.dst` 写的,
67
+ 于是"等对端可写"实际等的是读端,两个 socket 都几乎永远可写,`select()`
68
+ 立刻返回,整个循环退化成 100% CPU 忙等,真正的背压从未被等待。
69
+ 实测后果:1MB 回程在有背压时只送出去 128KB 就收工(见 `test_relay_1mb_*`)。
70
+ """
71
+
72
+ sock: socket.socket
73
+ read_from: _Direction # 这个 socket 是它的 src
74
+ write_to: _Direction # 这个 socket 是它的 dst
75
+
76
+
77
+ def _finish(direction: _Direction) -> bool:
78
+ """缓冲区已空 → 补发 FIN 并宣告这一向结束;返回"是否已结束"。
79
+
80
+ FIN 必须跟在数据后面。 旧实现在读到 EOF 时先 `shutdown(SHUT_WR)` 再 flush,
81
+ 结果 FIN 超车插到还没发出去的数据前面:上游立刻看到 EOF 提前收工,剩下的数据
82
+ 在 Windows 上又会因"关闭后再 send"报 `10058` 直接丢掉,响应被截断。
83
+ """
84
+ if direction.buffer:
85
+ return False # 还没写完,FIN 得等
86
+ if not direction.dst_shutdown:
87
+ _shutdown_write(direction.dst)
88
+ direction.dst_shutdown = True
89
+ return True
90
+
91
+
92
+ def relay_bidirectional(
93
+ client: socket.socket,
94
+ upstream: socket.socket,
95
+ *,
96
+ idle_timeout: float = 300.0,
97
+ half_close_grace: float = 30.0,
98
+ response_grace: float = 2.0,
99
+ drain_grace: float = 2.0,
100
+ chunk: int = CHUNK,
101
+ ) -> tuple[int, int, str]:
102
+ """把 `client` 与 `upstream` 双向对接,返回 `(bytes_up, bytes_down, reason)`。
103
+
104
+ `response_grace`:客户端干净 FIN 之后,再给"响应"这么多时间。
105
+ 这样既不会像"等双向都 FIN"那样在长连接上卡到 idle 超时,
106
+ 也不会像"立刻退出"那样把还没到达的响应掐掉。
107
+
108
+ `drain_grace`:对端还有未被读走的残留字节时,最多再等这么久。
109
+ 防止"发完就 FIN、不读响应"的客户端把连接挂到 `half_close_grace`。
110
+ """
111
+ directions = [
112
+ _Direction(src=client, dst=upstream, is_up=True),
113
+ _Direction(src=upstream, dst=client, is_up=False),
114
+ ]
115
+ for sock in (client, upstream):
116
+ with contextlib.suppress(OSError):
117
+ sock.setblocking(False)
118
+
119
+ up, down = directions[0], directions[1]
120
+ watch = [
121
+ _Watch(sock=client, read_from=up, write_to=down),
122
+ _Watch(sock=upstream, read_from=down, write_to=up),
123
+ ]
124
+
125
+ sel = selectors.DefaultSelector()
126
+ for entry in watch:
127
+ sel.register(entry.sock, _mask_for(entry), entry)
128
+
129
+ last_activity = time.monotonic()
130
+ half_closed_at: float | None = None
131
+ request_closed_at: float | None = None # 客户端干净 FIN 的时刻
132
+ response_seen = False
133
+ upstream_quiet_since: float | None = None # 上游恢复安静的时刻(响应已发完)
134
+ unread_since: float | None = None # 首次发现"对端还有未读字节"的时刻
135
+ reason = REASON_OK
136
+
137
+ def request_still_pending() -> bool:
138
+ """请求方向是否还有"没送出去"的东西(缓冲没写空 / 源还没读到 EOF)。
139
+
140
+ 为什么必须有这个判断:
141
+ `request_closed_at` 只记录"收到客户端 FIN"的时刻,而 FIN 不等于"数据已送达"
142
+ 并发/高负载下,1MB 的尾部可能还堆在我们自己的发送缓冲里没 flush 出去。
143
+ 如果这时候按 `response_grace` 直接收工,尾部就丢了,
144
+ 表现是偶发的"数据短一截 / sha256 对不上"(这种 bug 最难查)。
145
+ 所以:只要请求方向还有待发数据,任何"温和收工"都不许生效。
146
+ """
147
+ return bool(up.buffer) or not up.src_closed
148
+
149
+ def response_still_pending() -> bool:
150
+ """回程方向是否还有"没送到客户端"的东西。
151
+
152
+ 与 `request_still_pending()` 同源:任何"温和收工"都必须等两个方向的缓冲
153
+ 都写空。只检查请求方向的话,上游一 FIN 就会带着满缓冲的回程收工,同样是截断。
154
+ """
155
+ return bool(down.buffer)
156
+
157
+ try:
158
+ while True:
159
+ now = time.monotonic()
160
+ if now - last_activity > idle_timeout:
161
+ reason = REASON_IDLE_TIMEOUT
162
+ break
163
+ pending = request_still_pending() or response_still_pending()
164
+ if (
165
+ request_closed_at is not None
166
+ and now - request_closed_at > response_grace
167
+ and not response_seen
168
+ and not pending
169
+ ):
170
+ # 客户端已发完、请求也确实全部送达、且一直没响应 → 正常收工(不等 idle 超时)
171
+ break
172
+ if upstream_quiet_since is not None and now - upstream_quiet_since > response_grace and not pending:
173
+ # 响应已经发完并转发给客户端,上游也安静了 → 正常收工
174
+ break
175
+ if unread_since is not None and now - unread_since > drain_grace and not pending:
176
+ # 对端一直不读走残留字节(例如"发完就 FIN、不读响应"的客户端)
177
+ # → 不能无限等,按 drain_grace 收工,避免把连接挂到 half_close_grace。
178
+ break
179
+ if half_closed_at is not None and now - half_closed_at > half_close_grace:
180
+ reason = REASON_HALF_CLOSE_TIMEOUT
181
+ break
182
+
183
+ # 先把积压数据推出去(非阻塞,能写多少写多少)
184
+ # 注意变量名别叫 entry,那是 `_Watch`(上面注册用的),mypy 会串型。
185
+ for direction in directions:
186
+ if not direction.buffer:
187
+ continue
188
+ try:
189
+ direction.flush()
190
+ except OSError:
191
+ reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
192
+ return _result(directions, reason)
193
+ if direction.src_closed and _finish(direction):
194
+ direction.done = True
195
+ if all(d.done for d in directions):
196
+ return _result(directions, REASON_OK)
197
+
198
+ # 等"要写的那个 socket"可写,不是等读端(见 `_Watch`)
199
+ _sync_masks(sel, watch)
200
+
201
+ events = sel.select(timeout=0.25)
202
+ if not events:
203
+ continue
204
+
205
+ alive = True
206
+ for key, event_mask in events:
207
+ entry = key.data
208
+ assert isinstance(entry, _Watch)
209
+ if event_mask & selectors.EVENT_WRITE:
210
+ direction = entry.write_to
211
+ try:
212
+ drained = direction.flush()
213
+ except OSError:
214
+ reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
215
+ return _result(directions, reason)
216
+ if drained:
217
+ if (
218
+ not direction.is_up
219
+ and request_closed_at is not None
220
+ and response_seen
221
+ ):
222
+ # 回程缓冲写空 → 开始计时"上游安静"
223
+ upstream_quiet_since = time.monotonic()
224
+ if direction.src_closed and _finish(direction):
225
+ direction.done = True
226
+ if all(d.done for d in directions):
227
+ return _result(directions, REASON_OK)
228
+ direction = entry.read_from
229
+ if event_mask & selectors.EVENT_READ and not direction.src_closed:
230
+ try:
231
+ data = direction.src.recv(chunk)
232
+ except BlockingIOError:
233
+ continue
234
+ except OSError:
235
+ # 连接被重置(RST)而不是干净 FIN
236
+ direction.src_closed = True
237
+ direction.rst = True
238
+ data = b""
239
+ if data:
240
+ last_activity = time.monotonic()
241
+ if not direction.is_up:
242
+ response_seen = True # 已经看到响应
243
+ upstream_quiet_since = None
244
+ if _append(direction, data):
245
+ try:
246
+ flushed = direction.flush()
247
+ except OSError:
248
+ reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
249
+ return _result(directions, reason)
250
+ if flushed and not direction.is_up and request_closed_at is not None:
251
+ # 响应已全部写回客户端 → 开始计时"上游安静"
252
+ upstream_quiet_since = time.monotonic()
253
+ else:
254
+ # 读到 EOF:把 FIN 传给对端,然后按"这一向已结束"处理
255
+ direction.src_closed = True
256
+ # 顺序不能反:先 flush 再 FIN。缓冲区没写空时 `_finish()`
257
+ # 会拒绝发 FIN,等写事件把缓冲推空后再补,否则 FIN 超车,
258
+ # 上游提前收工,剩下的数据在 Windows 上还会因"关闭后再 send"
259
+ # 报 10058 丢干净(实测 1MB 只回来 128KB)。
260
+ try:
261
+ direction.flush()
262
+ except OSError:
263
+ reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
264
+ return _result(directions, reason)
265
+ direction.done = _finish(direction)
266
+
267
+ if direction.rst:
268
+ # RST:没有 FIN 语义,等另一半自然结束或超时
269
+ if half_closed_at is None:
270
+ half_closed_at = time.monotonic()
271
+ continue
272
+
273
+ if all(d.done for d in directions):
274
+ return _result(directions, REASON_OK)
275
+
276
+ if direction.is_up:
277
+ # 客户端干净 FIN 且请求已全部转发:
278
+ # 给响应留 `response_grace`,而不是傻等对端也 FIN。
279
+ if _peer_has_unread(direction.dst) or request_still_pending():
280
+ # 关键:对端(客户端)写端虽然收到 FIN,但内核里可能还有
281
+ # 未读走的字节(并发 sendall 的最后一段);
282
+ # 或者是我们自己还有没 flush 出去的待发数据。
283
+ # 这两种情况下收工都会丢尾部(表现为 1MB sha256 不一致)。
284
+ # 但也不能无限等(有客户端"发完就 FIN、根本不读"),
285
+ # 所以记一个起始时刻,由 drain_grace 兜底。
286
+ if unread_since is None:
287
+ unread_since = time.monotonic()
288
+ if half_closed_at is None:
289
+ half_closed_at = time.monotonic()
290
+ continue
291
+ unread_since = None
292
+ if request_closed_at is None:
293
+ request_closed_at = time.monotonic()
294
+ else:
295
+ # 上游 FIN:响应到此为止;
296
+ # 只有"客户端此前已 FIN + 没有残留未读 + 请求方向确实发完了"才能立刻收工
297
+ if (
298
+ request_closed_at is not None
299
+ and not _peer_has_unread(direction.dst)
300
+ and not request_still_pending()
301
+ and not response_still_pending() # 回程缓冲没写空就不许收工
302
+ ):
303
+ return _result(directions, REASON_OK)
304
+ if half_closed_at is None:
305
+ half_closed_at = time.monotonic()
306
+ if not alive: # pragma: no cover - 防御性分支
307
+ break
308
+ finally:
309
+ sel.close()
310
+
311
+ return _result(directions, reason)
312
+
313
+
314
+ def _append(direction: _Direction, data: bytes) -> bool:
315
+ """写入待发缓冲;返回 True 表示需要尝试 flush。"""
316
+ if len(direction.buffer) + len(data) > MAX_BUFFER:
317
+ # 背压:超过上限时只保留到上限(多余丢弃会破坏 TCP 语义),
318
+ # 因此这里改为阻塞式写走,保证不丢数据(fd 已是非阻塞,用循环 + 短暂等待)。
319
+ direction.buffer.extend(data)
320
+ _drain_blocking(direction)
321
+ return False
322
+ direction.buffer.extend(data)
323
+ return True
324
+
325
+
326
+ def _drain_blocking(direction: _Direction) -> None:
327
+ """缓冲超限时的兜底:带超时地写走(避免无界内存增长)。"""
328
+ deadline = time.monotonic() + 30.0
329
+ sel = selectors.DefaultSelector()
330
+ try:
331
+ sel.register(direction.dst, selectors.EVENT_WRITE)
332
+ while direction.buffer and time.monotonic() < deadline:
333
+ if direction.flush():
334
+ return
335
+ sel.select(timeout=0.5)
336
+ finally:
337
+ sel.close()
338
+
339
+
340
+ def _mask_for(entry: _Watch) -> int:
341
+ """这个 socket 现在要等什么:读它(某条方向的 src)还是写它(另一条方向的 dst)。"""
342
+ mask = 0
343
+ if not entry.read_from.src_closed:
344
+ mask |= selectors.EVENT_READ
345
+ if entry.write_to.buffer: # 有积压才等可写,否则就是忙等
346
+ mask |= selectors.EVENT_WRITE
347
+ return mask
348
+
349
+
350
+ def _sync_masks(sel: selectors.BaseSelector, watch: list[_Watch]) -> None:
351
+ for entry in watch:
352
+ mask = _mask_for(entry)
353
+ try:
354
+ if mask:
355
+ sel.modify(entry.sock, mask, entry)
356
+ else:
357
+ sel.unregister(entry.sock)
358
+ except (KeyError, ValueError): # pragma: no cover - 已注销
359
+ if mask:
360
+ with contextlib.suppress(KeyError, ValueError):
361
+ sel.register(entry.sock, mask, entry)
362
+
363
+
364
+ def _peer_has_unread(sock: socket.socket) -> bool:
365
+ """对端 socket 里是否还有未被我们读走的字节。
366
+
367
+ 用途:决定"收到 FIN 后能不能立刻收工"。
368
+ 只要内核缓冲里还有数据,收工就会把它丢掉(并发写入场景下表现为偶发截断)。
369
+ 非阻塞 peek 失败时返回 False(保守放行,由超时兜底)。
370
+ """
371
+ try:
372
+ peek = getattr(sock, "recv", None)
373
+ if peek is None: # pragma: no cover - 理论不可达
374
+ return False
375
+ data = sock.recv(1, socket.MSG_PEEK)
376
+ return bool(data)
377
+ except BlockingIOError:
378
+ return False
379
+ except OSError:
380
+ return False
381
+
382
+
383
+ def _result(directions: list[_Direction], reason: str) -> tuple[int, int, str]:
384
+ up = next((d.bytes_moved for d in directions if d.is_up), 0)
385
+ down = next((d.bytes_moved for d in directions if not d.is_up), 0)
386
+ return up, down, reason
387
+
388
+
389
+ def _shutdown_write(sock: socket.socket) -> None:
390
+ # 对端已 RST 时 shutdown 会报 ENOTCONN/WSAENOTCONN,这类错误没有补救动作;
391
+ # 统一忽略(之前的分支两个方向都是 pass,等价于吞掉所有 OSError)。
392
+ with contextlib.suppress(OSError):
393
+ sock.shutdown(socket.SHUT_WR)