refactor(xhttp): split fields by direction, expand outbound coverage

Audit panel xhttp config against xray-core's runtime paths and split
fields per direction so each side carries only what it actually uses:

- Bidirectional (must match): host, path, mode, all xPadding*,
  session*/seq*, uplinkData*/Key, scMaxEachPostBytes
- Server-only (inbound): noSSEHeader, scMaxBufferedPosts,
  scStreamUpServerSecs, serverMaxHeaderBytes
- Client-only (outbound): uplinkHTTPMethod, uplinkChunkSize,
  noGRPCHeader, scMinPostsIntervalMs, xmux

The inbound previously held client-only fields and the outbound was
missing every must-match field beyond host/path/mode — meaning a
panel-built outbound couldn't connect to an inbound with a custom
xPaddingKey/sessionKey/etc.

Headers stay on the inbound for URL-share purposes only; xray's
listener ignores them at runtime, but they travel through the share
link's `extra` blob so the client picks them up.

Renames the URL helpers (applyXhttpPadding* -> applyXhttpExtra*) since
the blob now carries more than padding, and folds path/host/mode into
the helper so each link generator's xhttp branch is one line.

Adds two enforcement points for xray's "uplinkHTTPMethod=GET only in
packet-up" rule: the GET option is disabled when mode != packet-up,
and a watcher on the outbound modal auto-clears GET when the user
switches modes.

Hides the XMUX block behind an `enableXmux` switch on the outbound
form (mirrors the QUIC Params toggle) so the section doesn't clutter
the form by default; fromJson auto-flips it on for outbounds with
saved xmux config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-07 19:26:40 +02:00
parent 3b64a62137
commit 42b2ebc00b
6 changed files with 482 additions and 156 deletions

View File

