mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-27 01:45:55 +00:00
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>
This commit is contained in:
parent
975d6d1bad
commit
2983ac3f8e
3 changed files with 155 additions and 2 deletions
|
|
@ -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=<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) {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue