mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-20 03:26:08 +00:00
fix(link): sanitize numeric quicParams taken from a share link's fm= param
The fm= finalmask blob was JSON-decoded and attached to streamSettings verbatim, both by the Go parser (outbound subscriptions) and the frontend import. Some providers emit duration strings for the strictly integer quicParams fields (e.g. keepAlivePeriod "10s"), and xray-core then refuses to load the whole config at startup - one bad subscription entry took the panel's Xray down on the next refresh. Coerce numeric strings, convert duration strings to whole seconds, and drop values that cannot be represented as integers; genuinely string-typed fields (congestion, bbrProfile, brutalUp/Down, udpHop) pass through untouched. Closes #5783
This commit is contained in:
@@ -218,6 +218,7 @@ function applyFinalMaskParam(stream: Raw, params: URLSearchParams): void {
|
||||
try {
|
||||
const parsed = JSON.parse(fm) as Record<string, unknown>;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
sanitizeFinalMaskQuicParams(parsed);
|
||||
stream.finalmask = parsed;
|
||||
}
|
||||
} catch {
|
||||
@@ -225,6 +226,48 @@ function applyFinalMaskParam(stream: Raw, params: URLSearchParams): void {
|
||||
}
|
||||
}
|
||||
|
||||
const QUIC_PARAMS_NUMERIC_KEYS = [
|
||||
'initStreamReceiveWindow',
|
||||
'maxStreamReceiveWindow',
|
||||
'initConnectionReceiveWindow',
|
||||
'maxConnectionReceiveWindow',
|
||||
'maxIdleTimeout',
|
||||
'keepAlivePeriod',
|
||||
'maxIncomingStreams',
|
||||
] as const;
|
||||
|
||||
const DURATION_SECONDS: Record<string, number> = { ms: 0.001, s: 1, m: 60, h: 3600 };
|
||||
|
||||
// xray-core rejects the whole config when these quicParams fields are not
|
||||
// plain integers (e.g. keepAlivePeriod "10s" from a foreign provider), so
|
||||
// coerce numeric/duration strings and drop anything unparseable (#5783).
|
||||
function sanitizeFinalMaskQuicParams(parsed: Record<string, unknown>): void {
|
||||
const raw = parsed.quicParams;
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
||||
const quic = raw as Record<string, unknown>;
|
||||
for (const key of QUIC_PARAMS_NUMERIC_KEYS) {
|
||||
if (!(key in quic)) continue;
|
||||
const value = quic[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
quic[key] = Math.trunc(value);
|
||||
continue;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const asNumber = Number(value);
|
||||
if (value.trim() !== '' && Number.isFinite(asNumber)) {
|
||||
quic[key] = Math.trunc(asNumber);
|
||||
continue;
|
||||
}
|
||||
const duration = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value.trim());
|
||||
if (duration) {
|
||||
quic[key] = Math.trunc(Number(duration[1]) * DURATION_SECONDS[duration[2]]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
delete quic[key];
|
||||
}
|
||||
}
|
||||
|
||||
function applySecurityParams(stream: Raw, params: URLSearchParams): void {
|
||||
if (stream.security === 'tls') {
|
||||
const tls = stream.tlsSettings as Raw;
|
||||
|
||||
@@ -308,6 +308,28 @@ describe('parseHysteria2Link', () => {
|
||||
expect(settings.packetSize).toBe('100-200');
|
||||
});
|
||||
|
||||
it('coerces string quicParams numerics under fm to integers — #5783', () => {
|
||||
const fm = encodeURIComponent(JSON.stringify({
|
||||
quicParams: {
|
||||
keepAlivePeriod: '10s',
|
||||
maxIdleTimeout: '30',
|
||||
initStreamReceiveWindow: 524288,
|
||||
maxIncomingStreams: true,
|
||||
brutalUp: '100 mbps',
|
||||
},
|
||||
}));
|
||||
const link = `hysteria2://78e7795a209c4c099f896a816fc8448f@news.domain.org:8443?security=tls&sni=news.domain.org&fm=${fm}#hy2-quic`;
|
||||
const out = parseHysteria2Link(link);
|
||||
expect(out).not.toBeNull();
|
||||
const finalmask = (out!.streamSettings as Record<string, unknown>).finalmask as Record<string, unknown>;
|
||||
const quic = finalmask.quicParams as Record<string, unknown>;
|
||||
expect(quic.keepAlivePeriod).toBe(10);
|
||||
expect(quic.maxIdleTimeout).toBe(30);
|
||||
expect(quic.initStreamReceiveWindow).toBe(524288);
|
||||
expect('maxIncomingStreams' in quic).toBe(false);
|
||||
expect(quic.brutalUp).toBe('100 mbps');
|
||||
});
|
||||
|
||||
it('round-trips the realm tlsConfig under fm', () => {
|
||||
const fm = encodeURIComponent(JSON.stringify({
|
||||
udp: [{
|
||||
|
||||
Reference in New Issue
Block a user