mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-14 00:26:06 +00:00
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:
@@ -61,6 +61,46 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses quic-params byte-stably
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`FinalMaskStreamSettingsSchema fixtures > parses realm-tls byte-stably 1`] = `
|
||||
{
|
||||
"tcp": [],
|
||||
"udp": [
|
||||
{
|
||||
"settings": {
|
||||
"stunServers": [
|
||||
"stun.l.google.com:19302",
|
||||
],
|
||||
"tlsConfig": {
|
||||
"allowInsecure": false,
|
||||
"alpn": [
|
||||
"h3",
|
||||
],
|
||||
"fingerprint": "chrome",
|
||||
"serverName": "example.com",
|
||||
},
|
||||
"url": "realm://public@example.com/my-realm",
|
||||
},
|
||||
"type": "realm",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`FinalMaskStreamSettingsSchema fixtures > parses salamander-gecko byte-stably 1`] = `
|
||||
{
|
||||
"tcp": [],
|
||||
"udp": [
|
||||
{
|
||||
"settings": {
|
||||
"packetSize": "100-200",
|
||||
"password": "swordfish",
|
||||
},
|
||||
"type": "salamander",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`FinalMaskStreamSettingsSchema fixtures > parses tcp-mask byte-stably 1`] = `
|
||||
{
|
||||
"tcp": [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseGeckoPacketSize } from '@/lib/xray/forms/transport/FinalMaskForm';
|
||||
import { FinalMaskStreamSettingsSchema } from '@/schemas/protocols/stream';
|
||||
|
||||
const fixtures = import.meta.glob<unknown>(
|
||||
@@ -24,3 +25,22 @@ describe('FinalMaskStreamSettingsSchema fixtures', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('parseGeckoPacketSize', () => {
|
||||
it('accepts positive ordered packet size ranges', () => {
|
||||
expect(parseGeckoPacketSize('512-1200')).toEqual({ min: 512, max: 1200 });
|
||||
expect(parseGeckoPacketSize('1200-1200')).toEqual({ min: 1200, max: 1200 });
|
||||
expect(parseGeckoPacketSize('1-2048')).toEqual({ min: 1, max: 2048 });
|
||||
});
|
||||
|
||||
it('rejects invalid packet size ranges', () => {
|
||||
expect(parseGeckoPacketSize('')).toBeNull();
|
||||
expect(parseGeckoPacketSize('0-1200')).toBeNull();
|
||||
expect(parseGeckoPacketSize('1200-512')).toBeNull();
|
||||
expect(parseGeckoPacketSize('512')).toBeNull();
|
||||
expect(parseGeckoPacketSize('512-abc')).toBeNull();
|
||||
// exceeds xray-core's gecko buffer (max 2048)
|
||||
expect(parseGeckoPacketSize('512-2049')).toBeNull();
|
||||
expect(parseGeckoPacketSize('512-9999')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"udp": [
|
||||
{
|
||||
"type": "realm",
|
||||
"settings": {
|
||||
"url": "realm://public@example.com/my-realm",
|
||||
"stunServers": ["stun.l.google.com:19302"],
|
||||
"tlsConfig": {
|
||||
"serverName": "example.com",
|
||||
"allowInsecure": false,
|
||||
"alpn": ["h3"],
|
||||
"fingerprint": "chrome"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"udp": [
|
||||
{ "type": "salamander", "settings": { "password": "swordfish", "packetSize": "100-200" } }
|
||||
]
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user