3 Commits b07fad0e69 ... 3fa4eddae3

Author SHA1 Message Date
  MHSanaei 3fa4eddae3 v3.4.0 18 hours ago
  MHSanaei 47fd6061b1 revert languages update 18 hours ago
  Rouzbeh† fea3c94b11 feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506) 18 hours ago
32 changed files with 512 additions and 59 deletions
  1. 4 2
      frontend/src/lib/xray/inbound-link.ts
  2. 23 3
      frontend/src/lib/xray/outbound-link-parser.ts
  3. 4 2
      frontend/src/lib/xray/stream-wire-normalize.ts
  4. 34 0
      frontend/src/lib/xray/xhttp-session-id.ts
  5. 1 1
      frontend/src/pages/inbounds/form/InboundFormModal.tsx
  6. 30 6
      frontend/src/pages/inbounds/form/transport/xhttp.tsx
  7. 36 4
      frontend/src/pages/xray/outbounds/transport/xhttp.tsx
  8. 36 4
      frontend/src/schemas/protocols/stream/xhttp.ts
  9. 1 0
      frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
  10. 16 8
      frontend/src/test/__snapshots__/stream.test.ts.snap
  11. 2 2
      frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json
  12. 8 3
      frontend/src/test/outbound-link-parser.test.ts
  13. 54 0
      frontend/src/test/xhttp-session-id.test.ts
  14. 1 1
      internal/config/version
  15. 19 2
      internal/sub/clash_service.go
  16. 14 6
      internal/sub/clash_service_test.go
  17. 15 1
      internal/sub/service.go
  18. 68 1
      internal/web/service/xray.go
  19. 81 0
      internal/web/service/xray_xhttp_session_test.go
  20. 5 1
      internal/web/translation/ar-EG.json
  21. 5 1
      internal/web/translation/en-US.json
  22. 5 1
      internal/web/translation/es-ES.json
  23. 5 1
      internal/web/translation/fa-IR.json
  24. 5 1
      internal/web/translation/id-ID.json
  25. 5 1
      internal/web/translation/ja-JP.json
  26. 5 1
      internal/web/translation/pt-BR.json
  27. 5 1
      internal/web/translation/ru-RU.json
  28. 5 1
      internal/web/translation/tr-TR.json
  29. 5 1
      internal/web/translation/uk-UA.json
  30. 5 1
      internal/web/translation/vi-VN.json
  31. 5 1
      internal/web/translation/zh-CN.json
  32. 5 1
      internal/web/translation/zh-TW.json

+ 4 - 2
frontend/src/lib/xray/inbound-link.ts

@@ -64,8 +64,10 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string,
 
   const stringFields = [
     'uplinkHTTPMethod',
-    'sessionPlacement',
-    'sessionKey',
+    'sessionIDPlacement',
+    'sessionIDKey',
+    'sessionIDTable',
+    'sessionIDLength',
     'seqPlacement',
     'seqKey',
     'uplinkDataPlacement',

+ 23 - 3
frontend/src/lib/xray/outbound-link-parser.ts

@@ -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];
   }

