diff options
| author | pwnnex <pwnnex@proton.me> | 2026-04-21 20:15:51 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-21 20:15:51 +0300 |
| commit | 2983ac3f8eb40499a5dbb8c0fd450fdc8a43ee08 (patch) | |
| tree | 552da151ad2298228d4971f9c7a8eed7de95f512 /web | |
| parent | 975d6d1bad4899f1123243c8b8c0392e17859295 (diff) | |
Fix xhttp xPadding settings missing from generated links (panel + subs) (#4065)
* 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=<range>` (flat, sing-box family reads this) and
an `extra=<url-encoded-json>` 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=<range>` (sing-box family) and
`extra=<url-encoded-json>` (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 <eternxles@gmail.com>
Diffstat (limited to 'web')
| -rw-r--r-- | web/assets/js/model/inbound.js | 58 | ||||
| -rw-r--r-- | web/assets/js/model/outbound.js | 28 |
2 files changed, 84 insertions, 2 deletions
diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index d0060fb4..1cef368b 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -1317,6 +1317,60 @@ class Inbound extends XrayCommonClass { return this.clientStats; } + // Copy the xPadding* settings into the query-string of a vless/trojan/ss + // link. Without this, the admin's custom xPaddingBytes range and (in + // obfs mode) the custom xPaddingKey / xPaddingHeader / placement / + // method never reach the client — the client keeps xray / sing-box's + // internal defaults and the server rejects every handshake with + // `invalid padding (...) length: 0`. + // + // Two encodings are emitted so each client family can pick at least + // one up: + // - x_padding_bytes=<range> flat, for sing-box-family clients + // - extra=<url-encoded-json> full blob, for xray-core clients + // + // Fields are only included when they actually have a value, so a + // default inbound yields the same URL it did before this helper. + static applyXhttpPaddingToParams(xhttp, params) { + if (!xhttp) return; + if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) { + params.set("x_padding_bytes", xhttp.xPaddingBytes); + } + const extra = {}; + if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) { + extra.xPaddingBytes = xhttp.xPaddingBytes; + } + if (xhttp.xPaddingObfsMode === true) { + extra.xPaddingObfsMode = true; + ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => { + if (typeof xhttp[k] === 'string' && xhttp[k].length > 0) { + extra[k] = xhttp[k]; + } + }); + } + if (Object.keys(extra).length > 0) { + params.set("extra", JSON.stringify(extra)); + } + } + + // VMess variant: VMess links are a base64-encoded JSON object, so we + // copy the padding fields directly into the JSON instead of building + // a query string. + static applyXhttpPaddingToObj(xhttp, obj) { + if (!xhttp || !obj) return; + if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) { + obj.x_padding_bytes = xhttp.xPaddingBytes; + } + if (xhttp.xPaddingObfsMode === true) { + obj.xPaddingObfsMode = true; + ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => { + if (typeof xhttp[k] === 'string' && xhttp[k].length > 0) { + obj[k] = xhttp[k]; + } + }); + } + } + get clients() { switch (this.protocol) { case Protocols.VMESS: return this.settings.vmesses; @@ -1530,6 +1584,7 @@ class Inbound extends XrayCommonClass { obj.path = xhttp.path; obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'); obj.type = xhttp.mode; + Inbound.applyXhttpPaddingToObj(xhttp, obj); } if (tls === 'tls') { @@ -1594,6 +1649,7 @@ class Inbound extends XrayCommonClass { params.set("path", xhttp.path); params.set("host", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host')); params.set("mode", xhttp.mode); + Inbound.applyXhttpPaddingToParams(xhttp, params); break; } @@ -1694,6 +1750,7 @@ class Inbound extends XrayCommonClass { params.set("path", xhttp.path); params.set("host", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host')); params.set("mode", xhttp.mode); + Inbound.applyXhttpPaddingToParams(xhttp, params); break; } @@ -1770,6 +1827,7 @@ class Inbound extends XrayCommonClass { params.set("path", xhttp.path); params.set("host", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host')); params.set("mode", xhttp.mode); + Inbound.applyXhttpPaddingToParams(xhttp, params); break; } diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 2a288c49..97602815 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -930,7 +930,13 @@ class Outbound extends CommonClass { } else if (network === 'httpupgrade') { stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host); } else if (network === 'xhttp') { - stream.xhttp = new xHTTPStreamSettings(json.path, json.host, json.mode); + // xHTTPStreamSettings positional args are (path, host, headers, ..., mode); + // passing `json.mode` as the 3rd argument used to land in the `headers` + // slot, dropping the mode on the floor. Build the object and set mode + // explicitly to avoid that. + const xh = new xHTTPStreamSettings(json.path, json.host); + if (json.mode) xh.mode = json.mode; + stream.xhttp = xh; } if (json.tls && json.tls == 'tls') { @@ -972,7 +978,25 @@ class Outbound extends CommonClass { } else if (type === 'httpupgrade') { stream.httpupgrade = new HttpUpgradeStreamSettings(path, host); } else if (type === 'xhttp') { - stream.xhttp = new xHTTPStreamSettings(path, host, mode); + // Same positional bug as in the VMess-JSON branch above: + // passing `mode` as the 3rd positional arg put it into the + // `headers` slot. Build explicitly instead. + const xh = new xHTTPStreamSettings(path, host); + if (mode) xh.mode = mode; + const xpb = url.searchParams.get('x_padding_bytes'); + if (xpb) xh.xPaddingBytes = xpb; + const extraRaw = url.searchParams.get('extra'); + if (extraRaw) { + try { + const extra = JSON.parse(extraRaw); + if (typeof extra.xPaddingBytes === 'string' && extra.xPaddingBytes) xh.xPaddingBytes = extra.xPaddingBytes; + if (extra.xPaddingObfsMode === true) xh.xPaddingObfsMode = true; + ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => { + if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k]; + }); + } catch (_) { /* ignore malformed extra */ } + } + stream.xhttp = xh; } if (security == 'tls') { |
