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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""路线 A · IPv4/IPv6 报文解析与改写 + 校验和重算。
|
|
2
|
+
|
|
3
|
+
为什么必须有这个模块:WinDivert 改写目的地址/端口后,IP 头校验和与
|
|
4
|
+
TCP/UDP 伪头校验和都会失效,漏算的表现就是"连接卡死在 SYN_SENT"
|
|
5
|
+
。pydivert 的 `recalculate_checksums()` 能做这件事,
|
|
6
|
+
但:
|
|
7
|
+
* 我们要在没有驱动、没有管理员权限的环境里也能验证(单测用字节样本);
|
|
8
|
+
* 需要明确的"改了什么、重算了什么"语义,便于排错。
|
|
9
|
+
|
|
10
|
+
因此本模块只做纯字节运算,不依赖 pydivert/驱动。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ipaddress
|
|
16
|
+
import socket
|
|
17
|
+
import struct
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
IPPROTO_TCP = 6
|
|
21
|
+
IPPROTO_UDP = 17
|
|
22
|
+
|
|
23
|
+
IPV4_MIN_HEADER = 20
|
|
24
|
+
IPV6_HEADER = 40
|
|
25
|
+
TCP_MIN_HEADER = 20
|
|
26
|
+
UDP_HEADER = 8
|
|
27
|
+
|
|
28
|
+
FIN = 0x01
|
|
29
|
+
SYN = 0x02
|
|
30
|
+
RST = 0x04
|
|
31
|
+
PSH = 0x08
|
|
32
|
+
ACK = 0x10
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PacketError(ValueError):
|
|
36
|
+
"""报文无法解析(长度不足/版本不支持/扩展头不支持)。"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ============================================================ 校验和 =======
|
|
40
|
+
def ones_complement_sum(data: bytes, initial: int = 0) -> int:
|
|
41
|
+
"""RFC 1071 的 16 位反码和。"""
|
|
42
|
+
total = initial
|
|
43
|
+
length = len(data)
|
|
44
|
+
if length & 1:
|
|
45
|
+
total += data[-1] << 8
|
|
46
|
+
length -= 1
|
|
47
|
+
# 用 4 字节步进减少 Python 循环次数
|
|
48
|
+
if length:
|
|
49
|
+
total += sum(struct.unpack(f"!{length // 2}H", data[:length]))
|
|
50
|
+
while total >> 16:
|
|
51
|
+
total = (total & 0xFFFF) + (total >> 16)
|
|
52
|
+
return total
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def finish_checksum(total: int) -> int:
|
|
56
|
+
"""反码和 → 校验和(取反)。"""
|
|
57
|
+
return (~total) & 0xFFFF
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def checksum(data: bytes, initial: int = 0) -> int:
|
|
61
|
+
return finish_checksum(ones_complement_sum(data, initial))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def pseudo_header_v4(src: str, dst: str, proto: int, length: int) -> bytes:
|
|
65
|
+
return (
|
|
66
|
+
socket.inet_aton(src)
|
|
67
|
+
+ socket.inet_aton(dst)
|
|
68
|
+
+ bytes([0, proto])
|
|
69
|
+
+ struct.pack("!H", length)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def pseudo_header_v6(src: str, dst: str, proto: int, length: int) -> bytes:
|
|
74
|
+
return (
|
|
75
|
+
socket.inet_pton(socket.AF_INET6, src)
|
|
76
|
+
+ socket.inet_pton(socket.AF_INET6, dst)
|
|
77
|
+
+ struct.pack("!I", length) # 32 位长度
|
|
78
|
+
+ b"\x00\x00\x00" # 3 字节零
|
|
79
|
+
+ bytes([proto])
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def verify_v4_header_checksum(packet: bytes) -> bool:
|
|
84
|
+
"""校验 IPv4 头校验和(重算后应与之相等)。"""
|
|
85
|
+
if len(packet) < IPV4_MIN_HEADER:
|
|
86
|
+
raise PacketError(f"IPv4 报文过短:{len(packet)}")
|
|
87
|
+
ihl = (packet[0] & 0x0F) * 4
|
|
88
|
+
return ones_complement_sum(packet[:ihl]) == 0xFFFF
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def verify_transport_checksum(packet: bytes) -> bool:
|
|
92
|
+
"""校验 IPv4 + TCP/UDP 的传输层校验和(含伪头)。"""
|
|
93
|
+
parsed = ParsedPacket.parse(packet)
|
|
94
|
+
return parsed.transport_checksum_valid
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ============================================================ 报文结构 =====
|
|
98
|
+
@dataclass(slots=True)
|
|
99
|
+
class ParsedPacket:
|
|
100
|
+
"""一条 IPv4/IPv6 报文的结构化视图(支持原地改写后再序列化)。"""
|
|
101
|
+
|
|
102
|
+
raw: bytearray
|
|
103
|
+
is_v6: bool
|
|
104
|
+
ip_header_len: int
|
|
105
|
+
proto: int
|
|
106
|
+
src_addr: str
|
|
107
|
+
dst_addr: str
|
|
108
|
+
src_port: int
|
|
109
|
+
dst_port: int
|
|
110
|
+
tcp_flags: int = 0
|
|
111
|
+
tcp_seq: int = 0
|
|
112
|
+
tcp_ack: int = 0
|
|
113
|
+
# 供排错/统计
|
|
114
|
+
total_length: int = 0
|
|
115
|
+
payload_len: int = 0
|
|
116
|
+
warnings: list[str] = field(default_factory=list)
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------ 解析 -----
|
|
119
|
+
@classmethod
|
|
120
|
+
def parse(cls, packet: bytes | bytearray) -> ParsedPacket:
|
|
121
|
+
raw = bytearray(packet)
|
|
122
|
+
if not raw:
|
|
123
|
+
raise PacketError("空报文")
|
|
124
|
+
version = raw[0] >> 4
|
|
125
|
+
if version == 4:
|
|
126
|
+
return cls._parse_v4(raw)
|
|
127
|
+
if version == 6:
|
|
128
|
+
return cls._parse_v6(raw)
|
|
129
|
+
raise PacketError(f"不支持的 IP 版本:{version}")
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def _parse_v4(cls, raw: bytearray) -> ParsedPacket:
|
|
133
|
+
if len(raw) < IPV4_MIN_HEADER:
|
|
134
|
+
raise PacketError(f"IPv4 报文过短:{len(raw)}")
|
|
135
|
+
ihl = (raw[0] & 0x0F) * 4
|
|
136
|
+
if ihl < IPV4_MIN_HEADER or len(raw) < ihl:
|
|
137
|
+
raise PacketError(f"IPv4 头长度非法:{ihl}")
|
|
138
|
+
total_length = struct.unpack_from("!H", raw, 2)[0] or len(raw)
|
|
139
|
+
proto = raw[9]
|
|
140
|
+
src = socket.inet_ntoa(bytes(raw[12:16]))
|
|
141
|
+
dst = socket.inet_ntoa(bytes(raw[16:20]))
|
|
142
|
+
parsed = cls(
|
|
143
|
+
raw=raw,
|
|
144
|
+
is_v6=False,
|
|
145
|
+
ip_header_len=ihl,
|
|
146
|
+
proto=proto,
|
|
147
|
+
src_addr=src,
|
|
148
|
+
dst_addr=dst,
|
|
149
|
+
src_port=0,
|
|
150
|
+
dst_port=0,
|
|
151
|
+
total_length=total_length,
|
|
152
|
+
)
|
|
153
|
+
parsed._parse_transport()
|
|
154
|
+
return parsed
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def _parse_v6(cls, raw: bytearray) -> ParsedPacket:
|
|
158
|
+
if len(raw) < IPV6_HEADER:
|
|
159
|
+
raise PacketError(f"IPv6 报文过短:{len(raw)}")
|
|
160
|
+
payload_len = struct.unpack_from("!H", raw, 4)[0]
|
|
161
|
+
next_header = raw[6]
|
|
162
|
+
src = socket.inet_ntop(socket.AF_INET6, bytes(raw[8:24]))
|
|
163
|
+
dst = socket.inet_ntop(socket.AF_INET6, bytes(raw[24:40]))
|
|
164
|
+
parsed = cls(
|
|
165
|
+
raw=raw,
|
|
166
|
+
is_v6=True,
|
|
167
|
+
ip_header_len=IPV6_HEADER,
|
|
168
|
+
proto=next_header,
|
|
169
|
+
src_addr=src,
|
|
170
|
+
dst_addr=dst,
|
|
171
|
+
src_port=0,
|
|
172
|
+
dst_port=0,
|
|
173
|
+
total_length=IPV6_HEADER + payload_len if payload_len else len(raw),
|
|
174
|
+
)
|
|
175
|
+
if next_header in (0, 43, 44, 60):
|
|
176
|
+
# 扩展头:本实现不解析
|
|
177
|
+
parsed.proto = 0
|
|
178
|
+
parsed.warnings.append(f"IPv6 扩展头未处理(NextHeader={next_header})")
|
|
179
|
+
parsed.payload_len = payload_len
|
|
180
|
+
return parsed
|
|
181
|
+
parsed._parse_transport()
|
|
182
|
+
return parsed
|
|
183
|
+
|
|
184
|
+
def _parse_transport(self) -> None:
|
|
185
|
+
offset = self.ip_header_len
|
|
186
|
+
available = len(self.raw) - offset
|
|
187
|
+
if self.proto == IPPROTO_TCP:
|
|
188
|
+
if available < TCP_MIN_HEADER:
|
|
189
|
+
raise PacketError(f"TCP 头长度不足:{available}")
|
|
190
|
+
self.src_port, self.dst_port = struct.unpack_from("!HH", self.raw, offset)
|
|
191
|
+
self.tcp_seq = struct.unpack_from("!I", self.raw, offset + 4)[0]
|
|
192
|
+
self.tcp_ack = struct.unpack_from("!I", self.raw, offset + 8)[0]
|
|
193
|
+
self.tcp_flags = self.raw[offset + 13]
|
|
194
|
+
data_offset = (self.raw[offset + 12] >> 4) * 4
|
|
195
|
+
if data_offset < TCP_MIN_HEADER or offset + data_offset > len(self.raw):
|
|
196
|
+
raise PacketError(f"TCP 数据偏移非法:{data_offset}")
|
|
197
|
+
self.payload_len = max(0, available - data_offset)
|
|
198
|
+
elif self.proto == IPPROTO_UDP:
|
|
199
|
+
if available < UDP_HEADER:
|
|
200
|
+
raise PacketError(f"UDP 头长度不足:{available}")
|
|
201
|
+
self.src_port, self.dst_port = struct.unpack_from("!HH", self.raw, offset)
|
|
202
|
+
self.payload_len = max(0, struct.unpack_from("!H", self.raw, offset + 4)[0] - UDP_HEADER)
|
|
203
|
+
else:
|
|
204
|
+
# ICMP 等:只填端口 0,交由上层按 proto 处理
|
|
205
|
+
self.payload_len = max(0, available)
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------ 属性 -----
|
|
208
|
+
@property
|
|
209
|
+
def is_syn_only(self) -> bool:
|
|
210
|
+
"""新连接的首包:SYN 置位且 ACK 未置位。"""
|
|
211
|
+
return self.proto == IPPROTO_TCP and bool(self.tcp_flags & SYN) and not (self.tcp_flags & ACK)
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def is_tcp(self) -> bool:
|
|
215
|
+
return self.proto == IPPROTO_TCP
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def is_udp(self) -> bool:
|
|
219
|
+
return self.proto == IPPROTO_UDP
|
|
220
|
+
|
|
221
|
+
def transport_length(self) -> int:
|
|
222
|
+
"""传输层段长度(含头,用于伪头长度字段)。"""
|
|
223
|
+
return max(0, self.total_length - self.ip_header_len)
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------ 改写 -----
|
|
226
|
+
def set_dst(self, addr: str, port: int) -> None:
|
|
227
|
+
"""改写目的地址与端口(不重算校验和:由 `recalculate_checksums()` 负责)。"""
|
|
228
|
+
ip = ipaddress.ip_address(addr)
|
|
229
|
+
if self.is_v6 != (ip.version == 6):
|
|
230
|
+
raise PacketError(f"地址版本不匹配:报文 is_v6={self.is_v6},地址 {addr}")
|
|
231
|
+
if self.is_v6:
|
|
232
|
+
self.raw[24:40] = ip.packed
|
|
233
|
+
else:
|
|
234
|
+
self.raw[16:20] = ip.packed
|
|
235
|
+
struct.pack_into("!H", self.raw, self.ip_header_len + 2, port)
|
|
236
|
+
self.dst_addr = addr
|
|
237
|
+
self.dst_port = port
|
|
238
|
+
|
|
239
|
+
def set_src(self, addr: str, port: int) -> None:
|
|
240
|
+
"""改写源地址与端口(用于构造回包, UDP 注入需要)。"""
|
|
241
|
+
ip = ipaddress.ip_address(addr)
|
|
242
|
+
if self.is_v6 != (ip.version == 6):
|
|
243
|
+
raise PacketError(f"地址版本不匹配:报文 is_v6={self.is_v6},地址 {addr}")
|
|
244
|
+
if self.is_v6:
|
|
245
|
+
self.raw[8:24] = ip.packed
|
|
246
|
+
else:
|
|
247
|
+
self.raw[12:16] = ip.packed
|
|
248
|
+
struct.pack_into("!H", self.raw, self.ip_header_len, port)
|
|
249
|
+
self.src_addr = addr
|
|
250
|
+
self.src_port = port
|
|
251
|
+
|
|
252
|
+
def recalculate_checksums(self) -> None:
|
|
253
|
+
"""重算 IP 头校验和 + TCP/UDP 校验和(python-can 风格的原地改写后必调)。"""
|
|
254
|
+
if self.is_v6:
|
|
255
|
+
self._recalc_v6()
|
|
256
|
+
else:
|
|
257
|
+
self._recalc_v4()
|
|
258
|
+
|
|
259
|
+
def _recalc_v4(self) -> None:
|
|
260
|
+
# ① IP 头校验和:先清零再算
|
|
261
|
+
struct.pack_into("!H", self.raw, 10, 0)
|
|
262
|
+
ihl = self.ip_header_len
|
|
263
|
+
struct.pack_into("!H", self.raw, 10, checksum(bytes(self.raw[:ihl])))
|
|
264
|
+
# ② 传输层校验和(含伪头)
|
|
265
|
+
if self.proto not in (IPPROTO_TCP, IPPROTO_UDP):
|
|
266
|
+
return
|
|
267
|
+
seg_len = min(self.transport_length(), len(self.raw) - ihl)
|
|
268
|
+
offset = ihl
|
|
269
|
+
# UDP 校验和为 0 表示"不校验"(IPv4 允许),但既然在改包就统算一遍
|
|
270
|
+
struct.pack_into("!H", self.raw, offset + (16 if self.proto == IPPROTO_TCP else 6), 0)
|
|
271
|
+
segment = bytes(self.raw[offset : offset + seg_len])
|
|
272
|
+
pseudo = pseudo_header_v4(self.src_addr, self.dst_addr, self.proto, seg_len)
|
|
273
|
+
value = checksum(segment, ones_complement_sum(pseudo))
|
|
274
|
+
struct.pack_into("!H", self.raw, offset + (16 if self.proto == IPPROTO_TCP else 6), value)
|
|
275
|
+
|
|
276
|
+
def _recalc_v6(self) -> None:
|
|
277
|
+
if self.proto not in (IPPROTO_TCP, IPPROTO_UDP):
|
|
278
|
+
return
|
|
279
|
+
ihl = self.ip_header_len
|
|
280
|
+
seg_len = min(self.transport_length(), len(self.raw) - ihl)
|
|
281
|
+
offset = ihl
|
|
282
|
+
csum_at = offset + (16 if self.proto == IPPROTO_TCP else 6)
|
|
283
|
+
if self.proto == IPPROTO_UDP:
|
|
284
|
+
# IPv6 下 UDP 校验和是强制的,0 视为非法 → 统一重算
|
|
285
|
+
struct.pack_into("!H", self.raw, csum_at, 0)
|
|
286
|
+
else:
|
|
287
|
+
struct.pack_into("!H", self.raw, csum_at, 0)
|
|
288
|
+
segment = bytes(self.raw[offset : offset + seg_len])
|
|
289
|
+
pseudo = pseudo_header_v6(self.src_addr, self.dst_addr, self.proto, seg_len)
|
|
290
|
+
value = checksum(segment, ones_complement_sum(pseudo))
|
|
291
|
+
if self.proto == IPPROTO_UDP and value == 0:
|
|
292
|
+
value = 0xFFFF # RFC 768:算出来是 0 时用 0xFFFF 表示
|
|
293
|
+
struct.pack_into("!H", self.raw, csum_at, value)
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------ 校验和验证 -----
|
|
296
|
+
@property
|
|
297
|
+
def ip_checksum_valid(self) -> bool:
|
|
298
|
+
if self.is_v6:
|
|
299
|
+
return True # IPv6 没有头校验和
|
|
300
|
+
return ones_complement_sum(bytes(self.raw[: self.ip_header_len])) == 0xFFFF
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def transport_checksum_valid(self) -> bool:
|
|
304
|
+
"""把校验和字段一起算进去,正确时反码和应为 0xFFFF。"""
|
|
305
|
+
if self.proto not in (IPPROTO_TCP, IPPROTO_UDP):
|
|
306
|
+
return True
|
|
307
|
+
ihl = self.ip_header_len
|
|
308
|
+
seg_len = min(self.transport_length(), len(self.raw) - ihl)
|
|
309
|
+
if seg_len <= 0:
|
|
310
|
+
return True
|
|
311
|
+
pseudo = (
|
|
312
|
+
pseudo_header_v6(self.src_addr, self.dst_addr, self.proto, seg_len)
|
|
313
|
+
if self.is_v6
|
|
314
|
+
else pseudo_header_v4(self.src_addr, self.dst_addr, self.proto, seg_len)
|
|
315
|
+
)
|
|
316
|
+
total = ones_complement_sum(bytes(self.raw[ihl : ihl + seg_len]), ones_complement_sum(pseudo))
|
|
317
|
+
return total == 0xFFFF
|
|
318
|
+
|
|
319
|
+
def to_bytes(self) -> bytes:
|
|
320
|
+
return bytes(self.raw)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ============================================================ 构造辅助 =====
|
|
324
|
+
def build_tcp_syn(src: str, sport: int, dst: str, dport: int, *, seq: int = 1000) -> bytes:
|
|
325
|
+
"""构造一个 IPv4 TCP SYN 报文(供单测/工具用,校验和正确)。"""
|
|
326
|
+
tcp = struct.pack("!HHIIBBHHH", sport, dport, seq, 0, 5 << 4, SYN, 64240, 0, 0)
|
|
327
|
+
total_len = IPV4_MIN_HEADER + len(tcp)
|
|
328
|
+
src_raw = socket.inet_aton(src)
|
|
329
|
+
dst_raw = socket.inet_aton(dst)
|
|
330
|
+
ip_header = struct.pack("!BBHHHBBH", 0x45, 0, total_len, 0x1234, 0x4000, 64, IPPROTO_TCP, 0) + src_raw + dst_raw
|
|
331
|
+
packet = bytearray(ip_header + tcp)
|
|
332
|
+
struct.pack_into("!H", packet, 10, checksum(bytes(packet[:IPV4_MIN_HEADER])))
|
|
333
|
+
pseudo = pseudo_header_v4(src, dst, IPPROTO_TCP, len(tcp))
|
|
334
|
+
struct.pack_into(
|
|
335
|
+
"!H", packet, IPV4_MIN_HEADER + 16, checksum(tcp, ones_complement_sum(pseudo))
|
|
336
|
+
)
|
|
337
|
+
return bytes(packet)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def build_udp(src: str, sport: int, dst: str, dport: int, payload: bytes = b"") -> bytes:
|
|
341
|
+
"""构造一个 IPv4 UDP 报文(校验和正确)。"""
|
|
342
|
+
udp_len = UDP_HEADER + len(payload)
|
|
343
|
+
udp = struct.pack("!HHHH", sport, dport, udp_len, 0) + payload
|
|
344
|
+
total_len = IPV4_MIN_HEADER + udp_len
|
|
345
|
+
ip_header = (
|
|
346
|
+
struct.pack("!BBHHHBBH", 0x45, 0, total_len, 0x4321, 0x4000, 64, IPPROTO_UDP, 0)
|
|
347
|
+
+ socket.inet_aton(src)
|
|
348
|
+
+ socket.inet_aton(dst)
|
|
349
|
+
)
|
|
350
|
+
packet = bytearray(ip_header + udp)
|
|
351
|
+
struct.pack_into("!H", packet, 10, checksum(bytes(packet[:IPV4_MIN_HEADER])))
|
|
352
|
+
pseudo = pseudo_header_v4(src, dst, IPPROTO_UDP, udp_len)
|
|
353
|
+
struct.pack_into("!H", packet, IPV4_MIN_HEADER + 6, checksum(udp, ones_complement_sum(pseudo)))
|
|
354
|
+
return bytes(packet)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def build_tcp_syn_v6(src: str, sport: int, dst: str, dport: int, *, seq: int = 2000) -> bytes:
|
|
358
|
+
"""构造一个 IPv6 TCP SYN 报文(Next Header = 6,无扩展头)。"""
|
|
359
|
+
tcp = struct.pack("!HHIIBBHHH", sport, dport, seq, 0, 5 << 4, SYN, 64240, 0, 0)
|
|
360
|
+
header = (
|
|
361
|
+
struct.pack("!IHBB", 0x60000000, len(tcp), IPPROTO_TCP, 64)
|
|
362
|
+
+ socket.inet_pton(socket.AF_INET6, src)
|
|
363
|
+
+ socket.inet_pton(socket.AF_INET6, dst)
|
|
364
|
+
)
|
|
365
|
+
packet = bytearray(header + tcp)
|
|
366
|
+
pseudo = pseudo_header_v6(src, dst, IPPROTO_TCP, len(tcp))
|
|
367
|
+
struct.pack_into("!H", packet, IPV6_HEADER + 16, checksum(tcp, ones_complement_sum(pseudo)))
|
|
368
|
+
return bytes(packet)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def redirect_packet(packet: bytes | bytearray, sink_addr: str, sink_port: int) -> bytes:
|
|
372
|
+
"""一步到位:解析 → 改写目的 → 重算校验和。"""
|
|
373
|
+
parsed = ParsedPacket.parse(packet)
|
|
374
|
+
parsed.set_dst(sink_addr, sink_port)
|
|
375
|
+
parsed.recalculate_checksums()
|
|
376
|
+
return parsed.to_bytes()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def reflect_packet(
|
|
380
|
+
packet: bytes | bytearray,
|
|
381
|
+
*,
|
|
382
|
+
src_addr: str,
|
|
383
|
+
src_port: int,
|
|
384
|
+
dst_addr: str,
|
|
385
|
+
dst_port: int,
|
|
386
|
+
) -> bytes:
|
|
387
|
+
"""把报文改写成"从别处发来",供反向路径使用(对应 WinDivert streamdump 的 reflect)。
|
|
388
|
+
|
|
389
|
+
两个方向都要用:
|
|
390
|
+
|
|
391
|
+
* 客户端 → 目标:改写成 `客户端:端口 → sink`,让 sink 收到这条连接;
|
|
392
|
+
* sink → 客户端:改写成 `原始目标:端口 → 客户端`,让客户端的 socket 认这条应答。
|
|
393
|
+
|
|
394
|
+
注意:本函数只改地址与端口,不改方向标志。
|
|
395
|
+
调用方还必须把报文标记为"入向"(`packet.direction = INBOUND`)
|
|
396
|
+
只改地址不翻方向的话,内核不会把它投递给本地 socket,握手永远完不成。
|
|
397
|
+
"""
|
|
398
|
+
parsed = ParsedPacket.parse(packet)
|
|
399
|
+
parsed.set_src(src_addr, src_port)
|
|
400
|
+
parsed.set_dst(dst_addr, dst_port)
|
|
401
|
+
parsed.recalculate_checksums()
|
|
402
|
+
return parsed.to_bytes()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
__all__ = [
|
|
406
|
+
"ACK",
|
|
407
|
+
"FIN",
|
|
408
|
+
"IPPROTO_TCP",
|
|
409
|
+
"IPPROTO_UDP",
|
|
410
|
+
"RST",
|
|
411
|
+
"SYN",
|
|
412
|
+
"PacketError",
|
|
413
|
+
"ParsedPacket",
|
|
414
|
+
"build_tcp_syn",
|
|
415
|
+
"build_tcp_syn_v6",
|
|
416
|
+
"build_udp",
|
|
417
|
+
"checksum",
|
|
418
|
+
"finish_checksum",
|
|
419
|
+
"ones_complement_sum",
|
|
420
|
+
"redirect_packet",
|
|
421
|
+
"reflect_packet",
|
|
422
|
+
"verify_transport_checksum",
|
|
423
|
+
"verify_v4_header_checksum",
|
|
424
|
+
]
|
proxycore/canproxy.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""一句话回答"我现在能不能转发进程流量"。
|
|
2
|
+
|
|
3
|
+
它会检查三件事并给出结论:
|
|
4
|
+
1. 是否管理员(WinDivert 与服务都需要)
|
|
5
|
+
2. WinDivert 驱动是否可用(pydivert 自带已签名的 WinDivert64.sys)
|
|
6
|
+
3. 配置里的 sink 端口是否可用、上游是否可达
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
python -m proxycore can-proxy # 用 dev.yaml 检查
|
|
10
|
+
python -m proxycore can-proxy --config samples/profiles/dev.yaml
|
|
11
|
+
python -m proxycore can-proxy --backend windivert # 指定要检查的后端
|
|
12
|
+
python -m proxycore can-proxy --json # 机器可读输出
|
|
13
|
+
|
|
14
|
+
退出码:0 = 可以转发进程流量;1 = 还不能(会打印缺什么、怎么补)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import ctypes
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import pathlib
|
|
24
|
+
import socket
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from proxycore.config import Config, ConfigError
|
|
28
|
+
from proxycore.proc import IS_WINDOWS
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_admin() -> bool:
|
|
32
|
+
if not IS_WINDOWS:
|
|
33
|
+
# os.geteuid 只在 POSIX 上存在;Windows 的 os 模块里根本没有这个名字,
|
|
34
|
+
# 所以走 getattr 而不是直接属性访问(否则 Windows 上静态检查与运行期都不好看)。
|
|
35
|
+
geteuid = getattr(os, "geteuid", None)
|
|
36
|
+
return bool(geteuid is not None and geteuid() == 0)
|
|
37
|
+
try:
|
|
38
|
+
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
39
|
+
except Exception: # noqa: BLE001
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def windivert_driver() -> tuple[bool, str]:
|
|
44
|
+
"""pydivert 自带 WinDivert64.sys(第三方已签名);能否加载取决于权限与 EDR。"""
|
|
45
|
+
try:
|
|
46
|
+
import pydivert
|
|
47
|
+
except ImportError:
|
|
48
|
+
return False, "未安装 pydivert(pip install pydivert psutil)"
|
|
49
|
+
package_dir = pathlib.Path(pydivert.__file__).parent
|
|
50
|
+
drivers = list(package_dir.rglob("WinDivert*.sys"))
|
|
51
|
+
if not drivers:
|
|
52
|
+
return False, "pydivert 里找不到 WinDivert*.sys"
|
|
53
|
+
driver = drivers[0]
|
|
54
|
+
try:
|
|
55
|
+
handle = pydivert.WinDivert("outbound and tcp and tcp.Syn and tcp.DstPort == 1")
|
|
56
|
+
handle.open()
|
|
57
|
+
handle.close()
|
|
58
|
+
return True, f"可打开({driver.name}, {driver.stat().st_size} 字节)"
|
|
59
|
+
except Exception as exc: # noqa: BLE001 - pydivert 抛的异常类型随版本变
|
|
60
|
+
return False, f"打不开({type(exc).__name__}: {exc})"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def port_state(host: str, port: int) -> tuple[bool, str]:
|
|
64
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
65
|
+
try:
|
|
66
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) if hasattr(
|
|
67
|
+
socket, "SO_EXCLUSIVEADDRUSE"
|
|
68
|
+
) else None
|
|
69
|
+
sock.bind((host, port))
|
|
70
|
+
return True, "空闲(可以监听)"
|
|
71
|
+
except OSError as exc:
|
|
72
|
+
return False, f"被占用或不可用({exc})"
|
|
73
|
+
finally:
|
|
74
|
+
sock.close()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def upstream_state(host: str, port: int) -> tuple[bool, str]:
|
|
78
|
+
try:
|
|
79
|
+
with socket.create_connection((host, port), timeout=1.5):
|
|
80
|
+
return True, "可达"
|
|
81
|
+
except OSError as exc:
|
|
82
|
+
return False, f"不可达({exc})"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _repo_root_str() -> str:
|
|
87
|
+
"""仓库根(已安装场景下退回当前目录)。路径解析复用 `cli` 里的实现。"""
|
|
88
|
+
from .cli import repo_root
|
|
89
|
+
|
|
90
|
+
root = repo_root()
|
|
91
|
+
return str(root) if root is not None else os.getcwd()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_config_path(raw: str) -> pathlib.Path:
|
|
95
|
+
from .cli import resolve_config_path as _resolve
|
|
96
|
+
|
|
97
|
+
return _resolve(raw)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main(argv: list[str] | None = None) -> int:
|
|
101
|
+
ap = argparse.ArgumentParser(prog="procproxy can-proxy", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
102
|
+
ap.add_argument("--config", default="samples/profiles/dev.yaml")
|
|
103
|
+
ap.add_argument("--backend", default="", choices=["", "null", "windivert"])
|
|
104
|
+
ap.add_argument("--json", action="store_true")
|
|
105
|
+
args = ap.parse_args(argv)
|
|
106
|
+
|
|
107
|
+
admin = is_admin()
|
|
108
|
+
wd_ok, wd_detail = windivert_driver()
|
|
109
|
+
|
|
110
|
+
config: Config | None = None
|
|
111
|
+
config_error = ""
|
|
112
|
+
try:
|
|
113
|
+
config = Config.load(resolve_config_path(args.config))
|
|
114
|
+
except (ConfigError, OSError) as exc:
|
|
115
|
+
config_error = str(exc)
|
|
116
|
+
|
|
117
|
+
backend = args.backend or (config.service.backend if config else "windivert")
|
|
118
|
+
sink_host = config.service.sink.host if config else "127.0.0.1"
|
|
119
|
+
sink_port = config.service.sink.port if config else 19080
|
|
120
|
+
sink_free, sink_detail = port_state(sink_host, sink_port)
|
|
121
|
+
|
|
122
|
+
upstreams = []
|
|
123
|
+
if config is not None:
|
|
124
|
+
for alias, upstream in config.profile.upstreams.items():
|
|
125
|
+
ok, detail = upstream_state(upstream.host, upstream.port)
|
|
126
|
+
upstreams.append({"alias": alias, "endpoint": f"{upstream.host}:{upstream.port}",
|
|
127
|
+
"reachable": ok, "detail": detail})
|
|
128
|
+
|
|
129
|
+
# ---- 结论 ----
|
|
130
|
+
if backend == "null":
|
|
131
|
+
can_proxy = False
|
|
132
|
+
verdict = "backend=null:只做联调,不抓包、不改道 → 不能转发真实进程流量"
|
|
133
|
+
howto = [
|
|
134
|
+
"把 backend 换成 windivert 并以管理员运行",
|
|
135
|
+
"python -m proxycore run --config samples/profiles/dev.yaml --backend windivert",
|
|
136
|
+
]
|
|
137
|
+
elif backend == "windivert":
|
|
138
|
+
if not IS_WINDOWS:
|
|
139
|
+
can_proxy, verdict, howto = False, "WinDivert 仅 Windows 可用", ["在 Windows 上跑"]
|
|
140
|
+
elif not admin:
|
|
141
|
+
can_proxy = False
|
|
142
|
+
verdict = "WinDivert 已就绪,但当前不是管理员 → 打不开驱动,无法改道"
|
|
143
|
+
howto = [
|
|
144
|
+
"以管理员打开 PowerShell / 终端(右键 → 以管理员身份运行),然后:",
|
|
145
|
+
" cd /d " + _repo_root_str(),
|
|
146
|
+
" python -m proxycore doctor --backend windivert",
|
|
147
|
+
" python -m proxycore run --config samples/profiles/dev.yaml --backend windivert --log-level DEBUG",
|
|
148
|
+
"另一个窗口:curl.exe https://example.invalid/ -o NUL",
|
|
149
|
+
]
|
|
150
|
+
elif not wd_ok:
|
|
151
|
+
can_proxy = False
|
|
152
|
+
verdict = f"管理员 OK,但 WinDivert 打不开:{wd_detail}"
|
|
153
|
+
howto = ["常见原因:EDR/杀软拦截 WinDivert 驱动;或已有别的 WinDivert 实例",
|
|
154
|
+
"诊断:sc query WinDivert"]
|
|
155
|
+
else:
|
|
156
|
+
can_proxy = True
|
|
157
|
+
verdict = "WinDivert 路线 A 已就绪(驱动与权限)"
|
|
158
|
+
howto = [
|
|
159
|
+
"python -m proxycore run --config samples/profiles/dev.yaml --backend windivert --log-level DEBUG",
|
|
160
|
+
"另一窗口验证:curl.exe https://example.invalid/ -o NUL",
|
|
161
|
+
"python -m proxycore check-route --target-pid curl.exe --expect proxied --live-required",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
# 能力检查之外还要确认"现在真的能跑":配置可用、sink 能绑、上游可达。
|
|
165
|
+
# 只报 can_proxy=True 却让 proxy 规则在启动/拨号时全部失败,等于误导。
|
|
166
|
+
if config is None:
|
|
167
|
+
can_proxy = False
|
|
168
|
+
verdict += f";但配置不可用:{config_error or '未知错误'}"
|
|
169
|
+
elif not sink_free:
|
|
170
|
+
can_proxy = False
|
|
171
|
+
verdict += f";但 sink 端口 {sink_host}:{sink_port} {sink_detail}"
|
|
172
|
+
elif not upstreams:
|
|
173
|
+
can_proxy = False
|
|
174
|
+
verdict += ";但配置里没有上游,命中 proxy 规则也没有代理可用"
|
|
175
|
+
elif not any(item["reachable"] for item in upstreams):
|
|
176
|
+
can_proxy = False
|
|
177
|
+
verdict += ";但配置中的所有上游都不可达,命中 proxy 的连接会失败(不会直连)"
|
|
178
|
+
|
|
179
|
+
if can_proxy:
|
|
180
|
+
verdict += ",可以转发进程流量"
|
|
181
|
+
|
|
182
|
+
report = {
|
|
183
|
+
"platform": sys.platform,
|
|
184
|
+
"is_admin": admin,
|
|
185
|
+
"backend": backend,
|
|
186
|
+
"windivert_driver": {"ok": wd_ok, "detail": wd_detail},
|
|
187
|
+
"config": {"ok": config is not None, "error": config_error},
|
|
188
|
+
"sink": {"endpoint": f"{sink_host}:{sink_port}", "free": sink_free, "detail": sink_detail},
|
|
189
|
+
"upstreams": upstreams,
|
|
190
|
+
"can_proxy_process_traffic": can_proxy,
|
|
191
|
+
"verdict": verdict,
|
|
192
|
+
"how_to": howto,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if args.json:
|
|
196
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
197
|
+
return 0 if can_proxy else 1
|
|
198
|
+
|
|
199
|
+
print()
|
|
200
|
+
print("=" * 70)
|
|
201
|
+
print(" 能不能转发进程流量?")
|
|
202
|
+
print("=" * 70)
|
|
203
|
+
rows = [
|
|
204
|
+
("平台", sys.platform, IS_WINDOWS),
|
|
205
|
+
("管理员权限", "是" if admin else "否(关键!)", admin),
|
|
206
|
+
("WinDivert 驱动", wd_detail, wd_ok),
|
|
207
|
+
(f"sink 端口 {sink_host}:{sink_port}", sink_detail, sink_free),
|
|
208
|
+
]
|
|
209
|
+
for name, value, ok in rows:
|
|
210
|
+
print(f" [{'OK ' if ok else 'NO '}] {name:<24} {value}")
|
|
211
|
+
for item in upstreams:
|
|
212
|
+
print(f" [{'OK ' if item['reachable'] else 'NO '}] 上游 {item['alias']:<17} "
|
|
213
|
+
f"{item['endpoint']} {item['detail']}")
|
|
214
|
+
if config_error:
|
|
215
|
+
print(f" [NO ] 配置 {args.config}: {config_error}")
|
|
216
|
+
|
|
217
|
+
print()
|
|
218
|
+
print(f"使用后端:{backend}")
|
|
219
|
+
print(f"结论:{'✅ ' if can_proxy else '❌ '}{verdict}")
|
|
220
|
+
print()
|
|
221
|
+
if howto:
|
|
222
|
+
print("怎么才能跑起来:")
|
|
223
|
+
for line in howto:
|
|
224
|
+
print(f" {line}")
|
|
225
|
+
print()
|
|
226
|
+
return 0 if can_proxy else 1
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__": # pragma: no cover - 也支持以模块方式直接运行
|
|
229
|
+
raise SystemExit(main())
|