feat(NetworkConfigManager): add support for multiple tunnel creation; implement dynamic interface naming and IP address generation for specified tunnel count
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 54s

This commit is contained in:
2026-01-22 20:14:27 +07:00
parent 2f09c9ebbb
commit e57d1316d2
+102 -38
View File
@@ -145,6 +145,7 @@ function NetworkConfigManager() {
const [templateName2, setTemplateName2] = useState(''); // Опциональное имя для сервера 2
const [templateIpsecPasswordId, setTemplateIpsecPasswordId] = useState(''); // Выбранный IPSec пароль
const [templateIpPoolId, setTemplateIpPoolId] = useState(''); // Выбранный IP пул
const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания
// === IPSec Passwords Management ===
const [ipsecPasswords, setIpsecPasswords] = useState([]);
@@ -518,6 +519,8 @@ function NetworkConfigManager() {
return;
}
const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100
const server1 = getServerInfo(templateServer1);
const server2 = getServerInfo(templateServer2);
@@ -526,50 +529,93 @@ function NetworkConfigManager() {
return;
}
// Генерируем имена интерфейсов (используем опциональные имена если указаны)
const name1 = templateName1.trim() || generateInterfaceName(server1, server2, templateType, templateNamePrefix);
const name2 = templateName2.trim() || generateInterfaceName(server2, server1, templateType, templateNamePrefix);
const newInterfaces = [];
const usedIps = new Set(getAllUsedIpsSet);
// Генерируем IP адреса
const localIp = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
if (!localIp) {
notify.error('Не удалось найти свободный Local IP адрес');
return;
// Создаем указанное количество туннелей
for (let i = 0; i < tunnelCount; i++) {
// Генерируем имена интерфейсов
// Если указаны имена вручную, используем их (для первого туннеля) или добавляем номер
let name1, name2;
if (templateName1.trim() && i === 0) {
name1 = templateName1.trim();
} else if (templateName1.trim()) {
name1 = `${templateName1.trim()}-${i + 1}`;
} else {
const baseName = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
name1 = tunnelCount > 1 ? `${baseName}-${i + 1}` : baseName;
}
if (templateName2.trim() && i === 0) {
name2 = templateName2.trim();
} else if (templateName2.trim()) {
name2 = `${templateName2.trim()}-${i + 1}`;
} else {
const baseName = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
name2 = tunnelCount > 1 ? `${baseName}-${i + 1}` : baseName;
}
// Генерируем IP адреса (учитываем уже использованные в этой итерации)
let localIp = null;
let remoteIp = null;
let attempts = 0;
const maxAttempts = 1000;
while (!localIp && attempts < maxAttempts) {
const candidateLocal = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
if (candidateLocal && !usedIps.has(candidateLocal)) {
const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, templateIpPoolId || null);
if (candidateRemote && !usedIps.has(candidateRemote)) {
localIp = candidateLocal;
remoteIp = candidateRemote;
usedIps.add(localIp);
usedIps.add(remoteIp);
break;
}
}
attempts++;
}
if (!localIp || !remoteIp) {
notify.error(`Не удалось найти свободные IP адреса для туннеля ${i + 1}`);
return;
}
// Создаем интерфейс
const newInterface = {
...getEmptyInterface(),
name: name1,
name2: name2,
type: templateType,
localIp: localIp,
remoteIp: remoteIp,
serverId: templateServer1,
serverId2: templateServer2,
ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && templateIpsecPasswordId ? templateIpsecPasswordId : '',
ipPoolId: templateIpPoolId || ''
};
// Проверяем конфликты с существующими интерфейсами
const conflicts = checkInterfaceIpConflict(newInterface);
if (conflicts.length > 0) {
notify.error(`Обнаружены конфликты IP адресов для туннеля ${i + 1}. Попробуйте еще раз.`);
return;
}
newInterfaces.push(newInterface);
}
const remoteIp = generateFreePrivateIp(null, true, localIp, templateIpPoolId || null);
if (!remoteIp) {
notify.error('Не удалось найти свободный Remote IP адрес');
return;
}
// Проверяем конфликты
const newInterface = {
...getEmptyInterface(),
name: name1,
name2: name2,
type: templateType,
localIp: localIp,
remoteIp: remoteIp,
serverId: templateServer1,
serverId2: templateServer2,
ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && templateIpsecPasswordId ? templateIpsecPasswordId : '',
ipPoolId: templateIpPoolId || ''
};
const conflicts = checkInterfaceIpConflict(newInterface);
if (conflicts.length > 0) {
notify.error('Обнаружены конфликты IP адресов. Попробуйте еще раз.');
return;
}
// Создаем интерфейс
// Добавляем все созданные интерфейсы
setConfig(prev => ({
...prev,
tunnelInterfaces: [...prev.tunnelInterfaces, newInterface],
tunnelInterfaces: [...prev.tunnelInterfaces, ...newInterfaces],
}));
notify.success(`Интерфейс создан: ${name1} (${localIp}${remoteIp})`);
if (tunnelCount === 1) {
notify.success(`Интерфейс создан: ${newInterfaces[0].name} (${newInterfaces[0].localIp}${newInterfaces[0].remoteIp})`);
} else {
notify.success(`Создано туннелей: ${tunnelCount}`);
}
// Закрываем модал и сбрасываем значения
setInterfaceTemplateModalOpen(false);
@@ -581,6 +627,7 @@ function NetworkConfigManager() {
setTemplateName2('');
setTemplateIpsecPasswordId('');
setTemplateIpPoolId('');
setTemplateTunnelCount(1);
};
// === CRUD для Interfaces ===
@@ -3303,14 +3350,31 @@ function NetworkConfigManager() {
setTemplateName1('');
setTemplateName2('');
setTemplateIpsecPasswordId('');
setTemplateIpPoolId('');
setTemplateTunnelCount(1);
}}
onSubmit={handleCreateInterfaceFromTemplate}
title="Создать интерфейс из шаблона"
submitLabel="Создать интерфейс"
submitLabel={templateTunnelCount > 1 ? `Создать ${templateTunnelCount} туннелей` : "Создать интерфейс"}
submitIcon={IconSparkles}
size="lg"
>
<div className="row g-3">
<div className="col-md-6">
<label className="form-label required">Количество туннелей</label>
<input
type="number"
className="form-control"
value={templateTunnelCount}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setTemplateTunnelCount(Math.max(1, Math.min(100, value)));
}}
min="1"
max="100"
/>
<div className="form-text">Сколько туннелей создать между выбранными серверами (1-100)</div>
</div>
<div className="col-md-6">
<label className="form-label required">Тип интерфейса</label>
<FormField