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 { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive'; 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 { 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, }, }; case 'xdrive': return { network: 'xdrive', xdriveSettings: XDriveStreamSettingsSchema.parse({}) }; case 'masque': return { network: 'masque', masqueSettings: {} }; 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 { return { ...newStreamSlice('hysteria'), security: 'tls', tlsSettings: { serverName: '', alpn: ['h3'], fingerprint: '', echConfigList: '', verifyPeerCertByName: '', pinnedPeerCertSha256: '', }, }; } // MASQUE needs TLS (xray-core refuses it otherwise) and dials HTTP/3 unless the ALPN // offers h2 alone, so it starts on h3 like Hysteria. export function masqueStreamSlice(): Record { const { tlsSettings } = hysteriaStreamSlice(); return { ...newStreamSlice('masque'), security: 'tls', tlsSettings }; } // 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 | undefined, next: string, ): Record { if (next === 'hysteria') return hysteriaStreamSlice(); if (next === 'masque') return masqueStreamSlice(); 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 = { ...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({}); }