feat(hysteria2): emit UDP port hopping in subscriptions and share links

UDP Hop (finalmask.quicParams.udpHop.ports) was configurable but never surfaced in generated configs, so clients kept using the single listening port (#4789).

Share links (frontend genHysteriaLink + sub genHysteriaLink) now keep a numeric port in the authority and carry the hop range as the v2rayN-compatible mport query param, so v2rayN and other System.Uri-based importers can parse the link. Clash output sets mihomos native ports field.

Closes #4789
This commit is contained in:
MHSanaei
2026-06-02 15:01:18 +02:00
parent 2f12b34635
commit 13d02f01fc
5 changed files with 87 additions and 0 deletions

View File

@@ -626,6 +626,11 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string {
applyFinalMaskToParams(stream.finalmask, params);
const hopPorts = stream.finalmask?.quicParams?.udpHop?.ports?.trim() ?? '';
if (hopPorts.length > 0) {
params.set('mport', hopPorts);
}
const url = new URL(`${scheme}://${clientAuth}@${address}:${port}`);
for (const [key, value] of params) url.searchParams.set(key, value);
url.hash = encodeURIComponent(remark);

View File

@@ -131,6 +131,33 @@ describe('genHysteriaLink', () => {
expect(link).toMatchSnapshot();
});
}
it('emits the UDP hop range as the v2rayN-compatible mport param', () => {
const [, raw] = fixtures[0];
const withHop = {
...raw,
settings: { ...(raw.settings as Record<string, unknown>), version: 2 },
streamSettings: {
...(raw.streamSettings as Record<string, unknown>),
finalmask: { quicParams: { udpHop: { ports: '20000-50000', interval: '5-10' } } },
},
};
const typed = InboundSchema.parse(withHop);
const client = (raw.settings as { clients: Array<{ auth: string }> }).clients[0];
const link = genHysteriaLink({
inbound: typed,
address: 'example.test',
port: typed.port,
remark: 'hop-test',
clientAuth: client.auth,
});
expect(link.startsWith('hysteria2://')).toBe(true);
expect(link).toContain(`@example.test:${typed.port}`);
expect(link).toContain('mport=20000-50000');
expect(link.endsWith('#hop-test')).toBe(true);
});
});
describe('genWireguardLink + genWireguardConfig', () => {