XHTTP packet-up over H2 建议增加 stale upload socket 检查,避免长 idle 后复用失效连接
### 完整性要求
- [x] 我读完了 issue 模板中的所有注释,确保填写符合要求。
- [x] 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
- [x] 我提供了完整的配置文件和日志,而不是出于自己的判断只给出截取的部分。
- [x] 我搜索了 issues, 没有发现已提出的类似问题。
- [x] 问题在 Release 最新的版本上可以成功复现
### 描述
环境:
* Xray-core v26.6.1
* XHTTP
* mode: packet-up
* HTTP/2
* Android / iOS 等移动网络环境下均可能受影响,尤其是长时间息屏、Doze、网络切换、NAT 状态过期之后
背景:
近期在我向 Shadowrocket 反馈Bug后,它的测试版更新中有一条修复:
> fix(xhttp): avoid reusing stale h2 packet-up sockets
这个问题表现为:XHTTP packet-up over H2 在长时间息屏或 idle 后,VPN 状态看似仍然存在,但实际连接可能已经进入一种“幽灵连接”状态;关闭 VPN 重开后恢复。Shadowrocket 测试版加入相关修复后,同样配置下该问题已恢复正常。
因此想请 Xray-core 侧也检查一下当前 XHTTP packet-up over H2 是否缺少类似的 stale socket 状态判断。
源码观察:
在 `transport/internet/splithttp/client.go` 中,`DefaultDialerClient.PostPacket()` 对 H2/H3 的处理大致是直接调用:
```go
resp, err := c.client.Do(req)
if err != nil {
c.closed = true
return err
}
```
也就是说,H2/H3 packet-up 路径似乎是先复用 `http.Client` / `http2.Transport` 内部连接池里的连接,只有 `Do()` 返回错误后才把当前 client 标记为 closed。
这里的问题是:如果底层 H2 upload socket 在长时间 idle、移动端息屏、网络恢复、NAT 超时、链路切换后已经处于 half-dead / stale 状态,但 Go 的 H2 transport 仍认为该连接可复用,那么 Xray 当前逻辑似乎没有在发送 packet-up POST 前主动判断这条 H2 复用连接是否已经失效。
`DefaultDialerClient.IsClosed()` 当前也只是检查本地 `closed` bool,并不代表底层 H2 socket 仍然健康。因此 XMUX 层可能仍然认为该 client 可用。
对比 HTTP/1.1 分支:
同一个 `PostPacket()` 中,HTTP/1.1 packet-up 分支对 upload raw pooled connection 有更细的容错逻辑:如果从 pool 里取出的旧连接写入失败,会继续尝试其它连接或新连接。代码注释里也明确承认 pooled connection 在此期间被对端关闭导致写失败是正常情况。
但 H2/H3 packet-up 分支目前没有看到等价的 stale pooled connection 处理逻辑。它更多依赖 `http.Client.Do(req)` 自身返回错误,然后才标记 closed。这和 Shadowrocket 所说的 “avoid reusing stale h2 packet-up sockets” 似乎不是同一级别的保护。
可能的风险点:
1. H2 upload socket 长 idle 后已被 NAT、系统、省电策略或中间链路清理,但本地 H2 transport 仍复用它。
2. packet-up 下一次 POST 仍走旧 H2 连接,可能造成请求卡住或延迟失败。
3. `IsClosed()` 只有本地 bool,无法提前反映底层 socket 是否 stale。
4. XMUX 的 `hMaxRequestTimes` / `hMaxReusableSecs` 可以降低长期复用概率,但这属于周期性轮换,不等价于“发送前判断当前 H2 socket 是否已经失效”。
5. 如果用户手动配置了 xmux,某些默认轮换参数可能未生效,进一步增加旧 H2 连接被长期复用的概率。
建议改进方向:
1. 为 XHTTP packet-up over H2 增加 stale socket 检查,不只在 `Do()` 返回错误后才标记 closed。
2. 在长时间 idle 后的第一次 packet-up POST 前,考虑主动丢弃旧 H2 client / 旧 H2 transport idle connections,或创建新的 `DefaultDialerClient`。
3. 可以使用 `httptrace.GotConn` 记录 `Reused`、`WasIdle`、`IdleTime`,当 H2 连接 idle 时间超过一定阈值时避免继续用于 packet-up upload。
4. 对 packet-up H2 POST 增加更明确的写入阶段超时,避免 stale socket 导致 upload loop 长时间等待。
5. 在 `httptrace.WroteRequest` 中检查 `WroteRequestInfo.Err`,而不是只把 WroteRequest 当成“写入已发生”的信号。
6. 如果请求体尚未成功写出,可以考虑在 fresh H2 connection 上安全重试一次;如果已经写出,则避免重复提交 packet。
7. Android 网络恢复、VPN 网络变化、长息屏恢复后,可以考虑主动标记当前 H2 packet-up client 不再可复用。
期望行为:
XHTTP packet-up over H2 在长 idle、息屏恢复、网络切换或 NAT 状态过期后,不应继续复用可能已经 stale 的 H2 upload socket。即使 Go 的 http2 transport 仍保留该连接,Xray 也应尽量在 packet-up 层避免使用过旧或状态不确定的 H2 连接。
这个问题较难稳定复现,但 Shadowrocket 已经针对相同方向做了修复,因此希望 Xray-core 也能检查是否需要加入类似的状态检查或调试日志。
### 重现方式
该问题小概率且较难稳定复现。反馈重点不是单一复现样例,而是参考 Shadowrocket 测试版已修复的同类问题:fix(xhttp): avoid reusing stale h2 packet-up sockets。希望检查 Xray 当前 H2 packet-up 路径是否缺少类似 stale socket 状态判断。
### 客户端配置
.
### 服务端配置
.
### 客户端日志
.
### 服务端日志
.
关闭于 2026-06-20 5 条评论