From 2983ac3f8eb40499a5dbb8c0fd450fdc8a43ee08 Mon Sep 17 00:00:00 2001 From: pwnnex Date: Tue, 21 Apr 2026 20:15:51 +0300 Subject: Fix xhttp xPadding settings missing from generated links (panel + subs) (#4065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: propagate xhttp xPadding settings into generated subscription links The four `genXLink` helpers in `sub/subService.go` only copied `path`, `host` and `mode` out of `xhttpSettings` when building vmess:// / vless:// / trojan:// / ss:// URLs. Everything else — `xPaddingBytes`, `xPaddingObfsMode`, `xPaddingKey`, `xPaddingHeader`, `xPaddingPlacement`, `xPaddingMethod` — was silently dropped. That meant an admin who set, say, `xPaddingBytes: "80-600"` plus obfs mode with a custom `xPaddingKey` on the inbound had a server config that no client could match from the copy-pasted link: the client kept the xray/sing-box internal defaults (`100-1000`, `x_padding`, `Referer`), hit the server, and was rejected by invalid padding (queryInHeader=Referer, key=x_padding) length: 0 The user-visible symptom on OpenWRT / Podkop / sing-box was "xhttp inbound just won't connect" — no obvious pointer to what was actually wrong because the link itself *looks* complete. Fix: * New helper `applyXhttpPaddingParams(xhttp, params)` writes `x_padding_bytes=` (flat, sing-box family reads this) and an `extra=` blob carrying the full set of xhttp settings (xray-core family reads this). Both encodings are emitted side-by-side so every mainstream client can pick at least one up. * All four link generators (`genVmessLink` via the obj map, `genVlessLink`, `genTrojanLink`, `genShadowsocksLink`) now invoke the copy. * Obfs-only fields (`xPaddingKey`, `xPaddingHeader`, `xPaddingPlacement`, `xPaddingMethod`) are only included when `xPaddingObfsMode` is actually true and the admin filled them in. An inbound with no custom padding produces exactly the same URL as before — existing subscriptions are unaffected. * Also propagate xhttp xPadding settings into the panel's own Info/QR links The previous commit covered the subscription service (sub/subService.go). The admin-panel side — the "Copy URL" / QR / Info buttons inside inbound details — has four more xhttp-emitting link generators in `web/assets/js/model/inbound.js` (`genVmessLink`, `genVLESSLink`, `genTrojanLink`, `genSSLink`) that had the exact same gap: only `path`, `host` and `mode` were copied. Mirror the server-side fix on the client: * Add two static helpers on `Inbound`: - `Inbound.applyXhttpPaddingToParams(xhttp, params)` for `vless://` / `trojan://` / `ss://` style URLs — writes `x_padding_bytes=` (sing-box family) and `extra=` (xray-core family). - `Inbound.applyXhttpPaddingToObj(xhttp, obj)` for the VMess base64 JSON body — sets the same fields directly on the object. * Call them from all four link generators so an admin who enables obfs mode + a custom `xPaddingKey` / `xPaddingHeader` actually gets a working URL from the panel. * Only non-empty fields are emitted, so default inbounds produce exactly the same URL as before. Also fixes a latent positional-args bug in `web/assets/js/model/outbound.js`: both VMess-JSON (L933) and `fromParamLink` (L975) were calling `new xHTTPStreamSettings(path, host, mode)` — but the 3rd positional arg of the constructor is `headers`, not `mode`, so `mode` was landing in the `headers` slot and the actual `mode` field stayed at its default. Construct explicitly and set `mode` by name; while here, also pick up `x_padding_bytes` and the `extra` JSON blob from the imported URL so the symmetric case of importing a padded link works too. --------- Co-authored-by: pwnnex --- sub/subService.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) (limited to 'sub') diff --git a/sub/subService.go b/sub/subService.go index a4d38485..bf9c029c 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -250,6 +250,21 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { obj["host"] = searchHost(headers) } obj["mode"], _ = xhttp["mode"].(string) + // 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. + 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 + } + } + } } security, _ := stream["security"].(string) obj["tls"] = security @@ -408,6 +423,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { params["host"] = searchHost(headers) } params["mode"], _ = xhttp["mode"].(string) + applyXhttpPaddingParams(xhttp, params) } security, _ := stream["security"].(string) if security == "tls" { @@ -604,6 +620,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string params["host"] = searchHost(headers) } params["mode"], _ = xhttp["mode"].(string) + applyXhttpPaddingParams(xhttp, params) } security, _ := stream["security"].(string) if security == "tls" { @@ -803,6 +820,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st params["host"] = searchHost(headers) } params["mode"], _ = xhttp["mode"].(string) + applyXhttpPaddingParams(xhttp, params) } security, _ := stream["security"].(string) @@ -1057,6 +1075,59 @@ 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. +// +// 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. +// +// Two encodings are written so every popular client can read at least one: +// +// - x_padding_bytes= — flat param, understood by sing-box and its +// derivatives (Podkop, OpenWRT sing-box, Karing, NekoBox, …). +// - extra= — 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) { + if xhttp == nil { + return + } + + 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 + } + } + } + + if len(extra) > 0 { + if b, err := json.Marshal(extra); err == nil { + params["extra"] = string(b) + } + } +} + func searchHost(headers any) string { data, _ := headers.(map[string]any) for k, v := range data { -- cgit v1.2.3