fix(socks5): 隧道转发阶段补 idle timeout,防慢客户端耗尽 fd

之前 _forward() 直接 await src.read(TUNNEL_CHUNK),无 timeout。
已通过 SOCKS5 握手的客户端可以一直挂着不发数据,占住 fd 不放,
直到 LimitNOFILE=65535 才被内核拒。

握手/认证/请求阶段已经全部用 asyncio.wait_for + config.timeout,
但同一个 timeout 字段没覆盖 tunnel 阶段,语义不一致。

修复:
  engine/server.py:381-407  _forward 每次 read 包 wait_for, 触发
                          TimeoutError 时 break 走 _tunnel 的 finally
                          清理 (关闭 writer, 取消对端 forward 任务)

验证 (tests/smoke_tunnel_timeout.py):
  - 客户端只握手不进 tunnel, 服务端在 config.timeout 秒后关闭
  - 反向: git stash 掉修复, 客户端永远不被关闭, 服务端报
    'Task was destroyed but it is pending' — 证明修复前后行为差
    异真实存在

models.Instance.timeout 注释: 说明该字段覆盖 tunnel 阶段
This commit is contained in:
cnbugs
2026-08-10 23:28:46 +08:00
parent f948a410b6
commit ee8c0b83db
3 changed files with 157 additions and 2 deletions
+3
View File
@@ -13,6 +13,9 @@ class Instance(db.Model):
engine = db.Column(db.String(16), default="builtin") # builtin / 3proxy
listen_host = db.Column(db.String(64), default="0.0.0.0")
listen_port = db.Column(db.Integer, nullable=False)
# 统一超时(秒):覆盖 SOCKS5 握手/认证/请求读取,以及隧道阶段每段 read。
# 客户端在任意阶段(含 tunnel)空闲超过此值会被服务端主动断开,避免空连接
# 占满 fd 直到 LimitNOFILE 上限。
timeout = db.Column(db.Integer, default=30)
enabled = db.Column(db.Boolean, default=True)
notes = db.Column(db.Text)