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,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:]
|