mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-14 00:26:06 +00:00
fix(hysteria2): emit pinSHA256 as hex in subscriptions, not base64
Hysteria2 clients backed by Xray-core hex-decode the pinSHA256 URI param and crash on the base64 value the panel stores for pinnedPeerCertSha256 (xray-core native TLS format). Normalize each pin to bare lowercase hex when building the Hysteria link, accepting base64, bare hex, and colon-separated openssl fingerprints; values that are neither are passed through untouched. Applied in both the backend subscription generator and the frontend link builder. The pcs share-link and JSON-sub paths keep base64 for their consumers. Fixes #4818.
This commit is contained in:
@@ -578,6 +578,28 @@ export interface GenHysteriaLinkInput {
|
||||
clientAuth: string;
|
||||
}
|
||||
|
||||
// Hysteria2's pinSHA256 must be a 64-char lowercase hex string — Xray-core
|
||||
// clients hex-decode it and crash on a base64 value. The panel stores pins as
|
||||
// base64 (xray-core's native TLS format / the generate button) or hex, either
|
||||
// bare or colon-separated as `openssl x509 -fingerprint -sha256` emits it. Each
|
||||
// entry is coerced to bare hex. Values that are neither a 32-byte hex nor a
|
||||
// 32-byte base64 SHA-256 pass through unchanged.
|
||||
function hysteriaPinHex(pin: string): string {
|
||||
const stripped = pin.trim().replace(/:/g, '');
|
||||
if (/^[0-9a-fA-F]{64}$/.test(stripped)) return stripped.toLowerCase();
|
||||
try {
|
||||
const binary = atob(pin.trim().replace(/-/g, '+').replace(/_/g, '/'));
|
||||
if (binary.length !== 32) return pin;
|
||||
let hex = '';
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
hex += binary.charCodeAt(i).toString(16).padStart(2, '0');
|
||||
}
|
||||
return hex;
|
||||
} catch {
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
|
||||
// Hysteria share link: hysteria://<auth>@<host>:<port>?<query>#<remark>.
|
||||
// The URL scheme is "hysteria2" when settings.version === 2 (hysteria v2
|
||||
// AKA hysteria2), "hysteria" otherwise. Salamander obfuscation pulls its
|
||||
@@ -611,7 +633,7 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string {
|
||||
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('pinSHA256', tls.settings.pinnedPeerCertSha256.join(','));
|
||||
params.set('pinSHA256', tls.settings.pinnedPeerCertSha256.map(hysteriaPinHex).join(','));
|
||||
}
|
||||
|
||||
const udpMasks = stream.finalmask?.udp;
|
||||
|
||||
@@ -158,6 +158,43 @@ describe('genHysteriaLink', () => {
|
||||
expect(link).toContain('mport=20000-50000');
|
||||
expect(link.endsWith('#hop-test')).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes pinSHA256 to hex for base64, raw-hex and colon-hex pins (issue #4818)', () => {
|
||||
const [, raw] = fixtures[0];
|
||||
const base64Pin = 'yEfdI5XQl4wHgLggHEsomosoFZfUfCdfLXfT+W2N6cQ=';
|
||||
const hexPin = '84491c0312d9e70f519ce24659a2ca7d9c4ec59dc86417ece426945e0f939293';
|
||||
const colonPin = 'C8:47:DD:23:95:D0:97:8C:07:80:B8:20:1C:4B:28:9A:8B:28:15:97:D4:7C:27:5F:2D:77:D3:F9:6D:8D:E9:C4';
|
||||
const stream = raw.streamSettings as Record<string, unknown>;
|
||||
const tls = stream.tlsSettings as Record<string, unknown>;
|
||||
const tlsClientSettings = tls.settings as Record<string, unknown>;
|
||||
const withPins = {
|
||||
...raw,
|
||||
streamSettings: {
|
||||
...stream,
|
||||
tlsSettings: {
|
||||
...tls,
|
||||
settings: { ...tlsClientSettings, pinnedPeerCertSha256: [base64Pin, hexPin, colonPin] },
|
||||
},
|
||||
},
|
||||
};
|
||||
const typed = InboundSchema.parse(withPins);
|
||||
const client = (raw.settings as { clients: Array<{ auth: string }> }).clients[0];
|
||||
|
||||
const link = genHysteriaLink({
|
||||
inbound: typed,
|
||||
address: 'example.test',
|
||||
port: typed.port,
|
||||
remark: 'pin-test',
|
||||
clientAuth: client.auth,
|
||||
});
|
||||
|
||||
const pin = new URL(link).searchParams.get('pinSHA256');
|
||||
expect(pin).toBe(
|
||||
'c847dd2395d0978c0780b8201c4b289a8b281597d47c275f2d77d3f96d8de9c4,' +
|
||||
'84491c0312d9e70f519ce24659a2ca7d9c4ec59dc86417ece426945e0f939293,' +
|
||||
'c847dd2395d0978c0780b8201c4b289a8b281597d47c275f2d77d3f96d8de9c4',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('genWireguardLink + genWireguardConfig', () => {
|
||||
|
||||
Reference in New Issue
Block a user