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": if self.config.auth_method != "userpass":
return True return True
# 认证请求 # RFC 1929 认证请求格式:
header = await asyncio.wait_for(self.reader.read(256), # +----+------+----------+------+----------+
timeout=self.config.timeout) # |VER | ULEN | UNAME | PLEN | PASSWD |
if len(header) < 2: # +----+------+----------+------+----------+
return False # | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
ver, ulen = struct.unpack("!BB", header[:2]) # +----+------+----------+------+----------+
# 注意:整个请求是一个连续 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: if ver != 1:
return False return False
# 读用户名 offset = 2
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")
# 读密码长度 # 2. 读取剩余需要的数据
plen_byte = await asyncio.wait_for(self.reader.read(1), needed = ulen + 1 # 用户名 + 密码长度字节
timeout=self.config.timeout) while len(data) < offset + needed:
if not plen_byte: chunk = await asyncio.wait_for(self.reader.read(256),
return False timeout=self.config.timeout)
plen = plen_byte[0] if not chunk:
return False
data += chunk
# 读密码 username = data[offset:offset + ulen].decode("utf-8", errors="replace")
passwd = await asyncio.wait_for(self.reader.read(plen), offset += ulen
timeout=self.config.timeout) plen = data[offset]
if len(passwd) != plen: offset += 1
return False
passwd = passwd.decode("utf-8", errors="replace") # 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 兼容验证) # 验证(使用 bcrypt 兼容验证)
user = self.user_service.get_user(username) user = self.user_service.get_user(username)
@@ -221,20 +238,32 @@ class ConnectionHandler:
return None, None, None, None return None, None, None, None
async def _reply(self, rep, bnd_addr="0.0.0.0", bnd_port=0): async def _reply(self, rep, bnd_addr="0.0.0.0", bnd_port=0):
"""发送 SOCKS5 响应""" """发送 SOCKS5 响应 (RFC 1928 §6).
addr_bytes = b"\x00\x00\x00\x00"
if bnd_addr and "." not in str(bnd_addr): 格式: VER(1) REP(1) RSV(1) ATYP(1) BND.ADDR(4/16字节) BND.PORT(2字节)
# IPv6 IPv4 是 4 字节 packed, IPv6 是 16 字节, 域名不允许出现在 reply.
"""
atyp = ATYP_IPV4
if bnd_addr:
try: try:
addr_bytes = ipaddress.IPv6Address(str(bnd_addr)).packed ip = ipaddress.ip_address(str(bnd_addr))
except Exception: if ip.version == 6:
pass 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( self.writer.write(struct.pack(
"!BBBB5sH", "!BBBB",
SOCKS_VERSION, rep, 0x00, ATYP_IPV4, SOCKS_VERSION, rep, 0x00, atyp,
addr_bytes, bnd_port ) + addr_bytes + struct.pack("!H", bnd_port))
))
await self.writer.drain() await self.writer.drain()
async def _handle_connect(self, addr, port): async def _handle_connect(self, addr, port):