feat(NetworkConfigManager): enhance tunnel creation logic to support multiple tunnels with dynamic server pairing and improved IP address generation; update UI to reflect changes in tunnel count and server selection
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m46s

This commit is contained in:
2026-01-22 20:37:04 +07:00
parent 84b4f7e795
commit 297ea4af34
+152 -51
View File
@@ -509,63 +509,67 @@ function NetworkConfigManager() {
// === Создание интерфейса из шаблона === // === Создание интерфейса из шаблона ===
const handleCreateInterfaceFromTemplate = () => { const handleCreateInterfaceFromTemplate = () => {
if (!templateServer1 || !templateServer2) {
notify.error('Выберите оба сервера');
return;
}
if (templateServer1 === templateServer2) {
notify.error('Серверы должны быть разными');
return;
}
const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100 const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100
const server1 = getServerInfo(templateServer1); // Если количество туннелей больше 1, создаем туннели между разными парами серверов
const server2 = getServerInfo(templateServer2); if (tunnelCount > 1) {
// Получаем список всех серверов
const availableServers = servers.filter(s => s.id && s.ip);
if (!server1 || !server2) { if (availableServers.length < 2) {
notify.error('Не удалось найти информацию о серверах'); notify.error('Недостаточно серверов для создания нескольких туннелей (нужно минимум 2)');
return; 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]);
}
}
if (serverPairs.length < tunnelCount) {
notify.error(`Недостаточно пар серверов для создания ${tunnelCount} туннелей. Доступно пар: ${serverPairs.length}`);
return;
}
// Берем первые N пар
const pairsToUse = serverPairs.slice(0, tunnelCount);
const newInterfaces = []; const newInterfaces = [];
const usedIps = new Set(getAllUsedIpsSet); const usedIps = new Set(getAllUsedIpsSet);
// Создаем указанное количество туннелей // Создаем туннели для каждой пары
for (let i = 0; i < tunnelCount; i++) { pairsToUse.forEach((pair, index) => {
// Генерируем имена интерфейсов const server1Id = pair[0];
// Если указаны имена вручную, используем их (для первого туннеля) или добавляем номер const server2Id = pair[1];
let name1, name2; const server1 = getServerInfo(server1Id);
if (templateName1.trim() && i === 0) { const server2 = getServerInfo(server2Id);
name1 = templateName1.trim();
} else if (templateName1.trim()) { if (!server1 || !server2) {
name1 = `${templateName1.trim()}-${i + 1}`; notify.error(`Не удалось найти информацию о серверах для пары ${index + 1}`);
} else { return;
const baseName = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
name1 = tunnelCount > 1 ? `${baseName}-${i + 1}` : baseName;
} }
if (templateName2.trim() && i === 0) { // Генерируем имена интерфейсов
name2 = templateName2.trim(); const baseName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
} else if (templateName2.trim()) { const baseName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
name2 = `${templateName2.trim()}-${i + 1}`; const name1 = templateName1.trim() ? `${templateName1.trim()}-${index + 1}` : baseName1;
} else { const name2 = templateName2.trim() ? `${templateName2.trim()}-${index + 1}` : baseName2;
const baseName = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
name2 = tunnelCount > 1 ? `${baseName}-${i + 1}` : baseName;
}
// Генерируем IP адреса (учитываем уже использованные в этой итерации) // Генерируем IP адреса (учитываем уже использованные в этой итерации)
// Важно: Local IP и Remote IP должны быть разными и в одной сети /30
let localIp = null; let localIp = null;
let remoteIp = null; let remoteIp = null;
let attempts = 0; let attempts = 0;
const maxAttempts = 1000; const maxAttempts = 1000;
while (!localIp && attempts < maxAttempts) { while ((!localIp || !remoteIp || localIp === remoteIp) && attempts < maxAttempts) {
const candidateLocal = generateFreePrivateIp(null, false, null, templateIpPoolId || null); const candidateLocal = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
if (candidateLocal && !usedIps.has(candidateLocal)) { if (candidateLocal && !usedIps.has(candidateLocal)) {
const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, templateIpPoolId || null); const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, templateIpPoolId || null);
if (candidateRemote && !usedIps.has(candidateRemote)) { if (candidateRemote && !usedIps.has(candidateRemote) && candidateRemote !== candidateLocal) {
localIp = candidateLocal; localIp = candidateLocal;
remoteIp = candidateRemote; remoteIp = candidateRemote;
usedIps.add(localIp); usedIps.add(localIp);
@@ -576,12 +580,98 @@ function NetworkConfigManager() {
attempts++; attempts++;
} }
if (!localIp || !remoteIp) { if (!localIp || !remoteIp || localIp === remoteIp) {
notify.error(`Не удалось найти свободные IP адреса для туннеля ${i + 1}`); notify.error(`Не удалось найти свободные IP адреса для туннеля ${index + 1} (${server1.dns || server1.ip} ↔ ${server2.dns || server2.ip})`);
return; return;
} }
// Создаем интерфейс // Создаем интерфейс
const newInterface = {
...getEmptyInterface(),
name: name1,
name2: name2,
type: templateType,
localIp: localIp,
remoteIp: remoteIp,
serverId: server1Id,
serverId2: server2Id,
ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && templateIpsecPasswordId ? templateIpsecPasswordId : '',
ipPoolId: templateIpPoolId || ''
};
// Проверяем конфликты с существующими интерфейсами
const conflicts = checkInterfaceIpConflict(newInterface);
if (conflicts.length > 0) {
notify.error(`Обнаружены конфликты IP адресов для туннеля ${index + 1}. Попробуйте еще раз.`);
return;
}
newInterfaces.push(newInterface);
});
if (newInterfaces.length === 0) {
return; // Ошибки уже показаны выше
}
// Добавляем все созданные интерфейсы
setConfig(prev => ({
...prev,
tunnelInterfaces: [...prev.tunnelInterfaces, ...newInterfaces],
}));
notify.success(`Создано туннелей: ${newInterfaces.length} между разными парами серверов`);
// Закрываем модал и сбрасываем значения
setInterfaceTemplateModalOpen(false);
setTemplateServer1('');
setTemplateServer2('');
setTemplateType('GRE');
setTemplateNamePrefix('');
setTemplateName1('');
setTemplateName2('');
setTemplateIpsecPasswordId('');
setTemplateIpPoolId('');
setTemplateTunnelCount(1);
return;
}
// Если количество туннелей = 1, используем старую логику с выбором двух серверов
if (!templateServer1 || !templateServer2) {
notify.error('Выберите оба сервера');
return;
}
if (templateServer1 === templateServer2) {
notify.error('Серверы должны быть разными');
return;
}
const server1 = getServerInfo(templateServer1);
const server2 = getServerInfo(templateServer2);
if (!server1 || !server2) {
notify.error('Не удалось найти информацию о серверах');
return;
}
// Генерируем имена интерфейсов (используем опциональные имена если указаны)
const name1 = templateName1.trim() || generateInterfaceName(server1, server2, templateType, templateNamePrefix);
const name2 = templateName2.trim() || generateInterfaceName(server2, server1, templateType, templateNamePrefix);
// Генерируем IP адреса
const localIp = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
if (!localIp) {
notify.error('Не удалось найти свободный Local IP адрес');
return;
}
const remoteIp = generateFreePrivateIp(null, true, localIp, templateIpPoolId || null);
if (!remoteIp || remoteIp === localIp) {
notify.error('Не удалось найти свободный Remote IP адрес (отличающийся от Local IP)');
return;
}
// Проверяем конфликты
const newInterface = { const newInterface = {
...getEmptyInterface(), ...getEmptyInterface(),
name: name1, name: name1,
@@ -595,27 +685,19 @@ function NetworkConfigManager() {
ipPoolId: templateIpPoolId || '' ipPoolId: templateIpPoolId || ''
}; };
// Проверяем конфликты с существующими интерфейсами
const conflicts = checkInterfaceIpConflict(newInterface); const conflicts = checkInterfaceIpConflict(newInterface);
if (conflicts.length > 0) { if (conflicts.length > 0) {
notify.error(`Обнаружены конфликты IP адресов для туннеля ${i + 1}. Попробуйте еще раз.`); notify.error('Обнаружены конфликты IP адресов. Попробуйте еще раз.');
return; return;
} }
newInterfaces.push(newInterface); // Создаем интерфейс
}
// Добавляем все созданные интерфейсы
setConfig(prev => ({ setConfig(prev => ({
...prev, ...prev,
tunnelInterfaces: [...prev.tunnelInterfaces, ...newInterfaces], tunnelInterfaces: [...prev.tunnelInterfaces, newInterface],
})); }));
if (tunnelCount === 1) { notify.success(`Интерфейс создан: ${name1} (${localIp} ↔ ${remoteIp})`);
notify.success(`Интерфейс создан: ${newInterfaces[0].name} (${newInterfaces[0].localIp} ↔ ${newInterfaces[0].remoteIp})`);
} else {
notify.success(`Создано туннелей: ${tunnelCount}`);
}
// Закрываем модал и сбрасываем значения // Закрываем модал и сбрасываем значения
setInterfaceTemplateModalOpen(false); setInterfaceTemplateModalOpen(false);
@@ -3236,7 +3318,11 @@ function NetworkConfigManager() {
min="1" min="1"
max="100" max="100"
/> />
<div className="form-text">Сколько туннелей создать между выбранными серверами (1-100)</div> <div className="form-text">
{templateTunnelCount > 1
? `Будет создано ${templateTunnelCount} туннелей между разными парами серверов (каждый с каждым)`
: 'Сколько туннелей создать между выбранными серверами (1-100)'}
</div>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label required">Тип интерфейса</label> <label className="form-label required">Тип интерфейса</label>
@@ -3259,6 +3345,8 @@ function NetworkConfigManager() {
/> />
<div className="form-text">Будет добавлен перед именем интерфейса</div> <div className="form-text">Будет добавлен перед именем интерфейса</div>
</div> </div>
{templateTunnelCount === 1 && (
<>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label required">Сервер 1</label> <label className="form-label required">Сервер 1</label>
<ServerAutocompleteInput <ServerAutocompleteInput
@@ -3295,6 +3383,19 @@ function NetworkConfigManager() {
) : null; ) : null;
})()} })()}
</div> </div>
</>
)}
{templateTunnelCount > 1 && (
<div className="col-12">
<div className="alert alert-info">
<strong>Режим множественного создания:</strong> Будет создано {templateTunnelCount} туннелей между разными парами серверов.
<br />
Туннели будут созданы между всеми возможными парами серверов (каждый с каждым).
<br />
<small>Доступно серверов: {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)}</small>
</div>
</div>
)}
{templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => { {templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => {
const server1 = getServerInfo(templateServer1); const server1 = getServerInfo(templateServer1);
const server2 = getServerInfo(templateServer2); const server2 = getServerInfo(templateServer2);