From ba27b74be88db54c8b3c99826259965cb4a06941 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 14:55:57 +0100 Subject: [PATCH] feat(xray): keep the XHTTP server's h2Params xray-core 26.10.31 (#7295) added xhttpSettings.h2Params, a Go net/http HTTP2Config the XHTTP server applies to its HTTP/2 listener. The inbound form parsed xhttpSettings through a Zod object that strips unknown keys, so a value set through the JSON editor vanished on the next save; it is now kept as an optional record. h2Params tunes the server only. Share links already carry an allowlist of XHTTP keys, but the JSON subscription copies the inbound's xhttpSettings minus a denylist, which now drops h2Params too. --- frontend/src/schemas/protocols/stream/xhttp.ts | 2 ++ frontend/src/test/inbound-form-adapter.test.ts | 15 +++++++++++++++ internal/sub/json_service.go | 1 + internal/sub/json_service_test.go | 13 +++++++++++++ 4 files changed, 31 insertions(+) diff --git a/frontend/src/schemas/protocols/stream/xhttp.ts b/frontend/src/schemas/protocols/stream/xhttp.ts index 1c558e72b..4b92f8a81 100644 --- a/frontend/src/schemas/protocols/stream/xhttp.ts +++ b/frontend/src/schemas/protocols/stream/xhttp.ts @@ -108,6 +108,8 @@ export const XHttpStreamSettingsSchema = z.preprocess( serverMaxHeaderBytes: z.number().int().min(0).default(0), uplinkHTTPMethod: z.string().default(''), headers: WsHeaderMapSchema.default({}), + // Server-only Go net/http HTTP2Config (xray-core 26.10.31); never put into share links. + h2Params: z.record(z.string(), z.unknown()).optional(), // Client-side fields stored on inbound for subscription propagation. // The server listener ignores them at runtime, but the panel embeds // them in share-link `extra` blobs so the same xhttp config can diff --git a/frontend/src/test/inbound-form-adapter.test.ts b/frontend/src/test/inbound-form-adapter.test.ts index c0180a65a..0f47e0aff 100644 --- a/frontend/src/test/inbound-form-adapter.test.ts +++ b/frontend/src/test/inbound-form-adapter.test.ts @@ -228,6 +228,21 @@ describe('formValuesToWirePayload', () => { expect(payload.sniffing).toBe(''); }); + // xray-core 26.10.31 (#7295): the XHTTP server reads h2Params; the form must not strip it. + it('keeps an XHTTP inbound h2Params set through JSON', () => { + const h2Params = { MaxConcurrentStreams: 512, MaxReadFrameSize: 1048576 }; + const values = rawInboundToFormValues({ + ...vlessRow, + streamSettings: { + network: 'xhttp', + security: 'none', + xhttpSettings: { path: '/x', mode: 'auto', h2Params }, + }, + }); + const payload = formValuesToWirePayload(values); + expect(JSON.parse(payload.streamSettings).xhttpSettings.h2Params).toEqual(h2Params); + }); + it('omits nodeId when null', () => { const values = rawInboundToFormValues({ ...vlessRow, nodeId: null }); const payload = formValuesToWirePayload(values); diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index caa9cc1e9..35468cb5f 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -809,6 +809,7 @@ func (s *SubJsonService) streamData(stream string, clientKey string) map[string] delete(xhttp, "scMaxBufferedPosts") delete(xhttp, "scStreamUpServerSecs") delete(xhttp, "serverMaxHeaderBytes") + delete(xhttp, "h2Params") // 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). diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index 4a03af723..e89950af1 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -155,6 +155,19 @@ func TestSubJsonServiceInjectsGlobalFinalMask(t *testing.T) { } } +// h2Params (xray-core 26.10.31, #7295) tunes the XHTTP server's HTTP/2; clients must not get it. +func TestSubJsonServiceDropsXhttpServerOnlyH2Params(t *testing.T) { + svc := NewSubJsonService("", "", "", "", nil) + stream := svc.streamData(`{"network":"xhttp","security":"none","xhttpSettings":{"path":"/x","mode":"auto","h2Params":{"MaxConcurrentStreams":512}}}`, "") + xhttp, _ := stream["xhttpSettings"].(map[string]any) + if xhttp == nil || xhttp["path"] != "/x" { + t.Fatalf("xhttpSettings = %#v, want the inbound's path kept", stream["xhttpSettings"]) + } + if h2, ok := xhttp["h2Params"]; ok { + t.Fatalf("xhttpSettings.h2Params = %#v, want the server-only key dropped", h2) + } +} + func TestSubJsonServiceMergesWithExistingFinalMask(t *testing.T) { finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}` svc := NewSubJsonService("", "", finalMask, "", nil)