diff --git a/internal/util/link/outbound.go b/internal/util/link/outbound.go index b4c7bd77a..8179a8475 100644 --- a/internal/util/link/outbound.go +++ b/internal/util/link/outbound.go @@ -622,7 +622,22 @@ func applyTransport(stream map[string]any, p url.Values) { if m := p.Get("mode"); m != "" { xh["mode"] = m } - // A few advanced xhttp fields that are commonly carried + // The panel's own emitters carry the advanced xhttp knobs as a + // snake_case x_padding_bytes plus an extra= blob, not as top-level + // camelCase params. Mirror the TS parser's precedence (snake_case alias + // -> extra JSON -> explicit camelCase) so a re-imported share link keeps + // them instead of silently reverting to the stream defaults. + if v := p.Get("x_padding_bytes"); v != "" { + xh["xPaddingBytes"] = v + } + if extra := p.Get("extra"); extra != "" { + var parsed map[string]any + if err := json.Unmarshal([]byte(extra), &parsed); err == nil { + for k, v := range parsed { + xh[k] = v + } + } + } for _, k := range []string{"xPaddingBytes", "scMaxEachPostBytes", "scMinPostsIntervalMs", "uplinkChunkSize"} { if v := p.Get(k); v != "" { xh[k] = v diff --git a/internal/util/link/outbound_helpers_test.go b/internal/util/link/outbound_helpers_test.go index 66f46b011..ee0b2dbc6 100644 --- a/internal/util/link/outbound_helpers_test.go +++ b/internal/util/link/outbound_helpers_test.go @@ -156,6 +156,27 @@ func TestParse_WSAndGRPCTransport(t *testing.T) { } } +func TestParse_XhttpExtraAndSnakeCaseFields(t *testing.T) { + q := url.Values{} + q.Set("type", "xhttp") + q.Set("encryption", "none") + q.Set("security", "none") + q.Set("mode", "auto") + q.Set("x_padding_bytes", "1-50") + q.Set("extra", `{"mode":"auto","xPaddingBytes":"1-50","scMaxEachPostBytes":"1000000"}`) + res, err := ParseLink("vless://uuid@h.com:443?" + q.Encode() + "#r") + if err != nil { + t.Fatalf("parse: %v", err) + } + xh := streamSub(t, res, "xhttpSettings") + if xh["xPaddingBytes"] != "1-50" { + t.Errorf("xPaddingBytes = %v, want 1-50 (dropped from the snake_case/extra payload the emitter writes)", xh["xPaddingBytes"]) + } + if xh["scMaxEachPostBytes"] != "1000000" { + t.Errorf("scMaxEachPostBytes = %v, want 1000000 (dropped from the extra blob)", xh["scMaxEachPostBytes"]) + } +} + func TestParse_TCPHTTPHeader(t *testing.T) { res, err := ParseLink("vless://uuid@h.com:443?type=tcp&headerType=http&host=ex.com&path=%2F") if err != nil {