feat(tls): surface pinnedPeerCertSha256 in panel, share links, and subs

Adds a panel-only `pinnedPeerCertSha256` field on TLS settings with a tags input and a random-hash generator. The hashes ride share links as `pcs` (v2rayN-compatible), Clash sub as `pin-sha256`, and JSON sub as `pinnedPeerCertSha256`, while remaining stripped from the run-config sent to xray-core.
This commit is contained in:
MHSanaei
2026-05-28 19:32:10 +02:00
parent c5b5606bf5
commit 3f0b7fbe97
23 changed files with 320 additions and 1 deletions

View File

@@ -225,6 +225,9 @@ export function genVmessLink(input: GenVmessLinkInput): string {
if (tlsSettings.serverName.length > 0) obj.sni = tlsSettings.serverName;
if (tlsSettings.settings.fingerprint.length > 0) obj.fp = tlsSettings.settings.fingerprint;
if (tlsSettings.alpn.length > 0) obj.alpn = tlsSettings.alpn.join(',');
if (tlsSettings.settings.pinnedPeerCertSha256.length > 0) {
obj.pcs = tlsSettings.settings.pinnedPeerCertSha256.join(',');
}
}
applyExternalProxyTLSObj(externalProxy, obj, tls);
@@ -349,6 +352,9 @@ export function genVlessLink(input: GenVlessLinkInput): string {
params.set('alpn', tls.alpn.join(','));
if (tls.serverName.length > 0) params.set('sni', tls.serverName);
if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
if (tls.settings.pinnedPeerCertSha256.length > 0) {
params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
}
if (stream.network === 'tcp' && flow.length > 0) params.set('flow', flow);
}
applyExternalProxyTLSParams(externalProxy, params, security);
@@ -428,6 +434,9 @@ function writeTlsParams(stream: NonNullable<Inbound['streamSettings']>, params:
params.set('alpn', tls.alpn.join(','));
if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
if (tls.serverName.length > 0) params.set('sni', tls.serverName);
if (tls.settings.pinnedPeerCertSha256.length > 0) {
params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
}
}
// Reality query-string writer shared by VLESS and Trojan. Preserves the

View File

