From fea3c94b111a3e31bb6406b941bc2f60beffe19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:38:16 +0200 Subject: [PATCH] feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) xray-core v26.6.22 (PR #6258) renamed the XHTTP session config keys sessionPlacement/sessionKey to sessionIDPlacement/sessionIDKey (no fallback kept in core) and added sessionIDTable (predefined charset name or literal ASCII) and sessionIDLength (range, e.g. 16-32, lower bound > 0). Panel changes: - Schema (xhttp.ts): rename the two keys, add sessionIDTable/sessionIDLength, and a z.preprocess that lifts legacy keys off stored configs so an upgraded panel never silently drops a saved session setting. - Wire normalize + share-link build/parse: rename keys, emit the two new fields, and accept legacy sessionPlacement/sessionKey from old share links. - Inbound + outbound XHTTP forms: rename field paths, add a sessionIDTable autocomplete (9 predefined tables + free ASCII) and a sessionIDLength range input shown only when a table is set, with light client validation (ASCII table, length min > 0; xray enforces the room-size minimum server-side). - Subscription (service.go) and Clash (clash_service.go) builders: emit the renamed + new keys, with a legacy fallback for not-yet-resaved inbounds. - Locales: add sessionIDTable/sessionIDLength labels + hints in all 13 files. Two sibling v26.6.22 XHTTP commits need no panel change and are covered by the core bump alone: #6332 (XHTTP/3 closes QUIC/UDP) and #6320 (udpHop honors the existing dialerProxy). Co-Authored-By: Claude Opus 4.8 (1M context) * test(xhttp): add Session ID Table to inbound form-blocks snapshot The new sessionIDTable input renders by default in the inbound XHTTP form, so its label joins the field-structure snapshot. sessionIDLength stays conditional (only shown when a table is set), so it does not appear here. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(xhttp): migrate legacy session keys in the running xray config The Zod preprocess plus the subscription/Clash fallbacks only covered the panel UI and share-link output. The config handed to the running xray-core process is built from the raw stored streamSettings in GetXrayConfig, which did not rewrite the renamed XHTTP session keys — so a pre-upgrade inbound (or template outbound) stored with a non-default sessionPlacement was emitted unchanged and dropped by xray-core v26.6.22, until the admin re-saved it. Lift sessionPlacement/sessionKey onto sessionIDPlacement/sessionIDKey at config-generation time, in the existing inbound stream-rewrite block (next to the tls/reality/externalProxy handling) and across template outbounds. The lift is idempotent and leaves unchanged configs byte-identical so the hot-reload diff never sees a spurious change. Also tighten validateSessionIDLength to reject an inverted range (e.g. 32-16) in addition to the existing lower-bound > 0 check. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(xray): avoid summed-capacity allocation in mergeSubscriptionOutbounds CodeQL go/allocation-size-overflow flagged the pre-sized make() whose capacity was a sum of three slice lengths. Grow the slice via append on a nil slice instead; same result, no overflow-prone capacity expression. --- frontend/src/lib/xray/inbound-link.ts | 6 +- frontend/src/lib/xray/outbound-link-parser.ts | 26 +++++- .../src/lib/xray/stream-wire-normalize.ts | 6 +- frontend/src/lib/xray/xhttp-session-id.ts | 34 ++++++++ .../pages/inbounds/form/InboundFormModal.tsx | 2 +- .../pages/inbounds/form/transport/xhttp.tsx | 36 +++++++-- .../pages/xray/outbounds/transport/xhttp.tsx | 40 ++++++++- .../src/schemas/protocols/stream/xhttp.ts | 40 ++++++++- .../inbound-form-blocks.test.tsx.snap | 1 + .../test/__snapshots__/stream.test.ts.snap | 24 ++++-- .../stream/xhttp-extra-placement.json | 4 +- .../src/test/outbound-link-parser.test.ts | 11 ++- frontend/src/test/xhttp-session-id.test.ts | 54 +++++++++++++ internal/sub/clash_service.go | 21 ++++- internal/sub/clash_service_test.go | 20 +++-- internal/sub/service.go | 16 +++- internal/web/service/xray.go | 69 +++++++++++++++- .../web/service/xray_xhttp_session_test.go | 81 +++++++++++++++++++ internal/web/translation/ar-EG.json | 4 + internal/web/translation/en-US.json | 4 + internal/web/translation/es-ES.json | 4 + internal/web/translation/fa-IR.json | 4 + internal/web/translation/id-ID.json | 4 + internal/web/translation/ja-JP.json | 4 + internal/web/translation/pt-BR.json | 4 + internal/web/translation/ru-RU.json | 4 + internal/web/translation/tr-TR.json | 4 + internal/web/translation/uk-UA.json | 4 + internal/web/translation/vi-VN.json | 4 + internal/web/translation/zh-CN.json | 4 + internal/web/translation/zh-TW.json | 4 + 31 files changed, 498 insertions(+), 45 deletions(-) create mode 100644 frontend/src/lib/xray/xhttp-session-id.ts create mode 100644 frontend/src/test/xhttp-session-id.test.ts create mode 100644 internal/web/service/xray_xhttp_session_test.go diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 82376bac1..a3fccca3c 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -64,8 +64,10 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record; // match the schema's authoring order so diffs read naturally. const XHTTP_STRING_KEYS = [ 'xPaddingBytes', 'xPaddingKey', 'xPaddingHeader', 'xPaddingPlacement', - 'xPaddingMethod', 'sessionPlacement', 'sessionKey', 'seqPlacement', - 'seqKey', 'uplinkDataPlacement', 'uplinkDataKey', 'scMaxEachPostBytes', - 'scMinPostsIntervalMs', 'scStreamUpServerSecs', 'uplinkHTTPMethod', + 'xPaddingMethod', 'sessionIDPlacement', 'sessionIDKey', 'sessionIDTable', + 'sessionIDLength', 'seqPlacement', 'seqKey', 'uplinkDataPlacement', + 'uplinkDataKey', 'scMaxEachPostBytes', 'scMinPostsIntervalMs', + 'scStreamUpServerSecs', 'uplinkHTTPMethod', ] as const; +// Legacy share links (pre xray-core #6258) carry sessionPlacement/sessionKey. +// Map them onto the renamed keys so old links still import. Mirrors the +// schema-level migrateLegacyXhttp. +const XHTTP_LEGACY_ALIASES: Record = { + sessionPlacement: 'sessionIDPlacement', + sessionKey: 'sessionIDKey', +}; const XHTTP_NUMBER_KEYS = [ 'scMaxBufferedPosts', 'serverMaxHeaderBytes', 'uplinkChunkSize', ] as const; @@ -81,12 +89,24 @@ function applyXhttpStringFromParams(xhttp: Raw, params: URLSearchParams): void { const v = params.get(k); if (v !== null && v !== '') xhttp[k] = asBool(v); } + // Fill renamed keys from legacy params only when the new key is absent. + for (const [legacy, renamed] of Object.entries(XHTTP_LEGACY_ALIASES)) { + if (xhttp[renamed] === undefined) { + const v = params.get(legacy); + if (v !== null && v !== '') xhttp[renamed] = v; + } + } } function applyXhttpStringFromJson(xhttp: Raw, json: Record): void { for (const k of XHTTP_STRING_KEYS) { if (typeof json[k] === 'string') xhttp[k] = json[k]; } + for (const [legacy, renamed] of Object.entries(XHTTP_LEGACY_ALIASES)) { + if (xhttp[renamed] === undefined && typeof json[legacy] === 'string') { + xhttp[renamed] = json[legacy]; + } + } for (const k of XHTTP_NUMBER_KEYS) { if (typeof json[k] === 'number') xhttp[k] = json[k]; } diff --git a/frontend/src/lib/xray/stream-wire-normalize.ts b/frontend/src/lib/xray/stream-wire-normalize.ts index 645e8b309..e74e450fb 100644 --- a/frontend/src/lib/xray/stream-wire-normalize.ts +++ b/frontend/src/lib/xray/stream-wire-normalize.ts @@ -16,8 +16,10 @@ const PACKET_UP_FIELDS = [ const STREAM_UP_SERVER_FIELDS = ['scStreamUpServerSecs'] as const; const PLACEMENT_STRING_FIELDS = [ - 'sessionPlacement', - 'sessionKey', + 'sessionIDPlacement', + 'sessionIDKey', + 'sessionIDTable', + 'sessionIDLength', 'seqPlacement', 'seqKey', 'uplinkDataPlacement', diff --git a/frontend/src/lib/xray/xhttp-session-id.ts b/frontend/src/lib/xray/xhttp-session-id.ts new file mode 100644 index 000000000..b166f7648 --- /dev/null +++ b/frontend/src/lib/xray/xhttp-session-id.ts @@ -0,0 +1,34 @@ +// Client-side validation for xray-core #6258 sessionIDTable / sessionIDLength. +// xray-core also enforces a room-size minimum (sum(table^k for k in +// from..to) >= 2<<30) server-side; we deliberately skip replicating that +// big-int check and only catch the cheap, obvious mistakes here. + +// xray-core requires the charset table to be ASCII-only. +export function validateSessionIDTable(_rule: unknown, value: unknown): Promise { + const str = typeof value === 'string' ? value : ''; + if (str === '') return Promise.resolve(); + // eslint-disable-next-line no-control-regex + if (/[^\x00-\x7f]/.test(str)) { + return Promise.reject(new Error('sessionIDTable must contain only ASCII characters')); + } + return Promise.resolve(); +} + +// A dash-range like "8-16" or a single "8". The lower bound must be > 0 +// (xray rejects sessionIDLength.from <= 0 when a table is set). +export function validateSessionIDLength(_rule: unknown, value: unknown): Promise { + const str = typeof value === 'string' ? value.trim() : ''; + if (str === '') return Promise.resolve(); + if (!/^\d+(?:-\d+)?$/.test(str)) { + return Promise.reject(new Error('Use a length or range, e.g. 8 or 8-16')); + } + const parts = str.split('-'); + const from = Number(parts[0]); + if (!Number.isFinite(from) || from <= 0) { + return Promise.reject(new Error('sessionIDLength minimum must be greater than 0')); + } + if (parts.length === 2 && Number(parts[1]) < from) { + return Promise.reject(new Error('sessionIDLength range upper bound must be ≥ lower bound')); + } + return Promise.resolve(); +} diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index 779adbd77..b46542b0a 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -708,7 +708,7 @@ export default function InboundFormModal({ // etc., not empty strings). // Seed each network's settings blob with its Zod schema defaults so // every Form.Item inside the network sub-form has a defined starting - // value. XHTTP in particular has ~20 fields (sessionPlacement, + // value. XHTTP in particular has ~20 fields (sessionIDPlacement, // seqPlacement, xPaddingMethod, uplinkHTTPMethod, ...) whose value // is the literal "" sentinel meaning "let xray-core pick its // default". Without seeding "", the Form.Item reads `undefined` and diff --git a/frontend/src/pages/inbounds/form/transport/xhttp.tsx b/frontend/src/pages/inbounds/form/transport/xhttp.tsx index f5808469f..d597ff8bf 100644 --- a/frontend/src/pages/inbounds/form/transport/xhttp.tsx +++ b/frontend/src/pages/inbounds/form/transport/xhttp.tsx @@ -1,9 +1,10 @@ import { useTranslation } from 'react-i18next'; -import { Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd'; +import { AutoComplete, Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd'; import { HeaderMapEditor } from '@/components/form'; import type { InboundFormValues } from '@/schemas/forms/inbound-form'; -import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp'; +import { XHTTP_SESSION_ID_TABLES, XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp'; +import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id'; const XMUX_DEFAULTS = XHttpXmuxSchema.parse({}); @@ -11,7 +12,8 @@ export default function XhttpForm({ form }: { form: FormInstance )} )} + + ({ value: v }))} + placeholder="Base62" + /> + + {xhttpSessionIDTable && ( + + + + )} ); }} + + ({ value: v }))} + placeholder="Base62" + /> + + + {() => { + const table = form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'sessionIDTable', + ]); + if (!table) return null; + return ( + + + + ); + }} + ; -export const XHttpStreamSettingsSchema = z.object({ +// Predefined sessionIDTable names xray-core accepts as a shorthand for a +// charset (splithttp.PredefinedTable, xray-core #6258). A literal ASCII +// charset string is also accepted. +export const XHTTP_SESSION_ID_TABLES = [ + 'ALPHABET', 'Alphabet', 'BASE36', 'Base62', 'HEX', + 'alphabet', 'base36', 'hex', 'number', +] as const; + +// xray-core #6258 renamed sessionPlacement/sessionKey to +// sessionIDPlacement/sessionIDKey (no fallback kept in core) and added +// sessionIDTable/sessionIDLength. Lift any legacy keys persisted by an older +// panel onto the new names so a saved inbound/outbound never silently loses +// its session setting, then drop the legacy keys so we never emit both. +function migrateLegacyXhttp(v: unknown): unknown { + if (v == null || typeof v !== 'object' || Array.isArray(v)) return v; + const o = { ...(v as Record) }; + if (o.sessionIDPlacement === undefined && o.sessionPlacement !== undefined) { + o.sessionIDPlacement = o.sessionPlacement; + } + if (o.sessionIDKey === undefined && o.sessionKey !== undefined) { + o.sessionIDKey = o.sessionKey; + } + delete o.sessionPlacement; + delete o.sessionKey; + return o; +} + +export const XHttpStreamSettingsSchema = z.preprocess(migrateLegacyXhttp, z.object({ path: z.string().default('/'), host: z.string().default(''), mode: XHttpModeSchema.default('auto'), @@ -35,8 +62,13 @@ export const XHttpStreamSettingsSchema = z.object({ xPaddingHeader: z.string().default(''), xPaddingPlacement: z.string().default(''), xPaddingMethod: z.string().default(''), - sessionPlacement: z.string().default(''), - sessionKey: z.string().default(''), + sessionIDPlacement: z.string().default(''), + sessionIDKey: z.string().default(''), + // sessionIDTable: a predefined name (XHTTP_SESSION_ID_TABLES) or a literal + // ASCII charset. sessionIDLength: dash-range string (e.g. '8-16'); only + // honored when a table is set. xray-core enforces the room-size minimum. + sessionIDTable: z.string().default(''), + sessionIDLength: z.string().default(''), seqPlacement: z.string().default(''), seqKey: z.string().default(''), uplinkDataPlacement: z.string().default(''), @@ -65,5 +97,5 @@ export const XHttpStreamSettingsSchema = z.object({ // Never present on the wire — outbound modal strips it via the // form-to-wire adapter. enableXmux: z.boolean().default(false), -}); +})); export type XHttpStreamSettings = z.infer; diff --git a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap index 8b2edf102..abe414faa 100644 --- a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap +++ b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap @@ -118,6 +118,7 @@ exports[`inbound transport forms > XhttpForm field structure is stable 1`] = ` "Uplink HTTP Method", "Padding Obfs Mode", "Session Placement", + "Session ID Table", "Sequence Placement", "No SSE Header", "XMUX", diff --git a/frontend/src/test/__snapshots__/stream.test.ts.snap b/frontend/src/test/__snapshots__/stream.test.ts.snap index 34caecb42..dde53e4ef 100644 --- a/frontend/src/test/__snapshots__/stream.test.ts.snap +++ b/frontend/src/test/__snapshots__/stream.test.ts.snap @@ -53,8 +53,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = ` "seqKey": "", "seqPlacement": "", "serverMaxHeaderBytes": 0, - "sessionKey": "", - "sessionPlacement": "", + "sessionIDKey": "", + "sessionIDLength": "", + "sessionIDPlacement": "", + "sessionIDTable": "", "uplinkChunkSize": 0, "uplinkDataKey": "", "uplinkDataPlacement": "", @@ -87,8 +89,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-padding byte-stably "seqKey": "", "seqPlacement": "", "serverMaxHeaderBytes": 0, - "sessionKey": "", - "sessionPlacement": "", + "sessionIDKey": "", + "sessionIDLength": "", + "sessionIDPlacement": "", + "sessionIDTable": "", "uplinkChunkSize": 0, "uplinkDataKey": "", "uplinkDataPlacement": "", @@ -121,8 +125,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-placement byte-stab "seqKey": "X-Seq", "seqPlacement": "cookie", "serverMaxHeaderBytes": 0, - "sessionKey": "X-Session", - "sessionPlacement": "header", + "sessionIDKey": "X-Session", + "sessionIDLength": "", + "sessionIDPlacement": "header", + "sessionIDTable": "", "uplinkChunkSize": 0, "uplinkDataKey": "u", "uplinkDataPlacement": "query", @@ -158,8 +164,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-tuning byte-stably "seqKey": "", "seqPlacement": "", "serverMaxHeaderBytes": 16384, - "sessionKey": "", - "sessionPlacement": "", + "sessionIDKey": "", + "sessionIDLength": "", + "sessionIDPlacement": "", + "sessionIDTable": "", "uplinkChunkSize": 8192, "uplinkDataKey": "", "uplinkDataPlacement": "", diff --git a/frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json b/frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json index 26a9181b7..00a6f06ec 100644 --- a/frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json +++ b/frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json @@ -4,8 +4,8 @@ "path": "/sp", "host": "edge.example.test", "mode": "auto", - "sessionPlacement": "header", - "sessionKey": "X-Session", + "sessionIDPlacement": "header", + "sessionIDKey": "X-Session", "seqPlacement": "cookie", "seqKey": "X-Seq", "uplinkDataPlacement": "query", diff --git a/frontend/src/test/outbound-link-parser.test.ts b/frontend/src/test/outbound-link-parser.test.ts index 28dad1de8..0198fa958 100644 --- a/frontend/src/test/outbound-link-parser.test.ts +++ b/frontend/src/test/outbound-link-parser.test.ts @@ -93,6 +93,7 @@ describe('parseVmessLink — XHTTP advanced fields', () => { scMaxBufferedPosts: 50, tls: 'tls', }; + // legacy sessionKey must alias onto the renamed sessionIDKey (#6258) const link = `vmess://${Base64.encode(JSON.stringify(json))}`; const out = parseVmessLink(link); const xhttp = (out?.streamSettings as Record).xhttpSettings as Record; @@ -101,7 +102,8 @@ describe('parseVmessLink — XHTTP advanced fields', () => { expect(xhttp.xPaddingHeader).toBe('X-Pad'); expect(xhttp.xPaddingPlacement).toBe('header'); expect(xhttp.xPaddingMethod).toBe('random'); - expect(xhttp.sessionKey).toBe('X-Session'); + expect(xhttp.sessionIDKey).toBe('X-Session'); + expect(xhttp.sessionKey).toBeUndefined(); expect(xhttp.seqKey).toBe('X-Seq'); expect(xhttp.noSSEHeader).toBe(true); expect(xhttp.scMaxBufferedPosts).toBe(50); @@ -135,7 +137,8 @@ describe('parseVlessLink — XHTTP advanced fields', () => { + '?type=xhttp&security=tls&host=edge.example&path=%2Fsp' + '&xPaddingObfsMode=true&xPaddingKey=secret-key&xPaddingHeader=X-Pad' + '&xPaddingPlacement=header&xPaddingMethod=random' - + '&sessionKey=X-Session&seqKey=X-Seq&noSSEHeader=true' + + '&sessionIDKey=X-Session&sessionIDTable=Base62&sessionIDLength=16-32' + + '&seqKey=X-Seq&noSSEHeader=true' + '&scMaxBufferedPosts=50' + '#imported-pad'; const out = parseVlessLink(link); @@ -145,7 +148,9 @@ describe('parseVlessLink — XHTTP advanced fields', () => { expect(xhttp.xPaddingHeader).toBe('X-Pad'); expect(xhttp.xPaddingPlacement).toBe('header'); expect(xhttp.xPaddingMethod).toBe('random'); - expect(xhttp.sessionKey).toBe('X-Session'); + expect(xhttp.sessionIDKey).toBe('X-Session'); + expect(xhttp.sessionIDTable).toBe('Base62'); + expect(xhttp.sessionIDLength).toBe('16-32'); expect(xhttp.seqKey).toBe('X-Seq'); expect(xhttp.noSSEHeader).toBe(true); expect(xhttp.scMaxBufferedPosts).toBe(50); diff --git a/frontend/src/test/xhttp-session-id.test.ts b/frontend/src/test/xhttp-session-id.test.ts new file mode 100644 index 000000000..b2c155d5f --- /dev/null +++ b/frontend/src/test/xhttp-session-id.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; + +import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id'; +import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp'; + +// xray-core #6258: sessionPlacement/sessionKey were renamed to +// sessionIDPlacement/sessionIDKey. The schema must lift legacy keys off +// stored configs so an upgraded panel never silently drops them. +describe('XHttpStreamSettingsSchema legacy migration', () => { + it('lifts legacy sessionPlacement/sessionKey onto the renamed keys', () => { + const parsed = XHttpStreamSettingsSchema.parse({ + sessionPlacement: 'cookie', + sessionKey: 'x_session', + }); + expect(parsed.sessionIDPlacement).toBe('cookie'); + expect(parsed.sessionIDKey).toBe('x_session'); + // legacy keys must not survive — we never emit both names + expect((parsed as Record).sessionPlacement).toBeUndefined(); + expect((parsed as Record).sessionKey).toBeUndefined(); + }); + + it('prefers an explicit new key over a legacy one', () => { + const parsed = XHttpStreamSettingsSchema.parse({ + sessionPlacement: 'cookie', + sessionIDPlacement: 'header', + }); + expect(parsed.sessionIDPlacement).toBe('header'); + }); + + it('defaults the new fields to empty', () => { + const parsed = XHttpStreamSettingsSchema.parse({}); + expect(parsed.sessionIDTable).toBe(''); + expect(parsed.sessionIDLength).toBe(''); + }); +}); + +describe('sessionID validators', () => { + it('accepts empty and ASCII tables, rejects non-ASCII', async () => { + await expect(validateSessionIDTable(null, '')).resolves.toBeUndefined(); + await expect(validateSessionIDTable(null, 'Base62')).resolves.toBeUndefined(); + await expect(validateSessionIDTable(null, 'ABCdef0123')).resolves.toBeUndefined(); + await expect(validateSessionIDTable(null, ' café')).rejects.toThrow(); + }); + + it('accepts a positive length/range, rejects zero or junk', async () => { + await expect(validateSessionIDLength(null, '')).resolves.toBeUndefined(); + await expect(validateSessionIDLength(null, '8')).resolves.toBeUndefined(); + await expect(validateSessionIDLength(null, '16-32')).resolves.toBeUndefined(); + await expect(validateSessionIDLength(null, '8-8')).resolves.toBeUndefined(); + await expect(validateSessionIDLength(null, '0-16')).rejects.toThrow(); + await expect(validateSessionIDLength(null, '32-16')).rejects.toThrow(); + await expect(validateSessionIDLength(null, 'abc')).rejects.toThrow(); + }); +}); diff --git a/internal/sub/clash_service.go b/internal/sub/clash_service.go index 2e165e55f..13d1811a7 100644 --- a/internal/sub/clash_service.go +++ b/internal/sub/clash_service.go @@ -404,8 +404,10 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any { stringFields := []xhttpStringField{ {"xPaddingBytes", "x-padding-bytes", ""}, {"uplinkHTTPMethod", "uplink-http-method", ""}, - {"sessionPlacement", "session-placement", ""}, - {"sessionKey", "session-key", ""}, + {"sessionIDPlacement", "session-id-placement", ""}, + {"sessionIDKey", "session-id-key", ""}, + {"sessionIDTable", "session-id-table", ""}, + {"sessionIDLength", "session-id-length", ""}, {"seqPlacement", "seq-placement", ""}, {"seqKey", "seq-key", ""}, {"uplinkDataPlacement", "uplink-data-placement", ""}, @@ -420,6 +422,21 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any { } } + // Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey. + // Fall back to them so not-yet-resaved configs still map. Mirrors the + // frontend migration. + for _, f := range []xhttpStringField{ + {"sessionPlacement", "session-id-placement", ""}, + {"sessionKey", "session-id-key", ""}, + } { + if _, exists := opts[f.dst]; exists { + continue + } + if v, ok := xhttp[f.src].(string); ok && v != "" { + opts[f.dst] = v + } + } + // Bool fields (truthy only) if v, ok := xhttp["noGRPCHeader"].(bool); ok && v { opts["no-grpc-header"] = true diff --git a/internal/sub/clash_service_test.go b/internal/sub/clash_service_test.go index cdcb55f1e..7301124b4 100644 --- a/internal/sub/clash_service_test.go +++ b/internal/sub/clash_service_test.go @@ -330,8 +330,10 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) { "xPaddingPlacement": "queryInHeader", "xPaddingMethod": "tokenish", "uplinkHTTPMethod": "POST", - "sessionPlacement": "query", - "sessionKey": "sess", + "sessionIDPlacement": "query", + "sessionIDKey": "sess", + "sessionIDTable": "Base62", + "sessionIDLength": "16-32", "seqPlacement": "header", "seqKey": "seq", "uplinkDataPlacement": "body", @@ -377,11 +379,17 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) { if opts["uplink-http-method"] != "POST" { t.Errorf("uplink-http-method = %v", opts["uplink-http-method"]) } - if opts["session-placement"] != "query" { - t.Errorf("session-placement = %v", opts["session-placement"]) + if opts["session-id-placement"] != "query" { + t.Errorf("session-id-placement = %v", opts["session-id-placement"]) } - if opts["session-key"] != "sess" { - t.Errorf("session-key = %v", opts["session-key"]) + if opts["session-id-key"] != "sess" { + t.Errorf("session-id-key = %v", opts["session-id-key"]) + } + if opts["session-id-table"] != "Base62" { + t.Errorf("session-id-table = %v", opts["session-id-table"]) + } + if opts["session-id-length"] != "16-32" { + t.Errorf("session-id-length = %v", opts["session-id-length"]) } if opts["seq-placement"] != "header" { t.Errorf("seq-placement = %v", opts["seq-placement"]) diff --git a/internal/sub/service.go b/internal/sub/service.go index 99d298671..76d9c26dd 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -1731,7 +1731,7 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any { stringFields := []string{ "uplinkHTTPMethod", - "sessionPlacement", "sessionKey", + "sessionIDPlacement", "sessionIDKey", "sessionIDTable", "sessionIDLength", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes", "scMinPostsIntervalMs", @@ -1750,6 +1750,20 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any { } } + // Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey. + // Lift them onto the renamed keys so links from not-yet-resaved configs + // still carry the session settings. Mirrors the frontend migration. + for legacy, renamed := range map[string]string{ + "sessionPlacement": "sessionIDPlacement", + "sessionKey": "sessionIDKey", + } { + if _, exists := extra[renamed]; !exists { + if v, ok := xhttp[legacy].(string); ok && len(v) > 0 { + extra[renamed] = v + } + } + } + for _, field := range []string{"uplinkChunkSize"} { if v, ok := nonZeroShareValue(xhttp[field]); ok { extra[field] = v diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go index abed4e2dc..741f49373 100644 --- a/internal/web/service/xray.go +++ b/internal/web/service/xray.go @@ -121,6 +121,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { xrayConfig.API = ensureAPIServices(xrayConfig.API) xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy) xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig) + // Template outbounds authored before the xray-core #6258 XHTTP rename may + // still carry sessionPlacement/sessionKey; lift them too (same reason as + // the per-inbound lift below). + xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs) _, _, _ = s.inboundService.AddTraffic(nil, nil) @@ -251,6 +255,12 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { delete(stream, "externalProxy") + // xray-core v26.6.22 (#6258) renamed the XHTTP session keys and + // kept no fallback. Lift legacy sessionPlacement/sessionKey onto the + // new names here so inbounds stored before the rename keep working + // without the admin re-saving them. + liftXhttpSessionIDKeys(stream) + newStream, err := json.MarshalIndent(stream, "", " ") if err != nil { return nil, err @@ -576,7 +586,7 @@ func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) { return } } - merged := make([]any, 0, len(prepend)+len(templateOutbounds)+len(appendList)) + var merged []any merged = append(merged, prepend...) merged = append(merged, templateOutbounds...) merged = append(merged, appendList...) @@ -1078,3 +1088,60 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool { func (s *XrayService) DidXrayCrash() bool { return !s.IsXrayRunning() && !isManuallyStopped.Load() } + +// liftXhttpSessionIDKeys renames the legacy XHTTP session keys +// (sessionPlacement/sessionKey) to the v26.6.22 #6258 names +// (sessionIDPlacement/sessionIDKey) inside a streamSettings map. xray-core kept +// no fallback for the old names, so a config stored before the rename would be +// silently ignored by the engine. Returns true if it changed anything. +func liftXhttpSessionIDKeys(stream map[string]any) bool { + xhttp, ok := stream["xhttpSettings"].(map[string]any) + if !ok { + return false + } + changed := false + for legacy, renamed := range map[string]string{ + "sessionPlacement": "sessionIDPlacement", + "sessionKey": "sessionIDKey", + } { + v, has := xhttp[legacy] + if !has { + continue + } + if _, exists := xhttp[renamed]; !exists { + xhttp[renamed] = v + } + delete(xhttp, legacy) + changed = true + } + return changed +} + +// liftOutboundsXhttpSessionIDKeys applies liftXhttpSessionIDKeys to every +// outbound's streamSettings in the raw outbounds array. The original bytes are +// returned untouched when nothing needs lifting, so an unchanged config never +// looks modified to the hot-reload diff. +func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMessage { + if len(raw) == 0 { + return raw + } + var outbounds []map[string]any + if err := json.Unmarshal(raw, &outbounds); err != nil { + return raw + } + changed := false + for _, ob := range outbounds { + if stream, ok := ob["streamSettings"].(map[string]any); ok { + if liftXhttpSessionIDKeys(stream) { + changed = true + } + } + } + if !changed { + return raw + } + if rewritten, err := json.Marshal(outbounds); err == nil { + return rewritten + } + return raw +} diff --git a/internal/web/service/xray_xhttp_session_test.go b/internal/web/service/xray_xhttp_session_test.go new file mode 100644 index 000000000..a122f99ca --- /dev/null +++ b/internal/web/service/xray_xhttp_session_test.go @@ -0,0 +1,81 @@ +package service + +import ( + "encoding/json" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/util/json_util" +) + +// xray-core v26.6.22 (#6258) renamed the XHTTP session keys with no fallback. +// The lift must rewrite stored configs at config-generation time so pre-upgrade +// inbounds/outbounds keep working without a manual re-save. +func TestLiftXhttpSessionIDKeys(t *testing.T) { + t.Run("lifts legacy keys and drops them", func(t *testing.T) { + stream := map[string]any{ + "xhttpSettings": map[string]any{ + "sessionPlacement": "cookie", + "sessionKey": "x_session", + }, + } + if !liftXhttpSessionIDKeys(stream) { + t.Fatal("expected changed=true") + } + xhttp := stream["xhttpSettings"].(map[string]any) + if xhttp["sessionIDPlacement"] != "cookie" || xhttp["sessionIDKey"] != "x_session" { + t.Fatalf("renamed keys missing: %#v", xhttp) + } + if _, ok := xhttp["sessionPlacement"]; ok { + t.Fatal("legacy sessionPlacement still present") + } + if _, ok := xhttp["sessionKey"]; ok { + t.Fatal("legacy sessionKey still present") + } + }) + + t.Run("keeps an explicit new key over the legacy one", func(t *testing.T) { + stream := map[string]any{ + "xhttpSettings": map[string]any{ + "sessionPlacement": "cookie", + "sessionIDPlacement": "header", + }, + } + liftXhttpSessionIDKeys(stream) + xhttp := stream["xhttpSettings"].(map[string]any) + if xhttp["sessionIDPlacement"] != "header" { + t.Fatalf("explicit new key was overwritten: %v", xhttp["sessionIDPlacement"]) + } + }) + + t.Run("no-op without xhttpSettings or legacy keys", func(t *testing.T) { + if liftXhttpSessionIDKeys(map[string]any{"wsSettings": map[string]any{}}) { + t.Fatal("expected no change for non-xhttp stream") + } + if liftXhttpSessionIDKeys(map[string]any{"xhttpSettings": map[string]any{"path": "/"}}) { + t.Fatal("expected no change when no legacy keys present") + } + }) +} + +func TestLiftOutboundsXhttpSessionIDKeys(t *testing.T) { + raw := json_util.RawMessage(`[{"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"sessionKey":"x_session","sessionPlacement":"query"}}}]`) + out := liftOutboundsXhttpSessionIDKeys(raw) + + var parsed []map[string]any + if err := json.Unmarshal(out, &parsed); err != nil { + t.Fatalf("unmarshal rewritten outbounds: %v", err) + } + xhttp := parsed[0]["streamSettings"].(map[string]any)["xhttpSettings"].(map[string]any) + if xhttp["sessionIDKey"] != "x_session" || xhttp["sessionIDPlacement"] != "query" { + t.Fatalf("outbound keys not lifted: %#v", xhttp) + } + if _, ok := xhttp["sessionKey"]; ok { + t.Fatal("legacy sessionKey survived in outbound") + } + + // Unchanged input must return byte-identical output (no spurious hot-reload). + clean := json_util.RawMessage(`[{"protocol":"freedom"}]`) + if got := liftOutboundsXhttpSessionIDKeys(clean); string(got) != string(clean) { + t.Fatalf("clean outbounds were rewritten: %s", got) + } +} diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 04ff7a4ef..33dcc68c2 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -534,6 +534,10 @@ "paddingMethod": "طريقة Padding", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "جدول معرّف الجلسة", + "sessionIDTableHint": "مجموعة الأحرف لتوليد معرّف الجلسة: اسم معرّف مسبقًا (ALPHABET، Base62، hex، number، …) أو سلسلة ASCII. اتركه فارغًا لاستخدام الإعداد الافتراضي لـ xray-core.", + "sessionIDLength": "طول معرّف الجلسة", + "sessionIDLengthHint": "طول أو نطاق (مثل 8-16) لمعرّف الجلسة المُولَّد. يُستخدم فقط عند تعيين جدول معرّف الجلسة؛ يجب أن يكون الحد الأدنى أكبر من 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 87bed42df..e473ead14 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -534,6 +534,10 @@ "paddingMethod": "Padding Method", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Session ID Table", + "sessionIDTableHint": "Charset for generated session IDs: a predefined name (ALPHABET, Base62, hex, number, …) or a literal ASCII string. Leave empty for xray-core's default.", + "sessionIDLength": "Session ID Length", + "sessionIDLengthHint": "Length or range (e.g. 8-16) of generated session IDs. Only used when a Session ID Table is set; minimum must be greater than 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index e795a7633..8bb29c668 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -534,6 +534,10 @@ "paddingMethod": "Método de Padding", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Tabla de Session ID", + "sessionIDTableHint": "Conjunto de caracteres para generar los session ID: un nombre predefinido (ALPHABET, Base62, hex, number, …) o una cadena ASCII literal. Déjalo vacío para el valor por defecto de xray-core.", + "sessionIDLength": "Longitud de Session ID", + "sessionIDLengthHint": "Longitud o rango (p. ej. 8-16) del session ID generado. Solo se usa cuando hay una Tabla de Session ID definida; el mínimo debe ser mayor que 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index b617c7d4f..5a959ed3a 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -534,6 +534,10 @@ "paddingMethod": "روش Padding", "sessionPlacement": "محل نشست", "sessionKey": "کلید نشست", + "sessionIDTable": "جدول شناسه نشست", + "sessionIDTableHint": "مجموعه نویسه‌ها برای تولید شناسه نشست: یک نام از پیش‌تعریف‌شده (ALPHABET، Base62، hex، number، …) یا یک رشته ASCII. برای مقدار پیش‌فرض xray-core خالی بگذارید.", + "sessionIDLength": "طول شناسه نشست", + "sessionIDLengthHint": "طول یا بازه (مثلاً 8-16) شناسه نشست تولیدشده. فقط وقتی جدول شناسه نشست تنظیم شده باشد استفاده می‌شود؛ کمینه باید بزرگ‌تر از 0 باشد.", "sequencePlacement": "محل Sequence", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "محل داده Uplink", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index a393725a0..e2aa1feed 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -534,6 +534,10 @@ "paddingMethod": "Metode Padding", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Tabel Session ID", + "sessionIDTableHint": "Kumpulan karakter untuk membuat session ID: nama yang telah ditentukan (ALPHABET, Base62, hex, number, …) atau string ASCII literal. Kosongkan untuk default xray-core.", + "sessionIDLength": "Panjang Session ID", + "sessionIDLengthHint": "Panjang atau rentang (mis. 8-16) session ID yang dibuat. Hanya digunakan saat Tabel Session ID disetel; nilai minimum harus lebih besar dari 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index db8a7bd17..d9fb88d93 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -555,6 +555,10 @@ "paddingMethod": "Padding 方法", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "セッション ID テーブル", + "sessionIDTableHint": "セッション ID 生成に使う文字セット:定義済みの名前(ALPHABET、Base62、hex、number など)またはリテラル ASCII 文字列。空欄で xray-core の既定値を使用します。", + "sessionIDLength": "セッション ID の長さ", + "sessionIDLengthHint": "生成するセッション ID の長さまたは範囲(例: 8-16)。セッション ID テーブルを設定したときのみ有効です。最小値は 0 より大きい必要があります。", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index c8ef0106f..18718468e 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -555,6 +555,10 @@ "paddingMethod": "Método de Padding", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Tabela de Session ID", + "sessionIDTableHint": "Conjunto de caracteres para gerar session IDs: um nome predefinido (ALPHABET, Base62, hex, number, …) ou uma string ASCII literal. Deixe vazio para o padrão do xray-core.", + "sessionIDLength": "Comprimento do Session ID", + "sessionIDLengthHint": "Comprimento ou intervalo (ex.: 8-16) do session ID gerado. Usado apenas quando uma Tabela de Session ID está definida; o mínimo deve ser maior que 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index d5863435e..4aae57960 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -555,6 +555,10 @@ "paddingMethod": "Padding Method", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Таблица Session ID", + "sessionIDTableHint": "Набор символов для генерации session ID: предопределённое имя (ALPHABET, Base62, hex, number, …) или строка ASCII. Оставьте пустым для значения xray-core по умолчанию.", + "sessionIDLength": "Длина Session ID", + "sessionIDLengthHint": "Длина или диапазон (например, 8-16) генерируемого session ID. Используется только когда задана таблица; минимум должен быть больше 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 11368d03c..c2baa9e64 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -534,6 +534,10 @@ "paddingMethod": "Padding Yöntemi", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Oturum Kimliği Tablosu", + "sessionIDTableHint": "Oturum kimliği üretmek için karakter kümesi: önceden tanımlı bir ad (ALPHABET, Base62, hex, number, …) veya düz ASCII dizesi. xray-core varsayılanı için boş bırakın.", + "sessionIDLength": "Oturum Kimliği Uzunluğu", + "sessionIDLengthHint": "Üretilen oturum kimliğinin uzunluğu veya aralığı (örn. 8-16). Yalnızca bir Oturum Kimliği Tablosu ayarlandığında kullanılır; en küçük değer 0'dan büyük olmalıdır.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index ef40d17bb..da199ebf3 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -534,6 +534,10 @@ "paddingMethod": "Padding Method", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Таблиця Session ID", + "sessionIDTableHint": "Набір символів для генерації session ID: попередньо визначене ім'я (ALPHABET, Base62, hex, number, …) або рядок ASCII. Залиште порожнім для значення xray-core за замовчуванням.", + "sessionIDLength": "Довжина Session ID", + "sessionIDLengthHint": "Довжина або діапазон (напр., 8-16) згенерованого session ID. Використовується лише коли задано таблицю; мінімум має бути більший за 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 42661dbf0..3441d4e38 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -555,6 +555,10 @@ "paddingMethod": "Phương thức Padding", "sessionPlacement": "Session Placement", "sessionKey": "Session Key", + "sessionIDTable": "Bảng Session ID", + "sessionIDTableHint": "Tập ký tự để tạo session ID: một tên định sẵn (ALPHABET, Base62, hex, number, …) hoặc chuỗi ASCII. Để trống để dùng mặc định của xray-core.", + "sessionIDLength": "Độ dài Session ID", + "sessionIDLengthHint": "Độ dài hoặc khoảng (ví dụ 8-16) của session ID được tạo. Chỉ dùng khi đã đặt Bảng Session ID; giá trị nhỏ nhất phải lớn hơn 0.", "sequencePlacement": "Sequence Placement", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink Data Placement", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 95ec3d2a4..ec7d38b6d 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -554,6 +554,10 @@ "paddingMethod": "Padding 方法", "sessionPlacement": "Session 位置", "sessionKey": "Session Key", + "sessionIDTable": "会话 ID 字符表", + "sessionIDTableHint": "生成会话 ID 使用的字符集:预定义名称(ALPHABET、Base62、hex、number 等)或字面 ASCII 字符串。留空则使用 xray-core 默认值。", + "sessionIDLength": "会话 ID 长度", + "sessionIDLengthHint": "生成会话 ID 的长度或范围(如 8-16)。仅在设置了会话 ID 字符表时生效;最小值必须大于 0。", "sequencePlacement": "Sequence 位置", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink 数据位置", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 03a970dca..1b9dc9dec 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -534,6 +534,10 @@ "paddingMethod": "Padding 方法", "sessionPlacement": "Session 位置", "sessionKey": "Session Key", + "sessionIDTable": "工作階段 ID 字元表", + "sessionIDTableHint": "產生工作階段 ID 使用的字元集:預定義名稱(ALPHABET、Base62、hex、number 等)或字面 ASCII 字串。留空則使用 xray-core 預設值。", + "sessionIDLength": "工作階段 ID 長度", + "sessionIDLengthHint": "產生工作階段 ID 的長度或範圍(如 8-16)。僅在設定了工作階段 ID 字元表時生效;最小值必須大於 0。", "sequencePlacement": "Sequence 位置", "sequenceKey": "Sequence Key", "uplinkDataPlacement": "Uplink 資料位置",