feat(finalmask): support Salamander packetSize (Gecko) and Realm tlsConfig for Hysteria2 (#5278)

* feat(finalmask): support salamander packetSize (Gecko) and realm tlsConfig

Hysteria v2.9.1/v2.9.2 added two finalmask features that the pinned
Xray-core (26.6.1, 94ffd50) already supports but the panel UI did not
expose: Salamander's packetSize range (Gecko, XTLS/Xray-core#6198) and
the Realm UDP hole-punching mask's optional tlsConfig (XTLS/Xray-core#6137).

Add typed schemas and form fields for both, keeping UdpMaskSchema.settings
permissive per the existing finalmask design note. packetSize reuses the
existing dash-range preprocess (like udpHop.ports) so it round-trips under
the fm= share-link param with no new URI key; realm tlsConfig emits xray's
flat TLSConfig shape (serverName/alpn/fingerprint/allowInsecure).

Verified against the bundled Xray 26.6.1: configs with packetSize and
realm tlsConfig validate (Configuration OK.), plain salamander stays
backward-compatible, and a malformed packetSize is correctly rejected by
the salamander mask builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(finalmask): add snapshots for salamander-gecko and realm-tls fixtures

vitest run does not auto-create missing snapshots in CI mode, so the two
new fixtures need committed snapshot entries. Verified under node:22 that
finalmask.test.ts passes (6/6) with these snapshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(finalmask): polished Gecko UX with core-grounded validation

Fold PR #5281's Gecko work into the Realm tlsConfig base:

- Replace the plain packetSize input with a Salamander/Gecko mode
  selector and validated Min/Max number inputs.
- parseGeckoPacketSize enforces xray-core's real bound
  (1 <= min <= max <= 2048, the gecko buffer size) so the panel
  rejects configs core would reject at runtime.
- Accurate Gecko description; add parser unit tests.
- Drop the unused Salamander/Realm settings schemas; settings stay
  permissive and are validated at the form level.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Rouzbeh†
2026-06-15 00:21:31 +02:00
committed by GitHub
parent 7c737820d1
commit dab0add191
6 changed files with 300 additions and 17 deletions
@@ -288,6 +288,46 @@ describe('parseHysteria2Link', () => {
expect((udp[0].settings as Record<string, unknown>).password).toBe('ftwfgb9655hh2mgo');
});
it('round-trips the salamander packetSize (Gecko) under fm', () => {
const fm = encodeURIComponent(JSON.stringify({
udp: [{ type: 'salamander', settings: { password: 'ftwfgb9655hh2mgo', packetSize: '100-200' } }],
}));
const link = `hysteria2://78e7795a209c4c099f896a816fc8448f@news.domain.org:8443?security=tls&sni=news.domain.org&fm=${fm}#hy2-gecko`;
const out = parseHysteria2Link(link);
expect(out).not.toBeNull();
const finalmask = (out!.streamSettings as Record<string, unknown>).finalmask as Record<string, unknown>;
const udp = finalmask.udp as Array<Record<string, unknown>>;
const settings = udp[0].settings as Record<string, unknown>;
expect(udp[0].type).toBe('salamander');
expect(settings.password).toBe('ftwfgb9655hh2mgo');
expect(settings.packetSize).toBe('100-200');
});
it('round-trips the realm tlsConfig under fm', () => {
const fm = encodeURIComponent(JSON.stringify({
udp: [{
type: 'realm',
settings: {
url: 'realm://public@example.com/my-realm',
stunServers: ['stun.l.google.com:19302'],
tlsConfig: { serverName: 'example.com', alpn: ['h3'], fingerprint: 'chrome', allowInsecure: false },
},
}],
}));
const link = `hysteria2://auth@srv:443?security=tls&sni=srv&fm=${fm}#hy2-realm`;
const out = parseHysteria2Link(link);
expect(out).not.toBeNull();
const finalmask = (out!.streamSettings as Record<string, unknown>).finalmask as Record<string, unknown>;
const udp = finalmask.udp as Array<Record<string, unknown>>;
const settings = udp[0].settings as Record<string, unknown>;
expect(udp[0].type).toBe('realm');
expect(settings.url).toBe('realm://public@example.com/my-realm');
const tlsConfig = settings.tlsConfig as Record<string, unknown>;
expect(tlsConfig.serverName).toBe('example.com');
expect(tlsConfig.alpn).toEqual(['h3']);
expect(tlsConfig.fingerprint).toBe('chrome');
});
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>;