@@ -572,6 +572,21 @@ export default function InboundFormModal({
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
};
const generateRandomPinHash = () => {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b);
const hash = btoa(binary);
const current = (form.getFieldValue(
['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
) as string[] | undefined) ?? [];
form.setFieldValue(
['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
[...current, hash],
);
};
const setCertFromPanel = async (certName: number) => {
setSaving(true);
try {
@@ -2826,6 +2841,29 @@ export default function InboundFormModal({
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
>
<Space.Compact block>
<Form.Item
name={['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256']}
noStyle
>
<Select
mode="tags"
tokenSeparators={[',', ' ']}
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
style={{ width: 'calc(100% - 32px)' }}
/>
</Form.Item>
<Button
icon={<ReloadOutlined />}
onClick={generateRandomPinHash}
title={t('pages.inbounds.form.generateRandomPin')}
/>
</Space.Compact>
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={getNewEchCert}>

View File

@@ -51,6 +51,7 @@ export type TlsCert = z.infer<typeof TlsCertSchema>;
export const TlsClientSettingsSchema = z.object({
fingerprint: UtlsFingerprintSchema.default('chrome'),
echConfigList: z.string().default(''),
pinnedPeerCertSha256: z.array(z.string()).default([]),
});
export type TlsClientSettings = z.infer<typeof TlsClientSettingsSchema>;
@@ -67,6 +68,6 @@ export const TlsStreamSettingsSchema = z.object({
certificates: z.array(TlsCertSchema).default([]),
alpn: z.array(AlpnSchema).default(['h2', 'http/1.1']),
echServerKeys: z.string().default(''),
settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '' }),
settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '', pinnedPeerCertSha256: [] }),
});
export type TlsStreamSettings = z.infer<typeof TlsStreamSettingsSchema>;

View File

@@ -70,6 +70,7 @@ exports[`InboundSchema (full) fixtures > parses hysteria-v1-tls byte-stably 1`]
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [],
},
},
},
@@ -207,6 +208,7 @@ exports[`InboundSchema (full) fixtures > parses trojan-ws-tls byte-stably 1`] =
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [],
},
},
"wsSettings": {
@@ -378,6 +380,7 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [],
},
},
"wsSettings": {
@@ -394,6 +397,97 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
}
`;
exports[`InboundSchema (full) fixtures > parses vless-ws-tls-pinned byte-stably 1`] = `
{
"down": 0,
"enable": true,
"expiryTime": 0,
"id": 43,
"listen": "",
"port": 443,
"protocol": "vless",
"remark": "alice-vless-ws-tls-pinned",
"settings": {
"clients": [
{
"comment": "",
"email": "alice@example.test",
"enable": true,
"expiryTime": 0,
"flow": "",
"id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
"limitIp": 0,
"reset": 0,
"subId": "abc123def",
"tgId": 0,
"totalGB": 0,
},
],
"decryption": "none",
"encryption": "none",
"fallbacks": [],
},
"sniffing": {
"destOverride": [
"http",
"tls",
"quic",
"fakedns",
],
"domainsExcluded": [],
"enabled": true,
"ipsExcluded": [],
"metadataOnly": false,
"routeOnly": false,
},
"streamSettings": {
"network": "ws",
"security": "tls",
"tlsSettings": {
"alpn": [
"h2",
"http/1.1",
],
"certificates": [
{
"buildChain": false,
"certificateFile": "/etc/ssl/certs/cdn.example.test.crt",
"keyFile": "/etc/ssl/private/cdn.example.test.key",
"oneTimeLoading": false,
"usage": "encipherment",
},
],
"cipherSuites": "",
"disableSystemRoot": false,
"echServerKeys": "",
"enableSessionResumption": false,
"maxVersion": "1.3",
"minVersion": "1.2",
"rejectUnknownSni": false,
"serverName": "cdn.example.test",
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",
],
},
},
"wsSettings": {
"acceptProxyProtocol": false,
"headers": {},
"heartbeatPeriod": 0,
"host": "cdn.example.test",
"path": "/ws",
},
},
"tag": "inbound-vless-pinned-1",
"total": 0,
"up": 0,
}
`;
exports[`InboundSchema (full) fixtures > parses vmess-tcp-tls byte-stably 1`] = `
{
"down": 0,
@@ -468,6 +562,7 @@ exports[`InboundSchema (full) fixtures > parses vmess-tcp-tls byte-stably 1`] =
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [],
},
},
},

View File

