소스 검색

fix(xhttp): stop injecting scMaxEachPostBytes/scMinPostsIntervalMs defaults (#5141)

The panel seeded xhttp configs with scMaxEachPostBytes=1000000 and
scMinPostsIntervalMs=30 — xray-core''s own defaults — and emitted them
into every generated config and share link. The literal
scMinPostsIntervalMs=30 is a stable DPI fingerprint that Russia''s TSPU
keys on to block connections on mobile networks.

New configs no longer seed these values (empty schema/template defaults,
so xray-core applies its internal defaults). For configs already stored
with the old defaults, the link/subscription builders now drop values
equal to xray-core''s defaults instead of advertising them — covering
panel share links, the raw subscription, and the JSON subscription
without requiring every inbound to be re-saved. Non-default values the
user set deliberately are still emitted.
MHSanaei 9 시간 전
부모
커밋
60da6bed15

+ 1 - 0
.gitattributes

@@ -8,3 +8,4 @@ DockerEntrypoint.sh text eol=lf
 # with core.autocrlf=true doesn't show phantom CRLF-only "modified" diffs.
 frontend/src/generated/** text eol=lf
 frontend/public/openapi.json text eol=lf
+frontend\src\test\__snapshots__\** text eol=lf

+ 7 - 1
frontend/src/lib/xray/inbound-link.ts

@@ -59,9 +59,15 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string,
     'uplinkDataKey',
     'scMaxEachPostBytes',
   ] as const;
+  // Values matching xray-core's own defaults stay off the wire — old panels
+  // seeded them into every config and the literal values are a DPI
+  // fingerprint (#5141). Mirrors the sub service's filter.
+  const coreDefaults: Partial<Record<(typeof stringFields)[number], string>> = {
+    scMaxEachPostBytes: '1000000',
+  };
   for (const k of stringFields) {
     const v = xhttp[k];
-    if (typeof v === 'string' && v.length > 0) extra[k] = v;
+    if (typeof v === 'string' && v.length > 0 && v !== coreDefaults[k]) extra[k] = v;
   }
 
   // Headers on the wire are a record; emit them as a map upstream's

+ 1 - 1
frontend/src/lib/xray/outbound-link-parser.ts

@@ -114,7 +114,7 @@ function buildStream(network: string, security: string): Raw {
     case 'xhttp':
       stream.xhttpSettings = {
         path: '/', host: '', mode: 'auto', headers: {},
-        xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
+        xPaddingBytes: '100-1000',
       };
       break;
     default:

+ 2 - 0
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -125,6 +125,8 @@ export function normalizeXhttpForWire(
   }
 
   dropEmptyStrings(out, PLACEMENT_STRING_FIELDS);
+  // Empty tuning fields mean "use xray-core's default" — never emit them.
+  dropEmptyStrings(out, ['scMaxEachPostBytes', 'scMinPostsIntervalMs', 'scStreamUpServerSecs']);
 
   if (!hasMeaningfulHeaders(out.headers)) {
     delete out.headers;

+ 1 - 1
frontend/src/pages/xray/outbounds/outbound-form-helpers.ts

@@ -45,7 +45,7 @@ export function newStreamSlice(network: string): Record<string, unknown> {
         network: 'xhttp',
         xhttpSettings: {
           path: '/', host: '', mode: '', headers: [],
-          xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
+          xPaddingBytes: '100-1000',
         },
       };
     case 'hysteria':

+ 5 - 2
frontend/src/schemas/protocols/stream/xhttp.ts

@@ -41,7 +41,10 @@ export const XHttpStreamSettingsSchema = z.object({
   seqKey: z.string().default(''),
   uplinkDataPlacement: z.string().default(''),
   uplinkDataKey: z.string().default(''),
-  scMaxEachPostBytes: z.string().default('1000000'),
+  // Empty default on purpose: xray-core already defaults to 1MB/30ms, and
+  // baking the literal values into every config and share link gives DPI a
+  // stable fingerprint (#5141 — TSPU keys on scMinPostsIntervalMs=30).
+  scMaxEachPostBytes: z.string().default(''),
   noSSEHeader: z.boolean().default(false),
   scMaxBufferedPosts: z.number().int().min(0).default(30),
   scStreamUpServerSecs: z.string().default('20-80'),
@@ -51,7 +54,7 @@ export const XHttpStreamSettingsSchema = z.object({
   // Outbound-only fields. Server (inbound) listener ignores these. The
   // panel embeds them in share-link `extra` blobs so the same xhttp
   // config can roundtrip on both sides.
-  scMinPostsIntervalMs: z.string().default('30'),
+  scMinPostsIntervalMs: z.string().default(''),
   uplinkChunkSize: z.number().int().min(0).default(0),
   noGRPCHeader: z.boolean().default(false),
   xmux: XHttpXmuxSchema.optional(),

+ 6 - 6
frontend/src/test/__snapshots__/stream.test.ts.snap

@@ -47,8 +47,8 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = `
     "noSSEHeader": false,
     "path": "/sp",
     "scMaxBufferedPosts": 30,
-    "scMaxEachPostBytes": "1000000",
-    "scMinPostsIntervalMs": "30",
+    "scMaxEachPostBytes": "",
+    "scMinPostsIntervalMs": "",
     "scStreamUpServerSecs": "20-80",
     "seqKey": "",
     "seqPlacement": "",
@@ -81,8 +81,8 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-padding byte-stably
     "noSSEHeader": false,
     "path": "/sp",
     "scMaxBufferedPosts": 30,
-    "scMaxEachPostBytes": "1000000",
-    "scMinPostsIntervalMs": "30",
+    "scMaxEachPostBytes": "",
+    "scMinPostsIntervalMs": "",
     "scStreamUpServerSecs": "20-80",
     "seqKey": "",
     "seqPlacement": "",
@@ -115,8 +115,8 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-placement byte-stab
     "noSSEHeader": false,
     "path": "/sp",
     "scMaxBufferedPosts": 30,
-    "scMaxEachPostBytes": "1000000",
-    "scMinPostsIntervalMs": "30",
+    "scMaxEachPostBytes": "",
+    "scMinPostsIntervalMs": "",
     "scStreamUpServerSecs": "20-80",
     "seqKey": "X-Seq",
     "seqPlacement": "cookie",

+ 9 - 0
internal/sub/json_service.go

@@ -217,6 +217,15 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
 			delete(xhttp, "scMaxBufferedPosts")
 			delete(xhttp, "scStreamUpServerSecs")
 			delete(xhttp, "serverMaxHeaderBytes")
+			// Values matching xray-core's own defaults stay off the wire:
+			// old panels seeded them into every stored config and the
+			// literal scMinPostsIntervalMs=30 is a DPI fingerprint (#5141).
+			if v, _ := xhttp["scMaxEachPostBytes"].(string); v == "" || v == "1000000" {
+				delete(xhttp, "scMaxEachPostBytes")
+			}
+			if v, _ := xhttp["scMinPostsIntervalMs"].(string); v == "" || v == "30" {
+				delete(xhttp, "scMinPostsIntervalMs")
+			}
 		}
 	}
 	return streamSettings

+ 9 - 1
internal/sub/service.go

@@ -1661,8 +1661,16 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
 		"uplinkDataPlacement", "uplinkDataKey",
 		"scMaxEachPostBytes", "scMinPostsIntervalMs",
 	}
+	// Values matching xray-core's own defaults are redundant on the wire and
+	// the literal scMinPostsIntervalMs=30 is a known DPI fingerprint (#5141).
+	// Old panels seeded these defaults into every xhttp inbound, so filter
+	// them here instead of requiring every stored config to be re-saved.
+	coreDefaults := map[string]string{
+		"scMaxEachPostBytes":   "1000000",
+		"scMinPostsIntervalMs": "30",
+	}
 	for _, field := range stringFields {
-		if v, ok := xhttp[field].(string); ok && len(v) > 0 {
+		if v, ok := xhttp[field].(string); ok && len(v) > 0 && v != coreDefaults[field] {
 			extra[field] = v
 		}
 	}

+ 3 - 1
internal/util/link/outbound.go

@@ -545,9 +545,11 @@ func buildStream(network, security string) map[string]any {
 	case "httpupgrade":
 		stream["httpupgradeSettings"] = map[string]any{"path": "/", "host": "", "headers": map[string]any{}}
 	case "xhttp":
+		// No scMaxEachPostBytes/scMinPostsIntervalMs seed: xray-core's own
+		// defaults apply, and the literal values fingerprint traffic (#5141).
 		stream["xhttpSettings"] = map[string]any{
 			"path": "/", "host": "", "mode": "auto", "headers": map[string]any{},
-			"xPaddingBytes": "100-1000", "scMaxEachPostBytes": "1000000",
+			"xPaddingBytes": "100-1000",
 		}
 	default:
 		stream["tcpSettings"] = map[string]any{"header": map[string]any{"type": "none"}}