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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m46s
This commit is contained in:
@@ -509,6 +509,133 @@ function NetworkConfigManager() {
|
|||||||
|
|
||||||
// === Создание интерфейса из шаблона ===
|
// === Создание интерфейса из шаблона ===
|
||||||
const handleCreateInterfaceFromTemplate = () => {
|
const handleCreateInterfaceFromTemplate = () => {
|
||||||
|
const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100
|
||||||
|
|
||||||
|
// Если количество туннелей больше 1, создаем туннели между разными парами серверов
|
||||||
|
if (tunnelCount > 1) {
|
||||||
|
// Получаем список всех серверов
|
||||||
|
const availableServers = servers.filter(s => s.id && s.ip);
|
||||||
|
|
||||||
|
if (availableServers.length < 2) {
|
||||||
|
notify.error('Недостаточно серверов для создания нескольких туннелей (нужно минимум 2)');
|
||||||
|
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 usedIps = new Set(getAllUsedIpsSet);
|
||||||
|
|
||||||
|
// Создаем туннели для каждой пары
|
||||||
|
pairsToUse.forEach((pair, index) => {
|
||||||
|
const server1Id = pair[0];
|
||||||
|
const server2Id = pair[1];
|
||||||
|
const server1 = getServerInfo(server1Id);
|
||||||
|
const server2 = getServerInfo(server2Id);
|
||||||
|
|
||||||
|
if (!server1 || !server2) {
|
||||||
|
notify.error(`Не удалось найти информацию о серверах для пары ${index + 1}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем имена интерфейсов
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Генерируем IP адреса (учитываем уже использованные в этой итерации)
|
||||||
|
// Важно: Local IP и Remote IP должны быть разными и в одной сети /30
|
||||||
|
let localIp = null;
|
||||||
|
let remoteIp = null;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 1000;
|
||||||
|
|
||||||
|
while ((!localIp || !remoteIp || localIp === remoteIp) && 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) && candidateRemote !== candidateLocal) {
|
||||||
|
localIp = candidateLocal;
|
||||||
|
remoteIp = candidateRemote;
|
||||||
|
usedIps.add(localIp);
|
||||||
|
usedIps.add(remoteIp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localIp || !remoteIp || localIp === remoteIp) {
|
||||||
|
notify.error(`Не удалось найти свободные IP адреса для туннеля ${index + 1} (${server1.dns || server1.ip} ↔ ${server2.dns || server2.ip})`);
|
||||||
|
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) {
|
if (!templateServer1 || !templateServer2) {
|
||||||
notify.error('Выберите оба сервера');
|
notify.error('Выберите оба сервера');
|
||||||
return;
|
return;
|
||||||
@@ -519,8 +646,6 @@ function NetworkConfigManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100
|
|
||||||
|
|
||||||
const server1 = getServerInfo(templateServer1);
|
const server1 = getServerInfo(templateServer1);
|
||||||
const server2 = getServerInfo(templateServer2);
|
const server2 = getServerInfo(templateServer2);
|
||||||
|
|
||||||
@@ -529,93 +654,50 @@ function NetworkConfigManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newInterfaces = [];
|
// Генерируем имена интерфейсов (используем опциональные имена если указаны)
|
||||||
const usedIps = new Set(getAllUsedIpsSet);
|
const name1 = templateName1.trim() || generateInterfaceName(server1, server2, templateType, templateNamePrefix);
|
||||||
|
const name2 = templateName2.trim() || generateInterfaceName(server2, server1, templateType, templateNamePrefix);
|
||||||
|
|
||||||
// Создаем указанное количество туннелей
|
// Генерируем IP адреса
|
||||||
for (let i = 0; i < tunnelCount; i++) {
|
const localIp = generateFreePrivateIp(null, false, null, templateIpPoolId || null);
|
||||||
// Генерируем имена интерфейсов
|
if (!localIp) {
|
||||||
// Если указаны имена вручную, используем их (для первого туннеля) или добавляем номер
|
notify.error('Не удалось найти свободный Local IP адрес');
|
||||||
let name1, name2;
|
return;
|
||||||
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 || remoteIp === localIp) {
|
||||||
|
notify.error('Не удалось найти свободный Remote IP адрес (отличающийся от Local 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 => ({
|
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,42 +3345,57 @@ function NetworkConfigManager() {
|
|||||||
/>
|
/>
|
||||||
<div className="form-text">Будет добавлен перед именем интерфейса</div>
|
<div className="form-text">Будет добавлен перед именем интерфейса</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
{templateTunnelCount === 1 && (
|
||||||
<label className="form-label required">Сервер 1</label>
|
<>
|
||||||
<ServerAutocompleteInput
|
<div className="col-md-6">
|
||||||
value={templateServer1}
|
<label className="form-label required">Сервер 1</label>
|
||||||
onChange={(val) => setTemplateServer1(val)}
|
<ServerAutocompleteInput
|
||||||
servers={servers.filter(s => s.id !== templateServer2 && s.ip !== templateServer2)}
|
value={templateServer1}
|
||||||
placeholder="Выберите первый сервер..."
|
onChange={(val) => setTemplateServer1(val)}
|
||||||
/>
|
servers={servers.filter(s => s.id !== templateServer2 && s.ip !== templateServer2)}
|
||||||
{templateServer1 && (() => {
|
placeholder="Выберите первый сервер..."
|
||||||
const server1 = getServerInfo(templateServer1);
|
/>
|
||||||
return server1 ? (
|
{templateServer1 && (() => {
|
||||||
<div className="form-text">
|
const server1 = getServerInfo(templateServer1);
|
||||||
{server1.dns || server1.ip}
|
return server1 ? (
|
||||||
{server1.country && ` • ${countryToFlag(server1.country)} ${server1.country}`}
|
<div className="form-text">
|
||||||
</div>
|
{server1.dns || server1.ip}
|
||||||
) : null;
|
{server1.country && ` • ${countryToFlag(server1.country)} ${server1.country}`}
|
||||||
})()}
|
</div>
|
||||||
</div>
|
) : null;
|
||||||
<div className="col-md-6">
|
})()}
|
||||||
<label className="form-label required">Сервер 2</label>
|
</div>
|
||||||
<ServerAutocompleteInput
|
<div className="col-md-6">
|
||||||
value={templateServer2}
|
<label className="form-label required">Сервер 2</label>
|
||||||
onChange={(val) => setTemplateServer2(val)}
|
<ServerAutocompleteInput
|
||||||
servers={servers.filter(s => s.id !== templateServer1 && s.ip !== templateServer1)}
|
value={templateServer2}
|
||||||
placeholder="Выберите второй сервер..."
|
onChange={(val) => setTemplateServer2(val)}
|
||||||
/>
|
servers={servers.filter(s => s.id !== templateServer1 && s.ip !== templateServer1)}
|
||||||
{templateServer2 && (() => {
|
placeholder="Выберите второй сервер..."
|
||||||
const server2 = getServerInfo(templateServer2);
|
/>
|
||||||
return server2 ? (
|
{templateServer2 && (() => {
|
||||||
<div className="form-text">
|
const server2 = getServerInfo(templateServer2);
|
||||||
{server2.dns || server2.ip}
|
return server2 ? (
|
||||||
{server2.country && ` • ${countryToFlag(server2.country)} ${server2.country}`}
|
<div className="form-text">
|
||||||
</div>
|
{server2.dns || server2.ip}
|
||||||
) : null;
|
{server2.country && ` • ${countryToFlag(server2.country)} ${server2.country}`}
|
||||||
})()}
|
</div>
|
||||||
</div>
|
) : null;
|
||||||
|
})()}
|
||||||
|
</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);
|
||||||
|
|||||||
Reference in New Issue
Block a user