fix: 修复 SOCKS5 账号密码认证时的 TCP 流读取 bug

问题原因:RFC 1929 认证请求是一个连续的 TCP 数据包,
原代码多次独立调用 reader.read(),第一次 read 已经把
整个认证请求都读入了,后续 read 调用阻塞超时。

修复方案:改用累积缓冲区方式解析认证请求,先累积所有
到达的数据,再逐步解析用户名和密码,避免不必要的阻塞
This commit is contained in:
Your Name
2026-07-16 23:50:41 +08:00
parent e44802ae40
commit b32256930f
+64 -35
View File
@@ -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):