From b32256930fddc200b913522533951209766dac97 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 16 Jul 2026 23:50:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20SOCKS5=20=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=AF=86=E7=A0=81=E8=AE=A4=E8=AF=81=E6=97=B6=E7=9A=84?= =?UTF-8?q?=20TCP=20=E6=B5=81=E8=AF=BB=E5=8F=96=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题原因:RFC 1929 认证请求是一个连续的 TCP 数据包, 原代码多次独立调用 reader.read(),第一次 read 已经把 整个认证请求都读入了,后续 read 调用阻塞超时。 修复方案:改用累积缓冲区方式解析认证请求,先累积所有 到达的数据,再逐步解析用户名和密码,避免不必要的阻塞 --- engine/server.py | 99 +++++++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/engine/server.py b/engine/server.py index e4ae816..9f6df53 100644 --- a/engine/server.py +++ b/engine/server.py @@ -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):