diff --git a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx index 314585c4..f5e4dde8 100644 --- a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx +++ b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx @@ -42,6 +42,7 @@ import { SERVER_PROTOCOLS, } from './outbound-form-constants'; import { + applyNetworkChange, buildAddModeValues, hysteriaStreamSlice, newStreamSlice, @@ -231,20 +232,8 @@ export default function OutboundFormModal({ // wsSettings, etc.) so the DU branch matches. Preserve security if // the new network supports it, otherwise force back to 'none'. function onNetworkChange(next: string) { - if (next === 'hysteria') { - form.setFieldValue('streamSettings', hysteriaStreamSlice()); - return; - } - const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none'; - const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } }); - const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } }); - const newSecurity = - currentSecurity === 'tls' && !stillAllowed - ? 'none' - : currentSecurity === 'reality' && !stillReality - ? 'none' - : currentSecurity; - form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity }); + const stream = (form.getFieldValue('streamSettings') ?? {}) as Record; + form.setFieldValue('streamSettings', applyNetworkChange(protocol, stream, next)); } function onXmuxToggle(checked: boolean) { diff --git a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts index be4475be..af720a40 100644 --- a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts +++ b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts @@ -1,4 +1,5 @@ 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'; @@ -74,6 +75,33 @@ export function hysteriaStreamSlice(): Record { }; } +// 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(); + 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({}); }