fix(inbounds): reject incompatible VLESS fallbacks and prevent Xray crashes (#7003)

Xray refuses a VLESS inbound with server decryption whenever fallbacks is non-nil, even an empty list, so an API or imported inbound with decryption and fallbacks: [] took every inbound down. Empty fallback arrays are now normalized away on add, update, config build and node push; non-empty ones are refused with decryption, enabling decryption over stored fallback rules is refused, and a legacy conflicting row holds back the running core instead of stopping it. The form, API docs and locales follow. Closes #7001.
This commit is contained in:
Hamed
2026-11-03 18:22:30 +03:30
committed by GitHub
parent fee7b1621d
commit 19f2f21d81
31 changed files with 853 additions and 79 deletions
@@ -226,11 +226,14 @@ export default function InboundFormModal({
const getV = methods.getValues as unknown as (name?: string) => unknown;
const control = methods.control;
const [saving, setSaving] = useState(false);
const savedInboundId = useRef<number | null>(null);
const [scanning, setScanning] = useState(false);
const [scanResult, setScanResult] = useState<RealityScanResult | null>(null);
const [activeTab, setActiveTab] = useState('basic');
const {
fallbacks,
hasStoredFallbacks,
fallbacksLoaded,
fallbackChildOptions,
loadFallbacks,
saveFallbacks,
@@ -250,6 +253,7 @@ export default function InboundFormModal({
* always empty. Offer it only then; `listen`/`custom` work for local inbounds.
*/
const nodeShareOptionAvailable = selectableNodes.length > 0 && isNodeEligible;
const vlessDecryption = useWatch({ control, name: 'settings.decryption' }) ?? '';
const vlessEncryption = useWatch({ control, name: 'settings.encryption' }) ?? '';
const ssMethod = useWatch({ control, name: 'settings.method' });
const isSSWith2022 = isSS2022({
@@ -295,7 +299,10 @@ export default function InboundFormModal({
streamSettings: { network },
settings: { network: wSsNetwork, allowedNetwork: wTunnelNetwork, udp: mixedUdpOn },
});
const hasVlessDecryption =
protocol === Protocols.VLESS && vlessDecryption !== '' && vlessDecryption !== 'none';
const isFallbackHost =
!hasVlessDecryption &&
(protocol === Protocols.VLESS || protocol === Protocols.TROJAN) &&
network === 'tcp' &&
(security === 'tls' || security === 'reality');
@@ -437,6 +444,7 @@ export default function InboundFormModal({
? withoutClients(rawInboundToFormValues(dbInbound))
: buildAddModeValues();
methods.reset(initial);
savedInboundId.current = null;
setScanResult(null);
setActiveTab('basic');
const initialTag = (initial.tag ?? '') as string;
@@ -573,19 +581,46 @@ export default function InboundFormModal({
);
return;
}
if (saving) return;
if (!fallbacksLoaded) {
messageApi.error(t('pages.inbounds.fallbacks.saveFailed'));
return;
}
if (!isFallbackHost && fallbacks.length > 0) {
setActiveTab('protocol');
messageApi.error(
t(
hasVlessDecryption
? 'pages.inbounds.fallbacks.decryptionConflict'
: 'pages.inbounds.fallbacks.needsTls',
),
);
return;
}
setSaving(true);
try {
const payload = formValuesToWirePayload(parsed.data, { omitClients: mode === 'edit' });
const url =
mode === 'edit' && dbInbound
? `/panel/api/inbounds/update/${dbInbound.id}`
: '/panel/api/inbounds/add';
// Persist an explicit removal before the backend checks the new decryption setting.
if (!isFallbackHost && hasStoredFallbacks && dbInbound) {
if (!(await saveFallbacks(dbInbound.id))) {
messageApi.error(t('pages.inbounds.fallbacks.saveFailed'));
return;
}
}
const existingId = mode === 'edit' ? dbInbound?.id : savedInboundId.current;
const payload = formValuesToWirePayload(parsed.data, { omitClients: !!existingId });
const url = existingId
? `/panel/api/inbounds/update/${existingId}`
: '/panel/api/inbounds/add';
const msg = await HttpUtil.post(url, payload);
if (msg?.success) {
if (isFallbackHost) {
const obj = msg.obj as { id?: number; Id?: number } | null;
const masterId = mode === 'edit' ? dbInbound!.id : (obj?.id ?? obj?.Id ?? 0);
if (masterId) await saveFallbacks(masterId);
const masterId = existingId ?? obj?.id ?? obj?.Id ?? 0;
if (masterId) savedInboundId.current = masterId;
if (masterId && !(await saveFallbacks(masterId))) {
messageApi.error(t('pages.inbounds.fallbacks.saveFailed'));
return;
}
}
onSaved();
onClose();
@@ -783,6 +818,7 @@ export default function InboundFormModal({
const fallbacksCard = (
<FallbacksCard
canAdd={isFallbackHost && fallbacksLoaded}
fallbacks={fallbacks}
fallbackChildOptions={fallbackChildOptions}
addFallback={addFallback}
@@ -834,9 +870,18 @@ export default function InboundFormModal({
/>
)}
{isFallbackHost && fallbacksCard}
{(isFallbackHost || fallbacks.length > 0) && fallbacksCard}
{hasVlessDecryption && (
<Alert
className="mt-12"
type="warning"
showIcon
title={t('pages.inbounds.fallbacks.decryptionConflict')}
/>
)}
{(protocol === Protocols.VLESS || protocol === Protocols.TROJAN) &&
network === 'tcp' &&
!hasVlessDecryption &&
!isFallbackHost && (
<Alert
className="mt-12"
@@ -1175,7 +1220,9 @@ export default function InboundFormModal({
Protocols.TUIC,
Protocols.MASQUE,
] as string[]
).includes(protocol) || isFallbackHost
).includes(protocol) ||
isFallbackHost ||
fallbacks.length > 0
? [
{
key: 'protocol',