Skip to content

[Bug] legacy base64-JSON VMess subscription silently drops disguise host for h2 transport #2738

Description

@wangwei354

Verify steps

  • Ensure you are using the latest version of Mihomo or Mihomo Alpha from this repository.
  • This is something I can debug and fix — I'll send a pull request.
  • I have searched the issue tracker and did not find a related issue.
  • I have tested using the Alpha branch, and the issue still exists.
  • I have read the documentation and was unable to solve the issue.
  • This is an issue of the Mihomo core per se, not a derivative.

Mihomo version

Alpha HEAD c59c99a0。此 bug 自 2022-07 的 b23a0710 起就存在——那段代码从第一天起写错,从未正常工作过。

Alpha HEAD c59c99a0. The bug has existed since b23a0710 (2022-07) — the code has never been correct since it was first written.

What OS are you seeing the problem on?

与平台无关。问题出在分享链接导入逻辑,不涉及任何平台特定代码。

Platform-independent. The bug is in the share-link conversion layer, not in any platform-specific code.

Mihomo config

最小复现用例——一个 v2rayN 风格的 base64 编码 VMess 分享链接,net: h2 且带有伪装 host

Minimal reproducer — a v2rayN-style base64-encoded VMess share link that declares net: h2 with a disguise host:

proxies:
  - vmess://eyJ2IjoiMiIsInBzIjoiZGVtby1oMi1ob3N0LWxvc3QiLCJhZGQiOiJzZXJ2ZXIuZXhhbXBsZS5jb20iLCJwb3J0IjoiNDQzIiwiaWQiOiJiODMxMzgxZC02MzI0LTRkNTMtYWQ0Zi04Y2RhNDhiMzA4MTEiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6ImgyIiwidHlwZSI6Im5vbmUiLCJob3N0IjoiY2RuLmV4YW1wbGUuY29tIiwicGF0aCI6Ii9ncnBjIiwidGxzIjoidGxzIn0=

上面的 base64 负载解码后是:

The base64 payload decodes to:

{"v":"2","ps":"demo-h2-host-lost","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"}

导入后,生成的 proxy h2-opts.host 为空——订阅里指定的伪装域名 cdn.example.com 被静默丢弃。

After import, the resulting proxy has h2-opts.host empty — the intended disguise host cdn.example.com is silently lost.

Mihomo log

不适用——bug 是静默的,没有 error、warning 或 debug 日志。对上述分享链接运行 ConvertsV2Ray,实际得到的 h2-opts map 是:

N/A — the bug is silent. No error, no warning, no debug line. Running ConvertsV2Ray on the link above produces an h2-opts map that looks like:

map[string]any{
    "headers": map[string]any{"Host": []string{"cdn.example.com"}},
    "path":    "/grpc",
}

h2-opts.host 完全缺失;伪装 host 被写进了 HTTP2Options 根本不存在的 headers 键里,被解码器静默丢弃。

h2-opts.host is missing entirely; the disguise host lives inside a key (headers) that HTTP2Options does not have, so the decoder silently discards it.

Description

common/convert/converter.go:333-343case "h2": 分支——也就是老式 v2rayN 风格 base64-JSON VMess 分享链接的解析路径——把伪装 host 写到了错误的 map key 上。结果是:每一个使用 HTTP/2 transport 的 VMess 订阅节点在导入时都会静默丢掉自己的伪装 host。

The case "h2": branch in common/convert/converter.go:333-343 — the legacy v2rayN-style base64-JSON VMess share-link parser — writes the disguise host into the wrong map key. As a result, every VMess subscription node that uses HTTP/2 transport silently loses its disguise host during import.

当前代码 / Current code:

case "h2":
    headers := make(map[string]any)
    h2Opts := make(map[string]any)
    if host, ok := values["host"].(string); ok && host != "" {
        headers["Host"] = []string{host}    // ← 写进了错误的 map 和错误的 key / wrong map, wrong key
    }
    h2Opts["path"] = values["path"]
    h2Opts["headers"] = headers              // ← HTTP2Options 没有 Headers 字段 / HTTP2Options has no Headers field
    vmess["h2-opts"] = h2Opts

下游 schema / Downstream schema (adapter/outbound/vmess.go:80-83):

type HTTP2Options struct {
    Host []string `proxy:"host,omitempty"`
    Path string   `proxy:"path,omitempty"`
}

没有 Headers 字段。h2Opts["headers"]common/structure 解码时是一个未知 key(WeaklyTypedInput: true、未启用 ErrorUnused),被静默丢弃,里面的 host 值也随之丢失。

There is no Headers field. h2Opts["headers"] is an unknown key during common/structure decoding (WeaklyTypedInput: true, ErrorUnused not enabled) and is silently discarded; the host value placed inside it is lost with it.

正确的 key 应该是 h2Opts["host"] = []string{host}

The correct key is h2Opts["host"] = []string{host}.

#2555 / #2737 的关系 / Relation to #2555 / #2737

common/convert/v.go(新式 VMess AEAD / VLESS URL 解析器)里有一个平行 bug,已由 #2555 报告、#2737 修复。两者同源——都是从相邻的 case "http":(HTTP/1.1 camouflage)分支 copy-paste 过来的——但运行在不同的订阅格式上:

The parallel bug in common/convert/v.go (the modern VMess AEAD / VLESS URL parser) was reported as #2555 and fixed in #2737. Both originate from the same copy-paste pattern (cloned from the adjacent case "http": HTTP/1.1 camouflage block), but each runs on a different subscription format:

  • v.go handleVShareLink —— 新式 vmess://...?type=http&... / vless://...?type=http&... URL

  • converter.go case "h2": —— 老式 v2rayN 风格 base64 编码 JSON 的 vmess://<base64>

  • v.go handleVShareLink — modern vmess://...?type=http&... / vless://...?type=http&... URLs

  • converter.go case "h2": — legacy v2rayN-style base64-encoded JSON vmess://<base64>

#2737 没覆盖老式路径,因为入口和编码都不同。本 issue 跟踪老式路径的修复。

#2737 did not cover the legacy path because the entry point and encoding are different. This issue tracks the legacy path fix.

复现步骤 / Reproduction

  1. 从 Alpha HEAD 构建 mihomo / Build mihomo from Alpha HEAD.

  2. 导入任何 v2rayN 风格、JSON 里 "net": "h2"(或 "net": "http",converter.go 会把它 remap 成 "h2")且 "host" 非空的订阅。上面给出的最小 config 即可。

    Import any v2rayN-style subscription whose JSON has "net": "h2" (or "net": "http", which converter.go remaps to "h2") and a non-empty "host". The minimal config above works.

  3. 通过 /proxies API 或 config dump 查看加载后的 proxy——h2-opts.host 为空,订阅里指定的伪装 host 消失了。

    Inspect the loaded proxy via the /proxies API or a config dump — h2-opts.host is empty; the disguise host from the subscription is gone.

建议修复 / Proposed fix

直接照搬 #2737v.go 平行分支的修复思路:

Direct mirror of the fix applied by #2737 to the parallel v.go branch:

case "h2":
    h2Opts := make(map[string]any)
    if host, ok := values["host"].(string); ok && host != "" {
        h2Opts["host"] = []string{host}
    }
    if path, ok := values["path"].(string); ok && path != "" {
        h2Opts["path"] = path
    }
    vmess["h2-opts"] = h2Opts

我会准备 PR。

I'll prepare a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions