3 Angajamente b07fad0e69 ... 3fa4eddae3

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  MHSanaei 3fa4eddae3 v3.4.0 16 ore în urmă
  MHSanaei 47fd6061b1 revert languages update 16 ore în urmă
  Rouzbeh† fea3c94b11 feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506) 16 ore în urmă
32 a modificat fișierele cu 512 adăugiri și 59 ștergeri
  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 資料位置",