Przeglądaj źródła

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 <[email protected]>
pwnnex 4 dni temu
rodzic
commit
2983ac3f8e
3 zmienionych plików z 155 dodań i 2 usunięć
  1. 71 0
      sub/subService.go
  2. 58 0
      web/assets/js/model/inbound.js
  3. 26 2
      web/assets/js/model/outbound.js

+ 71 - 0
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=<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 {

+ 58 - 0
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;
         }
 

+ 26 - 2
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') {