From 60da6bed15eef3b911bb52f5e9c01604cf7ba36c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 12 Jun 2026 01:50:37 +0200 Subject: [PATCH] fix(xhttp): stop injecting scMaxEachPostBytes/scMinPostsIntervalMs defaults (#5141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitattributes | 1 + frontend/src/lib/xray/inbound-link.ts | 8 +++++++- frontend/src/lib/xray/outbound-link-parser.ts | 2 +- frontend/src/lib/xray/stream-wire-normalize.ts | 2 ++ .../pages/xray/outbounds/outbound-form-helpers.ts | 2 +- frontend/src/schemas/protocols/stream/xhttp.ts | 7 +++++-- frontend/src/test/__snapshots__/stream.test.ts.snap | 12 ++++++------ internal/sub/json_service.go | 9 +++++++++ internal/sub/service.go | 10 +++++++++- internal/util/link/outbound.go | 4 +++- 10 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.gitattributes b/.gitattributes index cd2f2f8f7..ceb414770 100644 --- a/.gitattributes +++ b/.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 \ No newline at end of file diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index bd3db73cf..4ca6e5e36 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -59,9 +59,15 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record> = { + 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 diff --git a/frontend/src/lib/xray/outbound-link-parser.ts b/frontend/src/lib/xray/outbound-link-parser.ts index 89c9c92bd..85760b37d 100644 --- a/frontend/src/lib/xray/outbound-link-parser.ts +++ b/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: diff --git a/frontend/src/lib/xray/stream-wire-normalize.ts b/frontend/src/lib/xray/stream-wire-normalize.ts index 5cb8dfbde..2d91338f3 100644 --- a/frontend/src/lib/xray/stream-wire-normalize.ts +++ b/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; diff --git a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts index af720a406..2ca300136 100644 --- a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts +++ b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts @@ -45,7 +45,7 @@ export function newStreamSlice(network: string): Record { network: 'xhttp', xhttpSettings: { path: '/', host: '', mode: '', headers: [], - xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000', + xPaddingBytes: '100-1000', }, }; case 'hysteria': diff --git a/frontend/src/schemas/protocols/stream/xhttp.ts b/frontend/src/schemas/protocols/stream/xhttp.ts index 7d2e79b8f..9b53810bd 100644 --- a/frontend/src/schemas/protocols/stream/xhttp.ts +++ b/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(), diff --git a/frontend/src/test/__snapshots__/stream.test.ts.snap b/frontend/src/test/__snapshots__/stream.test.ts.snap index 733dcd9c0..34caecb42 100644 --- a/frontend/src/test/__snapshots__/stream.test.ts.snap +++ b/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", diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 1cd7639f2..609052642 100644 --- a/internal/sub/json_service.go +++ b/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 diff --git a/internal/sub/service.go b/internal/sub/service.go index 522746fe3..3d322e03a 100644 --- a/internal/sub/service.go +++ b/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 } } diff --git a/internal/util/link/outbound.go b/internal/util/link/outbound.go index 4b11bf7c7..cfcf18b82 100644 --- a/internal/util/link/outbound.go +++ b/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"}}