mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-04 04:05:04 +03:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user