From b467d4c676ad16efe98c75967207c0a0a35c50cc Mon Sep 17 00:00:00 2001 From: mrchatam Date: Sat, 12 Sep 2026 12:43:45 +0330 Subject: [PATCH] feat(reality): warn when target cert chain is too small for ML-DSA-65 (#6470) * feat(reality): warn when target cert chain is too small for ML-DSA-65 Expose peer cert-chain DER size from the REALITY scanner and surface a UI warning when ML-DSA-65 is enabled but the chain is under xray-core's 3500-byte minimum, so silent fallback failures are easier to catch. Fixes #5973 * fix(reality): gate scanner ML-DSA tag and sync docs OpenAPI Only warn on short cert chains in the target scanner when ML-DSA-65 is enabled. Copy frontend/public/openapi.json to docs/public/openapi.json and fix oxfmt wrapping in the new test. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> --- docs/public/openapi.json | 8 ++ frontend/public/openapi.json | 8 ++ frontend/src/generated/examples.ts | 1 + frontend/src/generated/schemas.ts | 6 ++ frontend/src/generated/types.ts | 1 + frontend/src/generated/zod.ts | 1 + .../security/RealityTargetScannerModal.tsx | 29 ++++- .../pages/inbounds/form/security/reality.tsx | 36 ++++++- .../test/reality-mldsa-cert-chain.test.tsx | 102 ++++++++++++++++++ internal/web/service/reality_scan.go | 8 +- internal/web/translation/ar-EG.json | 2 + internal/web/translation/en-US.json | 2 + internal/web/translation/es-ES.json | 2 + internal/web/translation/fa-IR.json | 2 + internal/web/translation/id-ID.json | 2 + internal/web/translation/ja-JP.json | 2 + internal/web/translation/pt-BR.json | 2 + internal/web/translation/ru-RU.json | 2 + internal/web/translation/tr-TR.json | 2 + internal/web/translation/uk-UA.json | 2 + internal/web/translation/vi-VN.json | 2 + internal/web/translation/zh-CN.json | 2 + internal/web/translation/zh-TW.json | 2 + 23 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 frontend/src/test/reality-mldsa-cert-chain.test.tsx diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 8b2231561..1295a6c3b 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -3710,6 +3710,11 @@ "example": "h2", "type": "string" }, + "certChainBytes": { + "description": "CertChainBytes is the sum of DER lengths of the presented peer chain.\nxray-core ML-DSA-65 REALITY needs >= 3500 bytes (constant lives in xray-core).", + "example": 3427, + "type": "integer" + }, "certChainValid": { "description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.", "example": true, @@ -3792,6 +3797,7 @@ }, "required": [ "alpn", + "certChainBytes", "certChainValid", "certIssuer", "certSubject", @@ -7240,6 +7246,7 @@ "success": true, "obj": { "alpn": "h2", + "certChainBytes": 3427, "certChainValid": true, "certIssuer": "Google Trust Services", "certSubject": "cloudflare.com", @@ -7319,6 +7326,7 @@ "obj": [ { "alpn": "h2", + "certChainBytes": 3427, "certChainValid": true, "certIssuer": "Google Trust Services", "certSubject": "cloudflare.com", diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 8b2231561..1295a6c3b 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -3710,6 +3710,11 @@ "example": "h2", "type": "string" }, + "certChainBytes": { + "description": "CertChainBytes is the sum of DER lengths of the presented peer chain.\nxray-core ML-DSA-65 REALITY needs >= 3500 bytes (constant lives in xray-core).", + "example": 3427, + "type": "integer" + }, "certChainValid": { "description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.", "example": true, @@ -3792,6 +3797,7 @@ }, "required": [ "alpn", + "certChainBytes", "certChainValid", "certIssuer", "certSubject", @@ -7240,6 +7246,7 @@ "success": true, "obj": { "alpn": "h2", + "certChainBytes": 3427, "certChainValid": true, "certIssuer": "Google Trust Services", "certSubject": "cloudflare.com", @@ -7319,6 +7326,7 @@ "obj": [ { "alpn": "h2", + "certChainBytes": 3427, "certChainValid": true, "certIssuer": "Google Trust Services", "certSubject": "cloudflare.com", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 9814a2270..5a68cf385 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -916,6 +916,7 @@ export const EXAMPLES: Record = { }, "RealityScanResult": { "alpn": "h2", + "certChainBytes": 3427, "certChainValid": true, "certIssuer": "Google Trust Services", "certSubject": "cloudflare.com", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 686713907..e8248d938 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -3684,6 +3684,11 @@ export const SCHEMAS: Record = { "example": "h2", "type": "string" }, + "certChainBytes": { + "description": "CertChainBytes is the sum of DER lengths of the presented peer chain.\nxray-core ML-DSA-65 REALITY needs \u003e= 3500 bytes (constant lives in xray-core).", + "example": 3427, + "type": "integer" + }, "certChainValid": { "description": "CertChainValid ignores the name: a trusted chain presented for other names\nstill has serverNames the panel can offer instead of the failing SNI.", "example": true, @@ -3766,6 +3771,7 @@ export const SCHEMAS: Record = { }, "required": [ "alpn", + "certChainBytes", "certChainValid", "certIssuer", "certSubject", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index d0a60f467..5fcbd5576 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -835,6 +835,7 @@ export interface ProbeResultUI { export interface RealityScanResult { alpn: string; + certChainBytes: number; certChainValid: boolean; certIssuer: string; certSubject: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 2e8811949..46fdd7620 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -892,6 +892,7 @@ export type ProbeResultUI = z.infer; export const RealityScanResultSchema = z.object({ alpn: z.string(), + certChainBytes: z.number().int(), certChainValid: z.boolean(), certIssuer: z.string(), certSubject: z.string(), diff --git a/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx b/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx index b747a999e..30b73a956 100644 --- a/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx +++ b/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx @@ -5,11 +5,15 @@ import type { ColumnsType } from 'antd/es/table'; import type { RealityScanResult } from '@/generated/types'; +// xray-core ML-DSA-65 REALITY min peer cert-chain size (not defined in this repo). +export const MLDSA65_MIN_CERT_CHAIN_BYTES = 3500; + interface RealityTargetScannerModalProps { open: boolean; onClose: () => void; scanRealityCandidates: (targets?: string) => Promise; onPick: (result: RealityScanResult) => void; + mldsa65Enabled?: boolean; } export default function RealityTargetScannerModal({ @@ -17,6 +21,7 @@ export default function RealityTargetScannerModal({ onClose, scanRealityCandidates, onPick, + mldsa65Enabled = false, }: RealityTargetScannerModalProps) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); @@ -126,6 +131,28 @@ export default function RealityTargetScannerModal({ {t('pages.inbounds.form.scanCertInvalid')} ), }, + { + title: t('pages.inbounds.form.scanCertChain'), + dataIndex: 'certChainBytes', + key: 'certChainBytes', + width: 100, + render: (bytes: number) => { + if (!bytes) return '—'; + if (mldsa65Enabled && bytes < MLDSA65_MIN_CERT_CHAIN_BYTES) { + return ( + + {bytes} B + + ); + } + return `${bytes} B`; + }, + }, { title: t('pages.inbounds.form.scanLatency'), dataIndex: 'latencyMs', @@ -165,7 +192,7 @@ export default function RealityTargetScannerModal({ , ]} title={t('pages.inbounds.form.scanModalTitle')} - width={960} + width={1080} > diff --git a/frontend/src/pages/inbounds/form/security/reality.tsx b/frontend/src/pages/inbounds/form/security/reality.tsx index 4b0c0172c..add2590dd 100644 --- a/frontend/src/pages/inbounds/form/security/reality.tsx +++ b/frontend/src/pages/inbounds/form/security/reality.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { Alert, @@ -25,7 +25,9 @@ import { validateRealityTarget, } from '@/lib/xray/stream-wire-normalize'; import type { RealityScanResult } from '@/generated/types'; -import RealityTargetScannerModal from './RealityTargetScannerModal'; +import RealityTargetScannerModal, { + MLDSA65_MIN_CERT_CHAIN_BYTES, +} from './RealityTargetScannerModal'; interface RealityFormProps { saving: boolean; @@ -59,6 +61,18 @@ export default function RealityForm({ const { t } = useTranslation(); const { getFieldState, trigger } = useFormContext(); const [scannerOpen, setScannerOpen] = useState(false); + const mldsa65Seed = useWatch({ name: 'streamSettings.realitySettings.mldsa65Seed' }); + const mldsa65Verify = useWatch({ + name: 'streamSettings.realitySettings.settings.mldsa65Verify', + }); + const mldsa65Enabled = + (typeof mldsa65Seed === 'string' && mldsa65Seed.trim() !== '') || + (typeof mldsa65Verify === 'string' && mldsa65Verify.trim() !== ''); + const mldsaChainTooSmall = + !!scanResult && + mldsa65Enabled && + scanResult.certChainBytes > 0 && + scanResult.certChainBytes < MLDSA65_MIN_CERT_CHAIN_BYTES; /* * An untrusted certificate (self-signed fronting service on the LAN) is still * worth reading, so subject/issuer stay visible and only the verdict is added. @@ -130,7 +144,11 @@ export default function RealityForm({ {scanResult && ( + {mldsaChainTooSmall && ( +
+ {t('pages.inbounds.form.scanMldsaCertChainTooSmall', { + length: scanResult.certChainBytes, + min: MLDSA65_MIN_CERT_CHAIN_BYTES, + })} +
+ )} {scanResult.privateTarget && (
{t('pages.inbounds.form.scanPrivateNote')}
)} @@ -159,6 +185,9 @@ export default function RealityForm({ ? dayjs(scanResult.notAfter).format('YYYY-MM-DD HH:mm') : '—'} + + {scanResult.certChainBytes > 0 ? `${scanResult.certChainBytes} B` : '—'} + {scanResult.latencyMs > 0 ? `${scanResult.latencyMs} ms` : '—'} @@ -330,6 +359,7 @@ export default function RealityForm({ onClose={() => setScannerOpen(false)} scanRealityCandidates={scanRealityCandidates} onPick={(r) => applyRealityScanResult(r, true)} + mldsa65Enabled={mldsa65Enabled} /> ); diff --git a/frontend/src/test/reality-mldsa-cert-chain.test.tsx b/frontend/src/test/reality-mldsa-cert-chain.test.tsx new file mode 100644 index 000000000..7987911bb --- /dev/null +++ b/frontend/src/test/reality-mldsa-cert-chain.test.tsx @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; +import { Form } from 'antd'; +import type { ReactNode } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { RealityForm } from '@/pages/inbounds/form/security'; +import RealityTargetScannerModal from '@/pages/inbounds/form/security/RealityTargetScannerModal'; +import type { InboundFormValues } from '@/schemas/forms/inbound-form'; +import type { RealityScanResult } from '@/generated/types'; +import { renderWithProviders } from './test-utils'; + +const smallChain: RealityScanResult = { + alpn: 'h2', + certChainBytes: 3427, + certChainValid: true, + certIssuer: 'Google Trust Services', + certSubject: 'cloudflare.com', + certValid: true, + curveID: 'X25519', + feasible: true, + h2: true, + host: 'www.cloudflare.com', + ip: '104.16.124.96', + latencyMs: 180, + notAfter: '2026-08-01T00:00:00Z', + port: 443, + privateTarget: false, + reason: '', + serverNames: ['www.cloudflare.com'], + target: 'www.cloudflare.com:443', + tls13: true, + tlsVersion: '1.3', + x25519: true, +}; + +function FormHarness({ + children, + defaultValues, +}: { + children: ReactNode; + defaultValues?: Record; +}) { + const methods = useForm({ defaultValues: defaultValues as never }); + return ( + +
{children}
+
+ ); +} + +const noop = () => {}; + +function renderRealityForm( + scanResult: RealityScanResult | null, + defaultValues?: Record, +) { + return renderWithProviders( + + []} + applyRealityScanResult={noop} + randomizeShortIds={noop} + randomizeSpiderX={noop} + genRealityKeypair={noop} + clearRealityKeypair={noop} + genMldsa65={noop} + clearMldsa65={noop} + /> + , + ); +} + +describe('ML-DSA-65 cert chain warning', () => { + it('warns on the inbound form when ML-DSA-65 is on and the scanned chain is under 3500 bytes', () => { + const { getByText } = renderRealityForm(smallChain, { + streamSettings: { realitySettings: { mldsa65Seed: 'seed' } }, + }); + expect(getByText(/below the 3500-byte minimum required for ML-DSA-65/)).toBeTruthy(); + }); + + it('does not warn on the inbound form when ML-DSA-65 is off', () => { + const { queryByText } = renderRealityForm(smallChain); + expect(queryByText(/below the 3500-byte minimum required for ML-DSA-65/)).toBeNull(); + }); + + it('tags scanner rows whose cert chain is too small for ML-DSA-65', async () => { + const { findByText } = renderWithProviders( + [smallChain]} + onPick={noop} + mldsa65Enabled + />, + ); + expect(await findByText('3427 B')).toBeTruthy(); + }); +}); diff --git a/internal/web/service/reality_scan.go b/internal/web/service/reality_scan.go index 903156508..e32121703 100644 --- a/internal/web/service/reality_scan.go +++ b/internal/web/service/reality_scan.go @@ -57,7 +57,10 @@ type RealityScanResult struct { CertValid bool `json:"certValid" example:"true"` // CertChainValid ignores the name: a trusted chain presented for other names // still has serverNames the panel can offer instead of the failing SNI. - CertChainValid bool `json:"certChainValid" example:"true"` + CertChainValid bool `json:"certChainValid" example:"true"` + // CertChainBytes is the sum of DER lengths of the presented peer chain. + // xray-core ML-DSA-65 REALITY needs >= 3500 bytes (constant lives in xray-core). + CertChainBytes int `json:"certChainBytes" example:"3427"` CertSubject string `json:"certSubject" example:"cloudflare.com"` CertIssuer string `json:"certIssuer" example:"Google Trust Services"` NotAfter string `json:"notAfter" example:"2026-08-01T00:00:00Z"` @@ -253,6 +256,9 @@ func (s *ServerService) probeRealityAddr(dialHost string, port int, sni string, verifyHost := sni if len(st.PeerCertificates) > 0 { leaf := st.PeerCertificates[0] + for _, cert := range st.PeerCertificates { + res.CertChainBytes += len(cert.Raw) + } res.CertSubject = leaf.Subject.CommonName if res.CertSubject == "" && len(leaf.DNSNames) > 0 { res.CertSubject = leaf.DNSNames[0] diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 9930d6ad2..cb08f38ac 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -628,6 +628,8 @@ "scanCert": "الشهادة", "scanCertInvalid": "غير موثوق", "scanCertExpiry": "انتهاء صلاحية الشهادة", + "scanCertChain": "سلسلة الشهادات", + "scanMldsaCertChainTooSmall": "سلسلة شهادات الهدف {length} بايت، وهي أقل من الحد الأدنى {min} بايت المطلوب لـ ML-DSA-65. سيؤدي ذلك إلى فشل الاتصال بصمت. يُرجى اختيار هدف مختلف أو تعطيل ML-DSA-65.", "scanSniUsed": "SNI المستخدم", "scanPrivateNote": "تم الفحص عبر شبكة خاصة/محلية — هذا العنوان غير قابل للوصول من الإنترنت.", "scanPrivateConfirmTitle": "الهدف في شبكة محلية", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index e206fd101..902b3104a 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -642,6 +642,8 @@ "scanCert": "Certificate", "scanCertInvalid": "Not trusted", "scanCertExpiry": "Certificate expires", + "scanCertChain": "Cert chain", + "scanMldsaCertChainTooSmall": "Target certificate chain is {length} bytes, which is below the {min}-byte minimum required for ML-DSA-65. This will cause silent connection failures. Please choose a different target or disable ML-DSA-65.", "scanSniUsed": "SNI used", "scanPrivateNote": "Checked over a private/local network — this address is not reachable from the internet.", "scanPrivateConfirmTitle": "Target on a local network", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 7b46258e9..51af14c25 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -649,6 +649,8 @@ "scanCert": "Certificado", "scanCertInvalid": "No confiable", "scanCertExpiry": "El certificado caduca", + "scanCertChain": "Cadena de certificados", + "scanMldsaCertChainTooSmall": "La cadena de certificados del destino mide {length} bytes, por debajo del mínimo de {min} bytes que requiere ML-DSA-65. Las conexiones fallarán en silencio. Elige otro destino o desactiva ML-DSA-65.", "scanSniUsed": "SNI utilizado", "scanPrivateNote": "Comprobado en una red privada/local: esta dirección no es accesible desde internet.", "scanPrivateConfirmTitle": "Destino en una red local", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 0659378d5..d8e78ad07 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -640,6 +640,8 @@ "scanCert": "گواهی", "scanCertInvalid": "نامعتبر", "scanCertExpiry": "انقضای گواهی", + "scanCertChain": "زنجیره گواهی", + "scanMldsaCertChainTooSmall": "زنجیره گواهی هدف {length} بایت است که کمتر از حداقل {min} بایت مورد نیاز ML-DSA-65 است. این باعث شکست بی‌صدای اتصال می‌شود. هدف دیگری انتخاب کنید یا ML-DSA-65 را غیرفعال کنید.", "scanSniUsed": "SNI استفاده‌شده", "scanPrivateNote": "بررسی از طریق شبکهٔ خصوصی/محلی انجام شد — این نشانی از اینترنت قابل دسترسی نیست.", "scanPrivateConfirmTitle": "هدف در شبکهٔ محلی", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 32578c16d..8776c6df1 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -628,6 +628,8 @@ "scanCert": "Sertifikat", "scanCertInvalid": "Tidak tepercaya", "scanCertExpiry": "Sertifikat kedaluwarsa", + "scanCertChain": "Rantai sertifikat", + "scanMldsaCertChainTooSmall": "Rantai sertifikat target berukuran {length} byte, di bawah minimum {min} byte yang dibutuhkan ML-DSA-65. Ini akan menyebabkan koneksi gagal tanpa pesan error. Pilih target lain atau nonaktifkan ML-DSA-65.", "scanSniUsed": "SNI yang dipakai", "scanPrivateNote": "Diperiksa melalui jaringan privat/lokal — alamat ini tidak dapat dijangkau dari internet.", "scanPrivateConfirmTitle": "Target di jaringan lokal", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 5981cc52f..88bfa681c 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -649,6 +649,8 @@ "scanCert": "証明書", "scanCertInvalid": "信頼できません", "scanCertExpiry": "証明書の有効期限", + "scanCertChain": "証明書チェーン", + "scanMldsaCertChainTooSmall": "ターゲットの証明書チェーンは {length} バイトで、ML-DSA-65 に必要な下限 {min} バイトを下回っています。接続はエラーなく失敗します。別のターゲットを選ぶか ML-DSA-65 を無効にしてください。", "scanSniUsed": "使用した SNI", "scanPrivateNote": "プライベート/ローカルネットワーク経由で確認しました。このアドレスはインターネットからは到達できません。", "scanPrivateConfirmTitle": "ローカルネットワーク上のターゲット", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index d2892fd4c..6020d6c85 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -649,6 +649,8 @@ "scanCert": "Certificado", "scanCertInvalid": "Não confiável", "scanCertExpiry": "Certificado expira", + "scanCertChain": "Cadeia de certificados", + "scanMldsaCertChainTooSmall": "A cadeia de certificados do destino tem {length} bytes, abaixo do mínimo de {min} bytes exigido pelo ML-DSA-65. Isso causará falhas silenciosas de conexão. Escolha outro destino ou desative o ML-DSA-65.", "scanSniUsed": "SNI utilizado", "scanPrivateNote": "Verificado em uma rede privada/local — este endereço não é acessível pela internet.", "scanPrivateConfirmTitle": "Destino em uma rede local", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index cab7fbcec..c37421c14 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -651,6 +651,8 @@ "scanCert": "Сертификат", "scanCertInvalid": "Не доверенный", "scanCertExpiry": "Сертификат истекает", + "scanCertChain": "Цепочка сертификатов", + "scanMldsaCertChainTooSmall": "Цепочка сертификатов цели занимает {length} байт — меньше минимума {min} байт, требуемого для ML-DSA-65. Соединения будут молча уходить в fallback. Выберите другую цель или отключите ML-DSA-65.", "scanSniUsed": "Использованный SNI", "scanPrivateNote": "Проверено во внутренней (локальной) сети — этот адрес недоступен из интернета.", "scanPrivateConfirmTitle": "Цель в локальной сети", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 3386d22af..cb21e49ab 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -628,6 +628,8 @@ "scanCert": "Sertifika", "scanCertInvalid": "Güvenilmez", "scanCertExpiry": "Sertifika bitiş tarihi", + "scanCertChain": "Sertifika zinciri", + "scanMldsaCertChainTooSmall": "Hedef sertifika zinciri {length} bayt; ML-DSA-65 için gereken {min} bayt minimumunun altında. Bu, bağlantıların sessizce başarısız olmasına yol açar. Farklı bir hedef seçin veya ML-DSA-65'i kapatın.", "scanSniUsed": "Kullanılan SNI", "scanPrivateNote": "Özel/yerel ağ üzerinden kontrol edildi — bu adrese internetten erişilemez.", "scanPrivateConfirmTitle": "Hedef yerel ağda", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index db5f485c4..6e2170470 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -628,6 +628,8 @@ "scanCert": "Сертифікат", "scanCertInvalid": "Ненадійний", "scanCertExpiry": "Сертифікат діє до", + "scanCertChain": "Ланцюжок сертифікатів", + "scanMldsaCertChainTooSmall": "Ланцюжок сертифікатів цілі має {length} байт, що менше мінімуму {min} байт для ML-DSA-65. З’єднання мовчки потраплятимуть у fallback. Оберіть іншу ціль або вимкніть ML-DSA-65.", "scanSniUsed": "Використаний SNI", "scanPrivateNote": "Перевірено у внутрішній (локальній) мережі — ця адреса недоступна з інтернету.", "scanPrivateConfirmTitle": "Ціль у локальній мережі", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 016c6ead4..691560ef9 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -649,6 +649,8 @@ "scanCert": "Chứng chỉ", "scanCertInvalid": "Không tin cậy", "scanCertExpiry": "Chứng chỉ hết hạn", + "scanCertChain": "Chuỗi chứng chỉ", + "scanMldsaCertChainTooSmall": "Chuỗi chứng chỉ của đích dài {length} byte, thấp hơn mức tối thiểu {min} byte mà ML-DSA-65 yêu cầu. Kết nối sẽ thất bại im lặng. Hãy chọn đích khác hoặc tắt ML-DSA-65.", "scanSniUsed": "SNI đã dùng", "scanPrivateNote": "Đã kiểm tra qua mạng riêng/nội bộ — địa chỉ này không truy cập được từ internet.", "scanPrivateConfirmTitle": "Đích trong mạng nội bộ", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 3a12333f7..1562ffad2 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -648,6 +648,8 @@ "scanCert": "证书", "scanCertInvalid": "不受信任", "scanCertExpiry": "证书有效期至", + "scanCertChain": "证书链", + "scanMldsaCertChainTooSmall": "目标证书链为 {length} 字节,低于 ML-DSA-65 所需的 {min} 字节下限。这会导致连接静默失败。请更换目标或关闭 ML-DSA-65。", "scanSniUsed": "使用的 SNI", "scanPrivateNote": "已通过内网/本地网络检测 — 该地址无法从互联网访问。", "scanPrivateConfirmTitle": "目标位于本地网络", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 5118ac0fe..fa38613a7 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -628,6 +628,8 @@ "scanCert": "憑證", "scanCertInvalid": "不受信任", "scanCertExpiry": "憑證有效期限", + "scanCertChain": "憑證鏈", + "scanMldsaCertChainTooSmall": "目標憑證鏈為 {length} 位元組,低於 ML-DSA-65 所需的 {min} 位元組下限。這會導致連線靜默失敗。請更換目標或關閉 ML-DSA-65。", "scanSniUsed": "使用的 SNI", "scanPrivateNote": "已透過內網/本機網路檢測 — 此位址無法從網際網路存取。", "scanPrivateConfirmTitle": "目標位於本機網路",