mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-11 07:06:06 +00:00
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:
@@ -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="
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user