@@ -12,6 +12,8 @@ exports[`genInboundLinks orchestrator > vless-tcp-reality: byte-stable 1`] = `"v
exports[`genInboundLinks orchestrator > vless-ws-tls: byte-stable 1`] = `"vless://8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02@override.test:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`;
exports[`genInboundLinks orchestrator > vless-ws-tls-pinned: byte-stable 1`] = `"vless://8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02@override.test:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test&pcs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D%2CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB%3D#parity-test"`;
exports[`genInboundLinks orchestrator > vmess-tcp-tls: byte-stable 1`] = `"vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJwYXJpdHktdGVzdCIsCiAgImFkZCI6ICJvdmVycmlkZS50ZXN0IiwKICAicG9ydCI6IDg0NDMsCiAgImlkIjogIjExMTExMTExLTIyMjItNDMzMy04NDQ0LTU1NTU1NTU1NTU1NSIsCiAgInNjeSI6ICJhdXRvIiwKICAibmV0IjogInRjcCIsCiAgInRscyI6ICJ0bHMiLAogICJ0eXBlIjogIm5vbmUiLAogICJzbmkiOiAidm1lc3MuZXhhbXBsZS50ZXN0IiwKICAiZnAiOiAiY2hyb21lIiwKICAiYWxwbiI6ICJoMixodHRwLzEuMSIKfQ=="`;
exports[`genInboundLinks orchestrator > wireguard-server: byte-stable 1`] = `
@@ -38,6 +40,8 @@ exports[`genVlessLink > vless-tcp-reality: byte-stable 1`] = `"vless://22222222-
exports[`genVlessLink > vless-ws-tls: byte-stable 1`] = `"vless://8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02@example.test:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`;
exports[`genVlessLink > vless-ws-tls-pinned: byte-stable 1`] = `"vless://8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02@example.test:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test&pcs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D%2CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB%3D#parity-test"`;
exports[`genVmessLink > vmess-tcp-tls: byte-stable 1`] = `"vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJwYXJpdHktdGVzdCIsCiAgImFkZCI6ICJleGFtcGxlLnRlc3QiLAogICJwb3J0IjogODQ0MywKICAiaWQiOiAiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwKICAic2N5IjogImF1dG8iLAogICJuZXQiOiAidGNwIiwKICAidGxzIjogInRscyIsCiAgInR5cGUiOiAibm9uZSIsCiAgInNuaSI6ICJ2bWVzcy5leGFtcGxlLnRlc3QiLAogICJmcCI6ICJjaHJvbWUiLAogICJhbHBuIjogImgyLGh0dHAvMS4xIgp9"`;
exports[`genWireguardLink + genWireguardConfig > wireguard-server: byte-stable 1`] = `

View File

@@ -66,6 +66,7 @@ exports[`SecuritySettingsSchema fixtures > parses tls-cert-file byte-stably 1`]
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [],
},
},
}

View File

@@ -0,0 +1,80 @@
{
"id": 43,
"up": 0,
"down": 0,
"total": 0,
"remark": "alice-vless-ws-tls-pinned",
"enable": true,
"expiryTime": 0,
"listen": "",
"port": 443,
"tag": "inbound-vless-pinned-1",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic", "fakedns"],
"metadataOnly": false,
"routeOnly": false,
"ipsExcluded": [],
"domainsExcluded": []
},
"protocol": "vless",
"settings": {
"clients": [
{
"id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
"email": "alice@example.test",
"flow": "",
"limitIp": 0,
"totalGB": 0,
"expiryTime": 0,
"enable": true,
"tgId": 0,
"subId": "abc123def",
"comment": "",
"reset": 0
}
],
"decryption": "none",
"encryption": "none",
"fallbacks": []
},
"streamSettings": {
"network": "ws",
"wsSettings": {
"acceptProxyProtocol": false,
"path": "/ws",
"host": "cdn.example.test",
"headers": {},
"heartbeatPeriod": 0
},
"security": "tls",
"tlsSettings": {
"serverName": "cdn.example.test",
"minVersion": "1.2",
"maxVersion": "1.3",
"cipherSuites": "",
"rejectUnknownSni": false,
"disableSystemRoot": false,
"enableSessionResumption": false,
"certificates": [
{
"certificateFile": "/etc/ssl/certs/cdn.example.test.crt",
"keyFile": "/etc/ssl/private/cdn.example.test.key",
"oneTimeLoading": false,
"usage": "encipherment",
"buildChain": false
}
],
"alpn": ["h2", "http/1.1"],
"echServerKeys": "",
"settings": {
"fingerprint": "chrome",
"echConfigList": "",
"pinnedPeerCertSha256": [
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
]
}
}
}
}

View File

@@ -482,6 +482,9 @@ func (s *SubClashService) tlsData(tData map[string]any) map[string]any {
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
tlsData["fingerprint"] = fingerprint
}
if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
tlsData["pin-sha256"] = pins
}
return tlsData
}

View File

@@ -272,6 +272,9 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
tlsData["fingerprint"] = fingerprint
}
if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
tlsData["pinnedPeerCertSha256"] = pins
}
return tlsData
}

View File

@@ -809,6 +809,9 @@ func applyShareTLSParams(stream map[string]any, params map[string]string) {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if pins, ok := pinnedSha256List(tlsSettings); ok {
params["pcs"] = strings.Join(pins, ",")
}
}
}
@@ -831,9 +834,39 @@ func applyVmessTLSParams(stream map[string]any, obj map[string]any) {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
obj["fp"], _ = fpValue.(string)
}
if pins, ok := pinnedSha256List(tlsSettings); ok {
obj["pcs"] = strings.Join(pins, ",")
}
}
}
// pinnedSha256List extracts tlsSettings.settings.pinnedPeerCertSha256 as a
// []string. The field is panel-only (stripped before the run-config reaches
// xray-core via web/service/xray.go) but flows into share links so clients
// can pin the server's certificate hash.
func pinnedSha256List(tlsClientSettings any) ([]string, bool) {
raw, ok := searchKey(tlsClientSettings, "pinnedPeerCertSha256")
if !ok {
return nil, false
}
arr, ok := raw.([]any)
if !ok || len(arr) == 0 {
return nil, false
}
out := make([]string, 0, len(arr))
for _, v := range arr {
s, ok := v.(string)
if !ok || s == "" {
continue
}
out = append(out, s)
}
if len(out) == 0 {
return nil, false
}
return out, true
}
func applyShareRealityParams(stream map[string]any, params map[string]string) {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]any)

View File

@@ -536,6 +536,10 @@
"buildChain": "بناء السلسلة",
"echKey": "ECH key",
"echConfig": "تكوين ECH",
"pinnedPeerCertSha256": "SHA-256 لشهادة النظير المثبَّتة",
"pinnedPeerCertSha256Tip": "تجزئات SHA-256 المُرمَّزة بـ Base64 لشهادة النظير. للوحة فقط — لا تُكتب في إعدادات xray على الخادم، لكنها تُضمَّن في روابط المشاركة ليتمكَّن العملاء من تثبيت الشهادة.",
"pinnedPeerCertSha256Placeholder": "تجزئة (تجزئات) base64، مفصولة بفواصل",
"generateRandomPin": "إنشاء تجزئة عشوائية",
"getNewEchCert": "احصل على شهادة ECH جديدة",
"show": "عرض",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "Pinned Peer Cert SHA-256",
"pinnedPeerCertSha256Tip": "Base64-encoded SHA-256 hashes of the peer certificate. Panel-only — not written to the server's xray config, but included in share links so clients can pin the certificate.",
"pinnedPeerCertSha256Placeholder": "base64 hash(es), comma-separated",
"generateRandomPin": "Generate random hash",
"getNewEchCert": "Get New ECH Cert",
"show": "Show",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Construir cadena",
"echKey": "ECH key",
"echConfig": "Config ECH",
"pinnedPeerCertSha256": "SHA-256 del cert. del par fijado",
"pinnedPeerCertSha256Tip": "Hashes SHA-256 codificados en Base64 del certificado del par. Solo en el panel — no se escribe en la config xray del servidor, pero se incluye en los enlaces para que los clientes puedan fijar el certificado.",
"pinnedPeerCertSha256Placeholder": "hash(es) base64, separados por comas",
"generateRandomPin": "Generar hash aleatorio",
"getNewEchCert": "Obtener nuevo cert ECH",
"show": "Mostrar",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "ساخت زنجیره",
"echKey": "کلید ECH",
"echConfig": "پیکربندی ECH",
"pinnedPeerCertSha256": "SHA-256 پین‌شدهٔ گواهی همتا",
"pinnedPeerCertSha256Tip": "هش‌های SHA-256 با کدگذاری Base64 از گواهی همتا. فقط در پنل — در پیکربندی xray سرور نوشته نمی‌شود، اما در لینک‌های اشتراک‌گذاری گنجانده می‌شود تا کلاینت‌ها بتوانند گواهی را پین کنند.",
"pinnedPeerCertSha256Placeholder": "هش(های) base64، با کاما جدا شوند",
"generateRandomPin": "تولید هش تصادفی",
"getNewEchCert": "دریافت گواهی ECH جدید",
"show": "نمایش",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Bangun rantai",
"echKey": "ECH key",
"echConfig": "Konfig ECH",
"pinnedPeerCertSha256": "SHA-256 Sertifikat Peer Tersemat",
"pinnedPeerCertSha256Tip": "Hash SHA-256 berenkode Base64 dari sertifikat peer. Hanya panel — tidak ditulis ke konfig xray server, tetapi disertakan dalam link berbagi agar klien dapat menyematkan sertifikat.",
"pinnedPeerCertSha256Placeholder": "hash base64, dipisah koma",
"generateRandomPin": "Hasilkan hash acak",
"getNewEchCert": "Dapatkan sertifikat ECH baru",
"show": "Tampilkan",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "ピン留めピア証明書 SHA-256",
"pinnedPeerCertSha256Tip": "ピア証明書の Base64 エンコード SHA-256 ハッシュ。パネルのみ — サーバーの xray 設定には書き込まれませんが、共有リンクには含まれ、クライアントが証明書をピン留めできます。",
"pinnedPeerCertSha256Placeholder": "Base64 ハッシュ、カンマ区切り",
"generateRandomPin": "ランダムハッシュを生成",
"getNewEchCert": "新しい ECH 証明書を取得",
"show": "表示",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Construir cadeia",
"echKey": "ECH key",
"echConfig": "Config ECH",
"pinnedPeerCertSha256": "SHA-256 do cert. do par fixado",
"pinnedPeerCertSha256Tip": "Hashes SHA-256 codificados em Base64 do certificado do par. Apenas no painel — não é gravado na config xray do servidor, mas é incluído nos links de compartilhamento para que clientes possam fixar o certificado.",
"pinnedPeerCertSha256Placeholder": "hash(es) base64, separados por vírgula",
"generateRandomPin": "Gerar hash aleatório",
"getNewEchCert": "Obter novo certificado ECH",
"show": "Mostrar",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "Закреплённый SHA-256 сертификата пира",
"pinnedPeerCertSha256Tip": "SHA-256-хеши сертификата пира в кодировке Base64. Только для панели — не записывается в конфиг xray сервера, но включается в ссылки-приглашения, чтобы клиенты могли закрепить сертификат.",
"pinnedPeerCertSha256Placeholder": "Base64-хеш(и), через запятую",
"generateRandomPin": "Сгенерировать случайный хеш",
"getNewEchCert": "Получить новый ECH-сертификат",
"show": "Показать",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Zincir oluştur",
"echKey": "ECH key",
"echConfig": "ECH yapılandırması",
"pinnedPeerCertSha256": "Sabitlenmiş Peer Sertifikası SHA-256",
"pinnedPeerCertSha256Tip": "Peer sertifikasının Base64 kodlu SHA-256 hash'leri. Sadece panel — sunucunun xray yapılandırmasına yazılmaz, ancak istemcilerin sertifikayı sabitleyebilmesi için paylaşım bağlantılarına eklenir.",
"pinnedPeerCertSha256Placeholder": "base64 hash(ler), virgülle ayrılmış",
"generateRandomPin": "Rastgele hash üret",
"getNewEchCert": "Yeni ECH sertifikası al",
"show": "Göster",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"pinnedPeerCertSha256": "Закріплений SHA-256 сертифіката пира",
"pinnedPeerCertSha256Tip": "SHA-256-хеші сертифіката пира в кодуванні Base64. Лише панель — не записується в конфіг xray сервера, але додається до посилань спільного доступу, щоб клієнти могли закріпити сертифікат.",
"pinnedPeerCertSha256Placeholder": "Base64-хеш(і), через кому",
"generateRandomPin": "Згенерувати випадковий хеш",
"getNewEchCert": "Отримати новий ECH-сертифікат",
"show": "Показати",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "Tạo chuỗi",
"echKey": "ECH key",
"echConfig": "Cấu hình ECH",
"pinnedPeerCertSha256": "SHA-256 chứng chỉ peer đã ghim",
"pinnedPeerCertSha256Tip": "Hash SHA-256 mã hóa Base64 của chứng chỉ peer. Chỉ panel — không ghi vào cấu hình xray máy chủ, nhưng được đưa vào liên kết chia sẻ để client có thể ghim chứng chỉ.",
"pinnedPeerCertSha256Placeholder": "hash base64, phân tách bằng dấu phẩy",
"generateRandomPin": "Tạo hash ngẫu nhiên",
"getNewEchCert": "Lấy chứng chỉ ECH mới",
"show": "Hiện",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "构建证书链",
"echKey": "ECH key",
"echConfig": "ECH 配置",
"pinnedPeerCertSha256": "固定对端证书 SHA-256",
"pinnedPeerCertSha256Tip": "对端证书的 Base64 编码 SHA-256 哈希。仅面板使用 — 不写入服务器的 xray 配置,但会包含在分享链接中,以便客户端固定证书。",
"pinnedPeerCertSha256Placeholder": "base64 哈希,逗号分隔",
"generateRandomPin": "生成随机哈希",
"getNewEchCert": "获取新 ECH 证书",
"show": "显示",
"xver": "Xver",

View File

@@ -536,6 +536,10 @@
"buildChain": "建立憑證鏈",
"echKey": "ECH key",
"echConfig": "ECH 設定",
"pinnedPeerCertSha256": "釘選對端憑證 SHA-256",
"pinnedPeerCertSha256Tip": "對端憑證的 Base64 編碼 SHA-256 雜湊。僅面板使用 — 不寫入伺服器的 xray 設定,但會包含在分享連結中,以便用戶端釘選憑證。",
"pinnedPeerCertSha256Placeholder": "base64 雜湊,以逗號分隔",
"generateRandomPin": "產生隨機雜湊",
"getNewEchCert": "取得新 ECH 憑證",
"show": "顯示",
"xver": "Xver",