diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index b9d4b50..f605331 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -146,6 +146,7 @@ function NetworkConfigManager() { const [templateIpsecPasswordId, setTemplateIpsecPasswordId] = useState(''); // Выбранный IPSec пароль const [templateIpPoolId, setTemplateIpPoolId] = useState(''); // Выбранный IP пул const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания + const [templateServerPairs, setTemplateServerPairs] = useState([]); // Массив пар серверов для множественного создания // === IPSec Passwords Management === const [ipsecPasswords, setIpsecPasswords] = useState([]); @@ -511,41 +512,36 @@ function NetworkConfigManager() { const handleCreateInterfaceFromTemplate = () => { const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100 - // Если количество туннелей больше 1, создаем туннели между разными парами серверов + // Если количество туннелей больше 1, используем выбранные пары серверов if (tunnelCount > 1) { - // Получаем список всех серверов - const availableServers = servers.filter(s => s.id && s.ip); - - if (availableServers.length < 2) { - notify.error('Недостаточно серверов для создания нескольких туннелей (нужно минимум 2)'); + // Проверяем, что все пары заполнены + if (templateServerPairs.length !== tunnelCount) { + notify.error(`Необходимо настроить ${tunnelCount} пар серверов`); return; } - // Генерируем все возможные пары серверов - const serverPairs = []; - for (let i = 0; i < availableServers.length; i++) { - for (let j = i + 1; j < availableServers.length; j++) { - serverPairs.push([availableServers[i].id, availableServers[j].id]); + const invalidPairs = templateServerPairs.filter((pair, index) => { + if (!pair.server1 || !pair.server2) { + return true; } - } + if (pair.server1 === pair.server2) { + return true; + } + return false; + }); - if (serverPairs.length < tunnelCount) { - notify.error(`Недостаточно пар серверов для создания ${tunnelCount} туннелей. Доступно пар: ${serverPairs.length}`); + if (invalidPairs.length > 0) { + notify.error('Не все пары серверов настроены корректно. Проверьте, что выбраны разные серверы для каждой пары.'); return; } - // Берем первые N пар - const pairsToUse = serverPairs.slice(0, tunnelCount); - const newInterfaces = []; const usedIps = new Set(getAllUsedIpsSet); // Создаем туннели для каждой пары - pairsToUse.forEach((pair, index) => { - const server1Id = pair[0]; - const server2Id = pair[1]; - const server1 = getServerInfo(server1Id); - const server2 = getServerInfo(server2Id); + templateServerPairs.forEach((pair, index) => { + const server1 = getServerInfo(pair.server1); + const server2 = getServerInfo(pair.server2); if (!server1 || !server2) { notify.error(`Не удалось найти информацию о серверах для пары ${index + 1}`); @@ -555,8 +551,12 @@ function NetworkConfigManager() { // Генерируем имена интерфейсов const baseName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix); const baseName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix); - const name1 = templateName1.trim() ? `${templateName1.trim()}-${index + 1}` : baseName1; - const name2 = templateName2.trim() ? `${templateName2.trim()}-${index + 1}` : baseName2; + const name1 = pair.name1?.trim() || baseName1; + const name2 = pair.name2?.trim() || baseName2; + + // Используем настройки из пары или глобальные + const pairIpsecPasswordId = pair.ipsecPasswordId || templateIpsecPasswordId; + const pairIpPoolId = pair.ipPoolId || templateIpPoolId; // Генерируем IP адреса (учитываем уже использованные в этой итерации) // Важно: Local IP и Remote IP должны быть разными и в одной сети /30 @@ -566,9 +566,9 @@ function NetworkConfigManager() { const maxAttempts = 1000; while ((!localIp || !remoteIp || localIp === remoteIp) && attempts < maxAttempts) { - const candidateLocal = generateFreePrivateIp(null, false, null, templateIpPoolId || null); + const candidateLocal = generateFreePrivateIp(null, false, null, pairIpPoolId || null); if (candidateLocal && !usedIps.has(candidateLocal)) { - const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, templateIpPoolId || null); + const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, pairIpPoolId || null); if (candidateRemote && !usedIps.has(candidateRemote) && candidateRemote !== candidateLocal) { localIp = candidateLocal; remoteIp = candidateRemote; @@ -593,10 +593,10 @@ function NetworkConfigManager() { type: templateType, localIp: localIp, remoteIp: remoteIp, - serverId: server1Id, - serverId2: server2Id, - ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && templateIpsecPasswordId ? templateIpsecPasswordId : '', - ipPoolId: templateIpPoolId || '' + serverId: pair.server1, + serverId2: pair.server2, + ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && pairIpsecPasswordId ? pairIpsecPasswordId : '', + ipPoolId: pairIpPoolId || '' }; // Проверяем конфликты с существующими интерфейсами @@ -619,7 +619,7 @@ function NetworkConfigManager() { tunnelInterfaces: [...prev.tunnelInterfaces, ...newInterfaces], })); - notify.success(`Создано туннелей: ${newInterfaces.length} между разными парами серверов`); + notify.success(`Создано туннелей: ${newInterfaces.length}`); // Закрываем модал и сбрасываем значения setInterfaceTemplateModalOpen(false); @@ -632,6 +632,7 @@ function NetworkConfigManager() { setTemplateIpsecPasswordId(''); setTemplateIpPoolId(''); setTemplateTunnelCount(1); + setTemplateServerPairs([]); return; } @@ -3297,6 +3298,7 @@ function NetworkConfigManager() { setTemplateIpsecPasswordId(''); setTemplateIpPoolId(''); setTemplateTunnelCount(1); + setTemplateServerPairs([]); }} onSubmit={handleCreateInterfaceFromTemplate} title="Создать интерфейс из шаблона" @@ -3313,14 +3315,32 @@ function NetworkConfigManager() { value={templateTunnelCount} onChange={(e) => { const value = parseInt(e.target.value) || 1; - setTemplateTunnelCount(Math.max(1, Math.min(100, value))); + const newCount = Math.max(1, Math.min(100, value)); + setTemplateTunnelCount(newCount); + // Синхронизируем количество пар + if (newCount > templateServerPairs.length) { + const newPairs = [...templateServerPairs]; + while (newPairs.length < newCount) { + newPairs.push({ + server1: '', + server2: '', + name1: '', + name2: '', + ipsecPasswordId: '', + ipPoolId: '' + }); + } + setTemplateServerPairs(newPairs); + } else if (newCount < templateServerPairs.length) { + setTemplateServerPairs(templateServerPairs.slice(0, newCount)); + } }} min="1" max="100" />
{templateTunnelCount > 1 - ? `Будет создано ${templateTunnelCount} туннелей между разными парами серверов (каждый с каждым)` + ? `Настройте ${templateTunnelCount} пар серверов ниже` : 'Сколько туннелей создать между выбранными серверами (1-100)'}
@@ -3345,7 +3365,7 @@ function NetworkConfigManager() { />
Будет добавлен перед именем интерфейса
- {templateTunnelCount === 1 && ( + {(templateTunnelCount === 1) && ( <>
@@ -3387,15 +3407,274 @@ function NetworkConfigManager() { )} {templateTunnelCount > 1 && (
-
- Режим множественного создания: Будет создано {templateTunnelCount} туннелей между разными парами серверов. -
- Туннели будут созданы между всеми возможными парами серверов (каждый с каждым). -
- Доступно серверов: {servers.filter(s => s.id && s.ip).length}. Максимум пар: {Math.floor((servers.filter(s => s.id && s.ip).length * (servers.filter(s => s.id && s.ip).length - 1)) / 2)} + +
+ {Array.from({ length: templateTunnelCount }).map((_, index) => { + const pair = templateServerPairs[index] || { + server1: '', + server2: '', + name1: '', + name2: '', + ipsecPasswordId: '', + ipPoolId: '' + }; + + const updatePair = (updates) => { + const newPairs = [...templateServerPairs]; + while (newPairs.length < templateTunnelCount) { + newPairs.push({ + server1: '', + server2: '', + name1: '', + name2: '', + ipsecPasswordId: '', + ipPoolId: '' + }); + } + newPairs[index] = { ...pair, ...updates }; + setTemplateServerPairs(newPairs.slice(0, templateTunnelCount)); + }; + + const server1 = getServerInfo(pair.server1); + const server2 = getServerInfo(pair.server2); + + return ( +
+
+ Туннель {index + 1} +
+
+
+
+ + updatePair({ server1: val })} + servers={servers.filter(s => s.id !== pair.server2 && s.ip !== pair.server2)} + placeholder="Выберите первый сервер..." + /> + {server1 && ( +
+ {server1.dns || server1.ip} + {server1.country && ` • ${countryToFlag(server1.country)} ${server1.country}`} +
+ )} +
+
+ + updatePair({ server2: val })} + servers={servers.filter(s => s.id !== pair.server1 && s.ip !== pair.server1)} + placeholder="Выберите второй сервер..." + /> + {server2 && ( +
+ {server2.dns || server2.ip} + {server2.country && ` • ${countryToFlag(server2.country)} ${server2.country}`} +
+ )} +
+
+ + updatePair({ name1: e.target.value })} + placeholder={server1 && server2 ? generateInterfaceName(server1, server2, templateType, templateNamePrefix) : ''} + /> +
Оставьте пустым для автогенерации
+
+
+ + updatePair({ name2: e.target.value })} + placeholder={server2 && server1 ? generateInterfaceName(server2, server1, templateType, templateNamePrefix) : ''} + /> +
Оставьте пустым для автогенерации
+
+ {(templateType === 'IPSec' || templateType === 'GRE') && ( +
+ + +
+ )} +
+ + +
+
+
+
+ ); + })}
)} + {(templateTunnelCount === 1) && ( + <> +
+ + setTemplateName1(e.target.value)} + placeholder={templateServer1 && templateServer2 ? (() => { + const s1 = getServerInfo(templateServer1); + const s2 = getServerInfo(templateServer2); + return s1 && s2 ? generateInterfaceName(s1, s2, templateType, templateNamePrefix) : ''; + })() : ''} + /> +
Оставьте пустым для автогенерации
+
+
+ + setTemplateName2(e.target.value)} + placeholder={templateServer1 && templateServer2 ? (() => { + const s1 = getServerInfo(templateServer1); + const s2 = getServerInfo(templateServer2); + return s1 && s2 ? generateInterfaceName(s2, s1, templateType, templateNamePrefix) : ''; + })() : ''} + /> +
Оставьте пустым для автогенерации
+
+ + )} + {(templateType === 'IPSec' || templateType === 'GRE') && ( +
+ +
+ + +
+
+ Выберите сохраненный IPSec пароль или создайте новый (опционально для GRE). Будет использован для всех туннелей, если не указан индивидуально. +
+
+ )} +
+ + +
+ Выберите IP пул для подбора адресов или оставьте автоматический подбор. Будет использован для всех туннелей, если не указан индивидуально. +
+
+ {templateTunnelCount === 1 && templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => { + const server1 = getServerInfo(templateServer1); + const server2 = getServerInfo(templateServer2); + if (!server1 || !server2) return null; + + const autoName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix); + const autoName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix); + const previewName1 = templateName1.trim() || autoName1; + const previewName2 = templateName2.trim() || autoName2; + const previewLocalIp = generateFreePrivateIp(null, false, null, templateIpPoolId || null); + const previewRemoteIp = previewLocalIp ? generateFreePrivateIp(null, true, previewLocalIp, templateIpPoolId || null) : null; + + return ( + <> +
+
+
+ Предпросмотр интерфейса: +
+
+
+
+
+ Сервер 1 ({server1.dns || server1.ip}): +
+
Имя: {previewName1}
+
Local IP: {previewLocalIp || '—'}
+
+
+
+
+
+ Сервер 2 ({server2.dns || server2.ip}): +
+
Имя: {previewName2}
+
Local IP: {previewRemoteIp || '—'}
+
+
+
+ {previewLocalIp && previewRemoteIp && ( +
+ + {previewLocalIp} ↔ {previewRemoteIp} +
+ )} +
+
+
+
+ + ); + })()} {templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => { const server1 = getServerInfo(templateServer1); const server2 = getServerInfo(templateServer2);