Files
3x-ui/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts
T
MHSanaei 60da6bed15 fix(xhttp): stop injecting scMaxEachPostBytes/scMinPostsIntervalMs defaults (#5141)
The panel seeded xhttp configs with scMaxEachPostBytes=1000000 and
scMinPostsIntervalMs=30 — xray-core''s own defaults — and emitted them
into every generated config and share link. The literal
scMinPostsIntervalMs=30 is a stable DPI fingerprint that Russia''s TSPU
keys on to block connections on mobile networks.

New configs no longer seed these values (empty schema/template defaults,
so xray-core applies its internal defaults). For configs already stored
with the old defaults, the link/subscription builders now drop values
equal to xray-core''s defaults instead of advertising them — covering
panel share links, the raw subscription, and the JSON subscription
without requiring every inbound to be re-saved. Non-default values the
user set deliberately are still emitted.
2026-06-12 01:50:37 +02:00

108 lines
3.9 KiB
TypeScript

import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
import { canEnableReality, canEnableTls } from '@/lib/xray/protocol-capabilities';
import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
import { MUX_PROTOCOLS } from './outbound-form-constants';
export function isMuxAllowed(protocol: string, flow: string, network: string): boolean {
if (!MUX_PROTOCOLS.has(protocol)) return false;
if (protocol === 'vless' && flow) return false;
if (network === 'xhttp') return false;
return true;
}
// Per-network bootstrap. Mirrors the legacy class constructors so the
// initial state for each transport matches what xray-core expects.
export function newStreamSlice(network: string): Record<string, unknown> {
switch (network) {
case 'tcp':
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
case 'kcp':
return {
network: 'kcp',
kcpSettings: {
mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20,
cwndMultiplier: 1, maxSendingWindow: 2097152,
},
};
case 'ws':
return {
network: 'ws',
wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 },
};
case 'grpc':
return {
network: 'grpc',
grpcSettings: { serviceName: '', authority: '', multiMode: false },
};
case 'httpupgrade':
return {
network: 'httpupgrade',
httpupgradeSettings: { path: '/', host: '', headers: {} },
};
case 'xhttp':
return {
network: 'xhttp',
xhttpSettings: {
path: '/', host: '', mode: '', headers: [],
xPaddingBytes: '100-1000',
},
};
case 'hysteria':
return {
network: 'hysteria',
hysteriaSettings: {
version: 2,
auth: '',
udpIdleTimeout: 60,
},
};
default:
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
}
}
// Hysteria2 always rides its own QUIC transport with TLS — the panel never
// offers another transport or 'none' security for it.
export function hysteriaStreamSlice(): Record<string, unknown> {
return {
...newStreamSlice('hysteria'),
security: 'tls',
tlsSettings: {
serverName: '', alpn: ['h3'], fingerprint: '',
echConfigList: '', verifyPeerCertByName: '', pinnedPeerCertSha256: '',
},
};
}
// Network change cascade: swap the per-network sub-key (tcpSettings,
// wsSettings, etc.) so the DU branch matches. Carry over the security mode
// and its settings (tlsSettings/realitySettings, including SNI serverName)
// when the new network still supports it; otherwise fall back to 'none'.
// Dropping tlsSettings here silently wiped the spoofed SNI on save (#4791).
export function applyNetworkChange(
protocol: string,
prevStream: Record<string, unknown> | undefined,
next: string,
): Record<string, unknown> {
if (next === 'hysteria') return hysteriaStreamSlice();
const stream = prevStream ?? {};
const currentSecurity = (stream.security as string) ?? 'none';
const stillTls = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
const newSecurity =
currentSecurity === 'tls' && !stillTls
? 'none'
: currentSecurity === 'reality' && !stillReality
? 'none'
: currentSecurity;
const newStream: Record<string, unknown> = { ...newStreamSlice(next), security: newSecurity };
if (newSecurity === 'tls' && stream.tlsSettings) newStream.tlsSettings = stream.tlsSettings;
else if (newSecurity === 'reality' && stream.realitySettings) newStream.realitySettings = stream.realitySettings;
return newStream;
}
export function buildAddModeValues(): OutboundFormValues {
return rawOutboundToFormValues({});
}