Skip to content

fix: convert httpupgrade share links to ws with v2ray-http-upgrade - #3415

Merged
wwqgtxx merged 1 commit into
MetaCubeX:Alphafrom
Dv2Corn:fix/httpupgrade-share-links
Nov 6, 2026
Merged

wwqgtxx merged 1 commit into
MetaCubeX:Alphafrom
Dv2Corn:fix/httpupgrade-share-links

Conversation

@Dv2Corn

@Dv2Corn Dv2Corn commented Nov 5, 2026

Copy link
Copy Markdown

Summary

Converting a vless:// link with type=httpupgrade (or a base64 vmess:// link with "net":"httpupgrade") produces network: httpupgrade and a ws-opts without v2ray-http-upgrade. The outbounds only know HTTPUpgrade as a mode of the ws transport, so such a proxy silently falls back to plain TCP and never sends the upgrade request. Adding ed does not help: it only sets v2ray-http-upgrade-fast-open, which has no effect without v2ray-http-upgrade.

Fixes #2609

Root cause

The converter writes the link's type as network unchanged —

proxy["network"] = network
— and the shared ws/httpupgrade branch builds ws-opts but sets nothing that selects HTTPUpgrade, apart from fast-open when ed is present —

mihomo/common/convert/v.go

Lines 110 to 135 in f1f640b

case "ws", "httpupgrade":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
headers["Host"] = query.Get("host")
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
if earlyData := query.Get("ed"); earlyData != "" {
med, err := strconv.Atoi(earlyData)
if err != nil {
return fmt.Errorf("bad WebSocket max early data size: %v", err)
}
switch network {
case "ws":
wsOpts["max-early-data"] = med
wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol"
case "httpupgrade":
wsOpts["v2ray-http-upgrade-fast-open"] = true
}
}
if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" {
wsOpts["early-data-header-name"] = earlyDataHeader
}
proxy["ws-opts"] = wsOpts

The base64 VMess parser has the same branch —

vmess["network"] = network
and
case "ws", "httpupgrade":
headers := make(map[string]any)
wsOpts := make(map[string]any)
wsOpts["path"] = "/"
if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = host
}
if path, ok := values["path"].(string); ok && path != "" {
path := path
pathURL, err := url.Parse(path)
if err == nil {
query := pathURL.Query()
if earlyData := query.Get("ed"); earlyData != "" {
med, err := strconv.Atoi(earlyData)
if err == nil {
switch network {
case "ws":
wsOpts["max-early-data"] = med
wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol"
case "httpupgrade":
wsOpts["v2ray-http-upgrade-fast-open"] = true
}
query.Del("ed")
pathURL.RawQuery = query.Encode()
path = pathURL.String()
}
}
if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" {
wsOpts["early-data-header-name"] = earlyDataHeader
}
}
wsOpts["path"] = path
}
wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts

The outbounds handle only network: ws and read the mode from ws-opts.v2ray-http-upgrade —

switch v.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
Port: port,
Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
(same in vmess.go and trojan.go).

The trojan link parser has only a ws case, so type=httpupgrade there also ends up as network: httpupgrade without ws-opts —

network := strings.ToLower(query.Get("type"))
if network != "" {
trojan["network"] = network
}
switch network {
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
trojan["ws-opts"] = wsOpts

Change

  • common/convert/v.go (vless): for httpupgrade, set network: ws and ws-opts.v2ray-http-upgrade: true. Path, Host and the existing ed → v2ray-http-upgrade-fast-open handling are kept.
  • common/convert/converter.go (base64 vmess): the same.
  • common/convert/converter.go (trojan): accept httpupgrade in the ws case and set the same two fields.

ws links are not affected.

Tests

New tests in common/convert/converter_test.go:

  • TestConvertsV2RayVlessHTTPUpgrade: link without and with ed. Checks network: ws, v2ray-http-upgrade: true, path and Host, fast-open only with ed, no max-early-data, and that adapter.ParseProxy accepts the result.
  • TestConvertsV2RayVmessBase64HTTPUpgrade: the same for base64 VMess, with ed in the path query (it is removed from the path as before).
  • TestConvertsV2RayTrojanHTTPUpgrade: network: ws and v2ray-http-upgrade: true for a trojan link.
  • TestConvertsV2RayVlessWSNotHTTPUpgrade: a ws link with ed still gets max-early-data and no v2ray-http-upgrade.

Before the fix the three HTTPUpgrade tests fail (network is httpupgrade, v2ray-http-upgrade is missing). After the fix all tests in the package pass.

I also checked the wire behaviour with a throwaway test (not included). The converted vless proxy dialed a loopback listener. Before the fix the listener never received an HTTP request; reading one timed out. After the fix it got GET /up with Upgrade: websocket, no Sec-WebSocket-Key, and Host: cdn.example.com.

go test ./common/convert/ (with and without -tags with_gvisor), go vet ./common/convert/ and gofmt -l are clean (Go 1.26, linux/amd64).

Limitations

The trojan parser still does not read host or ed for ws/httpupgrade. That is how it already behaves for ws, and I left it unchanged here.

@wwqgtxx
wwqgtxx merged commit e9b9a1a into MetaCubeX:Alpha Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants