Преглед изворни кода

fix(inbound): strip XHTTP client-only fields from xray config, keep for subscriptions (#5349)

Inbound XMUX and other client-side xHTTP knobs were written into
bin/config.json even though xray-core's server listener ignores them.
Strip them in GenXrayInboundConfig while leaving the DB row intact so
buildXhttpExtra still pushes defaults to clients via share links.
nima1024m пре 14 часа
родитељ
комит
cdaf5f80db

+ 1 - 1
frontend/src/test/stream-wire-normalize.test.ts

@@ -66,7 +66,7 @@ describe('normalizeXhttpForWire stream-one', () => {
     expect(out).not.toHaveProperty('scMaxEachPostBytes');
   });
 
-  it('keeps inbound xmux when enableXmux is on (for the share-link extra)', () => {
+  it('keeps inbound xmux when enableXmux is on (stored for subscription extra; stripped from xray config on Go side)', () => {
     const out = normalizeXhttpForWire({
       path: '/app',
       mode: 'auto',

+ 48 - 1
internal/database/model/model.go

@@ -225,6 +225,49 @@ func jsonStringFieldFromRaw(r json.RawMessage) string {
 	return string(trimmed)
 }
 
+// StripInboundXhttpClientFields removes xHTTP knobs that belong on the
+// client dialer and subscription share-link extras only. xray-core's XHTTP
+// inbound listener does not consume them; the panel still stores them on
+// the inbound row so buildXhttpExtra can push defaults to clients.
+func StripInboundXhttpClientFields(streamSettings string) (string, bool) {
+	if streamSettings == "" {
+		return streamSettings, false
+	}
+	var stream map[string]any
+	if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
+		return streamSettings, false
+	}
+	if stream["network"] != "xhttp" {
+		return streamSettings, false
+	}
+	xhttp, ok := stream["xhttpSettings"].(map[string]any)
+	if !ok || len(xhttp) == 0 {
+		return streamSettings, false
+	}
+	clientOnly := []string{
+		"xmux",
+		"downloadSettings",
+		"scMinPostsIntervalMs",
+		"uplinkChunkSize",
+		"noGRPCHeader",
+	}
+	changed := false
+	for _, key := range clientOnly {
+		if _, has := xhttp[key]; has {
+			delete(xhttp, key)
+			changed = true
+		}
+	}
+	if !changed {
+		return streamSettings, false
+	}
+	out, err := json.MarshalIndent(stream, "", "  ")
+	if err != nil {
+		return streamSettings, false
+	}
+	return string(out), true
+}
+
 // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
 func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
 	listen := i.Listen
@@ -248,12 +291,16 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
 			settings = stripped
 		}
 	}
+	streamSettings := i.StreamSettings
+	if stripped, ok := StripInboundXhttpClientFields(streamSettings); ok {
+		streamSettings = stripped
+	}
 	return &xray.InboundConfig{
 		Listen:         json_util.RawMessage(listen),
 		Port:           i.Port,
 		Protocol:       protocol,
 		Settings:       json_util.RawMessage(settings),
-		StreamSettings: json_util.RawMessage(i.StreamSettings),
+		StreamSettings: json_util.RawMessage(streamSettings),
 		Tag:            i.Tag,
 		Sniffing:       json_util.RawMessage(i.Sniffing),
 	}

+ 82 - 0
internal/database/model/model_test.go

@@ -188,3 +188,85 @@ func TestInboundClientIpsUnmarshalJSONAcceptsBothShapes(t *testing.T) {
 		})
 	}
 }
+
+func TestStripInboundXhttpClientFields_RemovesClientOnlyKnobs(t *testing.T) {
+	stream := `{
+		"network": "xhttp",
+		"security": "reality",
+		"xhttpSettings": {
+			"path": "/app",
+			"host": "example.com",
+			"mode": "stream-one",
+			"xmux": { "maxConcurrency": "16-32" },
+			"downloadSettings": { "network": "xhttp" },
+			"scMinPostsIntervalMs": "20-40",
+			"uplinkChunkSize": 4096,
+			"noGRPCHeader": true
+		}
+	}`
+	out, changed := StripInboundXhttpClientFields(stream)
+	if !changed {
+		t.Fatal("expected client-only xhttp fields to be stripped")
+	}
+	if strings.Contains(out, `"xmux"`) {
+		t.Fatalf("xmux should be removed from xray config stream: %s", out)
+	}
+	for _, key := range []string{"downloadSettings", "scMinPostsIntervalMs", "uplinkChunkSize", "noGRPCHeader"} {
+		if strings.Contains(out, `"`+key+`"`) {
+			t.Fatalf("%s should be removed from xray config stream: %s", key, out)
+		}
+	}
+	var parsed map[string]any
+	if err := json.Unmarshal([]byte(out), &parsed); err != nil {
+		t.Fatalf("invalid JSON: %v", err)
+	}
+	xhttp := parsed["xhttpSettings"].(map[string]any)
+	if xhttp["path"] != "/app" || xhttp["host"] != "example.com" {
+		t.Fatalf("server fields must survive: %#v", xhttp)
+	}
+}
+
+func TestStripInboundXhttpClientFields_UnchangedWithoutClientFields(t *testing.T) {
+	stream := `{"network":"xhttp","xhttpSettings":{"path":"/app","mode":"stream-one"}}`
+	out, changed := StripInboundXhttpClientFields(stream)
+	if changed {
+		t.Fatalf("expected no change, got: %s", out)
+	}
+	if out != stream {
+		t.Fatalf("unchanged stream must be returned verbatim")
+	}
+}
+
+func TestStripInboundXhttpClientFields_NonXhttpPassthrough(t *testing.T) {
+	stream := `{"network":"ws","wsSettings":{"path":"/"}}`
+	out, changed := StripInboundXhttpClientFields(stream)
+	if changed || out != stream {
+		t.Fatalf("non-xhttp stream must pass through unchanged, got changed=%v out=%s", changed, out)
+	}
+}
+
+func TestGenXrayInboundConfig_OmitsInboundXmuxButDbRowUnchanged(t *testing.T) {
+	stream := `{
+		"network": "xhttp",
+		"xhttpSettings": {
+			"path": "/app",
+			"mode": "stream-one",
+			"xmux": { "maxConcurrency": "16-32", "hMaxRequestTimes": "600-900" }
+		}
+	}`
+	in := Inbound{
+		Protocol:       VLESS,
+		Port:           443,
+		Listen:         "0.0.0.0",
+		Tag:            "in-xhttp",
+		Settings:       `{"clients":[],"decryption":"none"}`,
+		StreamSettings: stream,
+	}
+	cfg := in.GenXrayInboundConfig()
+	if strings.Contains(string(cfg.StreamSettings), `"xmux"`) {
+		t.Fatalf("GenXrayInboundConfig must not emit xmux: %s", cfg.StreamSettings)
+	}
+	if strings.Contains(in.StreamSettings, `"xmux"`) == false {
+		t.Fatal("inbound row streamSettings must still carry xmux for subscriptions")
+	}
+}