Quellcode durchsuchen

feat(ui): validate the REALITY client version range at save time (#6126)

* feat(ui): validate the REALITY client version range at save time

The impossible range from PR #6125 — a max below the effective minimum
— could still be saved; the tooltip only helps a user who hovers it.
Add save-time validation mirroring xray-core's parser (up to three
dot-separated parts, each 0-255) on both fields, plus a cross-field
check that a non-empty max is not below a non-empty min. Errors are
field-level i18n keys following the REALITY target precedent, so the
modal stays open and points at the offending field instead of storing
a config that rejects every client.

A malformed min is reported by its own field and skipped by the max
comparison, so the user sees one precise error per field.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ui): reject untrimmed client versions and revalidate max on min edits

From review: the validators trimmed but the save path ships the value
verbatim, and xray-core's part parser accepts no surrounding
whitespace — so a green form could still save a config the core
refuses to load. Reject any value that differs from its trimmed form.

Also revalidate the max field after a min edit when max already
shows an error, so correcting the min clears the stale cross-field
message without waiting for the next submit.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
PathGao vor 4 Stunden
Ursprung
Commit
ca6955d88b

+ 57 - 0
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -104,6 +104,63 @@ export function validateRealityTarget(target: string): string | undefined {
   return undefined;
 }
 
