feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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.
This commit is contained in:
Rouzbeh†
2026-06-23 17:38:16 +02:00
committed by GitHub
parent b07fad0e69
commit fea3c94b11
31 changed files with 498 additions and 45 deletions
+4 -2
View File
@@ -64,8 +64,10 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string,
const stringFields = [ const stringFields = [
'uplinkHTTPMethod', 'uplinkHTTPMethod',
'sessionPlacement', 'sessionIDPlacement',
'sessionKey', 'sessionIDKey',
'sessionIDTable',
'sessionIDLength',
'seqPlacement', 'seqPlacement',
'seqKey', 'seqKey',
'uplinkDataPlacement', 'uplinkDataPlacement',
+23 -3
View File
@@ -24,10 +24,18 @@ type Raw = Record<string, unknown>;
// match the schema's authoring order so diffs read naturally. // match the schema's authoring order so diffs read naturally.
const XHTTP_STRING_KEYS = [ const XHTTP_STRING_KEYS = [
'xPaddingBytes', 'xPaddingKey', 'xPaddingHeader', 'xPaddingPlacement', 'xPaddingBytes', 'xPaddingKey', 'xPaddingHeader', 'xPaddingPlacement',
'xPaddingMethod', 'sessionPlacement', 'sessionKey', 'seqPlacement', 'xPaddingMethod', 'sessionIDPlacement', 'sessionIDKey', 'sessionIDTable',
'seqKey', 'uplinkDataPlacement', 'uplinkDataKey', 'scMaxEachPostBytes', 'sessionIDLength', 'seqPlacement', 'seqKey', 'uplinkDataPlacement',
'scMinPostsIntervalMs', 'scStreamUpServerSecs', 'uplinkHTTPMethod', 'uplinkDataKey', 'scMaxEachPostBytes', 'scMinPostsIntervalMs',
'scStreamUpServerSecs', 'uplinkHTTPMethod',
] as const; ] 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<string, string> = {
sessionPlacement: 'sessionIDPlacement',
sessionKey: 'sessionIDKey',
};
const XHTTP_NUMBER_KEYS = [ const XHTTP_NUMBER_KEYS = [
'scMaxBufferedPosts', 'serverMaxHeaderBytes', 'uplinkChunkSize', 'scMaxBufferedPosts', 'serverMaxHeaderBytes', 'uplinkChunkSize',
] as const; ] as const;
@@ -81,12 +89,24 @@ function applyXhttpStringFromParams(xhttp: Raw, params: URLSearchParams): void {
const v = params.get(k); const v = params.get(k);
if (v !== null && v !== '') xhttp[k] = asBool(v); 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<string, unknown>): void { function applyXhttpStringFromJson(xhttp: Raw, json: Record<string, unknown>): void {
for (const k of XHTTP_STRING_KEYS) { for (const k of XHTTP_STRING_KEYS) {
if (typeof json[k] === 'string') xhttp[k] = json[k]; 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) { for (const k of XHTTP_NUMBER_KEYS) {
if (typeof json[k] === 'number') xhttp[k] = json[k]; if (typeof json[k] === 'number') xhttp[k] = json[k];
} }
@@ -16,8 +16,10 @@ const PACKET_UP_FIELDS = [
const STREAM_UP_SERVER_FIELDS = ['scStreamUpServerSecs'] as const; const STREAM_UP_SERVER_FIELDS = ['scStreamUpServerSecs'] as const;
const PLACEMENT_STRING_FIELDS = [ const PLACEMENT_STRING_FIELDS = [
'sessionPlacement', 'sessionIDPlacement',
'sessionKey', 'sessionIDKey',
'sessionIDTable',
'sessionIDLength',
'seqPlacement', 'seqPlacement',
'seqKey', 'seqKey',
'uplinkDataPlacement', 'uplinkDataPlacement',
+34
View File
@@ -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<void> {
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<void> {
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();
}
@@ -708,7 +708,7 @@ export default function InboundFormModal({
// etc., not empty strings). // etc., not empty strings).
// Seed each network's settings blob with its Zod schema defaults so // Seed each network's settings blob with its Zod schema defaults so
// every Form.Item inside the network sub-form has a defined starting // 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 // seqPlacement, xPaddingMethod, uplinkHTTPMethod, ...) whose value
// is the literal "" sentinel meaning "let xray-core pick its // is the literal "" sentinel meaning "let xray-core pick its
// default". Without seeding "", the Form.Item reads `undefined` and // default". Without seeding "", the Form.Item reads `undefined` and
@@ -1,9 +1,10 @@
import { useTranslation } from 'react-i18next'; 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 { HeaderMapEditor } from '@/components/form';
import type { InboundFormValues } from '@/schemas/forms/inbound-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({}); const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});
@@ -11,7 +12,8 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
const { t } = useTranslation(); const { t } = useTranslation();
const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form); const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false; const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false;
const xhttpSessionPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionPlacement'], form); const xhttpSessionIDPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionIDPlacement'], form);
const xhttpSessionIDTable = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionIDTable'], form);
const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form); const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form);
const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form); const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form);
@@ -163,7 +165,7 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
</> </>
)} )}
<Form.Item <Form.Item
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']} name={['streamSettings', 'xhttpSettings', 'sessionIDPlacement']}
label={t('pages.inbounds.form.sessionPlacement')} label={t('pages.inbounds.form.sessionPlacement')}
> >
<Select <Select
@@ -176,14 +178,36 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
]} ]}
/> />
</Form.Item> </Form.Item>
{xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && ( {xhttpSessionIDPlacement && xhttpSessionIDPlacement !== 'path' && (
<Form.Item <Form.Item
name={['streamSettings', 'xhttpSettings', 'sessionKey']} name={['streamSettings', 'xhttpSettings', 'sessionIDKey']}
label={t('pages.inbounds.form.sessionKey')} label={t('pages.inbounds.form.sessionKey')}
> >
<Input placeholder="x_session" /> <Input placeholder="x_session" />
</Form.Item> </Form.Item>
)} )}
<Form.Item
name={['streamSettings', 'xhttpSettings', 'sessionIDTable']}
label={t('pages.inbounds.form.sessionIDTable')}
tooltip={t('pages.inbounds.form.sessionIDTableHint')}
rules={[{ validator: validateSessionIDTable }]}
>
<AutoComplete
allowClear
options={XHTTP_SESSION_ID_TABLES.map((v) => ({ value: v }))}
placeholder="Base62"
/>
</Form.Item>
{xhttpSessionIDTable && (
<Form.Item
name={['streamSettings', 'xhttpSettings', 'sessionIDLength']}
label={t('pages.inbounds.form.sessionIDLength')}
tooltip={t('pages.inbounds.form.sessionIDLengthHint')}
rules={[{ validator: validateSessionIDLength }]}
>
<Input placeholder="8-16" />
</Form.Item>
)}
<Form.Item <Form.Item
name={['streamSettings', 'xhttpSettings', 'seqPlacement']} name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
label={t('pages.inbounds.form.sequencePlacement')} label={t('pages.inbounds.form.sequencePlacement')}
@@ -1,8 +1,10 @@
import { useTranslation } from 'react-i18next'; 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 { HeaderMapEditor } from '@/components/form';
import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
import type { OutboundFormValues } from '@/schemas/forms/outbound-form'; import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
import { XHTTP_SESSION_ID_TABLES } from '@/schemas/protocols/stream/xhttp';
import { MODE_OPTIONS } from '../outbound-form-constants'; import { MODE_OPTIONS } from '../outbound-form-constants';
@@ -145,7 +147,7 @@ export default function XhttpForm({ form, onXmuxToggle }: XhttpFormProps) {
only matters when placement is not 'path'. */} only matters when placement is not 'path'. */}
<Form.Item <Form.Item
label={t('pages.inbounds.form.sessionPlacement')} label={t('pages.inbounds.form.sessionPlacement')}
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']} name={['streamSettings', 'xhttpSettings', 'sessionIDPlacement']}
> >
<Select <Select
placeholder="Default (path)" placeholder="Default (path)"
@@ -161,19 +163,49 @@ export default function XhttpForm({ form, onXmuxToggle }: XhttpFormProps) {
<Form.Item shouldUpdate noStyle> <Form.Item shouldUpdate noStyle>
{() => { {() => {
const placement = form.getFieldValue([ const placement = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'sessionPlacement', 'streamSettings', 'xhttpSettings', 'sessionIDPlacement',
]); ]);
if (!placement || placement === 'path') return null; if (!placement || placement === 'path') return null;
return ( return (
<Form.Item <Form.Item
label={t('pages.inbounds.form.sessionKey')} label={t('pages.inbounds.form.sessionKey')}
name={['streamSettings', 'xhttpSettings', 'sessionKey']} name={['streamSettings', 'xhttpSettings', 'sessionIDKey']}
> >
<Input placeholder="x_session" /> <Input placeholder="x_session" />
</Form.Item> </Form.Item>
); );
}} }}
</Form.Item> </Form.Item>
<Form.Item
label={t('pages.inbounds.form.sessionIDTable')}
tooltip={t('pages.inbounds.form.sessionIDTableHint')}
name={['streamSettings', 'xhttpSettings', 'sessionIDTable']}
rules={[{ validator: validateSessionIDTable }]}
>
<AutoComplete
allowClear
options={XHTTP_SESSION_ID_TABLES.map((v) => ({ value: v }))}
placeholder="Base62"
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const table = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'sessionIDTable',
]);
if (!table) return null;
return (
<Form.Item
label={t('pages.inbounds.form.sessionIDLength')}
tooltip={t('pages.inbounds.form.sessionIDLengthHint')}
name={['streamSettings', 'xhttpSettings', 'sessionIDLength']}
rules={[{ validator: validateSessionIDLength }]}
>
<Input placeholder="8-16" />
</Form.Item>
);
}}
</Form.Item>
<Form.Item <Form.Item
label={t('pages.inbounds.form.sequencePlacement')} label={t('pages.inbounds.form.sequencePlacement')}
name={['streamSettings', 'xhttpSettings', 'seqPlacement']} name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
+36 -4
View File
@@ -25,7 +25,34 @@ export const XHttpXmuxSchema = z.object({
}); });
export type XHttpXmux = z.infer<typeof XHttpXmuxSchema>; export type XHttpXmux = z.infer<typeof XHttpXmuxSchema>;
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<string, unknown>) };
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('/'), path: z.string().default('/'),
host: z.string().default(''), host: z.string().default(''),
mode: XHttpModeSchema.default('auto'), mode: XHttpModeSchema.default('auto'),
@@ -35,8 +62,13 @@ export const XHttpStreamSettingsSchema = z.object({
xPaddingHeader: z.string().default(''), xPaddingHeader: z.string().default(''),
xPaddingPlacement: z.string().default(''), xPaddingPlacement: z.string().default(''),
xPaddingMethod: z.string().default(''), xPaddingMethod: z.string().default(''),
sessionPlacement: z.string().default(''), sessionIDPlacement: z.string().default(''),
sessionKey: 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(''), seqPlacement: z.string().default(''),
seqKey: z.string().default(''), seqKey: z.string().default(''),
uplinkDataPlacement: 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 // Never present on the wire — outbound modal strips it via the
// form-to-wire adapter. // form-to-wire adapter.
enableXmux: z.boolean().default(false), enableXmux: z.boolean().default(false),
}); }));
export type XHttpStreamSettings = z.infer<typeof XHttpStreamSettingsSchema>; export type XHttpStreamSettings = z.infer<typeof XHttpStreamSettingsSchema>;
@@ -118,6 +118,7 @@ exports[`inbound transport forms > XhttpForm field structure is stable 1`] = `
"Uplink HTTP Method", "Uplink HTTP Method",
"Padding Obfs Mode", "Padding Obfs Mode",
"Session Placement", "Session Placement",
"Session ID Table",
"Sequence Placement", "Sequence Placement",
"No SSE Header", "No SSE Header",
"XMUX", "XMUX",
@@ -53,8 +53,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = `
"seqKey": "", "seqKey": "",
"seqPlacement": "", "seqPlacement": "",
"serverMaxHeaderBytes": 0, "serverMaxHeaderBytes": 0,
"sessionKey": "", "sessionIDKey": "",
"sessionPlacement": "", "sessionIDLength": "",
"sessionIDPlacement": "",
"sessionIDTable": "",
"uplinkChunkSize": 0, "uplinkChunkSize": 0,
"uplinkDataKey": "", "uplinkDataKey": "",
"uplinkDataPlacement": "", "uplinkDataPlacement": "",
@@ -87,8 +89,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-padding byte-stably
"seqKey": "", "seqKey": "",
"seqPlacement": "", "seqPlacement": "",
"serverMaxHeaderBytes": 0, "serverMaxHeaderBytes": 0,
"sessionKey": "", "sessionIDKey": "",
"sessionPlacement": "", "sessionIDLength": "",
"sessionIDPlacement": "",
"sessionIDTable": "",
"uplinkChunkSize": 0, "uplinkChunkSize": 0,
"uplinkDataKey": "", "uplinkDataKey": "",
"uplinkDataPlacement": "", "uplinkDataPlacement": "",
@@ -121,8 +125,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-placement byte-stab
"seqKey": "X-Seq", "seqKey": "X-Seq",
"seqPlacement": "cookie", "seqPlacement": "cookie",
"serverMaxHeaderBytes": 0, "serverMaxHeaderBytes": 0,
"sessionKey": "X-Session", "sessionIDKey": "X-Session",
"sessionPlacement": "header", "sessionIDLength": "",
"sessionIDPlacement": "header",
"sessionIDTable": "",
"uplinkChunkSize": 0, "uplinkChunkSize": 0,
"uplinkDataKey": "u", "uplinkDataKey": "u",
"uplinkDataPlacement": "query", "uplinkDataPlacement": "query",
@@ -158,8 +164,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-tuning byte-stably
"seqKey": "", "seqKey": "",
"seqPlacement": "", "seqPlacement": "",
"serverMaxHeaderBytes": 16384, "serverMaxHeaderBytes": 16384,
"sessionKey": "", "sessionIDKey": "",
"sessionPlacement": "", "sessionIDLength": "",
"sessionIDPlacement": "",
"sessionIDTable": "",
"uplinkChunkSize": 8192, "uplinkChunkSize": 8192,
"uplinkDataKey": "", "uplinkDataKey": "",
"uplinkDataPlacement": "", "uplinkDataPlacement": "",
@@ -4,8 +4,8 @@
"path": "/sp", "path": "/sp",
"host": "edge.example.test", "host": "edge.example.test",
"mode": "auto", "mode": "auto",
"sessionPlacement": "header", "sessionIDPlacement": "header",
"sessionKey": "X-Session", "sessionIDKey": "X-Session",
"seqPlacement": "cookie", "seqPlacement": "cookie",
"seqKey": "X-Seq", "seqKey": "X-Seq",
"uplinkDataPlacement": "query", "uplinkDataPlacement": "query",
@@ -93,6 +93,7 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
scMaxBufferedPosts: 50, scMaxBufferedPosts: 50,
tls: 'tls', tls: 'tls',
}; };
// legacy sessionKey must alias onto the renamed sessionIDKey (#6258)
const link = `vmess://${Base64.encode(JSON.stringify(json))}`; const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
const out = parseVmessLink(link); const out = parseVmessLink(link);
const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>; const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
@@ -101,7 +102,8 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
expect(xhttp.xPaddingHeader).toBe('X-Pad'); expect(xhttp.xPaddingHeader).toBe('X-Pad');
expect(xhttp.xPaddingPlacement).toBe('header'); expect(xhttp.xPaddingPlacement).toBe('header');
expect(xhttp.xPaddingMethod).toBe('random'); 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.seqKey).toBe('X-Seq');
expect(xhttp.noSSEHeader).toBe(true); expect(xhttp.noSSEHeader).toBe(true);
expect(xhttp.scMaxBufferedPosts).toBe(50); expect(xhttp.scMaxBufferedPosts).toBe(50);
@@ -135,7 +137,8 @@ describe('parseVlessLink — XHTTP advanced fields', () => {
+ '?type=xhttp&security=tls&host=edge.example&path=%2Fsp' + '?type=xhttp&security=tls&host=edge.example&path=%2Fsp'
+ '&xPaddingObfsMode=true&xPaddingKey=secret-key&xPaddingHeader=X-Pad' + '&xPaddingObfsMode=true&xPaddingKey=secret-key&xPaddingHeader=X-Pad'
+ '&xPaddingPlacement=header&xPaddingMethod=random' + '&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' + '&scMaxBufferedPosts=50'
+ '#imported-pad'; + '#imported-pad';
const out = parseVlessLink(link); const out = parseVlessLink(link);
@@ -145,7 +148,9 @@ describe('parseVlessLink — XHTTP advanced fields', () => {
expect(xhttp.xPaddingHeader).toBe('X-Pad'); expect(xhttp.xPaddingHeader).toBe('X-Pad');
expect(xhttp.xPaddingPlacement).toBe('header'); expect(xhttp.xPaddingPlacement).toBe('header');
expect(xhttp.xPaddingMethod).toBe('random'); 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.seqKey).toBe('X-Seq');
expect(xhttp.noSSEHeader).toBe(true); expect(xhttp.noSSEHeader).toBe(true);
expect(xhttp.scMaxBufferedPosts).toBe(50); expect(xhttp.scMaxBufferedPosts).toBe(50);
@@ -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<string, unknown>).sessionPlacement).toBeUndefined();
expect((parsed as Record<string, unknown>).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();
});
});
+19 -2
View File
@@ -404,8 +404,10 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
stringFields := []xhttpStringField{ stringFields := []xhttpStringField{
{"xPaddingBytes", "x-padding-bytes", ""}, {"xPaddingBytes", "x-padding-bytes", ""},
{"uplinkHTTPMethod", "uplink-http-method", ""}, {"uplinkHTTPMethod", "uplink-http-method", ""},
{"sessionPlacement", "session-placement", ""}, {"sessionIDPlacement", "session-id-placement", ""},
{"sessionKey", "session-key", ""}, {"sessionIDKey", "session-id-key", ""},
{"sessionIDTable", "session-id-table", ""},
{"sessionIDLength", "session-id-length", ""},
{"seqPlacement", "seq-placement", ""}, {"seqPlacement", "seq-placement", ""},
{"seqKey", "seq-key", ""}, {"seqKey", "seq-key", ""},
{"uplinkDataPlacement", "uplink-data-placement", ""}, {"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) // Bool fields (truthy only)
if v, ok := xhttp["noGRPCHeader"].(bool); ok && v { if v, ok := xhttp["noGRPCHeader"].(bool); ok && v {
opts["no-grpc-header"] = true opts["no-grpc-header"] = true
+14 -6
View File
@@ -330,8 +330,10 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) {
"xPaddingPlacement": "queryInHeader", "xPaddingPlacement": "queryInHeader",
"xPaddingMethod": "tokenish", "xPaddingMethod": "tokenish",
"uplinkHTTPMethod": "POST", "uplinkHTTPMethod": "POST",
"sessionPlacement": "query", "sessionIDPlacement": "query",
"sessionKey": "sess", "sessionIDKey": "sess",
"sessionIDTable": "Base62",
"sessionIDLength": "16-32",
"seqPlacement": "header", "seqPlacement": "header",
"seqKey": "seq", "seqKey": "seq",
"uplinkDataPlacement": "body", "uplinkDataPlacement": "body",
@@ -377,11 +379,17 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) {
if opts["uplink-http-method"] != "POST" { if opts["uplink-http-method"] != "POST" {
t.Errorf("uplink-http-method = %v", opts["uplink-http-method"]) t.Errorf("uplink-http-method = %v", opts["uplink-http-method"])
} }
if opts["session-placement"] != "query" { if opts["session-id-placement"] != "query" {
t.Errorf("session-placement = %v", opts["session-placement"]) t.Errorf("session-id-placement = %v", opts["session-id-placement"])
} }
if opts["session-key"] != "sess" { if opts["session-id-key"] != "sess" {
t.Errorf("session-key = %v", opts["session-key"]) 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" { if opts["seq-placement"] != "header" {
t.Errorf("seq-placement = %v", opts["seq-placement"]) t.Errorf("seq-placement = %v", opts["seq-placement"])
+15 -1
View File
@@ -1731,7 +1731,7 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
stringFields := []string{ stringFields := []string{
"uplinkHTTPMethod", "uplinkHTTPMethod",
"sessionPlacement", "sessionKey", "sessionIDPlacement", "sessionIDKey", "sessionIDTable", "sessionIDLength",
"seqPlacement", "seqKey", "seqPlacement", "seqKey",
"uplinkDataPlacement", "uplinkDataKey", "uplinkDataPlacement", "uplinkDataKey",
"scMaxEachPostBytes", "scMinPostsIntervalMs", "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"} { for _, field := range []string{"uplinkChunkSize"} {
if v, ok := nonZeroShareValue(xhttp[field]); ok { if v, ok := nonZeroShareValue(xhttp[field]); ok {
extra[field] = v extra[field] = v
+68 -1
View File
@@ -121,6 +121,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
xrayConfig.API = ensureAPIServices(xrayConfig.API) xrayConfig.API = ensureAPIServices(xrayConfig.API)
xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy) xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig) 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) _, _, _ = s.inboundService.AddTraffic(nil, nil)
@@ -251,6 +255,12 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
delete(stream, "externalProxy") 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, "", " ") newStream, err := json.MarshalIndent(stream, "", " ")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -576,7 +586,7 @@ func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
return return
} }
} }
merged := make([]any, 0, len(prepend)+len(templateOutbounds)+len(appendList)) var merged []any
merged = append(merged, prepend...) merged = append(merged, prepend...)
merged = append(merged, templateOutbounds...) merged = append(merged, templateOutbounds...)
merged = append(merged, appendList...) merged = append(merged, appendList...)
@@ -1078,3 +1088,60 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
func (s *XrayService) DidXrayCrash() bool { func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load() 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
}
@@ -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)
}
}
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "طريقة Padding", "paddingMethod": "طريقة Padding",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "sessionKey": "Session Key",
"sessionIDTable": "جدول معرّف الجلسة",
"sessionIDTableHint": "مجموعة الأحرف لتوليد معرّف الجلسة: اسم معرّف مسبقًا (ALPHABET، Base62، hex، number، …) أو سلسلة ASCII. اتركه فارغًا لاستخدام الإعداد الافتراضي لـ xray-core.",
"sessionIDLength": "طول معرّف الجلسة",
"sessionIDLengthHint": "طول أو نطاق (مثل 8-16) لمعرّف الجلسة المُولَّد. يُستخدم فقط عند تعيين جدول معرّف الجلسة؛ يجب أن يكون الحد الأدنى أكبر من 0.",
"sequencePlacement": "Sequence Placement", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding Method", "paddingMethod": "Padding Method",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Método de Padding", "paddingMethod": "Método de Padding",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "روش Padding", "paddingMethod": "روش Padding",
"sessionPlacement": "محل نشست", "sessionPlacement": "محل نشست",
"sessionKey": "کلید نشست", "sessionKey": "کلید نشست",
"sessionIDTable": "جدول شناسه نشست",
"sessionIDTableHint": "مجموعه نویسه‌ها برای تولید شناسه نشست: یک نام از پیش‌تعریف‌شده (ALPHABET، Base62، hex، number، …) یا یک رشته ASCII. برای مقدار پیش‌فرض xray-core خالی بگذارید.",
"sessionIDLength": "طول شناسه نشست",
"sessionIDLengthHint": "طول یا بازه (مثلاً 8-16) شناسه نشست تولیدشده. فقط وقتی جدول شناسه نشست تنظیم شده باشد استفاده می‌شود؛ کمینه باید بزرگ‌تر از 0 باشد.",
"sequencePlacement": "محل Sequence", "sequencePlacement": "محل Sequence",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "محل داده Uplink", "uplinkDataPlacement": "محل داده Uplink",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Metode Padding", "paddingMethod": "Metode Padding",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Padding 方法", "paddingMethod": "Padding 方法",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Método de Padding", "paddingMethod": "Método de Padding",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Padding Method", "paddingMethod": "Padding Method",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding Yöntemi", "paddingMethod": "Padding Yöntemi",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding Method", "paddingMethod": "Padding Method",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -555,6 +555,10 @@
"paddingMethod": "Phương thức Padding", "paddingMethod": "Phương thức Padding",
"sessionPlacement": "Session Placement", "sessionPlacement": "Session Placement",
"sessionKey": "Session Key", "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", "sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement", "uplinkDataPlacement": "Uplink Data Placement",
+4
View File
@@ -554,6 +554,10 @@
"paddingMethod": "Padding 方法", "paddingMethod": "Padding 方法",
"sessionPlacement": "Session 位置", "sessionPlacement": "Session 位置",
"sessionKey": "Session Key", "sessionKey": "Session Key",
"sessionIDTable": "会话 ID 字符表",
"sessionIDTableHint": "生成会话 ID 使用的字符集:预定义名称(ALPHABET、Base62、hex、number 等)或字面 ASCII 字符串。留空则使用 xray-core 默认值。",
"sessionIDLength": "会话 ID 长度",
"sessionIDLengthHint": "生成会话 ID 的长度或范围(如 8-16)。仅在设置了会话 ID 字符表时生效;最小值必须大于 0。",
"sequencePlacement": "Sequence 位置", "sequencePlacement": "Sequence 位置",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink 数据位置", "uplinkDataPlacement": "Uplink 数据位置",
+4
View File
@@ -534,6 +534,10 @@
"paddingMethod": "Padding 方法", "paddingMethod": "Padding 方法",
"sessionPlacement": "Session 位置", "sessionPlacement": "Session 位置",
"sessionKey": "Session Key", "sessionKey": "Session Key",
"sessionIDTable": "工作階段 ID 字元表",
"sessionIDTableHint": "產生工作階段 ID 使用的字元集:預定義名稱(ALPHABET、Base62、hex、number 等)或字面 ASCII 字串。留空則使用 xray-core 預設值。",
"sessionIDLength": "工作階段 ID 長度",
"sessionIDLengthHint": "產生工作階段 ID 的長度或範圍(如 8-16)。僅在設定了工作階段 ID 字元表時生效;最小值必須大於 0。",
"sequencePlacement": "Sequence 位置", "sequencePlacement": "Sequence 位置",
"sequenceKey": "Sequence Key", "sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink 資料位置", "uplinkDataPlacement": "Uplink 資料位置",