+ 4 - 2
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -16,8 +16,10 @@ const PACKET_UP_FIELDS = [
 const STREAM_UP_SERVER_FIELDS = ['scStreamUpServerSecs'] as const;
 
 const PLACEMENT_STRING_FIELDS = [
-  'sessionPlacement',
-  'sessionKey',
+  'sessionIDPlacement',
+  'sessionIDKey',
+  'sessionIDTable',
+  'sessionIDLength',
   'seqPlacement',
   'seqKey',
   'uplinkDataPlacement',

+ 34 - 0
frontend/src/lib/xray/xhttp-session-id.ts

@@ -0,0 +1,34 @@
+// Client-side validation for xray-core #6258 sessionIDTable / sessionIDLength.
+// xray-core also enforces a room-size minimum (sum(table^k for k in
+// from..to) >= 2<<30) server-side; we deliberately skip replicating that
+// big-int check and only catch the cheap, obvious mistakes here.
+
+// xray-core requires the charset table to be ASCII-only.
+export function validateSessionIDTable(_rule: unknown, value: unknown): Promise<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();
+}

+ 1 - 1
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -708,7 +708,7 @@ export default function InboundFormModal({
   // etc., not empty strings).
   // Seed each network's settings blob with its Zod schema defaults so
   // every Form.Item inside the network sub-form has a defined starting
-  // value. XHTTP in particular has ~20 fields (sessionPlacement,
+  // value. XHTTP in particular has ~20 fields (sessionIDPlacement,
   // seqPlacement, xPaddingMethod, uplinkHTTPMethod, ...) whose value
   // is the literal "" sentinel meaning "let xray-core pick its
   // default". Without seeding "", the Form.Item reads `undefined` and

+ 30 - 6
frontend/src/pages/inbounds/form/transport/xhttp.tsx

@@ -1,9 +1,10 @@
 import { useTranslation } from 'react-i18next';
-import { Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd';
+import { AutoComplete, Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd';
 
 import { HeaderMapEditor } from '@/components/form';
 import type { InboundFormValues } from '@/schemas/forms/inbound-form';
-import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
+import { XHTTP_SESSION_ID_TABLES, XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
+import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
 
 const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});
 
@@ -11,7 +12,8 @@ export default function XhttpForm({ form }: { form: FormInstance<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')}

+ 36 - 4
frontend/src/pages/xray/outbounds/transport/xhttp.tsx

@@ -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']}

+ 36 - 4
frontend/src/schemas/protocols/stream/xhttp.ts

@@ -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>;

+ 1 - 0
frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap

@@ -118,6 +118,7 @@ exports[`inbound transport forms > XhttpForm field structure is stable 1`] = `
   "Uplink HTTP Method",
   "Padding Obfs Mode",
   "Session Placement",
+  "Session ID Table",
   "Sequence Placement",
   "No SSE Header",
   "XMUX",

+ 16 - 8
frontend/src/test/__snapshots__/stream.test.ts.snap

@@ -53,8 +53,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = `
     "seqKey": "",
     "seqPlacement": "",
     "serverMaxHeaderBytes": 0,
-    "sessionKey": "",
-    "sessionPlacement": "",
+    "sessionIDKey": "",
+    "sessionIDLength": "",
+    "sessionIDPlacement": "",
+    "sessionIDTable": "",
     "uplinkChunkSize": 0,
     "uplinkDataKey": "",
     "uplinkDataPlacement": "",
@@ -87,8 +89,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-padding byte-stably
     "seqKey": "",
     "seqPlacement": "",
     "serverMaxHeaderBytes": 0,
-    "sessionKey": "",
-    "sessionPlacement": "",
+    "sessionIDKey": "",
+    "sessionIDLength": "",
+    "sessionIDPlacement": "",
+    "sessionIDTable": "",
     "uplinkChunkSize": 0,
     "uplinkDataKey": "",
     "uplinkDataPlacement": "",
@@ -121,8 +125,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-placement byte-stab
     "seqKey": "X-Seq",
     "seqPlacement": "cookie",
     "serverMaxHeaderBytes": 0,
-    "sessionKey": "X-Session",
-    "sessionPlacement": "header",
+    "sessionIDKey": "X-Session",
+    "sessionIDLength": "",
+    "sessionIDPlacement": "header",
+    "sessionIDTable": "",
     "uplinkChunkSize": 0,
     "uplinkDataKey": "u",
     "uplinkDataPlacement": "query",
@@ -158,8 +164,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-tuning byte-stably
     "seqKey": "",
     "seqPlacement": "",
     "serverMaxHeaderBytes": 16384,
-    "sessionKey": "",
-    "sessionPlacement": "",
+    "sessionIDKey": "",
+    "sessionIDLength": "",
+    "sessionIDPlacement": "",
+    "sessionIDTable": "",
     "uplinkChunkSize": 8192,
     "uplinkDataKey": "",
     "uplinkDataPlacement": "",

+ 2 - 2
frontend/src/test/golden/fixtures/stream/xhttp-extra-placement.json

@@ -4,8 +4,8 @@
     "path": "/sp",
     "host": "edge.example.test",
     "mode": "auto",
-    "sessionPlacement": "header",
-    "sessionKey": "X-Session",
+    "sessionIDPlacement": "header",
+    "sessionIDKey": "X-Session",
     "seqPlacement": "cookie",
     "seqKey": "X-Seq",
     "uplinkDataPlacement": "query",

+ 8 - 3
frontend/src/test/outbound-link-parser.test.ts

@@ -93,6 +93,7 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
       scMaxBufferedPosts: 50,
       tls: 'tls',
     };
+    // legacy sessionKey must alias onto the renamed sessionIDKey (#6258)
     const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
     const out = parseVmessLink(link);
     const xhttp = (out?.streamSettings as Record<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);

+ 54 - 0
frontend/src/test/xhttp-session-id.test.ts

@@ -0,0 +1,54 @@
+import { describe, expect, it } from 'vitest';
+
+import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
+import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
+
+// xray-core #6258: sessionPlacement/sessionKey were renamed to
+// sessionIDPlacement/sessionIDKey. The schema must lift legacy keys off
+// stored configs so an upgraded panel never silently drops them.
+describe('XHttpStreamSettingsSchema legacy migration', () => {
+  it('lifts legacy sessionPlacement/sessionKey onto the renamed keys', () => {
+    const parsed = XHttpStreamSettingsSchema.parse({
+      sessionPlacement: 'cookie',
+      sessionKey: 'x_session',
+    });
+    expect(parsed.sessionIDPlacement).toBe('cookie');
+    expect(parsed.sessionIDKey).toBe('x_session');
+    // legacy keys must not survive — we never emit both names
+    expect((parsed as Record<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();
+  });
+});

+ 1 - 1
internal/config/version

@@ -1 +1 @@
-3.3.1
+3.4.0

+ 19 - 2
internal/sub/clash_service.go

@@ -404,8 +404,10 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
 	stringFields := []xhttpStringField{
 		{"xPaddingBytes", "x-padding-bytes", ""},
 		{"uplinkHTTPMethod", "uplink-http-method", ""},
-		{"sessionPlacement", "session-placement", ""},
-		{"sessionKey", "session-key", ""},
+		{"sessionIDPlacement", "session-id-placement", ""},
+		{"sessionIDKey", "session-id-key", ""},
+		{"sessionIDTable", "session-id-table", ""},
+		{"sessionIDLength", "session-id-length", ""},
 		{"seqPlacement", "seq-placement", ""},
 		{"seqKey", "seq-key", ""},
 		{"uplinkDataPlacement", "uplink-data-placement", ""},
@@ -420,6 +422,21 @@ func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
 		}
 	}
 
+	// Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey.
+	// Fall back to them so not-yet-resaved configs still map. Mirrors the
+	// frontend migration.
+	for _, f := range []xhttpStringField{
+		{"sessionPlacement", "session-id-placement", ""},
+		{"sessionKey", "session-id-key", ""},
+	} {
+		if _, exists := opts[f.dst]; exists {
+			continue
+		}
+		if v, ok := xhttp[f.src].(string); ok && v != "" {
+			opts[f.dst] = v
+		}
+	}
+
 	// Bool fields (truthy only)
 	if v, ok := xhttp["noGRPCHeader"].(bool); ok && v {
 		opts["no-grpc-header"] = true

+ 14 - 6
internal/sub/clash_service_test.go

@@ -330,8 +330,10 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) {
 		"xPaddingPlacement":    "queryInHeader",
 		"xPaddingMethod":       "tokenish",
 		"uplinkHTTPMethod":     "POST",
-		"sessionPlacement":     "query",
-		"sessionKey":           "sess",
+		"sessionIDPlacement":   "query",
+		"sessionIDKey":         "sess",
+		"sessionIDTable":       "Base62",
+		"sessionIDLength":      "16-32",
 		"seqPlacement":         "header",
 		"seqKey":               "seq",
 		"uplinkDataPlacement":  "body",
@@ -377,11 +379,17 @@ func TestBuildXhttpClashOpts_FullFieldMapping(t *testing.T) {
 	if opts["uplink-http-method"] != "POST" {
 		t.Errorf("uplink-http-method = %v", opts["uplink-http-method"])
 	}
-	if opts["session-placement"] != "query" {
-		t.Errorf("session-placement = %v", opts["session-placement"])
+	if opts["session-id-placement"] != "query" {
+		t.Errorf("session-id-placement = %v", opts["session-id-placement"])
 	}
-	if opts["session-key"] != "sess" {
-		t.Errorf("session-key = %v", opts["session-key"])
+	if opts["session-id-key"] != "sess" {
+		t.Errorf("session-id-key = %v", opts["session-id-key"])
+	}
+	if opts["session-id-table"] != "Base62" {
+		t.Errorf("session-id-table = %v", opts["session-id-table"])
+	}
+	if opts["session-id-length"] != "16-32" {
+		t.Errorf("session-id-length = %v", opts["session-id-length"])
 	}
 	if opts["seq-placement"] != "header" {
 		t.Errorf("seq-placement = %v", opts["seq-placement"])

+ 15 - 1
internal/sub/service.go

@@ -1731,7 +1731,7 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
 
 	stringFields := []string{
 		"uplinkHTTPMethod",
-		"sessionPlacement", "sessionKey",
+		"sessionIDPlacement", "sessionIDKey", "sessionIDTable", "sessionIDLength",
 		"seqPlacement", "seqKey",
 		"uplinkDataPlacement", "uplinkDataKey",
 		"scMaxEachPostBytes", "scMinPostsIntervalMs",
@@ -1750,6 +1750,20 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
 		}
 	}
 
+	// Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey.
+	// Lift them onto the renamed keys so links from not-yet-resaved configs
+	// still carry the session settings. Mirrors the frontend migration.
+	for legacy, renamed := range map[string]string{
+		"sessionPlacement": "sessionIDPlacement",
+		"sessionKey":       "sessionIDKey",
+	} {
+		if _, exists := extra[renamed]; !exists {
+			if v, ok := xhttp[legacy].(string); ok && len(v) > 0 {
+				extra[renamed] = v
+			}
+		}
+	}
+
 	for _, field := range []string{"uplinkChunkSize"} {
 		if v, ok := nonZeroShareValue(xhttp[field]); ok {
 			extra[field] = v

+ 68 - 1
internal/web/service/xray.go

@@ -121,6 +121,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 	xrayConfig.API = ensureAPIServices(xrayConfig.API)
 	xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
 	xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig)
+	// Template outbounds authored before the xray-core #6258 XHTTP rename may
+	// still carry sessionPlacement/sessionKey; lift them too (same reason as
+	// the per-inbound lift below).
+	xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
 
 	_, _, _ = s.inboundService.AddTraffic(nil, nil)
 
@@ -251,6 +255,12 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 
 			delete(stream, "externalProxy")
 
+			// xray-core v26.6.22 (#6258) renamed the XHTTP session keys and
+			// kept no fallback. Lift legacy sessionPlacement/sessionKey onto the
+			// new names here so inbounds stored before the rename keep working
+			// without the admin re-saving them.
+			liftXhttpSessionIDKeys(stream)
+
 			newStream, err := json.MarshalIndent(stream, "", "  ")
 			if err != nil {
 				return nil, err
@@ -576,7 +586,7 @@ func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
 			return
 		}
 	}
-	merged := make([]any, 0, len(prepend)+len(templateOutbounds)+len(appendList))
+	var merged []any
 	merged = append(merged, prepend...)
 	merged = append(merged, templateOutbounds...)
 	merged = append(merged, appendList...)
@@ -1078,3 +1088,60 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
 func (s *XrayService) DidXrayCrash() bool {
 	return !s.IsXrayRunning() && !isManuallyStopped.Load()
 }
+
+// liftXhttpSessionIDKeys renames the legacy XHTTP session keys
+// (sessionPlacement/sessionKey) to the v26.6.22 #6258 names
+// (sessionIDPlacement/sessionIDKey) inside a streamSettings map. xray-core kept
+// no fallback for the old names, so a config stored before the rename would be
+// silently ignored by the engine. Returns true if it changed anything.
+func liftXhttpSessionIDKeys(stream map[string]any) bool {
+	xhttp, ok := stream["xhttpSettings"].(map[string]any)
+	if !ok {
+		return false
+	}
+	changed := false
+	for legacy, renamed := range map[string]string{
+		"sessionPlacement": "sessionIDPlacement",
+		"sessionKey":       "sessionIDKey",
+	} {
+		v, has := xhttp[legacy]
+		if !has {
+			continue
+		}
+		if _, exists := xhttp[renamed]; !exists {
+			xhttp[renamed] = v
+		}
+		delete(xhttp, legacy)
+		changed = true
+	}
+	return changed
+}
+
+// liftOutboundsXhttpSessionIDKeys applies liftXhttpSessionIDKeys to every
+// outbound's streamSettings in the raw outbounds array. The original bytes are
+// returned untouched when nothing needs lifting, so an unchanged config never
+// looks modified to the hot-reload diff.
+func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMessage {
+	if len(raw) == 0 {
+		return raw
+	}
+	var outbounds []map[string]any
+	if err := json.Unmarshal(raw, &outbounds); err != nil {
+		return raw
+	}
+	changed := false
+	for _, ob := range outbounds {
+		if stream, ok := ob["streamSettings"].(map[string]any); ok {
+			if liftXhttpSessionIDKeys(stream) {
+				changed = true
+			}
+		}
+	}
+	if !changed {
+		return raw
+	}
+	if rewritten, err := json.Marshal(outbounds); err == nil {
+		return rewritten
+	}
+	return raw
+}

+ 81 - 0
internal/web/service/xray_xhttp_session_test.go

@@ -0,0 +1,81 @@
+package service
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
+)
+
+// xray-core v26.6.22 (#6258) renamed the XHTTP session keys with no fallback.
+// The lift must rewrite stored configs at config-generation time so pre-upgrade
+// inbounds/outbounds keep working without a manual re-save.
+func TestLiftXhttpSessionIDKeys(t *testing.T) {
+	t.Run("lifts legacy keys and drops them", func(t *testing.T) {
+		stream := map[string]any{
+			"xhttpSettings": map[string]any{
+				"sessionPlacement": "cookie",
+				"sessionKey":       "x_session",
+			},
+		}
+		if !liftXhttpSessionIDKeys(stream) {
+			t.Fatal("expected changed=true")
+		}
+		xhttp := stream["xhttpSettings"].(map[string]any)
+		if xhttp["sessionIDPlacement"] != "cookie" || xhttp["sessionIDKey"] != "x_session" {
+			t.Fatalf("renamed keys missing: %#v", xhttp)
+		}
+		if _, ok := xhttp["sessionPlacement"]; ok {
+			t.Fatal("legacy sessionPlacement still present")
+		}
+		if _, ok := xhttp["sessionKey"]; ok {
+			t.Fatal("legacy sessionKey still present")
+		}
+	})
+
+	t.Run("keeps an explicit new key over the legacy one", func(t *testing.T) {
+		stream := map[string]any{
+			"xhttpSettings": map[string]any{
+				"sessionPlacement":   "cookie",
+				"sessionIDPlacement": "header",
+			},
+		}
+		liftXhttpSessionIDKeys(stream)
+		xhttp := stream["xhttpSettings"].(map[string]any)
+		if xhttp["sessionIDPlacement"] != "header" {
+			t.Fatalf("explicit new key was overwritten: %v", xhttp["sessionIDPlacement"])
+		}
+	})
+
+	t.Run("no-op without xhttpSettings or legacy keys", func(t *testing.T) {
+		if liftXhttpSessionIDKeys(map[string]any{"wsSettings": map[string]any{}}) {
+			t.Fatal("expected no change for non-xhttp stream")
+		}
+		if liftXhttpSessionIDKeys(map[string]any{"xhttpSettings": map[string]any{"path": "/"}}) {
+			t.Fatal("expected no change when no legacy keys present")
+		}
+	})
+}
+
+func TestLiftOutboundsXhttpSessionIDKeys(t *testing.T) {
+	raw := json_util.RawMessage(`[{"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"sessionKey":"x_session","sessionPlacement":"query"}}}]`)
+	out := liftOutboundsXhttpSessionIDKeys(raw)
+
+	var parsed []map[string]any
+	if err := json.Unmarshal(out, &parsed); err != nil {
+		t.Fatalf("unmarshal rewritten outbounds: %v", err)
+	}
+	xhttp := parsed[0]["streamSettings"].(map[string]any)["xhttpSettings"].(map[string]any)
+	if xhttp["sessionIDKey"] != "x_session" || xhttp["sessionIDPlacement"] != "query" {
+		t.Fatalf("outbound keys not lifted: %#v", xhttp)
+	}
+	if _, ok := xhttp["sessionKey"]; ok {
+		t.Fatal("legacy sessionKey survived in outbound")
+	}
+
+	// Unchanged input must return byte-identical output (no spurious hot-reload).
+	clean := json_util.RawMessage(`[{"protocol":"freedom"}]`)
+	if got := liftOutboundsXhttpSessionIDKeys(clean); string(got) != string(clean) {
+		t.Fatalf("clean outbounds were rewritten: %s", got)
+	}
+}

+ 5 - 1
internal/web/translation/ar-EG.json

@@ -6,7 +6,7 @@
   "cancel": "إلغاء",
   "close": "إغلاق",
   "save": "حفظ",
-  "logout": "تسجيل خروج ❤️",
+  "logout": "تسجيل خروج",
   "create": "إنشاء",
   "add": "إضافة",
   "remove": "إزالة",
@@ -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",

+ 5 - 1
internal/web/translation/en-US.json

@@ -6,7 +6,7 @@
   "cancel": "Cancel",
   "close": "Close",
   "save": "Save",
-  "logout": "Log Out ❤️",
+  "logout": "Log Out",
   "create": "Create",
   "add": "Add",
   "remove": "Remove",
@@ -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",

+ 5 - 1
internal/web/translation/es-ES.json

@@ -6,7 +6,7 @@
   "cancel": "Cancelar",
   "close": "Cerrar",
   "save": "Guardar",
-  "logout": "Cerrar Sesión ❤️",
+  "logout": "Cerrar Sesión",
   "create": "Crear",
   "add": "Añadir",
   "remove": "Quitar",
@@ -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",

+ 5 - 1
internal/web/translation/fa-IR.json

@@ -6,7 +6,7 @@
   "cancel": "انصراف",
   "close": "بستن",
   "save": "ذخیره",
-  "logout": "خروج ❤️",
+  "logout": "خروج",
   "create": "ایجاد",
   "add": "افزودن",
   "remove": "حذف",
@@ -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",

+ 5 - 1
internal/web/translation/id-ID.json

@@ -6,7 +6,7 @@
   "cancel": "Batal",
   "close": "Tutup",
   "save": "Simpan",
-  "logout": "Keluar ❤️",
+  "logout": "Keluar",
   "create": "Buat",
   "add": "Tambah",
   "remove": "Hapus",
@@ -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",

+ 5 - 1
internal/web/translation/ja-JP.json

@@ -6,7 +6,7 @@
   "cancel": "キャンセル",
   "close": "閉じる",
   "save": "保存",
-  "logout": "ログアウト ❤️",
+  "logout": "ログアウト",
   "create": "作成",
   "add": "追加",
   "remove": "削除",
@@ -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",

+ 5 - 1
internal/web/translation/pt-BR.json

@@ -6,7 +6,7 @@
   "cancel": "Cancelar",
   "close": "Fechar",
   "save": "Salvar",
-  "logout": "Sair ❤️",
+  "logout": "Sair",
   "create": "Criar",
   "add": "Adicionar",
   "remove": "Remover",
@@ -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",

+ 5 - 1
internal/web/translation/ru-RU.json

@@ -6,7 +6,7 @@
   "cancel": "Отмена",
   "close": "Закрыть",
   "save": "Сохранить",
-  "logout": "Выход ❤️",
+  "logout": "Выход",
   "create": "Создать",
   "add": "Добавить",
   "remove": "Удалить",
@@ -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",

+ 5 - 1
internal/web/translation/tr-TR.json

@@ -6,7 +6,7 @@
   "cancel": "İptal",
   "close": "Kapat",
   "save": "Kaydet",
-  "logout": "Çıkış Yap ❤️",
+  "logout": "Çıkış Yap",
   "create": "Oluştur",
   "add": "Ekle",
   "remove": "Kaldır",
@@ -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",

+ 5 - 1
internal/web/translation/uk-UA.json

@@ -6,7 +6,7 @@
   "cancel": "Скасувати",
   "close": "Закрити",
   "save": "Зберегти",
-  "logout": "Вийти ❤️",
+  "logout": "Вийти",
   "create": "Створити",
   "add": "Додати",
   "remove": "Видалити",
@@ -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",

+ 5 - 1
internal/web/translation/vi-VN.json

@@ -6,7 +6,7 @@
   "cancel": "Hủy bỏ",
   "close": "Đóng",
   "save": "Lưu",
-  "logout": "Đăng xuất ❤️",
+  "logout": "Đăng xuất",
   "create": "Tạo",
   "add": "Thêm",
   "remove": "Xóa",
@@ -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",

+ 5 - 1
internal/web/translation/zh-CN.json

@@ -6,7 +6,7 @@
   "cancel": "取消",
   "close": "关闭",
   "save": "保存",
-  "logout": "登出 ❤️",
+  "logout": "登出",
   "create": "创建",
   "add": "添加",
   "remove": "移除",
@@ -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 数据位置",

+ 5 - 1
internal/web/translation/zh-TW.json

@@ -6,7 +6,7 @@
   "cancel": "取消",
   "close": "關閉",
   "save": "儲存",
-  "logout": "登出 ❤️",
+  "logout": "登出",
   "create": "建立",
   "add": "新增",
   "remove": "移除",
@@ -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 資料位置",