TokenPLS 补充 HTTP/2 定时器保留连接路径及 mihomo XHTTP 内存影响

TokenPLS 在 Go #81996 中补充指出,仅在 HTTP/2 读循环退出时停止健康检查定时器,仍未覆盖空闲定时器路径:连接在响应体读取期间断开后,forgetStreamID 未检查 cc.closed,可重新启动 IdleConnTimeout;cleanup 在加锁前停止定时器也存在再次启动窗口。他建议连接关闭后禁止重置,并在设置 closed 后再次停止。作者报告,空闲超时为 1 分钟的 16 个连接在关闭 3 秒后,单独应用 defer t.Stop() 仍全部可达,加入补充修改后均不再可达;mihomo XHTTP 的 45 秒 ping 和 300 秒空闲超时会保留连接,曾导致约 50 MB 内存限制的 iOS 隧道每几分钟被系统终止。他提供的带测试回移植 PR 为 MetaCubeX/http #2,核验时仍开放、未合并。

作者原文

The client half has a second path that defer t.Stop() doesn't cover. forgetStreamID re-arms cc.idleTimer without checking cc.closed, so when the connection goes away under an open stream (the server or the network drops it while a response body is being read), the stream is forgotten after cleanup has run and the idle timer is armed again for the full IdleConnTimeout. cleanup also stops the idle timer before taking cc.mu, and a stream forgotten between that Stop and closed = true re-arms it the same way. For a connection that was never reused, the Reset also stretches the short MarkDead timer cleanup installs to IdleConnTimeout.

Two more lines close it (same code in net/http/internal/http2 and x/net/http2):

 func (cc *ClientConn) forgetStreamID(id uint32) {
 	...
-	if len(cc.streams) == 0 && cc.idleTimer != nil {
+	if len(cc.streams) == 0 && cc.idleTimer != nil && !cc.closed {
 		cc.idleTimer.Reset(cc.idleTimeout)
 
 func (rl *clientConnReadLoop) cleanup() {
 	...
 	cc.closed = true
+	if cc.idleTimer != nil {
+		cc.idleTimer.Stop()
+	}

With IdleConnTimeout set to 1 minute and the connection closed under an open response body, 16 of 16 client connections are still reachable 3 s after close with only defer t.Stop() applied, and none once these two changes are added.

We ran into this through mihomo's xhttp proxy transport, which opens one HTTP/2 connection per proxied stream, with a 45 s ping period and a 300 s idle timeout. On iOS, where the packet tunnel has a memory limit of about 50 MB, the retained connections got the tunnel killed every few minutes. The backport with a test is MetaCubeX/http#2.