fix(hysteria): standard geco share links and persistent uTLS None (#6325)

- Export standard gecko obfs query params in hysteria2 share links
- Enforce packet size bounds across Go and TypeScript link handlers
- Persist uTLS None explicitly and initialize new TLS inbounds to chrome
- Tear down stackTun safely without closeMu deadlock against WriteNotify

Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
This commit is contained in:
Rouzbeh†
2026-09-03 18:05:07 +03:30
committed by GitHub
parent f9898e0b24
commit 540caa4e93
14 changed files with 514 additions and 111 deletions
@@ -808,3 +808,35 @@ describe('parseOutboundLink dispatcher', () => {
expect(parseOutboundLink(' ')).toBeNull();
});
});
describe('obfs=gecko packetSize validation', () => {
const base = 'hysteria2://secret@1.2.3.4:443?security=tls&obfs=gecko&obfs-password=pw';
const packetSizeOf = (link: string): string | undefined => {
const out = parseHysteria2Link(link);
expect(out).not.toBeNull();
const finalmask = (out!.streamSettings as Record<string, unknown>).finalmask as
| Record<string, unknown>
| undefined;
const udp = (finalmask?.udp ?? []) as Array<Record<string, unknown>>;
const mask = udp.find((m) => m.type === 'salamander');
return (mask?.settings as Record<string, unknown> | undefined)?.packetSize as
| string
| undefined;
};
it('stores a valid range', () => {
expect(packetSizeOf(`${base}&minPacketSize=512&maxPacketSize=1200`)).toBe('512-1200');
});
it.each([
['min only', `${base}&minPacketSize=512`],
['max only', `${base}&maxPacketSize=1200`],
['non-numeric', `${base}&minPacketSize=abc&maxPacketSize=def`],
['zero min', `${base}&minPacketSize=0&maxPacketSize=1200`],
['inverted', `${base}&minPacketSize=1200&maxPacketSize=512`],
['over cap', `${base}&minPacketSize=512&maxPacketSize=4096`],
])('drops the %s range', (_name, link) => {
expect(packetSizeOf(link)).toBeUndefined();
});
});
@@ -0,0 +1,77 @@
/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
import {
createTlsSettingsWithDefaultCert,
createHysteriaTlsSettingsWithDefaultCert,
} from '@/lib/xray/inbound-tls-defaults';
import { genHysteriaLink } from '@/lib/xray/inbound-link';
import type { Inbound } from '@/schemas/api/inbound';
// uTLS None ('') must survive a schema parse; the old default flipped it to
// chrome on every save.
describe('TlsClientSettingsSchema fingerprint default', () => {
it('parses an omitted fingerprint as None, not chrome', () => {
const parsed = TlsStreamSettingsSchema.parse({});
expect(parsed.settings.fingerprint).toBe('');
});
it('keeps an explicit empty-string fingerprint through parse', () => {
const parsed = TlsStreamSettingsSchema.parse({
settings: {
fingerprint: '',
echConfigList: '',
pinnedPeerCertSha256: [],
verifyPeerCertByName: '',
},
});
expect(parsed.settings.fingerprint).toBe('');
});
it('initializes generic TLS inbounds with chrome fingerprint default', () => {
const tls = createTlsSettingsWithDefaultCert();
expect((tls.settings as Record<string, unknown>)?.fingerprint).toBe('chrome');
});
it('initializes hysteria TLS inbounds with empty fingerprint default', () => {
const tls = createHysteriaTlsSettingsWithDefaultCert();
expect((tls.settings as Record<string, unknown>)?.fingerprint).toBe('');
});
it('does not inject fp into the hysteria share link when fingerprint is None', () => {
const raw = {
id: 1,
port: 443,
protocol: 'hysteria',
settings: { version: 2, clients: [{ auth: 'secret' }] },
streamSettings: {
security: 'tls',
tlsSettings: {
serverName: 'hy.test',
alpn: ['h3'],
settings: {
fingerprint: '',
echConfigList: '',
pinnedPeerCertSha256: [],
verifyPeerCertByName: '',
},
},
finalmask: {
udp: [{ type: 'salamander', settings: { password: 'pw', packetSize: '512-1200' } }],
},
},
};
const link = genHysteriaLink({
inbound: raw as unknown as Inbound,
address: 'example.test',
remark: 'gecko',
clientAuth: 'secret',
});
expect(link).toContain('obfs=gecko');
expect(link).toContain('minPacketSize=512');
expect(link).toContain('maxPacketSize=1200');
expect(link).not.toContain('fp=');
expect(link).not.toContain('fm=');
});
});