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,106 @@
1
+ """SOCKS4 / SOCKS4a 客户端。
2
+
3
+ * 认证:SOCKS4 只有 `USERID`,没有口令认证 → `auth: ntlm/basic` 一律视为
4
+ `unsupported`(非 Windows 平台的 NTLM 必须标注 unsupported);
5
+ * SOCKS4a:当目标不是 IP 字面量时用 `0.0.0.x`(x≠0)+ USERID 后跟域名的形式传域名
6
+ (远端解析)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import ipaddress
13
+ import socket
14
+ from typing import Any
15
+
16
+ from ..types import Endpoint
17
+ from .base import (
18
+ BaseDialer,
19
+ TargetRefused,
20
+ UnsupportedAuth,
21
+ UpstreamDown,
22
+ optional_hostname,
23
+ recv_exact,
24
+ )
25
+
26
+ VER = 0x04
27
+ CMD_CONNECT = 0x01
28
+ CMD_BIND = 0x02
29
+
30
+ # VN=0 时的 CD 码语义
31
+ CD_REQUEST_GRANTED = 0x5A
32
+ CD_REJECTED = 0x5B
33
+ CD_NO_IDENTD = 0x5C
34
+ CD_IDENTD_MISMATCH = 0x5D
35
+
36
+ CD_MAP: dict[int, str] = {
37
+ CD_REQUEST_GRANTED: "request granted",
38
+ CD_REJECTED: "request rejected or failed",
39
+ CD_NO_IDENTD: "request rejected: identd not reachable",
40
+ CD_IDENTD_MISMATCH: "request rejected: identd user mismatch",
41
+ }
42
+
43
+ # 需要"远端解析域名"的标记地址(SOCKS4a)
44
+ SOCKS4A_MARKER = "0.0.0.1"
45
+
46
+
47
+ class Socks4Dialer(BaseDialer):
48
+ kind = "socks4"
49
+ supports_udp_flag = False
50
+
51
+ def __init__(self, upstream: Any, *, timeout: float = 15.0, resolver: Any = None) -> None:
52
+ super().__init__(upstream, timeout=timeout)
53
+ self.resolve_alias = resolver
54
+
55
+ def _userid(self) -> bytes:
56
+ return str(getattr(self.upstream, "user", "") or "").encode("utf-8") + b"\x00"
57
+
58
+ def _check_auth(self) -> None:
59
+ password = str(getattr(self.upstream, "password", "") or "")
60
+ auth = str(getattr(self.upstream, "auth", "none") or "none").lower()
61
+ if password or auth in {"basic", "ntlm"}:
62
+ raise UnsupportedAuth(
63
+ f"上游 {self.alias}: SOCKS4 不支持口令认证(auth={auth});"
64
+ "请改用 socks5,或去掉 password_ref"
65
+ )
66
+
67
+ def _request(self, sock: socket.socket, dst: Endpoint, hostname: str | None) -> None:
68
+ address = hostname or dst.addr
69
+ try:
70
+ ip = ipaddress.ip_address(address)
71
+ packed = ip.packed
72
+ tail = b""
73
+ except ValueError:
74
+ # SOCKS4a:0.0.0.x + 域名
75
+ packed = socket.inet_aton(SOCKS4A_MARKER)
76
+ tail = address.encode("idna") + b"\x00"
77
+ sock.sendall(bytes([VER, CMD_CONNECT]) + dst.port.to_bytes(2, "big") + packed + self._userid() + tail)
78
+ reply = recv_exact(sock, 8)
79
+ if reply[0] not in (0x00, 0x04):
80
+ raise UpstreamDown(f"上游 SOCKS4 回复版本异常: 0x{reply[0]:02x}")
81
+ code = reply[1]
82
+ if code != CD_REQUEST_GRANTED:
83
+ text = CD_MAP.get(code, f"unknown cd 0x{code:02x}")
84
+ if code == CD_REJECTED:
85
+ raise TargetRefused(f"目标不可达(经 SOCKS4 代理){address}:{dst.port}: {text}")
86
+ raise UpstreamDown(f"SOCKS4 请求被拒 {address}:{dst.port}: {text}")
87
+
88
+ def tunnel_over(self, sock: socket.socket, dst: Endpoint, hostname: str | None = None) -> socket.socket:
89
+ self._check_auth()
90
+ sock.settimeout(self.timeout)
91
+ address = optional_hostname(dst, hostname) or dst.addr
92
+ self._request(sock, dst, address)
93
+ return sock
94
+
95
+ def dial_tcp_sync(self, dst: Endpoint, hostname: str | None = None) -> socket.socket:
96
+ self._check_auth()
97
+ remote = optional_hostname(dst, hostname) or dst.addr
98
+ sock = self._connect_upstream()
99
+ try:
100
+ self._request(sock, dst, remote)
101
+ except BaseException:
102
+ with contextlib.suppress(OSError):
103
+ sock.close()
104
+ raise
105
+ sock.settimeout(None)
106
+ return sock
@@ -0,0 +1,266 @@
1
+ """SOCKS5 客户端(RFC 1928 / RFC 1929),含 UDP ASSOCIATE。
2
+
3
+ 硬约束:
4
+ * `hostname` 非空时必须用 `ATYP=0x03` 传域名(远端解析,防 DNS 泄漏);
5
+ * 认证失败 → `AuthFailed`;目标不可达 → `TargetRefused`;上游不可达 → `UpstreamDown`;
6
+ * 分片 UDP(`FRAG != 0`)→ 丢弃并记 WARN。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import contextlib
13
+ import socket
14
+ import struct
15
+ import time
16
+ from typing import Any
17
+
18
+ from ..types import Endpoint, UdpAssociation
19
+ from .base import (
20
+ AuthFailed,
21
+ BaseDialer,
22
+ TargetRefused,
23
+ UpstreamDown,
24
+ UpstreamError,
25
+ optional_hostname,
26
+ pack_port,
27
+ parse_port,
28
+ recv_exact,
29
+ )
30
+
31
+ VER = 0x05
32
+ AUTH_NONE = 0x00
33
+ AUTH_USERPASS = 0x02
34
+ AUTH_NO_ACCEPTABLE = 0xFF
35
+
36
+ CMD_CONNECT = 0x01
37
+ CMD_BIND = 0x02
38
+ CMD_UDP_ASSOCIATE = 0x03
39
+
40
+ ATYP_V4 = 0x01
41
+ ATYP_DOMAIN = 0x03
42
+ ATYP_V6 = 0x04
43
+
44
+ # RFC 1928 REP 码 → 语义
45
+ REP_SUCCESS = 0x00
46
+ REP_GENERAL_FAILURE = 0x01
47
+ REP_NOT_ALLOWED = 0x02
48
+ REP_NET_UNREACHABLE = 0x03
49
+ REP_HOST_UNREACHABLE = 0x04
50
+ REP_CONN_REFUSED = 0x05
51
+ REP_TTL_EXPIRED = 0x06
52
+ REP_CMD_UNSUPPORTED = 0x07
53
+ REP_ATYP_UNSUPPORTED = 0x08
54
+
55
+ REP_MAP: dict[int, str] = {
56
+ REP_SUCCESS: "success",
57
+ REP_GENERAL_FAILURE: "general failure",
58
+ REP_NOT_ALLOWED: "connection not allowed by ruleset",
59
+ REP_NET_UNREACHABLE: "network unreachable",
60
+ REP_HOST_UNREACHABLE: "host unreachable",
61
+ REP_CONN_REFUSED: "connection refused",
62
+ REP_TTL_EXPIRED: "TTL expired",
63
+ REP_CMD_UNSUPPORTED: "command not supported",
64
+ REP_ATYP_UNSUPPORTED: "address type not supported",
65
+ }
66
+
67
+ # 这些 REP 表示"上游本身没问题,是目标的事" → TargetRefused
68
+ _TARGET_REPS = {REP_CONN_REFUSED, REP_HOST_UNREACHABLE, REP_NET_UNREACHABLE, REP_TTL_EXPIRED}
69
+
70
+
71
+ def encode_address(host: str, port: int) -> bytes:
72
+ """按 ATYP 规则编码地址(域名优先透传,交上游解析)。"""
73
+ kind, raw, atyp = classify_address(host)
74
+ if kind == "domain":
75
+ payload = raw.encode("idna") if any(ord(c) > 127 for c in raw) else raw.encode("ascii", "ignore")
76
+ if len(payload) > 255:
77
+ raise UpstreamDown(f"域名过长({len(payload)} > 255): {raw}")
78
+ return bytes([ATYP_DOMAIN, len(payload)]) + payload + pack_port(port)
79
+ return bytes([atyp]) + raw + pack_port(port)
80
+
81
+
82
+ def classify_address(host: str) -> tuple[str, Any, int]:
83
+ """返回 `("ipv4"|"ipv6"|"domain", raw, atyp)`。"""
84
+ import ipaddress
85
+
86
+ try:
87
+ ip = ipaddress.ip_address(host)
88
+ except ValueError:
89
+ return "domain", host, ATYP_DOMAIN
90
+ if ip.version == 6:
91
+ return "ipv6", ip.packed, ATYP_V6
92
+ return "ipv4", ip.packed, ATYP_V4
93
+
94
+
95
+ def read_address(sock: socket.socket, atyp: int) -> Endpoint:
96
+ if atyp == ATYP_V4:
97
+ raw = recv_exact(sock, 4)
98
+ port = parse_port(recv_exact(sock, 2))
99
+ return Endpoint(addr=socket.inet_ntoa(raw), port=port, is_v6=False)
100
+ if atyp == ATYP_V6:
101
+ raw = recv_exact(sock, 16)
102
+ port = parse_port(recv_exact(sock, 2))
103
+ return Endpoint(addr=socket.inet_ntop(socket.AF_INET6, raw), port=port, is_v6=True)
104
+ if atyp == ATYP_DOMAIN:
105
+ length = recv_exact(sock, 1)[0]
106
+ host = recv_exact(sock, length).decode("latin1")
107
+ port = parse_port(recv_exact(sock, 2))
108
+ return Endpoint(addr=host, port=port, is_v6=False)
109
+ raise UpstreamDown(f"上游返回了不支持的 ATYP=0x{atyp:02x}")
110
+
111
+
112
+ def rep_to_error(rep: int, target: str) -> UpstreamError:
113
+ text = REP_MAP.get(rep, f"unknown rep 0x{rep:02x}")
114
+ if rep in _TARGET_REPS:
115
+ return TargetRefused(f"目标不可达(经代理){target}: {text}")
116
+ return UpstreamDown(f"上游拒绝请求 {target}: {text}")
117
+
118
+
119
+ class Socks5Dialer(BaseDialer):
120
+ kind = "socks5"
121
+ supports_udp_flag = True
122
+
123
+ def __init__(self, upstream: Any, *, timeout: float = 15.0, resolver: Any = None) -> None:
124
+ super().__init__(upstream, timeout=timeout)
125
+ # 供 ChainDialer 使用(把 chain 别名解析成 Upstream 对象)
126
+ self.resolve_alias = resolver
127
+
128
+ # ------------------------------------------------------------ 握手 ----
129
+ def _greeting(self) -> bytes:
130
+ user = str(getattr(self.upstream, "user", "") or "")
131
+ password = str(getattr(self.upstream, "password", "") or "")
132
+ if user or password:
133
+ return bytes([VER, 2, AUTH_NONE, AUTH_USERPASS])
134
+ return bytes([VER, 1, AUTH_NONE])
135
+
136
+ def handshake(self, sock: socket.socket) -> None:
137
+ sock.sendall(self._greeting())
138
+ reply = recv_exact(sock, 2)
139
+ if reply[0] != VER:
140
+ raise UpstreamDown(f"上游 SOCKS 版本不匹配: {reply[0]}")
141
+ method = reply[1]
142
+ if method == AUTH_NO_ACCEPTABLE:
143
+ raise AuthFailed("上游不接受任何认证方式(0xFF)")
144
+ if method == AUTH_USERPASS:
145
+ self._userpass_auth(sock)
146
+ elif method != AUTH_NONE:
147
+ raise UpstreamDown(f"上游选择了不支持的认证方式 0x{method:02x}")
148
+
149
+ def _userpass_auth(self, sock: socket.socket) -> None:
150
+ user = str(getattr(self.upstream, "user", "") or "").encode("utf-8")
151
+ password = str(getattr(self.upstream, "password", "") or "").encode("utf-8")
152
+ if len(user) > 255 or len(password) > 255:
153
+ raise AuthFailed("用户名/口令超过 255 字节")
154
+ sock.sendall(bytes([0x01, len(user)]) + user + bytes([len(password)]) + password)
155
+ reply = recv_exact(sock, 2)
156
+ if reply[1] != 0x00:
157
+ raise AuthFailed(f"上游认证失败(SOCKS5 用户名/口令,status=0x{reply[1]:02x})")
158
+
159
+ # ------------------------------------------------------------ 请求 ----
160
+ def _request(self, sock: socket.socket, cmd: int, dst: Endpoint, hostname: str | None) -> Endpoint:
161
+ address = hostname or dst.addr
162
+ sock.sendall(bytes([VER, cmd, 0x00]) + encode_address(address, dst.port))
163
+ head = recv_exact(sock, 4)
164
+ if head[0] != VER:
165
+ raise UpstreamDown(f"上游回复版本异常: {head[0]}")
166
+ if head[1] != REP_SUCCESS:
167
+ raise rep_to_error(head[1], f"{address}:{dst.port}")
168
+ return read_address(sock, head[3])
169
+
170
+ def tunnel_over(self, sock: socket.socket, dst: Endpoint, hostname: str | None = None) -> socket.socket:
171
+ """在已建立的连接之上再开一条 CONNECT(链式代理用)。"""
172
+ address = optional_hostname(dst, hostname) or dst.addr
173
+ sock.settimeout(self.timeout)
174
+ self.handshake(sock)
175
+ self._request(sock, CMD_CONNECT, dst, address)
176
+ return sock
177
+
178
+ # ------------------------------------------------------------ 拨号 ----
179
+ def dial_tcp_sync(self, dst: Endpoint, hostname: str | None = None) -> socket.socket:
180
+ remote = optional_hostname(dst, hostname) or dst.addr
181
+ sock = self._connect_upstream()
182
+ try:
183
+ self.handshake(sock)
184
+ self._request(sock, CMD_CONNECT, dst, remote)
185
+ except BaseException:
186
+ with contextlib.suppress(OSError):
187
+ sock.close()
188
+ raise
189
+ sock.settimeout(None)
190
+ return sock
191
+
192
+ # ------------------------------------------------------ UDP ASSOCIATE --
193
+ def dial_udp_sync(self, dst: Endpoint) -> UdpAssociation:
194
+ """建立 UDP 关联;按 RFC 1928 用 `0.0.0.0:0` 表示"还不知道要发去哪儿"。"""
195
+ if not self.supports_udp():
196
+ raise UpstreamDown(f"上游 {self.alias} 未启用 UDP(配置 upstream.udp=true 才可用)")
197
+ sock = self._connect_upstream()
198
+ try:
199
+ self.handshake(sock)
200
+ relay = self._request(sock, CMD_UDP_ASSOCIATE, Endpoint(addr="0.0.0.0", port=0), None)
201
+ except BaseException:
202
+ with contextlib.suppress(OSError):
203
+ sock.close()
204
+ raise
205
+ # 上游可能返回 0.0.0.0 / 内网地址 → 按"控制连接的同源地址"处理
206
+ peer_host = str(sock.getpeername()[0])
207
+ relay_host = relay.addr
208
+ if relay_host in {"0.0.0.0", "::"} or relay.port == 0 or relay_host.startswith("127."):
209
+ relay_host = peer_host
210
+ return UdpAssociation(
211
+ relay=Endpoint(addr=relay_host, port=relay.port),
212
+ control_sock=sock,
213
+ upstream=self.alias,
214
+ created_at=time.time(),
215
+ )
216
+
217
+ async def dial_udp(self, dst: Endpoint) -> UdpAssociation:
218
+ # UDP ASSOCIATE 是完整 TCP 握手 + 认证,不能直接跑在事件循环上。
219
+ return await asyncio.to_thread(self.dial_udp_sync, dst)
220
+
221
+
222
+ # ------------------------------------------------------------- 报文编解码 --
223
+ def build_udp_datagram(dst: Endpoint, payload: bytes, hostname: str | None = None) -> bytes:
224
+ """`RSV(2) | FRAG(1) | ATYP | ADDR | PORT | DATA`(FRAG 必须为 0)。"""
225
+ header = b"\x00\x00\x00" + encode_address(optional_hostname(dst, hostname) or dst.addr, dst.port)
226
+ return header + payload
227
+
228
+
229
+ def _require(packet: bytes, offset: int, length: int, what: str) -> None:
230
+ """回包来自不可信网络,任何字段都必须先确认长度再切。"""
231
+ if offset + length > len(packet):
232
+ raise UpstreamDown(f"UDP 回包过短:{what} 需要 {length} 字节,实际只剩 {len(packet) - offset}")
233
+
234
+
235
+ def parse_udp_datagram(packet: bytes) -> tuple[Endpoint, bytes]:
236
+ """解析回包;FRAG != 0 或长度不足直接丢弃(抛 `UpstreamDown`,调用方记 WARN)。
237
+
238
+ 报文布局:`RSV(2) | FRAG(1) | ATYP(1) | ADDR | PORT(2) | DATA`
239
+ """
240
+ if len(packet) < 4:
241
+ raise UpstreamDown(f"UDP 报文过短({len(packet)} 字节)")
242
+ _rsv1, _rsv2, frag, atyp = packet[0], packet[1], packet[2], packet[3]
243
+ if frag != 0x00:
244
+ raise UpstreamDown(f"不支持分片 UDP(FRAG=0x{frag:02x}),按协议规定丢弃")
245
+ offset = 4 # ADDR 从 ATYP 之后开始
246
+ if atyp == ATYP_V4:
247
+ _require(packet, offset, 4, "IPv4 地址")
248
+ addr = socket.inet_ntoa(packet[offset : offset + 4])
249
+ offset += 4
250
+ elif atyp == ATYP_V6:
251
+ _require(packet, offset, 16, "IPv6 地址")
252
+ addr = socket.inet_ntop(socket.AF_INET6, packet[offset : offset + 16])
253
+ offset += 16
254
+ elif atyp == ATYP_DOMAIN:
255
+ _require(packet, offset, 1, "域名长度")
256
+ length = packet[offset]
257
+ offset += 1
258
+ _require(packet, offset, length, "域名")
259
+ addr = packet[offset : offset + length].decode("latin1")
260
+ offset += length
261
+ else:
262
+ raise UpstreamDown(f"UDP 回包 ATYP 非法: 0x{atyp:02x}")
263
+ _require(packet, offset, 2, "端口")
264
+ port = struct.unpack(">H", packet[offset : offset + 2])[0]
265
+ offset += 2
266
+ return Endpoint(addr=addr, port=port, is_v6=atyp == ATYP_V6), packet[offset:]