fix(outbounds): preserve SNI/TLS settings on transport change

Changing the transport in the outbound edit modal rebuilt streamSettings
from scratch, dropping tlsSettings (and its serverName) while keeping
security: 'tls'. On save xray received TLS with an empty SNI, so SNI-spoof
tunnels connected but passed no traffic. Carry over tlsSettings/
realitySettings when the new network still supports the security mode,
via a new applyNetworkChange helper. Fixes #4791.
This commit is contained in:
MHSanaei 2026-06-03 16:00:22 +02:00
parent 039d05a743
commit 5fb18b8819
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 31 additions and 14 deletions

View file

@ -42,6 +42,7 @@ import {
SERVER_PROTOCOLS, SERVER_PROTOCOLS,
} from './outbound-form-constants'; } from './outbound-form-constants';
import { import {
applyNetworkChange,
buildAddModeValues, buildAddModeValues,
hysteriaStreamSlice, hysteriaStreamSlice,
newStreamSlice, newStreamSlice,
@ -231,20 +232,8 @@ export default function OutboundFormModal({
// wsSettings, etc.) so the DU branch matches. Preserve security if // wsSettings, etc.) so the DU branch matches. Preserve security if
// the new network supports it, otherwise force back to 'none'. // the new network supports it, otherwise force back to 'none'.
function onNetworkChange(next: string) { function onNetworkChange(next: string) {
if (next === 'hysteria') { const stream = (form.getFieldValue('streamSettings') ?? {}) as Record<string, unknown>;
form.setFieldValue('streamSettings', hysteriaStreamSlice()); form.setFieldValue('streamSettings', applyNetworkChange(protocol, stream, next));
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 });
} }
function onXmuxToggle(checked: boolean) { function onXmuxToggle(checked: boolean) {

View file

@ -1,4 +1,5 @@
import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter'; 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 type { OutboundFormValues } from '@/schemas/forms/outbound-form';
import { MUX_PROTOCOLS } from './outbound-form-constants'; import { MUX_PROTOCOLS } from './outbound-form-constants';
@ -74,6 +75,33 @@ export function hysteriaStreamSlice(): Record<string, unknown> {
}; };
} }
// 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 { export function buildAddModeValues(): OutboundFormValues {
return rawOutboundToFormValues({}); return rawOutboundToFormValues({});
} }