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
+9
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
@@ -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}>
@@ -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>;
@@ -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": [],
},
},
},
@@ -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`] = `
@@ -66,6 +66,7 @@ exports[`SecuritySettingsSchema fixtures > parses tls-cert-file byte-stably 1`]
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
"pinnedPeerCertSha256": [],
},
},
}
@@ -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="
]
}
}
}
}