From 803e010921e2e466d75a2c52fb34dd8954d69793 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 1 Jun 2026 19:21:29 +0200 Subject: [PATCH] fix(outbound): carry ALPN, fingerprint and UDP mask when importing a Hysteria2 link (#4760) parseHysteria2Link hardcoded alpn to h3 and never read fp, ech, or the fm (finalmask) param, so importing a Hysteria2 client URL as an outbound dropped the configured ALPN, fingerprint, and salamander UDP mask. Parse alpn (falling back to h3 only when absent), fp, ech, and the pcs pinned-cert key, and restore the UDP mask via applyFinalMaskParam. --- frontend/src/lib/xray/outbound-link-parser.ts | 10 ++++--- .../src/test/outbound-link-parser.test.ts | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/xray/outbound-link-parser.ts b/frontend/src/lib/xray/outbound-link-parser.ts index b887c0c5..1dd4ad9a 100644 --- a/frontend/src/lib/xray/outbound-link-parser.ts +++ b/frontend/src/lib/xray/outbound-link-parser.ts @@ -404,6 +404,7 @@ export function parseHysteria2Link(link: string): Raw | null { const address = url.hostname; const port = Number(url.port) || 443; const params = url.searchParams; + const alpn = params.get('alpn'); const stream: Raw = { network: 'hysteria', security: 'tls', @@ -412,13 +413,14 @@ export function parseHysteria2Link(link: string): Raw | null { }, tlsSettings: { serverName: params.get('sni') ?? '', - alpn: ['h3'], - fingerprint: '', - echConfigList: '', + alpn: alpn ? alpn.split(',') : ['h3'], + fingerprint: params.get('fp') ?? '', + echConfigList: params.get('ech') ?? '', verifyPeerCertByName: '', - pinnedPeerCertSha256: params.get('pinSHA256') ?? '', + pinnedPeerCertSha256: params.get('pcs') ?? '', }, }; + applyFinalMaskParam(stream, params); return { protocol: 'hysteria', tag: decodeRemark(url), diff --git a/frontend/src/test/outbound-link-parser.test.ts b/frontend/src/test/outbound-link-parser.test.ts index 7c7001dd..d3985f45 100644 --- a/frontend/src/test/outbound-link-parser.test.ts +++ b/frontend/src/test/outbound-link-parser.test.ts @@ -267,6 +267,32 @@ describe('parseHysteria2Link', () => { const out = parseHysteria2Link('hy2://auth@srv:443?sni=example.com'); expect(out?.protocol).toBe('hysteria'); }); + + it('parses alpn, fingerprint and the salamander UDP mask (fm) — #4760', () => { + const link = 'hysteria2://78e7795a209c4c099f896a816fc8448f@news.domain.org:8443?' + + 'alpn=h2%2Chttp%2F1.1&' + + 'fm=%7B%22udp%22%3A%5B%7B%22settings%22%3A%7B%22password%22%3A%22ftwfgb9655hh2mgo%22%7D%2C%22type%22%3A%22salamander%22%7D%5D%7D&' + + 'fp=chrome&obfs=salamander&obfs-password=655hh2mgo&security=tls&sni=news.domain.org' + + '#hy2-ej596ty350qs'; + const out = parseHysteria2Link(link); + expect(out).not.toBeNull(); + const stream = out!.streamSettings as Record; + const tls = stream.tlsSettings as Record; + expect(tls.alpn).toEqual(['h2', 'http/1.1']); + expect(tls.fingerprint).toBe('chrome'); + expect(tls.serverName).toBe('news.domain.org'); + const finalmask = stream.finalmask as Record; + expect(finalmask).toBeDefined(); + const udp = finalmask.udp as Array>; + expect(udp[0].type).toBe('salamander'); + expect((udp[0].settings as Record).password).toBe('ftwfgb9655hh2mgo'); + }); + + it('defaults alpn to h3 when the link omits it', () => { + const out = parseHysteria2Link('hysteria2://auth@srv:443?sni=example.com'); + const tls = (out!.streamSettings as Record).tlsSettings as Record; + expect(tls.alpn).toEqual(['h3']); + }); }); describe('parseVlessLink — extra / fm / x_padding_bytes (B20)', () => {