+/**
+ * Parses a REALITY client-version string the way xray-core's config loader
+ * does: one to three dot-separated numeric parts, each 0-255. Returns the
+ * parts padded to three entries, or undefined when the string is not a valid
+ * version.
+ */
+export function parseRealityClientVer(value: string): [number, number, number] | undefined {
+  const trimmed = value.trim();
+  if (!trimmed) return undefined;
+  const parts = trimmed.split('.');
+  if (parts.length > 3) return undefined;
+  const nums: number[] = [];
+  for (const part of parts) {
+    if (!/^\d+$/.test(part)) return undefined;
+    const n = Number(part);
+    if (n > 255) return undefined;
+    nums.push(n);
+  }
+  while (nums.length < 3) nums.push(0);
+  return nums as [number, number, number];
+}
+
+/**
+ * Validates a REALITY client-version field; empty means "not set" and is
+ * valid. The value is saved exactly as typed and xray-core's part parser
+ * accepts no surrounding whitespace, so a value that differs from its
+ * trimmed form is rejected rather than silently passed to the wire.
+ */
+export function validateRealityClientVer(value: string): string | undefined {
+  if (!value) return undefined;
+  if (value !== value.trim() || !parseRealityClientVer(value)) {
+    return 'pages.inbounds.form.clientVerInvalid';
+  }
+  return undefined;
+}
+
+/**
+ * Validates the max client-version field: format first, then that a non-empty
+ * max is not below a non-empty min (an inverted range rejects every client).
+ * An empty or malformed min is left to the min field's own validation.
+ */
+export function validateRealityMaxClientVer(max: string, min: string): string | undefined {
+  const formatError = validateRealityClientVer(max);
+  if (formatError) return formatError;
+  const maxParts = parseRealityClientVer(max);
+  const minParts = parseRealityClientVer(min);
+  if (!maxParts || !minParts) return undefined;
+  for (let i = 0; i < 3; i++) {
+    if (maxParts[i] !== minParts[i]) {
+      return maxParts[i] < minParts[i]
+        ? 'pages.inbounds.form.maxClientVerBelowMin'
+        : undefined;
+    }
+  }
+  return undefined;
+}
+
 function liftLegacyXhttpSessionKeys(obj: Record<string, unknown>): void {
   const lift = (legacy: string, renamed: string) => {
     const v = obj[legacy];

+ 28 - 1
frontend/src/pages/inbounds/form/security/reality.tsx

@@ -1,11 +1,16 @@
 import { useState } from 'react';
+import { useFormContext } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { Alert, Button, Collapse, Descriptions, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
 import { RadarChartOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
 
 import { FormField } from '@/components/form/rhf';
 import { UTLS_FINGERPRINT } from '@/schemas/primitives';
-import { validateRealityTarget } from '@/lib/xray/stream-wire-normalize';
+import {
+  validateRealityClientVer,
+  validateRealityMaxClientVer,
+  validateRealityTarget,
+} from '@/lib/xray/stream-wire-normalize';
 import type { RealityScanResult } from '@/generated/types';
 import RealityTargetScannerModal from './RealityTargetScannerModal';
 
@@ -39,7 +44,14 @@ export default function RealityForm({
   clearMldsa65,
 }: RealityFormProps) {
   const { t } = useTranslation();
+  const { getFieldState, trigger } = useFormContext();
   const [scannerOpen, setScannerOpen] = useState(false);
+  const maxClientVerPath = 'streamSettings.realitySettings.maxClientVer';
+  const revalidateMaxClientVer = () => {
+    if (getFieldState(maxClientVerPath).error) {
+      void trigger(maxClientVerPath);
+    }
+  };
   return (
     <>
       <FormField
@@ -128,6 +140,13 @@ export default function RealityForm({
         name={['streamSettings', 'realitySettings', 'minClientVer']}
         label={t('pages.inbounds.form.minClientVer')}
         tooltip={t('pages.inbounds.form.minClientVerHint')}
+        onAfterChange={revalidateMaxClientVer}
+        rules={{
+          validate: (value) => {
+            const errKey = validateRealityClientVer(typeof value === 'string' ? value : '');
+            return errKey ? errKey : true;
+          },
+        }}
       >
         <Input placeholder="26.3.27" />
       </FormField>
@@ -135,6 +154,14 @@ export default function RealityForm({
         name={['streamSettings', 'realitySettings', 'maxClientVer']}
         label={t('pages.inbounds.form.maxClientVer')}
         tooltip={t('pages.inbounds.form.maxClientVerHint')}
+        rules={{
+          validate: (value, formValues) => {
+            const max = typeof value === 'string' ? value : '';
+            const min = formValues?.streamSettings?.realitySettings?.minClientVer;
+            const errKey = validateRealityMaxClientVer(max, typeof min === 'string' ? min : '');
+            return errKey ? errKey : true;
+          },
+        }}
       >
         <Input placeholder="x.y.z" />
       </FormField>

+ 60 - 0
frontend/src/test/stream-wire-normalize.test.ts

@@ -7,6 +7,8 @@ import {
   normalizeSockoptForWire,
   normalizeStreamSettingsForWire,
   normalizeXhttpForWire,
+  validateRealityClientVer,
+  validateRealityMaxClientVer,
   validateRealityTarget,
 } from '@/lib/xray/stream-wire-normalize';
 import { InboundFormSchema } from '@/schemas/forms/inbound-form';
@@ -26,6 +28,64 @@ describe('validateRealityTarget', () => {
   });
 });
 
+describe('validateRealityClientVer', () => {
+  it('accepts empty (not set) and core-style versions', () => {
+    expect(validateRealityClientVer('')).toBeUndefined();
+    expect(validateRealityClientVer('26.3.27')).toBeUndefined();
+    expect(validateRealityClientVer('1.0.0')).toBeUndefined();
+    expect(validateRealityClientVer('26')).toBeUndefined();
+    expect(validateRealityClientVer('26.3')).toBeUndefined();
+    expect(validateRealityClientVer('0.0.255')).toBeUndefined();
+  });
+
+  it('rejects untrimmed values because the save path ships them verbatim', () => {
+    expect(validateRealityClientVer('26.3.27 ')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer(' 26.3.27')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer(' ')).toBe('pages.inbounds.form.clientVerInvalid');
+  });
+
+  it('rejects what the core parser rejects', () => {
+    expect(validateRealityClientVer('26.3.27.1')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer('26.3.256')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer('v26.3.27')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer('26..27')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer('26.3.')).toBe('pages.inbounds.form.clientVerInvalid');
+    expect(validateRealityClientVer('-1.0.0')).toBe('pages.inbounds.form.clientVerInvalid');
+  });
+});
+
+describe('validateRealityMaxClientVer', () => {
+  it('accepts an empty max, an empty min, and a valid range', () => {
+    expect(validateRealityMaxClientVer('', '26.3.27')).toBeUndefined();
+    expect(validateRealityMaxClientVer('27.0.0', '')).toBeUndefined();
+    expect(validateRealityMaxClientVer('26.3.27', '26.3.27')).toBeUndefined();
+    expect(validateRealityMaxClientVer('27.1.2', '26.3.27')).toBeUndefined();
+  });
+
+  it('rejects a max below the min, the stale-placeholder trap included', () => {
+    expect(validateRealityMaxClientVer('25.9.11', '26.3.27')).toBe(
+      'pages.inbounds.form.maxClientVerBelowMin',
+    );
+    expect(validateRealityMaxClientVer('26.3.26', '26.3.27')).toBe(
+      'pages.inbounds.form.maxClientVerBelowMin',
+    );
+  });
+
+  it('pads short versions like the core does before comparing', () => {
+    expect(validateRealityMaxClientVer('26', '26.0.0')).toBeUndefined();
+    expect(validateRealityMaxClientVer('26', '26.3')).toBe(
+      'pages.inbounds.form.maxClientVerBelowMin',
+    );
+  });
+
+  it('reports format errors before range errors and skips a malformed min', () => {
+    expect(validateRealityMaxClientVer('25.9', 'not-a-version')).toBeUndefined();
+    expect(validateRealityMaxClientVer('nope', '26.3.27')).toBe(
+      'pages.inbounds.form.clientVerInvalid',
+    );
+  });
+});
+
 describe('normalizeXhttpForWire stream-one', () => {
   it('drops packet-up and stream-up-only fields on inbound', () => {
     const out = normalizeXhttpForWire({

+ 2 - 0
internal/web/translation/ar-EG.json

@@ -638,6 +638,8 @@
         "maxClientVer": "أقصى إصدار للعميل",
         "minClientVerHint": "تركه فارغًا لا يعني بلا قيود: سيفرض Xray-core الحد الأدنى المدمج في إصدار النواة الذي تشغّله (26.3.27 في الإصدارات الحالية) ويرفض العملاء الذين يبلغون عن إصدار أقدم — بما في ذلك النوى الخارجية مثل Mihomo و sing-box. القيمة 1.0.0 تقبلها، مقابل السماح ببصمات TLS قديمة.",
         "maxClientVerHint": "تركه فارغًا يعني بلا حد أقصى. إذا عُيّن، يجب ألا يقل عن الحد الأدنى الفعلي — أدنى إصدار للعميل، أو الحد الأدنى المدمج في Xray-core عندما يكون ذلك الحقل فارغًا — وإلا سيُرفض جميع العملاء.",
+        "clientVerInvalid": "يجب أن يتكون إصدار العميل من ثلاثة أرقام كحد أقصى مفصولة بنقاط، كل منها 0-255 (مثل 26.3.27)",
+        "maxClientVerBelowMin": "أقصى إصدار للعميل يجب ألا يقل عن أدنى إصدار للعميل",
         "shortIds": "Short IDs",
         "realityTargetHint": "مطلوب. يجب أن يتضمّن منفذًا (مثل example.com:443). بدون منفذ يرفض Xray-core البدء.",
         "realityTargetRequired": "هدف REALITY مطلوب",

+ 2 - 0
internal/web/translation/en-US.json

@@ -650,6 +650,8 @@
         "maxClientVer": "Max Client Ver",
         "minClientVerHint": "Empty does not mean unrestricted: Xray-core then enforces the built-in minimum of the core build you run (26.3.27 in current releases) and rejects clients that report an older version — including third-party cores such as Mihomo and sing-box. Set 1.0.0 to accept them, at the cost of admitting outdated TLS fingerprints.",
         "maxClientVerHint": "Empty means no upper limit. If set, it must not be lower than the effective minimum — Min Client Ver, or Xray-core's built-in minimum when that field is empty — otherwise every client is rejected.",
+        "clientVerInvalid": "Client version must be up to three dot-separated numbers, each 0-255 (e.g. 26.3.27)",
+        "maxClientVerBelowMin": "Max Client Ver must not be lower than Min Client Ver",
         "shortIds": "Short IDs",
         "realityTargetHint": "Required. Must include a port (e.g. example.com:443). Without a port Xray-core refuses to start.",
         "realityTargetRequired": "REALITY target is required",

+ 2 - 0
internal/web/translation/es-ES.json

@@ -659,6 +659,8 @@
         "maxClientVer": "Máx. versión cliente",
         "minClientVerHint": "Vacío no significa sin restricción: Xray-core aplica entonces el mínimo integrado de la build del núcleo en uso (26.3.27 en las versiones actuales) y rechaza a los clientes que reportan una versión anterior, incluidos núcleos de terceros como Mihomo y sing-box. Con 1.0.0 se aceptan, a costa de admitir huellas TLS obsoletas.",
         "maxClientVerHint": "Vacío significa sin límite superior. Si se establece, no debe ser inferior al mínimo efectivo — la versión mínima del cliente o, si ese campo está vacío, el mínimo integrado de Xray-core — o todos los clientes serán rechazados.",
+        "clientVerInvalid": "La versión del cliente debe tener hasta tres números separados por puntos, cada uno 0-255 (p. ej. 26.3.27)",
+        "maxClientVerBelowMin": "La versión máxima del cliente no debe ser inferior a la versión mínima",
         "shortIds": "Short IDs",
         "realityTargetHint": "Obligatorio. Debe incluir un puerto (p. ej. example.com:443). Sin puerto, Xray-core no arranca.",
         "realityTargetRequired": "El destino REALITY es obligatorio",

+ 2 - 0
internal/web/translation/fa-IR.json

@@ -650,6 +650,8 @@
         "maxClientVer": "حداکثر نسخه کلاینت",
         "minClientVerHint": "خالی بودن به معنای بدون محدودیت نیست: در این حالت Xray-core حداقل داخلیِ نسخهٔ هسته‌ای را که اجرا می‌کنید (در نسخه‌های فعلی 26.3.27) اعمال می‌کند و کلاینت‌هایی را که نسخهٔ قدیمی‌تری اعلام می‌کنند رد می‌کند — از جمله هسته‌های شخص ثالث مانند Mihomo و sing-box. مقدار 1.0.0 آن‌ها را می‌پذیرد، به بهای پذیرش اثر انگشت‌های TLS قدیمی.",
         "maxClientVerHint": "خالی یعنی بدون سقف. در صورت تنظیم، نباید از حداقلِ مؤثر — حداقل نسخه کلاینت، و در صورت خالی بودن آن فیلد، حداقل داخلی Xray-core — کمتر باشد، وگرنه همهٔ کلاینت‌ها رد می‌شوند.",
+        "clientVerInvalid": "نسخهٔ کلاینت باید حداکثر سه عدد جداشده با نقطه باشد، هر یک 0-255 (مثلاً 26.3.27)",
+        "maxClientVerBelowMin": "حداکثر نسخهٔ کلاینت نباید از حداقل نسخهٔ کلاینت کمتر باشد",
         "shortIds": "Short IDها",
         "realityTargetHint": "الزامی است. باید شامل پورت باشد (مثلاً example.com:443). بدون پورت، Xray-core اجرا نمی‌شود.",
         "realityTargetRequired": "هدف REALITY الزامی است",

+ 2 - 0
internal/web/translation/id-ID.json

@@ -638,6 +638,8 @@
         "maxClientVer": "Maks. versi klien",
         "minClientVerHint": "Kosong bukan berarti tanpa batas: Xray-core akan memakai minimum bawaan dari build core yang dijalankan (26.3.27 pada rilis saat ini) dan menolak klien yang melaporkan versi lebih lama — termasuk core pihak ketiga seperti Mihomo dan sing-box. Isi 1.0.0 untuk menerimanya, dengan risiko mengizinkan sidik jari TLS yang usang.",
         "maxClientVerHint": "Kosong berarti tanpa batas atas. Jika diisi, tidak boleh lebih rendah dari minimum efektif — versi klien minimum, atau minimum bawaan Xray-core saat kolom itu kosong — atau semua klien akan ditolak.",
+        "clientVerInvalid": "Versi klien harus berupa maksimal tiga angka dipisah titik, masing-masing 0-255 (mis. 26.3.27)",
+        "maxClientVerBelowMin": "Versi klien maksimum tidak boleh lebih rendah dari versi klien minimum",
         "shortIds": "Short IDs",
         "realityTargetHint": "Wajib. Harus menyertakan port (mis. example.com:443). Tanpa port, Xray-core menolak untuk mulai.",
         "realityTargetRequired": "Target REALITY wajib diisi",

+ 2 - 0
internal/web/translation/ja-JP.json

@@ -659,6 +659,8 @@
         "maxClientVer": "最大クライアントバージョン",
         "minClientVerHint": "空欄は無制限ではありません。Xray-core は実行中のコアに組み込まれた最低バージョン(現行リリースでは 26.3.27)を適用し、それより古いバージョンを名乗るクライアント(Mihomo や sing-box などのサードパーティコアを含む)を拒否します。1.0.0 を設定すると許可されますが、古い TLS フィンガープリントも受け入れることになります。",
         "maxClientVerHint": "空欄は上限なしを意味します。設定する場合は実効的な下限(最小クライアントバージョン。その欄が空欄の場合は Xray-core 組み込みの最低バージョン)を下回らないでください。下回るとすべてのクライアントが拒否されます。",
+        "clientVerInvalid": "クライアントバージョンはドット区切りの数値(最大 3 つ、各 0-255)で指定してください(例:26.3.27)",
+        "maxClientVerBelowMin": "最大クライアントバージョンは最小クライアントバージョンを下回れません",
         "shortIds": "Short IDs",
         "realityTargetHint": "必須です。ポートを含める必要があります(例: example.com:443)。ポートがないと Xray-core は起動しません。",
         "realityTargetRequired": "REALITY ターゲットは必須です",

+ 2 - 0
internal/web/translation/pt-BR.json

@@ -659,6 +659,8 @@
         "maxClientVer": "Máx. versão cliente",
         "minClientVerHint": "Vazio não significa sem restrição: o Xray-core aplica o mínimo embutido da build do núcleo em uso (26.3.27 nas versões atuais) e rejeita clientes que reportam uma versão mais antiga — incluindo núcleos de terceiros como Mihomo e sing-box. Definir 1.0.0 os aceita, ao custo de admitir impressões digitais TLS desatualizadas.",
         "maxClientVerHint": "Vazio significa sem limite superior. Se definido, não deve ser menor que o mínimo efetivo — a versão mínima do cliente ou, se aquele campo estiver vazio, o mínimo embutido do Xray-core — ou todos os clientes serão rejeitados.",
+        "clientVerInvalid": "A versão do cliente deve ter até três números separados por pontos, cada um 0-255 (ex.: 26.3.27)",
+        "maxClientVerBelowMin": "A versão máxima do cliente não deve ser menor que a versão mínima",
         "shortIds": "Short IDs",
         "realityTargetHint": "Obrigatório. Deve incluir uma porta (ex.: example.com:443). Sem porta, o Xray-core não inicia.",
         "realityTargetRequired": "O alvo REALITY é obrigatório",

+ 2 - 0
internal/web/translation/ru-RU.json

@@ -659,6 +659,8 @@
         "maxClientVer": "Макс. версия клиента",
         "minClientVerHint": "Пустое поле не означает «без ограничений»: Xray-core применит встроенный минимум используемой сборки ядра (26.3.27 в текущих релизах) и отклонит клиентов, сообщающих более старую версию, — включая сторонние ядра, такие как Mihomo и sing-box. Значение 1.0.0 разрешит их, но допустит устаревшие TLS-отпечатки.",
         "maxClientVerHint": "Пустое поле — без верхнего предела. Если задано, значение не должно быть ниже действующего минимума — «Мин. версия клиента», а при пустом том поле — встроенного минимума Xray-core, иначе все клиенты будут отклонены.",
+        "clientVerInvalid": "Версия клиента — до трёх чисел через точку, каждое 0-255 (например 26.3.27)",
+        "maxClientVerBelowMin": "Макс. версия клиента не должна быть ниже минимальной версии клиента",
         "shortIds": "Short IDs",
         "realityTargetHint": "Обязательно. Должно содержать порт (например, example.com:443). Без порта Xray-core не запускается.",
         "realityTargetRequired": "Цель REALITY обязательна",

+ 2 - 0
internal/web/translation/tr-TR.json

@@ -638,6 +638,8 @@
         "maxClientVer": "Maks. Kullanıcı Sürümü",
         "minClientVerHint": "Boş bırakmak sınırsız demek değildir: Xray-core, çalıştırdığınız çekirdek sürümünün yerleşik alt sınırını (güncel sürümlerde 26.3.27) uygular ve daha eski sürüm bildiren istemcileri reddeder — Mihomo ve sing-box gibi üçüncü taraf çekirdekler dahil. 1.0.0 girmek onları kabul eder; bedeli eski TLS parmak izlerine izin vermektir.",
         "maxClientVerHint": "Boş, üst sınır yok demektir. Ayarlanırsa geçerli alt sınırın — Min. Kullanıcı Sürümü, o alan boşsa Xray-core'un yerleşik alt sınırı — altında olmamalıdır, aksi halde tüm istemciler reddedilir.",
+        "clientVerInvalid": "İstemci sürümü noktayla ayrılmış en fazla üç sayıdan oluşmalıdır, her biri 0-255 (örn. 26.3.27)",
+        "maxClientVerBelowMin": "Maks. istemci sürümü, en düşük istemci sürümünün altında olamaz",
         "shortIds": "Short IDs",
         "realityTargetHint": "Zorunlu. Bir port içermelidir (ör. example.com:443). Port belirtilmezse Xray-core başlamaz.",
         "realityTargetRequired": "REALITY hedefi zorunludur",

+ 2 - 0
internal/web/translation/uk-UA.json

@@ -638,6 +638,8 @@
         "maxClientVer": "Макс. версія клієнта",
         "minClientVerHint": "Порожнє поле не означає «без обмежень»: Xray-core застосує вбудований мінімум використовуваної збірки ядра (26.3.27 у поточних релізах) і відхилятиме клієнтів зі старішою версією — зокрема сторонні ядра, як-от Mihomo та sing-box. Значення 1.0.0 дозволить їх, але допустить застарілі TLS-відбитки.",
         "maxClientVerHint": "Порожнє поле — без верхньої межі. Якщо задано, значення не має бути нижчим за чинний мінімум — «Мін. версія клієнта», а коли те поле порожнє — вбудований мінімум Xray-core, інакше всіх клієнтів буде відхилено.",
+        "clientVerInvalid": "Версія клієнта — до трьох чисел через крапку, кожне 0-255 (наприклад 26.3.27)",
+        "maxClientVerBelowMin": "Макс. версія клієнта не має бути нижчою за мінімальну версію клієнта",
         "shortIds": "Short IDs",
         "realityTargetHint": "Обов'язково. Має містити порт (напр., example.com:443). Без порту Xray-core не запускається.",
         "realityTargetRequired": "Ціль REALITY обов'язкова",

+ 2 - 0
internal/web/translation/vi-VN.json

@@ -659,6 +659,8 @@
         "maxClientVer": "Phiên bản client tối đa",
         "minClientVerHint": "Để trống không có nghĩa là không giới hạn: Xray-core sẽ áp dụng mức tối thiểu tích hợp của bản core đang chạy (26.3.27 ở các bản phát hành hiện tại) và từ chối các client khai báo phiên bản cũ hơn — bao gồm các core bên thứ ba như Mihomo và sing-box. Đặt 1.0.0 để chấp nhận chúng, đổi lại là cho phép các dấu vân tay TLS lỗi thời.",
         "maxClientVerHint": "Để trống nghĩa là không có giới hạn trên. Nếu đặt, không được thấp hơn mức tối thiểu đang có hiệu lực — phiên bản client tối thiểu, hoặc mức tối thiểu tích hợp của Xray-core khi ô đó để trống — nếu không mọi client đều bị từ chối.",
+        "clientVerInvalid": "Phiên bản client phải gồm tối đa ba số cách nhau bằng dấu chấm, mỗi số 0-255 (ví dụ 26.3.27)",
+        "maxClientVerBelowMin": "Phiên bản client tối đa không được thấp hơn phiên bản client tối thiểu",
         "shortIds": "Short IDs",
         "realityTargetHint": "Bắt buộc. Phải bao gồm cổng (ví dụ example.com:443). Không có cổng, Xray-core sẽ không khởi động.",
         "realityTargetRequired": "Mục tiêu REALITY là bắt buộc",

+ 2 - 0
internal/web/translation/zh-CN.json

@@ -658,6 +658,8 @@
         "maxClientVer": "最大客户端版本",
         "minClientVerHint": "留空不等于不限制:Xray-core 会改用所运行内核版本的内置最低值(当前版本为 26.3.27),拒绝自报版本更低的客户端——包括 Mihomo、sing-box 等第三方内核。填 1.0.0 可放行它们,代价是允许过时的 TLS 指纹。",
         "maxClientVerHint": "留空表示无上限。若填写,不得低于实际生效的下限——最小客户端版本,该字段留空时则为 Xray-core 的内置最低值——否则所有客户端都会被拒绝。",
+        "clientVerInvalid": "客户端版本须为最多三段以点分隔的数字,每段 0-255(例如 26.3.27)",
+        "maxClientVerBelowMin": "最大客户端版本不得低于最小客户端版本",
         "shortIds": "Short IDs",
         "realityTargetHint": "必填。必须包含端口(例如 example.com:443)。没有端口时 Xray-core 将无法启动。",
         "realityTargetRequired": "REALITY 目标为必填项",

+ 2 - 0
internal/web/translation/zh-TW.json

@@ -638,6 +638,8 @@
         "maxClientVer": "最大客戶端版本",
         "minClientVerHint": "留空不等於不限制:Xray-core 會改用所執行核心版本的內建最低值(目前版本為 26.3.27),拒絕自報版本較低的客戶端——包括 Mihomo、sing-box 等第三方核心。填 1.0.0 可放行它們,代價是允許過時的 TLS 指紋。",
         "maxClientVerHint": "留空表示無上限。若填寫,不得低於實際生效的下限——最小客戶端版本,該欄位留空時則為 Xray-core 的內建最低值——否則所有客戶端都會被拒絕。",
+        "clientVerInvalid": "客戶端版本須為最多三段以點分隔的數字,每段 0-255(例如 26.3.27)",
+        "maxClientVerBelowMin": "最大客戶端版本不得低於最小客戶端版本",
         "shortIds": "Short IDs",
         "realityTargetHint": "必填。必須包含連接埠(例如 example.com:443)。沒有連接埠時 Xray-core 將無法啟動。",
         "realityTargetRequired": "REALITY 目標為必填項",