mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
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:
@@ -64,8 +64,10 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string,
|
||||
|
||||
const stringFields = [
|
||||
'uplinkHTTPMethod',
|
||||
'sessionPlacement',
|
||||
'sessionKey',
|
||||
'sessionIDPlacement',
|
||||
'sessionIDKey',
|
||||
'sessionIDTable',
|
||||
'sessionIDLength',
|
||||
'seqPlacement',
|
||||
'seqKey',
|
||||
'uplinkDataPlacement',
|
||||
|
||||
@@ -24,10 +24,18 @@ type Raw = Record<string, unknown>;
|
||||
// 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<string, string> = {
|
||||
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<string, unknown>): 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];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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).
|
||||
// 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
|
||||
|
||||
@@ -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<InboundFormValu
|
||||
const { t } = useTranslation();
|
||||
const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
|
||||
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 xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form);
|
||||
|
||||
@@ -163,7 +165,7 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionIDPlacement']}
|
||||
label={t('pages.inbounds.form.sessionPlacement')}
|
||||
>
|
||||
<Select
|
||||
@@ -176,14 +178,36 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && (
|
||||
{xhttpSessionIDPlacement && xhttpSessionIDPlacement !== 'path' && (
|
||||
<Form.Item
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionKey']}
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionIDKey']}
|
||||
label={t('pages.inbounds.form.sessionKey')}
|
||||
>
|
||||
<Input placeholder="x_session" />
|
||||
</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
|
||||
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
||||
label={t('pages.inbounds.form.sequencePlacement')}
|
||||
|
||||
@@ -1,8 +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 { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
|
||||
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';
|
||||
|
||||
@@ -145,7 +147,7 @@ export default function XhttpForm({ form, onXmuxToggle }: XhttpFormProps) {
|
||||
only matters when placement is not 'path'. */}
|
||||
<Form.Item
|
||||
label={t('pages.inbounds.form.sessionPlacement')}
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionIDPlacement']}
|
||||
>
|
||||
<Select
|
||||
placeholder="Default (path)"
|
||||
@@ -161,19 +163,49 @@ export default function XhttpForm({ form, onXmuxToggle }: XhttpFormProps) {
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const placement = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'sessionPlacement',
|
||||
'streamSettings', 'xhttpSettings', 'sessionIDPlacement',
|
||||
]);
|
||||
if (!placement || placement === 'path') return null;
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('pages.inbounds.form.sessionKey')}
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionKey']}
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionIDKey']}
|
||||
>
|
||||
<Input placeholder="x_session" />
|
||||
</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
|
||||
label={t('pages.inbounds.form.sequencePlacement')}
|
||||
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
||||
|
||||
@@ -25,7 +25,34 @@ export const XHttpXmuxSchema = z.object({
|
||||
});
|
||||
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('/'),
|
||||
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<typeof XHttpStreamSettingsSchema>;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, unknown>).xhttpSettings as Record<string, unknown>;
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
+15
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 数据位置",
|
||||
|
||||
@@ -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 資料位置",
|
||||
|
||||
Reference in New Issue
Block a user