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.
This commit is contained in:
MHSanaei
2026-06-01 19:21:29 +02:00
parent b6641439d4
commit 803e010921
2 changed files with 32 additions and 4 deletions

View File

@@ -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),

View File

@@ -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<string, unknown>;
const tls = stream.tlsSettings as Record<string, unknown>;
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<string, unknown>;
expect(finalmask).toBeDefined();
const udp = finalmask.udp as Array<Record<string, unknown>>;
expect(udp[0].type).toBe('salamander');
expect((udp[0].settings as Record<string, unknown>).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<string, unknown>).tlsSettings as Record<string, unknown>;
expect(tls.alpn).toEqual(['h3']);
});
});
describe('parseVlessLink — extra / fm / x_padding_bytes (B20)', () => {