@@ -582,28 +582,18 @@ func applyShareNetworkParams(stream map[string]any, streamNetwork string, params
applyPathAndHostParams(httpupgrade, params)
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]any)
applyPathAndHostParams(xhttp, params)
params["mode"], _ = xhttp["mode"].(string)
applyXhttpPaddingParams(xhttp, params)
applyXhttpExtraParams(xhttp, params)
}
}
func applyXhttpPaddingObj(xhttp map[string]any, obj map[string]any) {
// VMess base64 JSON supports arbitrary keys; copy the padding
// settings through so clients can match the server's xhttp
// xPaddingBytes range and, when the admin opted into obfs
// mode, the custom key / header / placement / method.
// applyXhttpExtraObj copies the bidirectional xhttp settings into the
// VMess base64 JSON link object. VMess supports arbitrary keys, so we
// flatten the SplitHTTPConfig "extra" fields directly onto obj.
func applyXhttpExtraObj(xhttp map[string]any, obj map[string]any) {
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
obj["x_padding_bytes"] = xpb
}
if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs {
obj["xPaddingObfsMode"] = true
for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} {
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
obj[field] = v
}
}
}
maps.Copy(obj, buildXhttpExtra(xhttp))
}
func applyVmessNetworkParams(stream map[string]any, network string, obj map[string]any) {
@@ -639,8 +629,10 @@ func applyVmessNetworkParams(stream map[string]any, network string, obj map[stri
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]any)
applyPathAndHostObj(xhttp, obj)
obj["mode"], _ = xhttp["mode"].(string)
applyXhttpPaddingObj(xhttp, obj)
if mode, ok := xhttp["mode"].(string); ok {
obj["mode"] = mode
}
applyXhttpExtraObj(xhttp, obj)
}
}
@@ -928,45 +920,33 @@ func searchKey(data any, key string) (any, bool) {
return nil, false
}
// applyXhttpPaddingParams copies the xPadding* fields from an xhttpSettings
// map into the URL query params of a vless:// / trojan:// / ss:// link.
// buildXhttpExtra walks an xhttpSettings map and returns the JSON blob
// that goes into the URL's `extra` param (or, for VMess, the link
// object). Carries ONLY the bidirectional fields from xray-core's
// SplitHTTPConfig — i.e. the ones the server enforces and the client
// must match. Strictly one-sided fields are excluded:
//
// Before this helper existed, only path / host / mode were propagated,
// so a server configured with a non-default xPaddingBytes (e.g. 80-600)
// or with xPaddingObfsMode=true + custom xPaddingKey / xPaddingHeader
// would silently diverge from the client: the client kept defaults,
// hit the server, and was rejected by its padding validation
// ("invalid padding" in the inbound log) — the client-visible symptom
// was "xhttp doesn't connect" on OpenWRT / sing-box.
// - server-only (noSSEHeader, scMaxBufferedPosts, scStreamUpServerSecs,
// serverMaxHeaderBytes) — client wouldn't read them, so emitting
// them just bloats the URL.
// - client-only (headers, uplinkHTTPMethod, uplinkChunkSize,
// noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) — the
// inbound config doesn't have them; the client configures them
// locally.
//
// Two encodings are written so every popular client can read at least one:
//
// - x_padding_bytes=<range> — flat param, understood by sing-box and its
// derivatives (Podkop, OpenWRT sing-box, Karing, NekoBox, …).
// - extra=<url-encoded-json> — full xhttp settings blob, which is how
// xray-core clients (v2rayNG, Happ, Furious, Exclave, …) pick up the
// obfs-mode key / header / placement / method.
//
// Anything that doesn't map to a non-empty value is skipped, so simple
// inbounds (no custom padding) produce exactly the same URL as before.
func applyXhttpPaddingParams(xhttp map[string]any, params map[string]string) {
// Truthy-only guards keep default inbounds emitting the same compact URL
// they did before this helper grew.
func buildXhttpExtra(xhttp map[string]any) map[string]any {
if xhttp == nil {
return
return nil
}
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
params["x_padding_bytes"] = xpb
}
extra := map[string]any{}
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
extra["xPaddingBytes"] = xpb
}
if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs {
extra["xPaddingObfsMode"] = true
// The obfs-mode-only fields: only populate the ones the admin
// actually set, so xray-core falls back to its own defaults for
// the rest instead of seeing spurious empty strings.
for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} {
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
extra[field] = v
@@ -974,7 +954,74 @@ func applyXhttpPaddingParams(xhttp map[string]any, params map[string]string) {
}
}
if len(extra) > 0 {
stringFields := []string{
"sessionPlacement", "sessionKey",
"seqPlacement", "seqKey",
"uplinkDataPlacement", "uplinkDataKey",
"scMaxEachPostBytes",
}
for _, field := range stringFields {
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
extra[field] = v
}
}
// Headers — emitted as the {name: value} map upstream's struct
// expects. The server runtime ignores this field, but the client
// (consuming the share link) honors it. Drop any "host" entry —
// host already wins as a top-level URL param.
if rawHeaders, ok := xhttp["headers"].(map[string]any); ok && len(rawHeaders) > 0 {
out := map[string]any{}
for k, v := range rawHeaders {
if strings.EqualFold(k, "host") {
continue
}
out[k] = v
}
if len(out) > 0 {
extra["headers"] = out
}
}
if len(extra) == 0 {
return nil
}
return extra
}
// applyXhttpExtraParams emits the full xhttp config into the URL query
// params of a vless:// / trojan:// / ss:// link. Sets path/host/mode at
// top level (xray's Build() always lets these win over `extra`) and packs
// everything else into a JSON `extra` param. Also writes the flat
// `x_padding_bytes` param sing-box-family clients understand.
//
// Without this, the admin's custom xPaddingBytes / sessionKey / etc. never
// reach the client and handshakes are silently rejected with
// `invalid padding (...) length: 0` — the client-visible symptom is
// "xhttp doesn't connect" on OpenWRT / sing-box.
//
// Two encodings are written so every popular client can read at least one:
//
// - x_padding_bytes=<range> — flat param, understood by sing-box and its
// derivatives (Podkop, OpenWRT sing-box, Karing, NekoBox, …).
// - extra=<url-encoded-json> — full xhttp settings blob, which is how
// xray-core clients (v2rayNG, Happ, Furious, Exclave, …) pick up the
// bidirectional fields beyond path/host/mode.
func applyXhttpExtraParams(xhttp map[string]any, params map[string]string) {
if xhttp == nil {
return
}
applyPathAndHostParams(xhttp, params)
if mode, ok := xhttp["mode"].(string); ok {
params["mode"] = mode
}
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
params["x_padding_bytes"] = xpb
}
extra := buildXhttpExtra(xhttp)
if extra != nil {
if b, err := json.Marshal(extra); err == nil {
params["extra"] = string(b)
}