Leadaxe:sing-box-lx 的 XHTTP 客户端未拆分 transport.path 中的查询串,致经 Cloudflare 中继访问 CF 站点返回 502

Leadaxe 在 sing-box-lx issue #36 说明根因与修复:XHTTP 客户端此前未把查询串从 transport.path 中拆出,整串经 path setter 上线,`/?proxyip=149.56.109.62` 被写成 `/%3Fproxyip=149.56.109.62/`(`?` 被百分号编码并带 stream-one 尾斜杠)。Xray 把 path 首个 `?` 之后当作请求 query(splithttp/config.go 的 GetNormalizedPath/GetNormalizedQuery),Shadowrocket 与 sing-box-extended 亦然,故它们正常。用户的中继是 edgetunnel 风格 Cloudflare Worker,从 URL query 或 `/proxyip=…` 路径段读取 proxyip;`%3F` 令其两者都读不到而回退默认 PROXYIP,非 CF 目标直连不受影响,CF 目标无可用 proxy IP 时 Worker 返回 502。修复见 Leadaxe/sing-box-lx@f2b121796:客户端现将 path 首个 `?` 之后原样作为请求 query 并叠加 session/seq/padding 参数,与 Xray 对齐,含单测,将在下个版本发布。临时办法:改用 edgetunnel 订阅生成器输出的 `/proxyip=…` 路径段形式(无 `?`,lx.12/lx.13 原样透传)。另答:无 sniff 时 sing-box 把 VLESS 请求头与首个 payload 分两次写,Xray 等最多 500ms 后合并;Content-Type/padding/头顺序与 Xray 一致、非本次因素;内核无请求体 trace。

作者原文

Thanks for the detailed report — the comparison table made this tractable.

Root cause (client side)

The XHTTP client did not split a query string out of transport.path. The whole string went through the path setter, so your

"path": "/?proxyip=149.56.109.62"

went on the wire as

:path = /%3Fproxyip=149.56.109.62/

(the ? percent-encoded, plus the stream-one trailing slash). Xray treats everything after the first ? in path as the request query (GetNormalizedPath / GetNormalizedQuery in transport/internet/splithttp/config.go), and so do Shadowrocket and sing-box-extended — which is why they worked.

Your relay is an edgetunnel-style Cloudflare Worker. It reads proxyip from the URL query (or from a /proxyip=… path segment). With %3F it saw neither, so for that connection it fell back to its default PROXYIP. Non-Cloudflare destinations are reached directly and don't need a proxy IP, hence they worked; Cloudflare-hosted destinations do, and with no usable proxy IP the Worker ends up returning 502. The ip=149.56.109.62 in your working traces is the proxy IP the Worker was told to use — and that's exactly the parameter we were losing.

Fix

Leadaxe/sing-box-lx@f2b121796 — the client now sends the part of path after the first ? verbatim as the request query, with session/seq/padding query parameters added on top of it, matching Xray. Unit tests cover /?proxyip=… for stream-one and path?query for packet-up / query placements. It will be in the next release.

Workaround for the current build

Until then you can use the path-segment form that edgetunnel's own subscription generator emits:

"path": "/proxyip=149.56.109.62"

That form contains no ?, so lx.12/lx.13 pass it through unchanged and the Worker picks it up. If you can confirm this works on your relay (cdn-cgi/trace showing ip=149.56.109.62), that would be a useful data point.

Your questions

  1. Without a sniff rule action, sing-box writes the VLESS request header as soon as the outbound is dialled and the first payload (ClientHello) in a separate write — one HTTP/2 DATA frame each. Xray waits up to 500 ms for the first payload and sends header + payload together. With sniff enabled the header and the cached ClientHello go out in one write. We're considering aligning the no-sniff case with Xray; it's independent of the bug above.
  2. Content-Type: application/grpc, padding placement and header order are the same as Xray's for stream-one and were not a factor here — the Worker only cared about the query.
  3. There's no request-body trace in the core; sing-box run --log.level debug shows the dial and the response status, not the bytes.

Leaving the issue open until you can confirm on your relay.