diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index edc4cf8..1e55647 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -67,7 +67,7 @@ const getEmptyGateway = () => ({ serverId: '', country: '', type: 'direct', - parentGatewayId: '', // Для рекурсивных - ID родительского gateway + parentGateways: [], // Для рекурсивных - массив родительских gateway с distance [{id, distance}] }); const getEmptyInterface = () => ({ @@ -144,8 +144,21 @@ function NetworkConfigManager() { try { const response = await api.get('/network-config'); const data = response.data || getDefaultConfig(); + + // Миграция старых данных: преобразуем parentGatewayId в parentGateways + const migratedGateways = (data.gateways || []).map(gw => { + if (gw.type === 'recursive' && gw.parentGatewayId && (!gw.parentGateways || gw.parentGateways.length === 0)) { + return { + ...gw, + parentGateways: [{ id: gw.parentGatewayId, distance: undefined }], + parentGatewayId: undefined // Удаляем старое поле + }; + } + return gw; + }); + setConfig({ - gateways: Array.isArray(data.gateways) ? data.gateways : [], + gateways: Array.isArray(migratedGateways) ? migratedGateways : [], tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], }); @@ -334,7 +347,17 @@ function NetworkConfigManager() { }; const handleEditGateway = (gateway) => { - setEditingGateway({ ...gateway }); + // Миграция старых данных при редактировании + const migratedGateway = gateway.type === 'recursive' && gateway.parentGatewayId && (!gateway.parentGateways || gateway.parentGateways.length === 0) + ? { + ...gateway, + parentGateways: [{ id: gateway.parentGatewayId, distance: undefined }] + } + : { + ...gateway, + parentGateways: gateway.parentGateways || [] + }; + setEditingGateway(migratedGateway); setGatewayModalMode('edit'); setGatewayModalOpen(true); }; @@ -346,36 +369,45 @@ function NetworkConfigManager() { return; } + // Очищаем пустые parentGateways и удаляем старое поле parentGatewayId + const cleanedData = { + ...gatewayData, + parentGateways: gatewayData.type === 'recursive' + ? (gatewayData.parentGateways || []).filter(p => p && p.id) + : [], + parentGatewayId: undefined // Удаляем старое поле + }; + if (gatewayModalMode === 'add') { // Проверка на дубликат IP - const existingGateway = config.gateways.find(g => g.ip === gatewayData.ip.trim()); + const existingGateway = config.gateways.find(g => g.ip === cleanedData.ip.trim()); if (existingGateway) { - notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); + notify.error(`Gateway с IP ${cleanedData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, - gateways: [...prev.gateways, gatewayData], + gateways: [...prev.gateways, cleanedData], })); notify.success('Gateway добавлен'); } else { // При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway - const currentGateway = config.gateways.find(g => g.id === gatewayData.id); + const currentGateway = config.gateways.find(g => g.id === cleanedData.id); if (!currentGateway) { notify.error('Gateway не найден'); return; } // Если IP изменился, проверяем, не занят ли он другим gateway - if (gatewayData.ip.trim() !== currentGateway.ip && - config.gateways.some(g => g.id !== gatewayData.id && g.ip === gatewayData.ip.trim())) { - notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); + if (cleanedData.ip.trim() !== currentGateway.ip && + config.gateways.some(g => g.id !== cleanedData.id && g.ip === cleanedData.ip.trim())) { + notify.error(`Gateway с IP ${cleanedData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, - gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g), + gateways: prev.gateways.map(g => g.id === cleanedData.id ? cleanedData : g), })); notify.success('Gateway обновлён'); } @@ -586,7 +618,7 @@ function NetworkConfigManager() { code += `# Связь с сервером: ${iface.server2Name}\n`; code += `# Local IP: ${iface.localIp}\n`; code += `# Remote IP: ${iface.remoteIp}\n`; - code += `/ip address add address=${iface.localIp}/32 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`; + code += `/ip address add address=${iface.localIp}/30 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`; code += '\n'; }); @@ -641,10 +673,13 @@ function NetworkConfigManager() { let validRoutesCount = 0; gateways.forEach(gw => { - const parent = getParentGateway(gw.parentGatewayId); + // Поддержка старого формата (parentGatewayId) и нового (parentGateways) + const parentGatewaysList = gw.parentGateways && Array.isArray(gw.parentGateways) && gw.parentGateways.length > 0 + ? gw.parentGateways + : (gw.parentGatewayId ? [{ id: gw.parentGatewayId, distance: undefined }] : []); - if (!parent) { - code += `# Gateway "${gw.ip || 'без IP'}" - родительский gateway/интерфейс не найден, пропущен\n`; + if (parentGatewaysList.length === 0) { + code += `# Gateway "${gw.ip || 'без IP'}" - родительские gateway/интерфейсы не указаны, пропущен\n`; return; } @@ -653,43 +688,57 @@ function NetworkConfigManager() { return; } - // Определяем IP родителя в зависимости от типа - const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; - - if (!parentIp) { - const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway'; - code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`; - return; - } - - validRoutesCount++; - - // Создаём маршрут для рекурсивного gateway - const parentLabel = parent.parentType === 'interface' - ? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})` - : `gateway ${parent.ip}`; - - code += `# Рекурсивный gateway: ${gw.ip}\n`; - code += `# Родительский: ${parentLabel}\n`; - - // Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP - const gatewayValue = parent.parentType === 'interface' - ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` - : parentIp; - - // Создаём маршрут к IP рекурсивного gateway через родительский - const comment = gw.description - ? `${gw.description} -> ${parentIp}` - : `Recursive: ${gw.ip} -> ${parentIp}`; - code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue} comment="${comment}"\n`; - - // Если у рекурсивного gateway есть описание с указанием на default route - if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { - code += `# Дополнительный default route через рекурсивный gateway\n`; - const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`; - code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue} comment="${defaultComment}"\n`; - } - code += '\n'; + // Обрабатываем каждый родительский gateway + parentGatewaysList.forEach((parentRef, parentIndex) => { + const parent = getParentGateway(parentRef.id); + + if (!parent) { + code += `# Gateway "${gw.ip}" - родительский gateway/интерфейс "${parentRef.id}" не найден, пропущен\n`; + return; + } + + // Определяем IP родителя в зависимости от типа + const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; + + if (!parentIp) { + const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway'; + code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`; + return; + } + + validRoutesCount++; + + // Создаём маршрут для рекурсивного gateway + const parentLabel = parent.parentType === 'interface' + ? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})` + : `gateway ${parent.ip}`; + + const distanceSuffix = parentRef.distance !== undefined && parentRef.distance !== null ? ` (distance: ${parentRef.distance})` : ''; + code += `# Рекурсивный gateway: ${gw.ip}${distanceSuffix}\n`; + code += `# Родительский: ${parentLabel}\n`; + + // Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP + const gatewayValue = parent.parentType === 'interface' + ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` + : parentIp; + + // Создаём маршрут к IP рекурсивного gateway через родительский + const comment = gw.description + ? `Recursive: ${gw.description} -> ${parentIp}${distanceSuffix}` + : `Recursive: ${gw.ip} -> ${parentIp}${distanceSuffix}`; + + // Формируем команду с distance, если указан + const distanceParam = parentRef.distance !== undefined && parentRef.distance !== null ? ` distance=${parentRef.distance}` : ''; + code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue}${distanceParam} comment="${comment}"\n`; + + // Если у рекурсивного gateway есть описание с указанием на default route + if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { + code += `# Дополнительный default route через рекурсивный gateway${distanceSuffix}\n`; + const defaultComment = gw.description ? `Recursive: ${gw.description} (default)${distanceSuffix}` : `Recursive: ${gw.ip} (default)${distanceSuffix}`; + code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue}${distanceParam} comment="${defaultComment}"\n`; + } + code += '\n'; + }); }); code += '# Проверка созданных маршрутов:\n'; @@ -921,9 +970,16 @@ function NetworkConfigManager() { const gatewayCountry = gateway.country || ''; const displayCountry = gatewayCountry || serverCountry; const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; - const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId - ? getParentGateway(gateway.parentGatewayId) - : null; + + // Поддержка старого формата (parentGatewayId) и нового (parentGateways) + const parentGatewaysList = gateway.type === 'recursive' + ? (gateway.parentGateways && Array.isArray(gateway.parentGateways) && gateway.parentGateways.length > 0 + ? gateway.parentGateways + : (gateway.parentGatewayId ? [{ id: gateway.parentGatewayId, distance: undefined }] : [])) + : []; + + // Получаем информацию о первом родительском gateway для отображения + const firstParent = parentGatewaysList.length > 0 ? getParentGateway(parentGatewaysList[0].id) : null; // Определяем тип индикатора (как в ServerCard) const indicatorType = gateway.type === 'recursive' ? 'EXIT' : 'JUMP'; @@ -935,12 +991,16 @@ function NetworkConfigManager() { if (gateway.type === 'direct') { // Для прямого gateway: 0.0.0.0/0 -> IP адрес displayText = gateway.ip ? `0.0.0.0/0 → ${gateway.ip}` : ''; - } else if (gateway.type === 'recursive' && parentGateway) { - // Для рекурсивного: IP адрес -> IP адрес родительского gateway - const parentIp = parentGateway.parentType === 'interface' - ? parentGateway.remoteIp - : parentGateway.ip; + } else if (gateway.type === 'recursive' && firstParent) { + // Для рекурсивного: IP адрес -> IP адрес первого родительского gateway + const parentIp = firstParent.parentType === 'interface' + ? firstParent.remoteIp + : firstParent.ip; displayText = gateway.ip && parentIp ? `${gateway.ip} → ${parentIp}` : ''; + // Если несколько родительских gateway, добавляем информацию + if (parentGatewaysList.length > 1) { + displayText += ` (+${parentGatewaysList.length - 1})`; + } } return ( @@ -967,36 +1027,32 @@ function NetworkConfigManager() { {/* Центральная часть: информация */} -
- {/* Верхняя строка: флаг, название, провайдер */} -
-
+
+ {/* Верхняя строка: флаг, название */} +
+
{displayCountry && ( {countryToFlag(displayCountry)} )} -
- {gateway.description || provider || gateway.ip || '—'} - {provider && ( - <> - - {provider} - - )} +
+ {gateway.description || gateway.ip || '—'}
- - {/* Зеленый блок с маршрутом */} - {displayText && ( - +
+ + {/* Маршрут ip->ip сразу под названием */} + {displayText && ( +
+ {displayText} - )} -
+
+ )} {/* Нижняя строка: DNS + badges */}
{server && ( - + {server.dns || server.ip || gateway.serverId} )} @@ -1102,30 +1158,32 @@ function NetworkConfigManager() {
{/* Центральная часть: информация */} -
+
{/* Верхняя строка: флаг, название */} -
-
+
+
{displayCountry && ( {countryToFlag(displayCountry)} )} -
+
{interfaceName}
- - {/* Зеленый блок с IP */} - {displayIp && displayIp !== '—' && ( - - → {displayIp} - - )}
+ {/* Маршрут между Local IP и Remote IP сразу под названием */} + {displayLocalIp !== '—' && displayRemoteIp !== '—' && ( +
+ + {displayLocalIp} → {displayRemoteIp} + +
+ )} + {/* Нижняя строка: DNS + badges */}
{displayServerName && ( - + {displayServerName} )} @@ -1447,9 +1505,13 @@ function NetworkConfigManager() { const gatewayCountry = gateway.country || ''; const displayCountry = gatewayCountry || serverCountry; const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; - const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId - ? getParentGateway(gateway.parentGatewayId) - : null; + // Поддержка старого формата (parentGatewayId) и нового (parentGateways) + const parentGatewaysList = gateway.type === 'recursive' + ? (gateway.parentGateways && Array.isArray(gateway.parentGateways) && gateway.parentGateways.length > 0 + ? gateway.parentGateways + : (gateway.parentGatewayId ? [{ id: gateway.parentGatewayId, distance: undefined }] : [])) + : []; + const firstParent = parentGatewaysList.length > 0 ? getParentGateway(parentGatewaysList[0].id) : null; return ( @@ -1482,14 +1544,17 @@ function NetworkConfigManager() { {gatewayType.label} - {parentGateway && ( + {firstParent && (
- → {parentGateway.parentType === 'interface' - ? `Интерфейс ${parentGateway.name || parentGateway.interfaceType || 'interface'} (${parentGateway.remoteIp || '—'})` - : parentGateway.ip - ? `Gateway ${parentGateway.ip}` + → {firstParent.parentType === 'interface' + ? `Интерфейс ${firstParent.name || firstParent.interfaceType || 'interface'} (${firstParent.remoteIp || '—'})` + : firstParent.ip + ? `Gateway ${firstParent.ip}` : 'Gateway (не указан IP)' } + {parentGatewaysList.length > 1 && ( + (+{parentGatewaysList.length - 1}) + )}
)} @@ -1826,7 +1891,7 @@ function NetworkConfigManager() { name="type" type="select" value={editingGateway.type} - onChange={(val) => setEditingGateway({ ...editingGateway, type: val, parentGatewayId: val === 'direct' ? '' : editingGateway.parentGatewayId })} + onChange={(val) => setEditingGateway({ ...editingGateway, type: val, parentGateways: val === 'direct' ? [] : (editingGateway.parentGateways || []) })} options={GATEWAY_TYPES.map(t => ({ value: t.value, label: `${t.label} - ${t.description}` }))} required /> @@ -1843,17 +1908,82 @@ function NetworkConfigManager() {
{editingGateway.type === 'recursive' && (
- - setEditingGateway({ ...editingGateway, parentGatewayId: val })} - gateways={config.gateways} - interfaces={config.tunnelInterfaces} - excludeGatewayId={editingGateway.id} - placeholder="Выберите родительский gateway или интерфейс..." - /> + +
+ {(editingGateway.parentGateways || []).map((parent, index) => { + const parentInfo = getParentGateway(parent.id); + return ( +
+
+
+
+ { + const newParents = [...(editingGateway.parentGateways || [])]; + newParents[index] = { ...newParents[index], id: val }; + setEditingGateway({ ...editingGateway, parentGateways: newParents }); + }} + gateways={config.gateways} + interfaces={config.tunnelInterfaces} + excludeGatewayId={editingGateway.id} + placeholder="Выберите родительский gateway или интерфейс..." + /> +
+
+ { + const newParents = [...(editingGateway.parentGateways || [])]; + newParents[index] = { ...newParents[index], distance: val ? parseInt(val) : undefined }; + setEditingGateway({ ...editingGateway, parentGateways: newParents }); + }} + placeholder="1" + /> +
+
+ +
+
+ {parentInfo && ( +
+ {parentInfo.parentType === 'interface' + ? `Интерфейс ${parentInfo.name || parentInfo.interfaceType || 'interface'} (${parentInfo.remoteIp || '—'})` + : `Gateway ${parentInfo.ip || '—'}` + } +
+ )} +
+
+ ); + })} + +
- Рекурсивный gateway может ссылаться на прямой gateway или на интерфейс (по его remote IP) + Рекурсивный gateway может ссылаться на несколько прямых gateway или интерфейсов. Distance определяет приоритет маршрута (меньше = выше приоритет).
)}