From 3f0b7fbe97050de750672fbcaa8d847d7aba3899 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Thu, 28 May 2026 19:32:10 +0200 Subject: [PATCH] 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. --- frontend/src/lib/xray/inbound-link.ts | 9 ++ .../src/pages/inbounds/InboundFormModal.tsx | 38 ++++++++ .../src/schemas/protocols/security/tls.ts | 3 +- .../__snapshots__/inbound-full.test.ts.snap | 95 +++++++++++++++++++ .../__snapshots__/inbound-link.test.ts.snap | 4 + .../test/__snapshots__/security.test.ts.snap | 1 + .../inbound-full/vless-ws-tls-pinned.json | 80 ++++++++++++++++ sub/subClashService.go | 3 + sub/subJsonService.go | 3 + sub/subService.go | 33 +++++++ web/translation/ar-EG.json | 4 + web/translation/en-US.json | 4 + web/translation/es-ES.json | 4 + web/translation/fa-IR.json | 4 + web/translation/id-ID.json | 4 + web/translation/ja-JP.json | 4 + web/translation/pt-BR.json | 4 + web/translation/ru-RU.json | 4 + web/translation/tr-TR.json | 4 + web/translation/uk-UA.json | 4 + web/translation/vi-VN.json | 4 + web/translation/zh-CN.json | 4 + web/translation/zh-TW.json | 4 + 23 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 frontend/src/test/golden/fixtures/inbound-full/vless-ws-tls-pinned.json diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 7615b56a..7a0fae8a 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -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, 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 diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx index 9713fbbd..c64a3b3a 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/InboundFormModal.tsx @@ -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({ > + + + +