fix: convert place disguise host in h2-opts.host for legacy base64-JSON VMess#2739
Merged
wwqgtxx merged 1 commit intoApr 22, 2026
Merged
Conversation
…ON VMess The case "h2" block in common/convert/converter.go (the legacy v2rayN base64-JSON VMess share-link parser) has been writing the disguise host into a non-existent h2-opts.headers.Host key since 5141ddc (2022-06). HTTP2Options has no Headers field, so common/structure silently drops the unknown key and every VMess h2 subscription loses its disguise host on import. The bug is the direct parallel of MetaCubeX#2555 / MetaCubeX#2737, which fixed the modern VMess AEAD / VLESS URL path in common/convert/v.go. Both branches were copy-pasted from the adjacent case "http" (HTTP/1.1 camouflage), whose HTTPOptions struct really does have a Headers field. HTTP2Options does not. The disguise host belongs in the top-level h2-opts.host. Align the h2 branch with HTTP2Options exactly: write host into h2-opts.host, default path to "/" (matching the xray URI spec and the adjacent case "http" / case "ws" branches), and drop the dead headers residue. Add two regression tests: one for explicit net=h2 and one for net=http, type=none which is remapped to h2. Fixes MetaCubeX#2738
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要 / Summary
修复 #2738。老式 v2rayN 风格 base64-JSON VMess 分享链接在
net: h2(或net: http经 remap 后)导入时,伪装host被静默丢失——common/convert/converter.go:333-343的case "h2":分支把host写进了HTTP2Options不存在的Headers字段里,解码器直接丢弃。Fixes #2738. Legacy v2rayN-style base64-JSON VMess share links with
net: h2(ornet: httpafter remap) silently lose their disguisehostduring import —common/convert/converter.go:333-343writeshostinto aHeadersfield thatHTTP2Optionsdoes not have, so the decoder silently discards it.本 PR 与 #2737 是同根源的姊妹 PR(都是
case "http":分支的 copy-paste 后代),但各自运行在不同的分享链接入口上,可独立合并:This PR is a sibling with the same root cause as #2737 — both originate from copy-paste of the adjacent
case "http":block — but each runs on a different share-link entry point. They merge independently:converter.go的 legacy v2rayN 风格 base64-JSON VMess 订阅 (vmess://<base64>)v.go的新式 VMess AEAD / VLESS URL (vmess://...?type=http&.../vless://...?type=http&...)复现 / Reproduction
最小复现用的 v2rayN 风格 base64 VMess 分享链接(内层 JSON 含
net: h2和伪装host):Minimal reproducer — a v2rayN-style base64-encoded VMess share link with
net: h2and a disguisehost:base64 解码后的 JSON:
The base64 payload decodes to:
{"v":"2","ps":"demo","add":"server.example.com","port":"443","id":"b831381d-6324-4d53-ad4f-8cda48b30811","aid":"0","scy":"auto","net":"h2","type":"none","host":"cdn.example.com","path":"/grpc","tls":"tls"}导入后
ConvertsV2Ray产出的h2-opts是:After
ConvertsV2Rayimport, the producedh2-optsis:h2-opts.host完全缺失。伪装 hostcdn.example.com被写到了下游没有定义的headers键里,解码成HTTP2Options后完全丢失。h2-opts.hostis missing entirely. The disguise hostcdn.example.comis stranded in aheaderskey that does not exist on the downstream struct, and is discarded when decoded intoHTTP2Options.根因 / Root cause
common/convert/converter.go:333-343当前代码:common/convert/converter.go:333-343currently:下游 schema (
adapter/outbound/vmess.go:80-83):Downstream schema (
adapter/outbound/vmess.go:80-83):没有
Headers字段。common/structure解码器(WeaklyTypedInput: true、未启ErrorUnused)把未知的headers键静默丢弃,host随之丢失。There is no
Headersfield. Thecommon/structuredecoder (WeaklyTypedInput: true,ErrorUnusednot enabled) silently drops the unknownheaderskey, and thehostvalue inside it is lost.问题如何引入 / How it was introduced
bug 分两步进入代码:
The bug entered the code in two steps:
f8366f6e(2022-06-07)feat: 新增grpc h2 http 等支持首次加入case "h2":分支。初版只有headers["User-Agent"] = RandUserAgent()——作者按照case "http":(HTTP/1.1 伪装)的模式建了一个headersmap,但 h2 transport 实际并不消费它。5141ddc9(2022-06-21)fix: Converter for vless/vmess/ss URI Scheme加入headers["Host"] = []string{host}——这就是 host-loss bug 具体引入的 commit。作者想正确处理 JSON 里的host字段,但误把它当成 HTTP/1.1 伪装场景(那里headers["Host"]确实是正确做法),而没有意识到 HTTP/2 transport 的目标 structHTTP2Options根本没有Headers字段。f8366f6e(2022-06-07)feat: 新增grpc h2 http 等支持first added thecase "h2":branch. The initial version only wroteheaders["User-Agent"] = RandUserAgent()— the author mirrored thecase "http":(HTTP/1.1 camouflage) pattern and created aheadersmap, but h2 transport does not actually consume it.5141ddc9(2022-06-21)fix: Converter for vless/vmess/ss URI Schemethen addedheaders["Host"] = []string{host}— this is the commit that introduced the host-loss bug. The author intended to handle the JSONhostfield correctly, but mistakenly treated it as an HTTP/1.1 camouflage scenario (whereheaders["Host"]is indeed the right target), without realizing that the H2 transport's downstream structHTTP2Optionshas noHeadersfield at all.为什么 block 长得像 HTTP/1.1 伪装 / Why the block looks like HTTP/1.1 camouflage
对照两个 transport 的 struct schema 就能看出"抄错模板"的由来:
Comparing the two transport struct schemas shows why the template was copied incorrectly:
HTTPOptions(HTTP/1.1 camouflage,adapter/outbound/vmess.go:74-78)HTTP2Options(adapter/outbound/vmess.go:80-83)Path[]stringstringHost[]string(直接字段 / top-level field)Headersmap[string][]string(真字段,由 HTTP/1.1 流量伪装消费 / real field, consumed by HTTP/1.1 traffic disguise)HTTP/1.1 伪装场景下
headers["Host"]是正确的目标——HTTP/1.1 请求本来就要靠 Host header 标识目标。但 HTTP/2 transport 的伪装点在外层 TLS SNI / ALPN,帧层 header 已加密且非伪装入口,所以HTTP2Options从 2020 年 commit5bd189f2引入以来就只有Host []string+Path string,从未有过Headers字段;transport/vmess/h2.go:H2Config同样只有Hosts + Path;执行层establishConn()里 HTTP/2 请求的 header 就硬编码一条Accept-Encoding: identity。整套链路都不读用户自定义的 H2 header。In the HTTP/1.1 camouflage case,
headers["Host"]is the correct target — HTTP/1.1 requests identify the target via the Host header itself. HTTP/2 transport, however, is disguised at the outer TLS SNI / ALPN layer, and its frame-level headers are encrypted and not a disguise channel.HTTP2Optionshas had exactlyHost []string+Path stringsince it was introduced in 2020 (commit5bd189f2) and never aHeadersfield;transport/vmess/h2.go:H2Configsimilarly has onlyHosts + Path; andestablishConn()hardcodes the sole HTTP/2 headerAccept-Encoding: identity. No code path in the chain reads a user-supplied H2 header.所以 legacy 分支的
h2Opts["headers"]与headers["Host"]完全是复制 HTTP/1.1 模板时带进来的残留:headersmap 纯粹没被消费,host值也因此在最简单的路径上丢失。The legacy branch's
h2Opts["headers"]andheaders["Host"]are therefore pure residue from copying the HTTP/1.1 template: theheadersmap is never consumed, and thehostvalue ends up lost on the most basic path.修复 / The fix
common/convert/converter.go(+5 / -6)三点改动:
Three changes:
host写进正确的 key:h2Opts["host"] = []string{host}。下游HTTP2Options.Host []string能正确消费。hostgoes into the correct key:h2Opts["host"] = []string{host}. DownstreamHTTP2Options.Host []stringcan now consume it.path默认"/":旧代码h2Opts["path"] = values["path"]直接把any(可能是nil,当 JSON 里 path 字段缺失时)赋进 map;解码器在isNil分支回退到空字符串"",导致 HTTP/2 请求 URL 没有路径。新代码默认"/",仅在 JSON 提供非空 path 时覆盖——与 xray URI spec、同文件case "http":和case "ws":的默认策略一致。考虑到该分支先前因 host 错位几乎不可能被用户配成可用节点(disguise host 丢失),这个默认值改动基本无实际兼容风险。pathdefaults to"/": The previous codeh2Opts["path"] = values["path"]raw-assignedany(which can benilwhen the JSON omitspath); the decoder falls back to an empty string""via theisNilbranch, resulting in an HTTP/2 request with an empty path. The new code defaults to"/", overridden only when the JSON provides a non-empty path — matching the xray URI spec and the default strategies in the adjacentcase "http":andcase "ws":blocks. Because the branch almost never produced a working node before (due to the host loss), this default change has essentially no compatibility risk.删掉无用的
headersmap:HTTP2Options 无对应字段,headers从 2022 年起就是死代码。Drop the dead
headersmap:HTTP2Optionshas no such field;headershas been dead code since 2022.common/convert/converter_test.go(+40, two new regression tests)TestConvertsV2RayVmessBase64H2Transport—— 显式net: h2输入路径TestConvertsV2RayVmessBase64HTTPRemappedToH2Transport——net: http, type: none经converter.go:296-305的 remap 规则落到"h2"的等价路径。单独覆盖是为了防止未来改动 remap 规则时引入回归。TestConvertsV2RayVmessBase64H2Transport— explicitnet: h2input pathTestConvertsV2RayVmessBase64HTTPRemappedToH2Transport— the equivalent path wherenet: http, type: noneis remapped to"h2"viaconverter.go:296-305. Covered separately to guard against regressions if that remap rule is changed later.两条测试通过精确匹配产出 map 锁住形状契约。已通过
git stash回退converter.go修复后跑这两个测试验证,确认它们会 FAIL(bug 精确重现),恢复后 PASS。Both tests lock the shape contract via precise map equality. Verified via
git stashthat reverting theconverter.gofix makes both tests fail with the exact bug signature; restoring the fix makes them pass.刻意不做 / Deliberately deferred
不创建
adapter/parser_test.go的端到端测试:该文件由 #2737 新建,本 PR 基于upstream/Alpha不含它。若本 PR 也创建同名文件,两个 PR 合并时会产生"新文件创建冲突"。本 bug 的症状是"数据错位到不存在字段"(不是"类型失配让解码直接 error"),converter 层的 map 形状断言已能精确锁定。未来若需要 legacy 分支的ParseProxy端到端覆盖,可在 #2737 合并后追加。No
adapter/parser_test.goend-to-end test added. That file is introduced by #2737; this PR, based onupstream/Alpha, does not include it. If this PR also created the same file, the two PRs would collide on "new file creation" at merge time. The failure mode here is "data routed to a non-existent field" (not "type mismatch causing a decode error"), and converter-level map-shape assertions are precise enough to lock it down. If legacy-branchParseProxyend-to-end coverage is desired later, it can be added in a follow-up once #2737 lands.Related