fix: 修复 SOCKS5 账号密码认证时的 TCP 流读取 bug
问题原因:RFC 1929 认证请求是一个连续的 TCP 数据包, 原代码多次独立调用 reader.read(),第一次 read 已经把 整个认证请求都读入了,后续 read 调用阻塞超时。 修复方案:改用累积缓冲区方式解析认证请求,先累积所有 到达的数据,再逐步解析用户名和密码,避免不必要的阻塞
This commit is contained in:
+64
-35
@@ -143,35 +143,52 @@ class ConnectionHandler:
|
||||
if self.config.auth_method != "userpass":
|
||||
return True
|
||||
|
||||
# 读认证请求头
|
||||
header = await asyncio.wait_for(self.reader.read(256),
|
||||
timeout=self.config.timeout)
|
||||
if len(header) < 2:
|
||||
return False
|
||||
ver, ulen = struct.unpack("!BB", header[:2])
|
||||
# RFC 1929 认证请求格式:
|
||||
# +----+------+----------+------+----------+
|
||||
# |VER | ULEN | UNAME | PLEN | PASSWD |
|
||||
# +----+------+----------+------+----------+
|
||||
# | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
|
||||
# +----+------+----------+------+----------+
|
||||
# 注意:整个请求是一个连续 TCP 流,可能一次性全部到达
|
||||
|
||||
data = b""
|
||||
# 1. 先读版本和用户名长度 (2 字节)
|
||||
while len(data) < 2:
|
||||
chunk = await asyncio.wait_for(self.reader.read(256),
|
||||
timeout=self.config.timeout)
|
||||
if not chunk:
|
||||
return False
|
||||
data += chunk
|
||||
|
||||
ver, ulen = struct.unpack("!BB", data[:2])
|
||||
if ver != 1:
|
||||
return False
|
||||
|
||||
# 读用户名
|
||||
username = await asyncio.wait_for(self.reader.read(ulen),
|
||||
timeout=self.config.timeout)
|
||||
if len(username) != ulen:
|
||||
return False
|
||||
username = username.decode("utf-8", errors="replace")
|
||||
offset = 2
|
||||
|
||||
# 读密码长度
|
||||
plen_byte = await asyncio.wait_for(self.reader.read(1),
|
||||
timeout=self.config.timeout)
|
||||
if not plen_byte:
|
||||
return False
|
||||
plen = plen_byte[0]
|
||||
# 2. 读取剩余需要的数据
|
||||
needed = ulen + 1 # 用户名 + 密码长度字节
|
||||
while len(data) < offset + needed:
|
||||
chunk = await asyncio.wait_for(self.reader.read(256),
|
||||
timeout=self.config.timeout)
|
||||
if not chunk:
|
||||
return False
|
||||
data += chunk
|
||||
|
||||
# 读密码
|
||||
passwd = await asyncio.wait_for(self.reader.read(plen),
|
||||
timeout=self.config.timeout)
|
||||
if len(passwd) != plen:
|
||||
return False
|
||||
passwd = passwd.decode("utf-8", errors="replace")
|
||||
username = data[offset:offset + ulen].decode("utf-8", errors="replace")
|
||||
offset += ulen
|
||||
plen = data[offset]
|
||||
offset += 1
|
||||
|
||||
# 3. 读取密码
|
||||
while len(data) < offset + plen:
|
||||
chunk = await asyncio.wait_for(self.reader.read(256),
|
||||
timeout=self.config.timeout)
|
||||
if not chunk:
|
||||
return False
|
||||
data += chunk
|
||||
|
||||
passwd = data[offset:offset + plen].decode("utf-8", errors="replace")
|
||||
|
||||
# 验证(使用 bcrypt 兼容验证)
|
||||
user = self.user_service.get_user(username)
|
||||
@@ -221,20 +238,32 @@ class ConnectionHandler:
|
||||
return None, None, None, None
|
||||
|
||||
async def _reply(self, rep, bnd_addr="0.0.0.0", bnd_port=0):
|
||||
"""发送 SOCKS5 响应。"""
|
||||
addr_bytes = b"\x00\x00\x00\x00"
|
||||
if bnd_addr and "." not in str(bnd_addr):
|
||||
# IPv6
|
||||
"""发送 SOCKS5 响应 (RFC 1928 §6).
|
||||
|
||||
格式: VER(1) REP(1) RSV(1) ATYP(1) BND.ADDR(4/16字节) BND.PORT(2字节)
|
||||
IPv4 是 4 字节 packed, IPv6 是 16 字节, 域名不允许出现在 reply.
|
||||
"""
|
||||
atyp = ATYP_IPV4
|
||||
if bnd_addr:
|
||||
try:
|
||||
addr_bytes = ipaddress.IPv6Address(str(bnd_addr)).packed
|
||||
except Exception:
|
||||
pass
|
||||
ip = ipaddress.ip_address(str(bnd_addr))
|
||||
if ip.version == 6:
|
||||
atyp = ATYP_IPV6
|
||||
addr_bytes = ip.packed
|
||||
else:
|
||||
atyp = ATYP_IPV4
|
||||
addr_bytes = ip.packed
|
||||
except ValueError:
|
||||
atyp = ATYP_IPV4
|
||||
addr_bytes = b"\x00\x00\x00\x00"
|
||||
else:
|
||||
atyp = ATYP_IPV4
|
||||
addr_bytes = b"\x00\x00\x00\x00"
|
||||
|
||||
self.writer.write(struct.pack(
|
||||
"!BBBB5sH",
|
||||
SOCKS_VERSION, rep, 0x00, ATYP_IPV4,
|
||||
addr_bytes, bnd_port
|
||||
))
|
||||
"!BBBB",
|
||||
SOCKS_VERSION, rep, 0x00, atyp,
|
||||
) + addr_bytes + struct.pack("!H", bnd_port))
|
||||
await self.writer.drain()
|
||||
|
||||
async def _handle_connect(self, addr, port):
|
||||
|
||||
Reference in New Issue
Block a user