import { useState, useEffect, useMemo } from 'react'; import api from './lib/api.js'; import { useNotify } from './components/NotifyProvider.jsx'; import FormModal from './components/FormModal.jsx'; import FormField from './components/FormField.jsx'; import ConfirmModal from './components/ConfirmModal.jsx'; import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx'; import GatewayAutocompleteInput from './components/GatewayAutocompleteInput.jsx'; import Tooltip from './components/Tooltip.jsx'; import { countryToFlag } from './utils/serverUtils.js'; import { IconNetwork, IconPlus, IconEdit, IconTrash, IconDatabase, IconServer, IconWorld, IconRouter, IconCopy, IconSearch, IconRefresh, IconWand, IconSparkles, IconFilter, IconX, IconCircleFilled, IconLayoutGrid, IconList, IconCode, IconArrowsRightLeft, IconLock, IconChevronDown, IconChevronUp, } from '@tabler/icons-react'; /** * Менеджер сетевых настроек * Справочник IP-адресов, интерфейсов и gateway для серверов */ // Типы провайдеров gateway const GATEWAY_PROVIDERS = [ { value: 'cloudflare', label: 'Cloudflare', color: 'orange' }, { value: 'bunny', label: 'Bunny CDN', color: 'yellow' }, { value: 'fastly', label: 'Fastly', color: 'red' }, { value: 'telegram', label: 'Telegram', color: 'blue' }, { value: 'hetzner', label: 'Hetzner', color: 'green' }, { value: 'yandex', label: 'Yandex Cloud', color: 'cyan' }, { value: 'custom', label: 'Другой', color: 'secondary' }, ]; // Типы туннельных интерфейсов const INTERFACE_TYPES = [ { value: 'GRE', label: 'GRE', color: 'blue' }, { value: 'WireGuard', label: 'WireGuard', color: 'green' }, { value: 'IPSec', label: 'IPSec', color: 'yellow' }, { value: 'VXLAN', label: 'VXLAN', color: 'purple' }, { value: 'OpenVPN', label: 'OpenVPN', color: 'orange' }, ]; // Типы gateway const GATEWAY_TYPES = [ { value: 'direct', label: 'Прямой', description: 'Прямой доступ в интернет', color: 'green' }, { value: 'recursive', label: 'Рекурсивный', description: 'Ссылается на другой gateway', color: 'blue' }, ]; // Шаблоны стандартных наборов gateways const GATEWAY_TEMPLATES = [ { id: 'standard-jumphost', name: 'Стандартный набор для Jumphost', description: 'Direct Gateway + Основной + EU-Шлюзы', gateways: [ { type: 'direct', description: 'Direct Gateway', ipTemplate: '{baseIp}.1', // Будет заменено на реальный IP country: '', }, { type: 'recursive', description: 'Основной', ipTemplate: '{baseIp}.1', country: '', parentGateways: [], // Будет заполнено пользователем }, { type: 'recursive', description: 'EU-Шлюз (VEESP/HIPHOS)', ipTemplate: '{baseIp}.2', country: 'SE', parentGateways: [], }, { type: 'recursive', description: 'EU-Шлюз (FIN)', ipTemplate: '{baseIp}.3', country: 'FI', parentGateways: [], }, ], }, { id: 'minimal-set', name: 'Минимальный набор', description: 'Direct Gateway + Основной', gateways: [ { type: 'direct', description: 'Direct Gateway', ipTemplate: '{baseIp}.1', country: '', }, { type: 'recursive', description: 'Основной', ipTemplate: '{baseIp}.1', country: '', parentGateways: [], }, ], }, ]; // Пустые объекты const getEmptyGateway = () => ({ id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, ip: '', description: '', serverId: '', country: '', type: 'direct', parentGateways: [], // Для рекурсивных - массив родительских gateway с distance [{id, distance}] }); const getEmptyInterface = () => ({ id: `if-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, name: '', name2: '', // Имя интерфейса на втором сервере (опционально) type: 'GRE', localIp: '', remoteIp: '', serverId: '', serverId2: '', // Второй сервер (опциональный) ipsecPasswordId: '', // ID IPSec пароля (опционально, только для IPSec) ipPoolId: '', // ID IP пула для выбора IP адресов (опционально) mtu: '', // MTU (опционально) ptrZoneReplaceFrom: '', // Что заменить в DNS домене для PTR зоны (например, "rt.shx") ptrZoneReplaceTo: '', // На что заменить (например, "shrt") }); const getEmptyIpPool = () => ({ id: `pool-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, name: '', cidr: '', description: '', }); const getDefaultConfig = () => ({ gateways: [], tunnelInterfaces: [], ipPools: [], }); function NetworkConfigManager() { const notify = useNotify(); // === Основные данные === const [config, setConfig] = useState(getDefaultConfig()); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); // === UI State === const [activeTab, setActiveTab] = useState('gateways'); const [searchTerm, setSearchTerm] = useState(''); const [providerFilter, setProviderFilter] = useState(''); const [serverFilter, setServerFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' const [ipRegistrySearch, setIpRegistrySearch] = useState(''); // Поиск в реестре IP const [expandedCategories, setExpandedCategories] = useState(new Set(['home', 'jumphost', 'exit'])); // Развернутые категории аккордеонов // === Modals === const [gatewayModalOpen, setGatewayModalOpen] = useState(false); const [gatewayModalMode, setGatewayModalMode] = useState('add'); const [editingGateway, setEditingGateway] = useState(null); const [interfaceModalOpen, setInterfaceModalOpen] = useState(false); const [interfaceModalMode, setInterfaceModalMode] = useState('add'); const [editingInterface, setEditingInterface] = useState(null); const [poolModalOpen, setPoolModalOpen] = useState(false); const [poolModalMode, setPoolModalMode] = useState('add'); const [editingPool, setEditingPool] = useState(null); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); const [deleteType, setDeleteType] = useState(''); // === Gateway Template Modal === const [gatewayTemplateModalOpen, setGatewayTemplateModalOpen] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(null); const [templateServerId, setTemplateServerId] = useState(''); const [templateBaseIp, setTemplateBaseIp] = useState(''); // Базовый IP для генерации (например, 10.9.9) const [templateGateways, setTemplateGateways] = useState([]); // Массив gateways из шаблона с возможностью редактирования // === Interface Template Modal === const [interfaceTemplateModalOpen, setInterfaceTemplateModalOpen] = useState(false); const [templateServer1, setTemplateServer1] = useState(''); const [templateServer2, setTemplateServer2] = useState(''); const [templateType, setTemplateType] = useState('GRE'); const [templateNamePrefix, setTemplateNamePrefix] = useState(''); const [templateName1, setTemplateName1] = useState(''); // Опциональное имя для сервера 1 const [templateName2, setTemplateName2] = useState(''); // Опциональное имя для сервера 2 const [templateIpsecPasswordId, setTemplateIpsecPasswordId] = useState(''); // Выбранный IPSec пароль const [templateIpPoolId, setTemplateIpPoolId] = useState(''); // Выбранный IP пул const [templateMtu, setTemplateMtu] = useState(''); // MTU для шаблона const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания const [templateServerPairs, setTemplateServerPairs] = useState([]); // Массив пар серверов для множественного создания // === IPSec Passwords Management === const [ipsecPasswords, setIpsecPasswords] = useState([]); const [ipsecPasswordModalOpen, setIpsecPasswordModalOpen] = useState(false); const [ipsecPasswordsListModalOpen, setIpsecPasswordsListModalOpen] = useState(false); const [editingIpsecPassword, setEditingIpsecPassword] = useState(null); const [ipsecPasswordModalMode, setIpsecPasswordModalMode] = useState('add'); // 'add' | 'edit' // === MikroTik Code Generation === const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false); const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]); // === Загрузка данных === useEffect(() => { fetchConfig(); fetchServers(); fetchIpsecPasswords(); }, []); const fetchConfig = async () => { setLoading(true); 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(migratedGateways) ? migratedGateways : [], tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], }); } catch (error) { if (error.response?.status !== 404) { console.error('Error fetching network config:', error); notify.error('Не удалось загрузить сетевые настройки'); } setConfig(getDefaultConfig()); } finally { setLoading(false); } }; const fetchServers = async () => { try { const response = await api.get('/servers'); setServers(Array.isArray(response.data) ? response.data : []); } catch (error) { console.error('Error fetching servers:', error); } }; const fetchIpsecPasswords = async () => { try { const response = await api.get('/ipsec-passwords'); setIpsecPasswords(Array.isArray(response.data) ? response.data : []); } catch (error) { console.error('Error fetching IPSec passwords:', error); setIpsecPasswords([]); } }; // === Сохранение === const handleSave = async () => { setSaving(true); try { await api.post('/network-config', { domains: config }); notify.success('Сетевые настройки сохранены'); } catch (error) { console.error('Error saving network config:', error); notify.error('Не удалось сохранить настройки'); } finally { setSaving(false); } }; // === Получение информации о сервере === const getServerInfo = (serverId) => { if (!serverId) return null; return servers.find(s => s.id === serverId || s.ip === serverId || s.dns === serverId); }; const getServerLabel = (serverId) => { const server = getServerInfo(serverId); if (!server) return serverId || '—'; return server.dns || server.ip; }; // === Определение категории сервера === const getServerCategory = (serverId) => { if (!serverId || serverId === '__unassigned__') return 'other'; const server = getServerInfo(serverId); if (!server) return 'other'; const serverType = server.type?.toLowerCase(); if (serverType === 'home') return 'home'; if (serverType === 'jumphost') return 'jumphost'; if (serverType === 'exit') return 'exit'; return 'other'; }; // === Получение названия категории === const getCategoryLabel = (category) => { switch (category) { case 'home': return 'Домашние (входные роутеры)'; case 'jumphost': return 'Jumphosts'; case 'exit': return 'Exit nodes'; default: return 'Прочие'; } }; // === Переключение аккордеона категории === const toggleCategory = (category) => { setExpandedCategories(prev => { const newSet = new Set(prev); if (newSet.has(category)) { newSet.delete(category); } else { newSet.add(category); } return newSet; }); }; // === Уникальные серверы для фильтра === const uniqueServersInConfig = useMemo(() => { const allServerIds = new Set([ ...(config.gateways || []).map(g => g.serverId).filter(Boolean), ...(config.tunnelInterfaces || []).map(i => i.serverId).filter(Boolean), ...(config.ipPools || []).map(p => p.serverId).filter(Boolean), ]); return Array.from(allServerIds).map(id => ({ id, label: getServerLabel(id), server: getServerInfo(id), })); }, [config.gateways, config.tunnelInterfaces, config.ipPools, servers]); // === Фильтрация === const filteredGateways = useMemo(() => { let result = [...(config.gateways || [])]; if (providerFilter) { // Фильтруем по провайдеру сервера result = result.filter(g => { const server = getServerInfo(g.serverId); return server?.provider === providerFilter; }); } if (serverFilter) { result = result.filter(g => g.serverId === serverFilter); } if (gatewayTypeFilter) { result = result.filter(g => g.type === gatewayTypeFilter); } if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(g => { const server = getServerInfo(g.serverId); return ( g.ip?.toLowerCase().includes(term) || g.country?.toLowerCase().includes(term) || server?.country?.toLowerCase().includes(term) || server?.provider?.toLowerCase().includes(term) || g.description?.toLowerCase().includes(term) || getServerLabel(g.serverId).toLowerCase().includes(term) ); }); } return result; }, [config.gateways, providerFilter, serverFilter, typeFilter, searchTerm, servers]); const filteredInterfaces = useMemo(() => { let result = [...(config.tunnelInterfaces || [])]; if (typeFilter) { result = result.filter(i => i.type === typeFilter); } if (serverFilter) { result = result.filter(i => i.serverId === serverFilter); } if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(i => i.name?.toLowerCase().includes(term) || i.localIp?.toLowerCase().includes(term) || i.remoteIp?.toLowerCase().includes(term) || i.type?.toLowerCase().includes(term) || getServerLabel(i.serverId).toLowerCase().includes(term) ); } return result; }, [config.tunnelInterfaces, typeFilter, serverFilter, searchTerm, servers]); const filteredPools = useMemo(() => { let result = [...(config.ipPools || [])]; if (serverFilter) { result = result.filter(p => p.serverId === serverFilter); } if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(p => { const server = getServerInfo(p.serverId); return ( p.name?.toLowerCase().includes(term) || p.cidr?.toLowerCase().includes(term) || p.description?.toLowerCase().includes(term) || getServerLabel(p.serverId).toLowerCase().includes(term) ); }); } return result; }, [config.ipPools, serverFilter, searchTerm, servers]); // === Группировка по серверам === const gatewaysByServer = useMemo(() => { const grouped = {}; filteredGateways.forEach(gw => { const key = gw.serverId || '__unassigned__'; if (!grouped[key]) grouped[key] = []; grouped[key].push(gw); }); return grouped; }, [filteredGateways]); const interfacesByServer = useMemo(() => { const grouped = {}; filteredInterfaces.forEach(iface => { // Группируем по первому серверу const key1 = iface.serverId || '__unassigned__'; if (!grouped[key1]) grouped[key1] = []; grouped[key1].push(iface); // Если есть второй сервер, также добавляем в его группу if (iface.serverId2) { const key2 = iface.serverId2; if (!grouped[key2]) grouped[key2] = []; grouped[key2].push(iface); } }); return grouped; }, [filteredInterfaces]); // === Группировка по категориям серверов === const gatewaysByCategory = useMemo(() => { const grouped = { home: {}, jumphost: {}, exit: {}, other: {} }; Object.entries(gatewaysByServer).forEach(([serverId, gateways]) => { const category = getServerCategory(serverId); if (!grouped[category][serverId]) { grouped[category][serverId] = []; } grouped[category][serverId] = gateways; }); return grouped; }, [gatewaysByServer, servers]); const interfacesByCategory = useMemo(() => { const grouped = { home: {}, jumphost: {}, exit: {}, other: {} }; Object.entries(interfacesByServer).forEach(([serverId, interfaces]) => { const category = getServerCategory(serverId); if (!grouped[category][serverId]) { grouped[category][serverId] = []; } grouped[category][serverId] = interfaces; }); return grouped; }, [interfacesByServer, servers]); // === Сброс фильтров === const resetFilters = () => { setSearchTerm(''); setProviderFilter(''); setServerFilter(''); setTypeFilter(''); setGatewayTypeFilter(''); }; const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter; // === Обработка шаблонов gateways === const handleOpenGatewayTemplate = () => { setSelectedTemplate(null); setTemplateServerId(''); setTemplateBaseIp(''); setTemplateGateways([]); setGatewayTemplateModalOpen(true); }; // Нормализация базового IP: если введен полный IP (4 октета), берем только первые 3 октета const normalizeBaseIp = (baseIp) => { if (!baseIp || !baseIp.trim()) return ''; const trimmed = baseIp.trim(); const parts = trimmed.split('.'); // Если 4 октета, возвращаем первые 3 if (parts.length === 4) { return parts.slice(0, 3).join('.'); } // Если 3 октета или меньше, возвращаем как есть return trimmed; }; const handleSelectTemplate = (template) => { setSelectedTemplate(template); // Инициализируем gateways из шаблона const initializedGateways = template.gateways.map((gw, index) => { let ip = ''; // Для прямых gateways используем IP сервера (если выбран сервер) if (gw.type === 'direct' && templateServerId) { const server = getServerInfo(templateServerId); if (server && server.ip) { ip = server.ip; } } // Для рекурсивных gateways генерируем из базового IP else if (gw.type === 'recursive' && templateBaseIp && gw.ipTemplate) { const normalizedBaseIp = normalizeBaseIp(templateBaseIp); if (normalizedBaseIp && normalizedBaseIp.split('.').length >= 3) { ip = gw.ipTemplate.replace('{baseIp}', normalizedBaseIp); } } return { ...gw, id: `template-gw-${index}`, ip: ip, description: gw.description || '', country: gw.country || '', serverId: templateServerId, // Сохраняем serverId для фильтрации parentGateways: gw.parentGateways ? [...gw.parentGateways] : [], }; }); setTemplateGateways(initializedGateways); }; // Обновление IP адресов при изменении базового IP или сервера useEffect(() => { if (selectedTemplate && templateGateways.length > 0) { const updatedGateways = templateGateways.map((gw, index) => { const originalTemplate = selectedTemplate.gateways[index]; // Обновляем serverId для всех gateways при изменении сервера const updatedGateway = { ...gw, serverId: templateServerId }; // Для прямых gateways используем IP сервера if (gw.type === 'direct' && templateServerId) { const server = getServerInfo(templateServerId); if (server && server.ip) { return { ...updatedGateway, ip: server.ip }; } } // Для рекурсивных gateways генерируем из базового IP else if (gw.type === 'recursive' && templateBaseIp && originalTemplate && originalTemplate.ipTemplate) { const normalizedBaseIp = normalizeBaseIp(templateBaseIp); if (normalizedBaseIp && normalizedBaseIp.split('.').length >= 3) { const templateIp = originalTemplate.ipTemplate.replace('{baseIp}', normalizedBaseIp); // Обновляем IP только если он соответствует шаблону (последний октет совпадает) или пустой const currentIpParts = gw.ip ? gw.ip.split('.') : []; const templateIpParts = templateIp.split('.'); // Если IP пустой или последний октет совпадает с шаблоном, обновляем if (!gw.ip || (currentIpParts.length === 4 && templateIpParts.length === 4 && currentIpParts[3] === templateIpParts[3])) { return { ...updatedGateway, ip: templateIp }; } } } return updatedGateway; }); // Обновляем только если есть изменения const hasChanges = updatedGateways.some((gw, i) => gw.ip !== templateGateways[i]?.ip || gw.serverId !== templateGateways[i]?.serverId); if (hasChanges) { setTemplateGateways(updatedGateways); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [templateBaseIp, templateServerId, selectedTemplate?.id]); const handleCreateGatewaysFromTemplate = () => { if (!selectedTemplate) { notify.error('Выберите шаблон'); return; } if (!templateServerId) { notify.error('Выберите сервер'); return; } if (!templateBaseIp || !templateBaseIp.trim()) { notify.error('Укажите базовый IP адрес (например, 10.9.9 или 45.8.248)'); return; } // Нормализуем базовый IP (берем первые 3 октета, если введен полный IP) const normalizedBaseIp = normalizeBaseIp(templateBaseIp); const baseIpParts = normalizedBaseIp.split('.'); if (baseIpParts.length < 3) { notify.error('Базовый IP должен содержать минимум 3 октета (например, 10.9.9)'); return; } const newGateways = []; // Для проверки конфликтов используем только IP из существующих gateways // Рекурсивные gateways могут использовать те же IP, что и интерфейсы - это нормально const usedGatewayIps = new Set(config.gateways.map(g => g.ip).filter(Boolean)); // Создаем маппинг между ID из шаблона и реальными ID после создания const templateIdToRealId = new Map(); templateGateways.forEach((gwTemplate, index) => { let ip = gwTemplate.ip || ''; // Для прямых gateways используем IP сервера if (gwTemplate.type === 'direct' && templateServerId) { const server = getServerInfo(templateServerId); if (server && server.ip) { ip = server.ip; } } // Для рекурсивных gateways генерируем из базового IP else if (gwTemplate.type === 'recursive' && !ip && selectedTemplate.gateways[index]?.ipTemplate) { const normalizedBaseIp = normalizeBaseIp(templateBaseIp); if (normalizedBaseIp && normalizedBaseIp.split('.').length >= 3) { ip = selectedTemplate.gateways[index].ipTemplate.replace('{baseIp}', normalizedBaseIp); } } // Если IP все еще пустой, пропускаем if (!ip || !ip.trim()) { notify.warning(`Gateway ${index + 1}: IP адрес не указан, пропущен`); return; } ip = ip.trim(); // Проверяем конфликты только с другими gateways // Для рекурсивных gateways разрешено использовать IP, которые уже используются в интерфейсах if (usedGatewayIps.has(ip)) { notify.warning(`IP ${ip} уже используется в другом gateway, пропущен`); return; } const newGatewayId = `gw-${Date.now()}-${index}-${Math.random().toString(16).slice(2, 6)}`; // Сохраняем маппинг ID templateIdToRealId.set(gwTemplate.id, newGatewayId); const newGateway = { id: newGatewayId, ip: ip, description: gwTemplate.description || '', serverId: templateServerId, country: gwTemplate.country || '', type: gwTemplate.type, parentGateways: gwTemplate.type === 'recursive' ? (gwTemplate.parentGateways || []).filter(p => p && p.id).map(p => { // Преобразуем ID родительского gateway из шаблона в реальный ID const realParentId = templateIdToRealId.get(p.id) || p.id; return { ...p, id: realParentId }; }) : [], }; newGateways.push(newGateway); // Добавляем IP в список использованных для проверки конфликтов внутри этой же операции создания usedGatewayIps.add(ip); }); if (newGateways.length === 0) { notify.error('Не удалось создать ни одного gateway'); return; } // Добавляем все gateways setConfig(prev => ({ ...prev, gateways: [...prev.gateways, ...newGateways], })); notify.success(`Создано gateways: ${newGateways.length}`); // Закрываем модал и сбрасываем значения setGatewayTemplateModalOpen(false); setSelectedTemplate(null); setTemplateServerId(''); setTemplateBaseIp(''); setTemplateGateways([]); }; // === CRUD для Gateways === const handleAddGateway = () => { setEditingGateway(getEmptyGateway()); setGatewayModalMode('add'); setGatewayModalOpen(true); }; const handleEditGateway = (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); }; const handleSaveGateway = (gatewayData) => { // Валидация: IP адрес обязателен if (!gatewayData.ip || !gatewayData.ip.trim()) { notify.error('IP адрес обязателен'); 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 === cleanedData.ip.trim()); if (existingGateway) { notify.error(`Gateway с IP ${cleanedData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, gateways: [...prev.gateways, cleanedData], })); notify.success('Gateway добавлен'); } else { // При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway const currentGateway = config.gateways.find(g => g.id === cleanedData.id); if (!currentGateway) { notify.error('Gateway не найден'); return; } // Если IP изменился, проверяем, не занят ли он другим gateway 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 === cleanedData.id ? cleanedData : g), })); notify.success('Gateway обновлён'); } setGatewayModalOpen(false); setEditingGateway(null); }; const handleDeleteGateway = (gateway) => { setItemToDelete(gateway); setDeleteType('gateway'); setDeleteModalOpen(true); }; // === Генерация имени интерфейса на основе серверов === // Для сервера 1: берем часть из DNS сервера 2 (например, msk) и имя хоста сервера 2 (ihor) // Для сервера 2: берем часть из DNS сервера 1 (например, swe) и имя хоста сервера 1 (hiphost) const generateInterfaceName = (currentServer, otherServer, type, prefix = '') => { // Парсим DNS имена const parseDnsParts = (dns) => { if (!dns) return null; const parts = dns.split('.'); if (parts.length >= 2) { return { hostname: parts[0], // первая часть (имя хоста) location: parts[1] // вторая часть (локация, например msk, swe) }; } return null; }; const currentParts = parseDnsParts(currentServer?.dns); const otherParts = parseDnsParts(otherServer?.dns); let name; if (currentParts && otherParts) { // Используем локацию из другого сервера и имя хоста из другого сервера // Например: для сервера 1 (hiphost.swe.shx.su) с сервером 2 (ihor.msk.rt.shx.su) // Имя на сервере 1: msk-ihor name = `${otherParts.location}-${otherParts.hostname}`; } else if (currentServer?.dns) { // Fallback: используем первую часть DNS name = currentServer.dns.split('.')[0]; } else if (currentServer?.ip) { // Fallback: используем IP name = currentServer.ip.split('.').join('-'); } else { name = 'server'; } const prefixPart = prefix ? `${prefix}-` : ''; return `${prefixPart}${name}`.toUpperCase(); }; // === Создание интерфейса из шаблона === const handleCreateInterfaceFromTemplate = () => { const tunnelCount = Math.max(1, Math.min(100, parseInt(templateTunnelCount) || 1)); // Ограничиваем от 1 до 100 // Если количество туннелей больше 1, используем выбранные пары серверов if (tunnelCount > 1) { // Проверяем, что все пары заполнены if (templateServerPairs.length !== tunnelCount) { notify.error(`Необходимо настроить ${tunnelCount} пар серверов`); return; } const invalidPairs = templateServerPairs.filter((pair, index) => { if (!pair.server1 || !pair.server2) { return true; } if (pair.server1 === pair.server2) { return true; } return false; }); if (invalidPairs.length > 0) { notify.error('Не все пары серверов настроены корректно. Проверьте, что выбраны разные серверы для каждой пары.'); return; } const newInterfaces = []; const usedIps = new Set(getAllUsedIpsSet); // Создаем Set с уже использованными IP из существующих интерфейсов // Создаем туннели для каждой пары templateServerPairs.forEach((pair, index) => { const server1 = getServerInfo(pair.server1); const server2 = getServerInfo(pair.server2); if (!server1 || !server2) { notify.error(`Не удалось найти информацию о серверах для пары ${index + 1}`); return; } // Генерируем имена интерфейсов const baseName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix); const baseName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix); const name1 = (pair.name1?.trim() || baseName1).toUpperCase(); const name2 = (pair.name2?.trim() || baseName2).toUpperCase(); // Используем настройки из пары или глобальные для MTU const pairMtu = pair.mtu || templateMtu; // Используем настройки из пары или глобальные const pairIpsecPasswordId = pair.ipsecPasswordId || templateIpsecPasswordId; const pairIpPoolId = pair.ipPoolId || templateIpPoolId; // Генерируем IP адреса (учитываем уже использованные в этой итерации) // Важно: Local IP и Remote IP должны быть разными и в одной сети /30 // Передаем usedIps в функцию, чтобы она учитывала IP, выделенные для предыдущих туннелей 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, pairIpPoolId || null, usedIps); if (candidateLocal && !usedIps.has(candidateLocal)) { const candidateRemote = generateFreePrivateIp(null, true, candidateLocal, pairIpPoolId || null, usedIps); 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: pair.server1, serverId2: pair.server2, ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && pairIpsecPasswordId ? pairIpsecPasswordId : '', ipPoolId: pairIpPoolId || '', mtu: pairMtu || '' }; // Проверяем конфликты с существующими интерфейсами 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(''); setTemplateMtu(''); setTemplateTunnelCount(1); setTemplateServerPairs([]); 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)).toUpperCase(); const name2 = (templateName2.trim() || generateInterfaceName(server2, server1, templateType, templateNamePrefix)).toUpperCase(); // Генерируем 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 = { ...getEmptyInterface(), name: name1, name2: name2, type: templateType, localIp: localIp, remoteIp: remoteIp, serverId: templateServer1, serverId2: templateServer2, ipsecPasswordId: (templateType === 'IPSec' || templateType === 'GRE') && templateIpsecPasswordId ? templateIpsecPasswordId : '', ipPoolId: templateIpPoolId || '', mtu: templateMtu || '' }; const conflicts = checkInterfaceIpConflict(newInterface); if (conflicts.length > 0) { notify.error('Обнаружены конфликты IP адресов. Попробуйте еще раз.'); return; } // Создаем интерфейс setConfig(prev => ({ ...prev, tunnelInterfaces: [...prev.tunnelInterfaces, newInterface], })); notify.success(`Интерфейс создан: ${name1} (${localIp} ↔ ${remoteIp})`); // Закрываем модал и сбрасываем значения setInterfaceTemplateModalOpen(false); setTemplateServer1(''); setTemplateServer2(''); setTemplateType('GRE'); setTemplateNamePrefix(''); setTemplateName1(''); setTemplateName2(''); setTemplateIpsecPasswordId(''); setTemplateIpPoolId(''); setTemplateMtu(''); setTemplateTunnelCount(1); }; // === CRUD для Interfaces === const handleAddInterface = () => { setEditingInterface(getEmptyInterface()); setInterfaceModalMode('add'); setInterfaceModalOpen(true); }; const handleEditInterface = (iface) => { setEditingInterface({ ...iface }); setInterfaceModalMode('edit'); setInterfaceModalOpen(true); }; // === Проверка пересечений IP адресов для интерфейсов === const checkInterfaceIpConflict = (ifaceData, excludeId = null) => { const conflicts = []; const allInterfaces = config.tunnelInterfaces || []; // Проверяем localIp if (ifaceData.localIp && ifaceData.localIp.trim()) { const conflictingLocal = allInterfaces.find(i => i.id !== excludeId && (i.localIp === ifaceData.localIp.trim() || i.remoteIp === ifaceData.localIp.trim()) ); if (conflictingLocal) { conflicts.push({ ip: ifaceData.localIp.trim(), type: 'localIp', conflictingInterface: conflictingLocal }); } } // Проверяем remoteIp if (ifaceData.remoteIp && ifaceData.remoteIp.trim()) { const conflictingRemote = allInterfaces.find(i => i.id !== excludeId && (i.localIp === ifaceData.remoteIp.trim() || i.remoteIp === ifaceData.remoteIp.trim()) ); if (conflictingRemote) { conflicts.push({ ip: ifaceData.remoteIp.trim(), type: 'remoteIp', conflictingInterface: conflictingRemote }); } } return conflicts; }; // === Получение множества всех используемых IP адресов === const getAllUsedIpsSet = useMemo(() => { const usedIps = new Set(); (config.tunnelInterfaces || []).forEach(iface => { if (iface.localIp && iface.localIp.trim()) { usedIps.add(iface.localIp.trim()); } if (iface.remoteIp && iface.remoteIp.trim()) { usedIps.add(iface.remoteIp.trim()); } }); return usedIps; }, [config.tunnelInterfaces]); // === Генерация свободного IP адреса из приватных диапазонов === // Генерирует IP в сетях /30 (четные базовые адреса) // Если указан ipPoolId, использует IP пул, иначе использует приватные диапазоны RFC 1918 // usedIpsOverride - опциональный Set уже использованных IP (если не передан, создается копия getAllUsedIpsSet) const generateFreePrivateIp = (excludeIp = null, forRemote = false, pairedLocalIp = null, ipPoolId = null, usedIpsOverride = null) => { // Создаем копию Set, чтобы не модифицировать оригинальный const usedIps = usedIpsOverride || new Set(getAllUsedIpsSet); if (excludeIp) { usedIps.delete(excludeIp.trim()); } // Если указан IP пул, используем его if (ipPoolId) { const pool = (config.ipPools || []).find(p => p.id === ipPoolId); if (pool && pool.cidr) { return generateIpFromPool(pool.cidr, usedIps, excludeIp, forRemote, pairedLocalIp); } } // Если подбираем Remote IP и есть Local IP, генерируем парный IP в той же сети /30 if (forRemote && pairedLocalIp) { const localIpParts = pairedLocalIp.trim().split('.'); if (localIpParts.length === 4) { const baseOctet = parseInt(localIpParts[3]); // Определяем базовый адрес сети /30 (округляем вниз до кратного 4) const networkBase = Math.floor(baseOctet / 4) * 4; const localOffset = baseOctet - networkBase; // Если Local IP уже .1, то Remote должен быть .2 // Если Local IP .2, то Remote должен быть .1 let remoteOffset; if (localOffset === 1) { remoteOffset = 2; } else if (localOffset === 2) { remoteOffset = 1; } else { // Если Local IP не .1 или .2, используем стандартную логику remoteOffset = localOffset === 0 ? 2 : (localOffset === 3 ? 1 : 2); } const remoteOctet = networkBase + remoteOffset; if (remoteOctet >= 0 && remoteOctet <= 255) { const remoteIp = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${remoteOctet}`; if (!usedIps.has(remoteIp)) { return remoteIp; } } } } // Приватные диапазоны для туннелей (RFC 1918) // Генерируем пары IP в сетях /30 (базовый адрес кратен 4) const ranges = [ { start: [10, 10, 0, 0], end: [10, 10, 255, 252] }, // 10.10.0.0/16 { start: [10, 0, 0, 0], end: [10, 255, 255, 252] }, // 10.0.0.0/8 { start: [172, 16, 0, 0], end: [172, 31, 255, 252] }, // 172.16.0.0/12 { start: [192, 168, 0, 0], end: [192, 168, 255, 252] }, // 192.168.0.0/16 ]; for (const range of ranges) { const [a, b, c, d] = range.start; const [aEnd, bEnd, cEnd, dEnd] = range.end; // Перебираем IP в диапазоне с шагом 4 (сети /30) for (let aVal = a; aVal <= aEnd; aVal++) { const bStart = (aVal === a) ? b : 0; const bEndVal = (aVal === aEnd) ? bEnd : 255; for (let bVal = bStart; bVal <= bEndVal; bVal++) { const cStart = (aVal === a && bVal === b) ? c : 0; const cEndVal = (aVal === aEnd && bVal === bEnd) ? cEnd : 255; for (let cVal = cStart; cVal <= cEndVal; cVal++) { const dStart = (aVal === a && bVal === b && cVal === c) ? d : 0; const dEndVal = (aVal === aEnd && bVal === bEnd && cVal === cEnd) ? dEnd : 252; // Перебираем только базовые адреса сетей /30 (кратные 4) for (let dVal = dStart; dVal <= dEndVal; dVal += 4) { // Local IP = базовый + 1, Remote IP = базовый + 2 // НЕ используем базовый (dVal) и broadcast (dVal + 3) const localIp = `${aVal}.${bVal}.${cVal}.${dVal + 1}`; const remoteIp = `${aVal}.${bVal}.${cVal}.${dVal + 2}`; // Проверяем, свободны ли оба IP if (!usedIps.has(localIp) && !usedIps.has(remoteIp)) { return forRemote ? remoteIp : localIp; } } } } } } // Если все IP заняты, возвращаем null return null; }; // === Генерация IP из пула === const generateIpFromPool = (cidr, usedIps, excludeIp = null, forRemote = false, pairedLocalIp = null) => { // Парсим CIDR (например, "10.10.0.0/24") const [network, prefixLength] = cidr.split('/'); if (!network || !prefixLength) { return null; } const prefix = parseInt(prefixLength); if (isNaN(prefix) || prefix < 0 || prefix > 32) { return null; } const networkParts = network.split('.'); if (networkParts.length !== 4) { return null; } const [a, b, c, d] = networkParts.map(Number); if (networkParts.some(p => isNaN(Number(p)))) { return null; } // Если подбираем Remote IP и есть Local IP, генерируем парный IP в той же сети /30 if (forRemote && pairedLocalIp) { const localIpParts = pairedLocalIp.trim().split('.'); if (localIpParts.length === 4) { // Проверяем, что Local IP входит в пул if (!isIpInCidr(pairedLocalIp, cidr)) { return null; // Local IP не в пуле, не можем сгенерировать Remote } const baseOctet = parseInt(localIpParts[3]); const networkBase = Math.floor(baseOctet / 4) * 4; const localOffset = baseOctet - networkBase; // Для сетей /30 проверяем, что Local IP не является сетевым адресом или broadcast // Local IP должен быть базовый + 1 или базовый + 2 if (prefix >= 30) { const networkAddress = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${networkBase}`; const broadcastAddress = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${networkBase + 3}`; if (pairedLocalIp === networkAddress || pairedLocalIp === broadcastAddress) { // Local IP является сетевым адресом или broadcast, не можем использовать return null; } // Проверяем, что Local IP находится в допустимом диапазоне (базовый + 1 или базовый + 2) if (localOffset !== 1 && localOffset !== 2) { // Local IP не в допустимом диапазоне для /30 return null; } } let remoteOffset; if (localOffset === 1) { remoteOffset = 2; // Если Local IP = базовый + 1, то Remote IP = базовый + 2 } else if (localOffset === 2) { remoteOffset = 1; // Если Local IP = базовый + 2, то Remote IP = базовый + 1 } else { // Если Local IP не в допустимом диапазоне, не можем сгенерировать парный Remote IP return null; } const remoteOctet = networkBase + remoteOffset; if (remoteOctet >= 0 && remoteOctet <= 255) { const remoteIp = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${remoteOctet}`; // Проверяем, что Remote IP входит в диапазон пула и отличается от Local IP // Также проверяем, что не используем сетевой адрес (networkBase) и broadcast (networkBase + 3) if (isIpInCidr(remoteIp, cidr) && !usedIps.has(remoteIp) && remoteIp !== pairedLocalIp) { // Для сетей /30 проверяем, что не используем сетевой адрес и broadcast if (prefix >= 30) { const networkAddress = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${networkBase}`; const broadcastAddress = `${localIpParts[0]}.${localIpParts[1]}.${localIpParts[2]}.${networkBase + 3}`; if (remoteIp === networkAddress || remoteIp === broadcastAddress) { // Remote IP является сетевым адресом или broadcast, не используем его // Продолжаем поиск в общем цикле ниже } else { return remoteIp; } } else { return remoteIp; } } } } // Если не удалось сгенерировать парный Remote IP, продолжаем поиск в общем цикле ниже // Не возвращаем null сразу, чтобы попробовать найти другую пару } // Вычисляем количество хостов в сети const hostBits = 32 - prefix; // Для /30 сетей используем шаг 4 (базовые адреса кратные 4) const step = prefix >= 30 ? 4 : 1; // Определяем диапазон для перебора в зависимости от префикса if (prefix >= 24) { // /24 - /32: меняем только последний октет // Для сетей /30 базовый адрес должен быть кратен 4 let startOctet = d; if (prefix >= 30) { // Округляем вниз до ближайшего кратного 4 startOctet = Math.floor(d / 4) * 4; } const maxHosts = Math.pow(2, hostBits); const endOctet = Math.min(d + maxHosts - 1, 255); // Перебираем IP в диапазоне for (let dVal = startOctet; dVal <= endOctet; dVal += step) { if (prefix >= 30 && dVal % 4 !== 0) continue; // Для /30 только кратные 4 // Local IP = базовый + 1, Remote IP = базовый + 2 const localIp = `${a}.${b}.${c}.${dVal + 1}`; const remoteIp = `${a}.${b}.${c}.${dVal + 2}`; // Для сетей /30 проверяем границы if (prefix >= 30) { if (dVal % 4 !== 0) continue; // Базовый адрес должен быть кратен 4 if (dVal + 2 > 255) continue; // Проверяем, что remoteIp не превышает 255 } // ВСЕГДА проверяем, что генерируемые IP не являются сетевыми или broadcast для любой сети /30 // Это важно даже если пул имеет префикс меньше /30 const localLastOctet = dVal + 1; const remoteLastOctet = dVal + 2; // Проверяем, что Local IP не является сетевым адресом (кратен 4) или broadcast (остаток 3 при делении на 4) if (localLastOctet % 4 === 0 || localLastOctet % 4 === 3) continue; // Проверяем, что Remote IP не является сетевым адресом (кратен 4) или broadcast (остаток 3 при делении на 4) if (remoteLastOctet % 4 === 0 || remoteLastOctet % 4 === 3) continue; // Проверяем, что IP входят в диапазон пула if (!isIpInCidr(localIp, cidr)) continue; if (!isIpInCidr(remoteIp, cidr)) continue; // Проверяем, что IP отличаются друг от друга if (localIp === remoteIp) continue; // Для всех префиксов проверяем, что оба IP свободны (для туннелей нужны пары) if (!usedIps.has(localIp) && !usedIps.has(remoteIp)) { return forRemote ? remoteIp : localIp; } } } else if (prefix >= 16) { // /16 - /23: меняем третий и четвертый октеты const thirdOctetRange = Math.pow(2, Math.max(0, 24 - prefix)); for (let cVal = c; cVal < c + thirdOctetRange && cVal <= 255; cVal++) { let dStart = (cVal === c) ? d : 0; // Для сетей /30 базовый адрес должен быть кратен 4 if (prefix >= 30) { dStart = Math.floor(dStart / 4) * 4; } const dEnd = 255; for (let dVal = dStart; dVal <= dEnd; dVal += step) { if (prefix >= 30 && dVal % 4 !== 0) continue; // Для /30 только кратные 4 const localIp = `${a}.${b}.${cVal}.${dVal + 1}`; // Local IP = базовый + 1 const remoteIp = `${a}.${b}.${cVal}.${dVal + 2}`; // Remote IP = базовый + 2 // Для сетей /30 проверяем границы if (prefix >= 30) { if (dVal + 2 > 255) continue; // Проверяем, что remoteIp не превышает 255 } // ВСЕГДА проверяем, что генерируемые IP не являются сетевыми или broadcast для любой сети /30 // Это важно даже если пул имеет префикс меньше /30 const localLastOctet = dVal + 1; const remoteLastOctet = dVal + 2; // Проверяем, что Local IP не является сетевым адресом (кратен 4) или broadcast (остаток 3 при делении на 4) if (localLastOctet % 4 === 0 || localLastOctet % 4 === 3) continue; // Проверяем, что Remote IP не является сетевым адресом (кратен 4) или broadcast (остаток 3 при делении на 4) if (remoteLastOctet % 4 === 0 || remoteLastOctet % 4 === 3) continue; // Проверяем, что IP входят в диапазон пула и отличаются if (!isIpInCidr(localIp, cidr)) continue; if (localIp === remoteIp) continue; // Проверяем, что Remote IP тоже входит в пул if (!isIpInCidr(remoteIp, cidr)) continue; // Для всех префиксов проверяем, что оба IP свободны (для туннелей нужны пары) if (!usedIps.has(localIp) && !usedIps.has(remoteIp)) { return forRemote ? remoteIp : localIp; } } } } else { // /0 - /15: слишком большой диапазон, используем упрощенную логику return null; } return null; }; // === Проверка, входит ли IP в CIDR === const isIpInCidr = (ip, cidr) => { const [network, prefixLength] = cidr.split('/'); if (!network || !prefixLength) return false; const prefix = parseInt(prefixLength); if (isNaN(prefix)) return false; const networkParts = network.split('.').map(Number); const ipParts = ip.split('.').map(Number); if (networkParts.length !== 4 || ipParts.length !== 4) return false; // Вычисляем маску сети const mask = (0xFFFFFFFF << (32 - prefix)) >>> 0; // Преобразуем IP в число const networkNum = (networkParts[0] << 24) | (networkParts[1] << 16) | (networkParts[2] << 8) | networkParts[3]; const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3]; return (networkNum & mask) === (ipNum & mask); }; // === Подбор IP для интерфейса === const handleSuggestLocalIp = () => { const ipPoolId = editingInterface?.ipPoolId || null; const freeIp = generateFreePrivateIp(editingInterface?.localIp, false, null, ipPoolId); if (freeIp) { setEditingInterface({ ...editingInterface, localIp: freeIp }); notify.success(`Подобран Local IP: ${freeIp}`); } else { notify.error('Не удалось найти свободный IP адрес'); } }; const handleSuggestRemoteIp = () => { // Если есть Local IP, генерируем парный Remote IP в той же сети /30 const ipPoolId = editingInterface?.ipPoolId || null; const freeIp = generateFreePrivateIp( editingInterface?.remoteIp, true, editingInterface?.localIp || null, ipPoolId ); if (freeIp) { setEditingInterface({ ...editingInterface, remoteIp: freeIp }); notify.success(`Подобран Remote IP: ${freeIp}`); } else { notify.error('Не удалось найти свободный IP адрес'); } }; // Функция для генерации PTR зоны с учетом замены const generatePtrZone = (serverDns, iface) => { if (!serverDns) return '—'; // Если указана замена PTR зоны, применяем её if (iface?.ptrZoneReplaceFrom && iface?.ptrZoneReplaceTo) { return serverDns.replace(iface.ptrZoneReplaceFrom, iface.ptrZoneReplaceTo); } return serverDns; }; // === Получение реестра всех используемых IP адресов === const ipRegistry = useMemo(() => { const registry = []; // Собираем IP из интерфейсов - каждая запись это отдельное использование IP (config.tunnelInterfaces || []).forEach(iface => { if (iface.localIp && iface.localIp.trim()) { const server = getServerInfo(iface.serverId); registry.push({ ip: iface.localIp.trim(), type: 'localIp', interface: iface, interfaceName: iface.name || iface.id, server: server, // localIp привязан к serverId serverName: server?.dns || server?.ip || iface.serverId || '—', ptrZone: generatePtrZone(server?.dns, iface) // PTR зона с учетом замены }); } if (iface.remoteIp && iface.remoteIp.trim()) { const server2 = getServerInfo(iface.serverId2); registry.push({ ip: iface.remoteIp.trim(), type: 'remoteIp', interface: iface, interfaceName: iface.name || iface.id, server: server2, // remoteIp привязан к serverId2 serverName: server2?.dns || server2?.ip || iface.serverId2 || '—', ptrZone: generatePtrZone(server2?.dns, iface) // PTR зона с учетом замены }); } }); // Проверяем конфликты (один IP используется в нескольких интерфейсах) const ipUsageMap = {}; registry.forEach(item => { if (!ipUsageMap[item.ip]) { ipUsageMap[item.ip] = []; } ipUsageMap[item.ip].push(item); }); // Добавляем флаг конфликта return registry.map(item => ({ ...item, hasConflict: ipUsageMap[item.ip].length > 1 })).sort((a, b) => { // Сначала конфликты, потом по IP, потом по типу if (a.hasConflict !== b.hasConflict) { return a.hasConflict ? -1 : 1; } if (a.ip !== b.ip) { return a.ip.localeCompare(b.ip); } return a.type.localeCompare(b.type); }); }, [config.tunnelInterfaces, servers]); // === Фильтрация реестра IP === const filteredIpRegistry = useMemo(() => { if (!ipRegistrySearch) return ipRegistry; const term = ipRegistrySearch.toLowerCase(); return ipRegistry.filter(item => item.ip.toLowerCase().includes(term) || item.interfaceName.toLowerCase().includes(term) || item.serverName.toLowerCase().includes(term) ); }, [ipRegistry, ipRegistrySearch]); const handleSaveInterface = (ifaceData) => { // Преобразуем имена интерфейсов в верхний регистр const normalizedData = { ...ifaceData, name: ifaceData.name ? ifaceData.name.toUpperCase() : '', name2: ifaceData.name2 ? ifaceData.name2.toUpperCase() : '', }; // Валидация: проверка пересечений IP адресов const conflicts = checkInterfaceIpConflict( normalizedData, interfaceModalMode === 'edit' ? normalizedData.id : null ); if (conflicts.length > 0) { const conflictMessages = conflicts.map(c => { const conflictName = c.conflictingInterface.name || c.conflictingInterface.id; return `${c.type === 'localIp' ? 'Local IP' : 'Remote IP'} ${c.ip} уже используется в интерфейсе "${conflictName}"`; }); notify.error(`Обнаружено пересечение IP адресов:\n${conflictMessages.join('\n')}`); return; } if (interfaceModalMode === 'add') { setConfig(prev => ({ ...prev, tunnelInterfaces: [...prev.tunnelInterfaces, normalizedData], })); notify.success('Интерфейс добавлен'); } else { setConfig(prev => ({ ...prev, tunnelInterfaces: prev.tunnelInterfaces.map(i => i.id === normalizedData.id ? normalizedData : i), })); notify.success('Интерфейс обновлён'); } setInterfaceModalOpen(false); setEditingInterface(null); }; const handleDeleteInterface = (iface) => { setItemToDelete(iface); setDeleteType('interface'); setDeleteModalOpen(true); }; // === CRUD для IPSec Passwords === const handleAddIpsecPassword = () => { setEditingIpsecPassword({ name: '', password: '', description: '' }); setIpsecPasswordModalMode('add'); setIpsecPasswordModalOpen(true); }; const handleEditIpsecPassword = async (passwordId) => { try { const response = await api.get(`/ipsec-passwords/${passwordId}`); setEditingIpsecPassword(response.data); setIpsecPasswordModalMode('edit'); setIpsecPasswordModalOpen(true); } catch (error) { console.error('Error fetching IPSec password:', error); notify.error('Не удалось загрузить пароль'); } }; const handleSaveIpsecPassword = async () => { if (!editingIpsecPassword.name || !editingIpsecPassword.password) { notify.error('Имя и пароль обязательны'); return; } try { let savedPasswordId = null; if (ipsecPasswordModalMode === 'add') { const response = await api.post('/ipsec-passwords', { name: editingIpsecPassword.name, password: editingIpsecPassword.password, description: editingIpsecPassword.description || '' }); notify.success('IPSec пароль создан'); // Получаем ID созданного пароля из ответа savedPasswordId = response.data?.id || null; } else { await api.put(`/ipsec-passwords/${editingIpsecPassword.id}`, { name: editingIpsecPassword.name, password: editingIpsecPassword.password, description: editingIpsecPassword.description || '' }); notify.success('IPSec пароль обновлён'); savedPasswordId = editingIpsecPassword.id; } await fetchIpsecPasswords(); // Закрываем модал создания/редактирования пароля setIpsecPasswordModalOpen(false); setEditingIpsecPassword(null); // Если модал списка паролей открыт, закрываем его if (ipsecPasswordsListModalOpen) { setIpsecPasswordsListModalOpen(false); } // Если создан новый пароль и открыт шаблон интерфейса, автоматически выбираем его if (savedPasswordId && interfaceTemplateModalOpen && (templateType === 'IPSec' || templateType === 'GRE')) { setTemplateIpsecPasswordId(savedPasswordId); } // Если создан новый пароль и открыт модал интерфейса с типом IPSec или GRE, автоматически выбираем его if (savedPasswordId && interfaceModalOpen && (editingInterface?.type === 'IPSec' || editingInterface?.type === 'GRE')) { setEditingInterface({ ...editingInterface, ipsecPasswordId: savedPasswordId }); } } catch (error) { console.error('Error saving IPSec password:', error); notify.error('Не удалось сохранить пароль'); } }; const handleDeleteIpsecPassword = async (passwordId) => { try { await api.delete(`/ipsec-passwords/${passwordId}`); notify.success('IPSec пароль удалён'); await fetchIpsecPasswords(); } catch (error) { console.error('Error deleting IPSec password:', error); notify.error('Не удалось удалить пароль'); } }; // === CRUD для IP Pools === const handleAddPool = () => { setEditingPool(getEmptyIpPool()); setPoolModalMode('add'); setPoolModalOpen(true); }; const handleEditPool = (pool) => { setEditingPool({ ...pool }); setPoolModalMode('edit'); setPoolModalOpen(true); }; const handleSavePool = (poolData) => { if (poolModalMode === 'add') { setConfig(prev => ({ ...prev, ipPools: [...prev.ipPools, poolData], })); notify.success('IP пул добавлен'); } else { setConfig(prev => ({ ...prev, ipPools: prev.ipPools.map(p => p.id === poolData.id ? poolData : p), })); notify.success('IP пул обновлён'); } setPoolModalOpen(false); setEditingPool(null); }; const handleDeletePool = (pool) => { setItemToDelete(pool); setDeleteType('pool'); setDeleteModalOpen(true); }; // === Подтверждение удаления === const executeDelete = () => { if (!itemToDelete) return; if (deleteType === 'gateway') { setConfig(prev => ({ ...prev, gateways: prev.gateways.filter(g => g.id !== itemToDelete.id), })); notify.success('Gateway удалён'); } else if (deleteType === 'interface') { setConfig(prev => ({ ...prev, tunnelInterfaces: prev.tunnelInterfaces.filter(i => i.id !== itemToDelete.id), })); notify.success('Интерфейс удалён'); } else if (deleteType === 'pool') { setConfig(prev => ({ ...prev, ipPools: prev.ipPools.filter(p => p.id !== itemToDelete.id), })); notify.success('IP пул удалён'); } setDeleteModalOpen(false); setItemToDelete(null); setDeleteType(''); }; // === Copy to clipboard === const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); notify.success('Скопировано'); } catch { notify.error('Не удалось скопировать'); } }; // === Вспомогательная функция: построение блоков MikroTik для списка интерфейсов === const buildMikrotikInterfaceBlocks = async (interfacesWithBothServers) => { if (!interfacesWithBothServers || interfacesWithBothServers.length === 0) { return []; } // Загружаем пароли для интерфейсов с IPSec const passwordIds = [...new Set(interfacesWithBothServers .filter(i => i.ipsecPasswordId && i.ipsecPasswordId.trim() !== '') .map(i => i.ipsecPasswordId.trim()) )]; const passwordMap = {}; for (const passwordId of passwordIds) { try { const response = await api.get(`/ipsec-passwords/${passwordId}`); const password = response.data?.password || ''; if (password) { passwordMap[passwordId] = password; } else { console.warn(`IPSec password ${passwordId} loaded but empty`); } } catch (error) { console.error(`Error loading IPSec password ${passwordId}:`, error); passwordMap[passwordId] = ''; } } // Группируем по серверам const interfacesByServer = {}; interfacesWithBothServers.forEach(iface => { const server1 = getServerInfo(iface.serverId); const server2 = getServerInfo(iface.serverId2); if (!server1 || !server2) return; const server1Name = server1.dns || server1.ip || iface.serverId; const server2Name = server2.dns || server2.ip || iface.serverId2; // Имя интерфейса для первого сервера const interfaceName1 = iface.name || `${iface.type}-tunnel`; // Имя интерфейса для второго сервера - ВСЕГДА используем name2 если указано, иначе name const interfaceName2 = iface.name2 || iface.name || `${iface.type}-tunnel`; // Для первого сервера - используем localIp как local, remoteIp как remote if (!interfacesByServer[server1Name]) { interfacesByServer[server1Name] = { serverInfo: server1, interfaces: [] }; } interfacesByServer[server1Name].interfaces.push({ interfaceName: interfaceName1, localIp: iface.localIp, remoteIp: iface.remoteIp, type: iface.type, server2Name, server2Info: server2, ipsecPasswordId: (iface.ipsecPasswordId && iface.ipsecPasswordId.trim() !== '') ? iface.ipsecPasswordId.trim() : null, ipsecPassword: (iface.ipsecPasswordId && iface.ipsecPasswordId.trim() !== '') ? (passwordMap[iface.ipsecPasswordId.trim()] || null) : null, mtu: iface.mtu || null }); // Для второго сервера - МЕНЯЕМ МЕСТАМИ: remoteIp становится localIp, localIp становится remoteIp // И ВСЕГДА используем name2 (или name если name2 не указано) if (!interfacesByServer[server2Name]) { interfacesByServer[server2Name] = { serverInfo: server2, interfaces: [] }; } interfacesByServer[server2Name].interfaces.push({ interfaceName: interfaceName2, // Используем name2 для второго сервера localIp: iface.remoteIp, // На втором сервере remoteIp первого становится localIp remoteIp: iface.localIp, // На втором сервере localIp первого становится remoteIp type: iface.type, server2Name: server1Name, server2Info: server1, ipsecPasswordId: (iface.ipsecPasswordId && iface.ipsecPasswordId.trim() !== '') ? iface.ipsecPasswordId.trim() : null, ipsecPassword: (iface.ipsecPasswordId && iface.ipsecPasswordId.trim() !== '') ? (passwordMap[iface.ipsecPasswordId.trim()] || null) : null, mtu: iface.mtu || null }); }); // Сортируем серверы по имени для консистентности const sortedServers = Object.entries(interfacesByServer).sort(([a], [b]) => a.localeCompare(b)); const blocks = []; sortedServers.forEach(([serverName, serverData]) => { const server = serverData.serverInfo; let code = `# Настройка IP адресов интерфейсов для сервера: ${serverName}\n`; if (server.provider) { code += `# Провайдер: ${server.provider}\n`; } if (server.country) { code += `# Страна: ${server.country}\n`; } if (server.ip && server.ip !== serverName) { code += `# IP: ${server.ip}\n`; } code += '\n'; // Проверяем, является ли это home роутером и есть ли GRE интерфейсы const isHomeRouter = server.type === 'home'; const hasGreInterfaces = serverData.interfaces.some(iface => iface.type === 'GRE'); // Для home роутеров с GRE интерфейсами создаём interface list if (isHomeRouter && hasGreInterfaces) { code += `# Создание interface list для GRE туннелей (если не существует)\n`; code += `:if ([/interface list find name=GREs] = "") do={ /interface list add name=GREs comment="GRE tunnels list" }\n`; code += '\n'; } serverData.interfaces.forEach((iface, ifaceIndex) => { code += `# Интерфейс ${ifaceIndex + 1}: ${iface.interfaceName} (${iface.type})\n`; code += `# Связь с сервером: ${iface.server2Name}\n`; code += `# Local IP: ${iface.localIp}\n`; code += `# Remote IP: ${iface.remoteIp}\n`; // Для GRE туннелей - создаем туннель, опционально с IPSec через ipsec-secret if (iface.type === 'GRE') { // Используем DNS домен для remote-address, если он есть, иначе IP const remoteAddress = iface.server2Info?.dns || iface.server2Info?.ip || iface.server2Name; // Проверяем, является ли текущий сервер home роутером const isHomeRouter = server.type === 'home'; // Если это home роутер, добавляем local-address с IP адресом этого сервера const localAddressParam = isHomeRouter && server.ip ? ` local-address=${server.ip}` : ''; // Проверяем наличие пароля: ipsecPasswordId должен быть не пустым и пароль должен быть загружен const hasIpsecPassword = iface.ipsecPasswordId && iface.ipsecPasswordId.trim() !== '' && iface.ipsecPassword && iface.ipsecPassword.trim() !== ''; if (hasIpsecPassword) { // GRE туннель с IPSec через ipsec-secret (автоматическая настройка) code += `# Создание GRE туннеля с IPSec (автоматическая настройка через ipsec-secret)\n`; if (isHomeRouter) { code += `# Home роутер: используется local-address=${server.ip}\n`; } const mtuParam = iface.mtu ? ` mtu=${iface.mtu}` : ''; code += `/interface gre add name="${iface.interfaceName}" remote-address=${remoteAddress}${localAddressParam} keepalive=10s allow-fast-path=no ipsec-secret="${iface.ipsecPassword}"${mtuParam}\n`; // Для home роутеров добавляем интерфейс в interface list GREs if (isHomeRouter) { code += `/interface list member add list=GREs interface="${iface.interfaceName}" comment="GRE tunnel to ${iface.server2Name}"\n`; } code += '\n'; } else { // GRE туннель без IPSec (голый GRE) code += `# Создание GRE туннеля (без IPSec)\n`; if (isHomeRouter) { code += `# Home роутер: используется local-address=${server.ip}\n`; } const mtuParam = iface.mtu ? ` mtu=${iface.mtu}` : ''; code += `/interface gre add name="${iface.interfaceName}" remote-address=${remoteAddress}${localAddressParam} keepalive=10s allow-fast-path=no${mtuParam}\n`; // Для home роутеров добавляем интерфейс в interface list GREs if (isHomeRouter) { code += `/interface list member add list=GREs interface="${iface.interfaceName}" comment="GRE tunnel to ${iface.server2Name}"\n`; } code += '\n'; } } // Добавляем IP адрес на интерфейс code += `/ip address add address=${iface.localIp}/30 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`; code += '\n'; }); code += '# Проверка настроенных адресов на этом сервере:\n'; code += '# /ip address print\n'; // Для home роутеров добавляем проверку interface list if (isHomeRouter && hasGreInterfaces) { code += '\n# Проверка interface list GREs:\n'; code += '# /interface list print\n'; code += '# /interface list member print where list=GREs\n'; } blocks.push({ type: 'interface-addresses', serverName, server, code }); }); return blocks; }; // === Генерация кода MikroTik для настройки IP адресов интерфейсов (всех) === const generateMikrotikInterfaceAddresses = async () => { const interfacesWithBothServers = (config.tunnelInterfaces || []).filter(i => i.serverId && i.serverId2 && i.localIp && i.remoteIp ); return buildMikrotikInterfaceBlocks(interfacesWithBothServers); }; // === Генерация кода MikroTik только для выбранных интерфейсов === const generateMikrotikInterfaceAddressesForSelection = async (selectedInterfaces) => { const interfacesWithBothServers = (selectedInterfaces || []).filter(i => i.serverId && i.serverId2 && i.localIp && i.remoteIp ); return buildMikrotikInterfaceBlocks(interfacesWithBothServers); }; // === Генерация кода MikroTik для рекурсивных маршрутов === const generateMikrotikRecursiveRoutes = async () => { const recursiveGateways = (config.gateways || []).filter(gw => gw.type === 'recursive'); const blocks = []; // Если есть рекурсивные gateway, генерируем блоки для них if (recursiveGateways.length > 0) { // Группируем по серверам для удобства const gatewaysByServer = {}; recursiveGateways.forEach(gw => { const serverId = gw.serverId || '__unassigned__'; if (!gatewaysByServer[serverId]) { gatewaysByServer[serverId] = []; } gatewaysByServer[serverId].push(gw); }); // Генерируем блоки для рекурсивных маршрутов по серверам Object.entries(gatewaysByServer).forEach(([serverId, gateways]) => { const server = getServerInfo(serverId); const serverName = server?.dns || server?.ip || serverId; let code = `# Рекурсивные маршруты для сервера: ${serverName}\n`; if (server?.provider) { code += `# Провайдер: ${server.provider}\n`; } if (server?.country) { code += `# Страна: ${server.country}\n`; } code += '\n'; code += '# Удаление существующих рекурсивных маршрутов (опционально)\n'; code += '/ip route remove [find comment~"Recursive"]\n\n'; let validRoutesCount = 0; gateways.forEach(gw => { // Поддержка старого формата (parentGatewayId) и нового (parentGateways) const parentGatewaysList = gw.parentGateways && Array.isArray(gw.parentGateways) && gw.parentGateways.length > 0 ? gw.parentGateways : (gw.parentGatewayId ? [{ id: gw.parentGatewayId, distance: undefined }] : []); if (parentGatewaysList.length === 0) { code += `# Gateway "${gw.ip || 'без IP'}" - родительские gateway/интерфейсы не указаны, пропущен\n`; return; } if (!gw.ip) { code += `# Gateway без IP адреса - пропущен\n`; return; } // Обрабатываем каждый родительский 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'; code += '# /ip route print where comment~"Recursive"\n'; code += `# Всего создано маршрутов: ${validRoutesCount}\n`; if (validRoutesCount > 0) { blocks.push({ type: 'recursive-routes', serverName, server, code }); } }); } // Генерируем блоки для IP адресов интерфейсов const interfaceBlocks = await generateMikrotikInterfaceAddresses(); if (interfaceBlocks && interfaceBlocks.length > 0) { blocks.push(...interfaceBlocks); } return blocks; }; // === Обработчик кнопки "Код для MikroTik" === const handleGenerateMikrotikCode = async () => { setLoading(true); try { const code = await generateMikrotikRecursiveRoutes(); setGeneratedMikrotikCode(code); setMikrotikCodeModalOpen(true); } catch (error) { console.error('Error generating MikroTik code:', error); notify.error('Ошибка при генерации кода MikroTik'); } finally { setLoading(false); } }; // === Генерация кода MikroTik для всех интерфейсов сервера === const handleGenerateMikrotikCodeForServer = async (serverId, type) => { if (!serverId) { notify.error('Сервер не указан'); return; } setLoading(true); try { if (type === 'interface') { // Получаем все интерфейсы для этого сервера (где сервер участвует как первый или второй) // Фильтруем только те, где оба сервера указаны и есть IP адреса const serverInterfaces = (config.tunnelInterfaces || []).filter(iface => (iface.serverId === serverId || iface.serverId2 === serverId) && iface.serverId && iface.serverId2 && iface.localIp && iface.remoteIp ); if (serverInterfaces.length === 0) { notify.error('Нет интерфейсов с полной конфигурацией для этого сервера'); setLoading(false); return; } const blocks = await generateMikrotikInterfaceAddressesForSelection(serverInterfaces); if (!blocks || blocks.length === 0) { notify.error('Нет данных для генерации кода MikroTik для этого сервера'); setLoading(false); return; } // Фильтруем блоки, чтобы показать только те, что относятся к данному серверу const server = getServerInfo(serverId); const serverName = server?.dns || server?.ip || serverId; const filteredBlocks = blocks.filter(block => block.serverName === serverName || block.server?.id === serverId ); if (filteredBlocks.length === 0) { notify.error('Не удалось сгенерировать код для этого сервера'); setLoading(false); return; } setGeneratedMikrotikCode(filteredBlocks); setMikrotikCodeModalOpen(true); } else { notify.error('Генерация кода для gateways по серверу пока не реализована'); } } catch (error) { console.error('Error generating MikroTik code for server:', error); notify.error('Ошибка при генерации кода MikroTik для этого сервера'); } finally { setLoading(false); } }; // === Генерация кода MikroTik только для одного интерфейса === const handleGenerateMikrotikCodeForInterface = async (iface) => { if (!iface) { notify.error('Интерфейс не найден'); return; } setLoading(true); try { const blocks = await generateMikrotikInterfaceAddressesForSelection([iface]); if (!blocks || blocks.length === 0) { notify.error('Нет данных для генерации кода MikroTik для этого интерфейса'); return; } setGeneratedMikrotikCode(blocks); setMikrotikCodeModalOpen(true); } catch (error) { console.error('Error generating MikroTik code for interface:', error); notify.error('Ошибка при генерации кода MikroTik для этого интерфейса'); } finally { setLoading(false); } }; // === Генерация кода MikroTik только для одного gateway === const handleGenerateMikrotikCodeForGateway = async (gateway) => { if (!gateway) { notify.error('Gateway не найден'); return; } setLoading(true); try { const blocks = []; const server = getServerInfo(gateway.serverId); const serverName = server?.dns || server?.ip || gateway.serverId || '__unassigned__'; if (gateway.type === 'recursive') { // Генерация кода для рекурсивного gateway if (!gateway.ip) { notify.error('Gateway без IP адреса - невозможно сгенерировать код'); setLoading(false); return; } // Поддержка старого формата (parentGatewayId) и нового (parentGateways) const parentGatewaysList = gateway.parentGateways && Array.isArray(gateway.parentGateways) && gateway.parentGateways.length > 0 ? gateway.parentGateways : (gateway.parentGatewayId ? [{ id: gateway.parentGatewayId, distance: undefined }] : []); if (parentGatewaysList.length === 0) { notify.error('Родительские gateway/интерфейсы не указаны'); setLoading(false); return; } let code = `# Рекурсивный маршрут для gateway: ${gateway.ip}\n`; if (server?.provider) { code += `# Провайдер: ${server.provider}\n`; } if (server?.country) { code += `# Страна: ${server.country}\n`; } code += '\n'; let validRoutesCount = 0; parentGatewaysList.forEach((parentRef, parentIndex) => { const parent = getParentGateway(parentRef.id); if (!parent) { code += `# Родительский gateway/интерфейс "${parentRef.id}" не найден, пропущен\n`; return; } // Определяем IP родителя в зависимости от типа const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; if (!parentIp) { const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway'; code += `# Родительский ${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: ${gateway.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 = gateway.description ? `Recursive: ${gateway.description} -> ${parentIp}${distanceSuffix}` : `Recursive: ${gateway.ip} -> ${parentIp}${distanceSuffix}`; // Формируем команду с distance, если указан const distanceParam = parentRef.distance !== undefined && parentRef.distance !== null ? ` distance=${parentRef.distance}` : ''; code += `/ip route add dst-address=${gateway.ip}/32 gateway=${gatewayValue}${distanceParam} comment="${comment}"\n`; // Если у рекурсивного gateway есть описание с указанием на default route if (gateway.description && (gateway.description.toLowerCase().includes('default') || gateway.description.toLowerCase().includes('0.0.0.0'))) { code += `# Дополнительный default route через рекурсивный gateway${distanceSuffix}\n`; const defaultComment = gateway.description ? `Recursive: ${gateway.description} (default)${distanceSuffix}` : `Recursive: ${gateway.ip} (default)${distanceSuffix}`; code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue}${distanceParam} comment="${defaultComment}"\n`; } code += '\n'; }); code += '# Проверка созданных маршрутов:\n'; code += '# /ip route print where comment~"Recursive"\n'; code += `# Всего создано маршрутов: ${validRoutesCount}\n`; if (validRoutesCount > 0) { blocks.push({ type: 'recursive-routes', serverName, server, code }); } } else if (gateway.type === 'direct') { // Генерация кода для прямого gateway if (!gateway.ip) { notify.error('Gateway без IP адреса - невозможно сгенерировать код'); setLoading(false); return; } let code = `# Прямой маршрут для gateway: ${gateway.ip}\n`; if (server?.provider) { code += `# Провайдер: ${server.provider}\n`; } if (server?.country) { code += `# Страна: ${server.country}\n`; } code += '\n'; const comment = gateway.description ? `Direct: ${gateway.description}` : `Direct: ${gateway.ip}`; code += `/ip route add dst-address=0.0.0.0/0 gateway=${gateway.ip} comment="${comment}"\n`; code += '\n'; code += '# Проверка созданного маршрута:\n'; code += '# /ip route print where comment~"Direct"\n'; blocks.push({ type: 'direct-route', serverName, server, code }); } else { notify.error('Неизвестный тип gateway'); setLoading(false); return; } if (blocks.length === 0) { notify.error('Нет данных для генерации кода MikroTik для этого gateway'); return; } setGeneratedMikrotikCode(blocks); setMikrotikCodeModalOpen(true); } catch (error) { console.error('Error generating MikroTik code for gateway:', error); notify.error('Ошибка при генерации кода MikroTik для этого gateway'); } finally { setLoading(false); } }; // === Копирование кода MikroTik === const handleCopyMikrotikCode = async (codeToCopy) => { if (!codeToCopy) { notify.error('Нет кода для копирования'); return; } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(codeToCopy); notify.success('Код скопирован в буфер обмена!'); } else { // Fallback для старых браузеров const textArea = document.createElement('textarea'); textArea.value = codeToCopy; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); notify.success('Код скопирован в буфер обмена!'); } catch (err) { notify.error('Не удалось скопировать код'); } document.body.removeChild(textArea); } } catch (err) { console.error('Error copying to clipboard:', err); notify.error('Не удалось скопировать код'); } }; // === Копирование отдельного блока кода === const handleCopyBlock = async (code) => { await handleCopyMikrotikCode(code); }; // === Копирование всего кода === const handleCopyAllMikrotikCode = async () => { const fullCode = Array.isArray(generatedMikrotikCode) && generatedMikrotikCode.length > 0 ? generatedMikrotikCode.map(block => block.code).join('\n\n') : ''; await handleCopyMikrotikCode(fullCode); }; // === Группировка блоков по серверам === const groupedMikrotikCode = useMemo(() => { if (!Array.isArray(generatedMikrotikCode) || generatedMikrotikCode.length === 0) { return {}; } const grouped = {}; generatedMikrotikCode.forEach(block => { const serverName = block.serverName || '__unassigned__'; if (!grouped[serverName]) { grouped[serverName] = { server: block.server, serverName: serverName, blocks: [] }; } grouped[serverName].blocks.push(block); }); return grouped; }, [generatedMikrotikCode]); // === Получение названия типа блока === const getBlockTypeLabel = (type) => { switch (type) { case 'recursive-routes': return 'Рекурсивные маршруты'; case 'interface-addresses': return 'IP адреса интерфейсов'; default: return 'Конфигурация'; } }; // === Получение цвета провайдера === const getProviderColor = (provider) => { if (!provider) return 'secondary'; // Ищем точное совпадение const exact = GATEWAY_PROVIDERS.find(gp => gp.value === provider || gp.label.toLowerCase() === provider.toLowerCase() ); if (exact) return exact.color; // Проверяем частичное совпадение const partial = GATEWAY_PROVIDERS.find(gp => provider.toLowerCase().includes(gp.value.toLowerCase()) || provider.toLowerCase().includes(gp.label.toLowerCase()) ); return partial?.color || 'secondary'; }; const getInterfaceTypeColor = (type) => { const t = INTERFACE_TYPES.find(it => it.value === type); return t?.color || 'secondary'; }; // === Рендер категории с аккордеоном === const renderCategoryAccordion = (category, itemsByServer, type) => { const categoryItems = Object.entries(itemsByServer); if (categoryItems.length === 0) return null; const isExpanded = expandedCategories.has(category); const totalItems = categoryItems.reduce((sum, [, items]) => sum + items.length, 0); return (
{/* Заголовок категории (кликабельный) */}
toggleCategory(category)} >
{/* Иконка сворачивания */} {isExpanded ? ( ) : ( )}

{getCategoryLabel(category)}

{categoryItems.length} {categoryItems.length === 1 ? 'сервер' : categoryItems.length < 5 ? 'сервера' : 'серверов'} • {totalItems} {type === 'gateway' ? (totalItems === 1 ? 'gateway' : 'gateways') : (totalItems === 1 ? 'интерфейс' : 'интерфейсов')}
{/* Контент (сворачиваемый) */} {isExpanded && (
{categoryItems .sort(([a], [b]) => { // Сортируем: сначала серверы с информацией, потом без привязки if (a === '__unassigned__') return 1; if (b === '__unassigned__') return -1; const serverA = getServerInfo(a); const serverB = getServerInfo(b); const nameA = serverA?.dns || serverA?.ip || a; const nameB = serverB?.dns || serverB?.ip || b; return nameA.localeCompare(nameB); }) .map(([serverId, items]) => renderServerGroup(serverId, items, type) )}
)}
); }; // === Рендер карточки сервера с его элементами === const renderServerGroup = (serverId, items, type) => { const server = getServerInfo(serverId); const isUnassigned = serverId === '__unassigned__'; return (
{/* Server header - улучшенная структура с четкой иерархией */}
{/* Левая часть: основная информация */}
{isUnassigned ? ( <>

Без привязки к серверу

) : ( <>
{/* Основная строка: название сервера */}

{server?.dns ? server.dns.split('.')[0] : (server?.name || serverId)}

{server?.country && ( {countryToFlag(server.country)} {server.country} )} {server?.provider && ( {server.provider} )}
{/* Дополнительная информация: DNS и IP */}
{server?.dns && (
DNS: {server.dns}
)} {server?.ip && (
IP: {server.ip}
)}
)}
{/* Правая часть: счетчик и действия */}
{items.length} {type === 'gateway' ? (items.length === 1 ? 'gateway' : 'gateways') : (items.length === 1 ? 'интерфейс' : 'интерфейсов')} {type === 'interface' && !isUnassigned && items.length > 0 && ( )}
{/* Items - улучшенное расположение */}
{items.length === 0 ? (
Нет элементов для отображения
) : (
{items.map(item => (
{type === 'gateway' ? renderGatewayCard(item) : type === 'interface' ? renderInterfaceCard(item, serverId) : type === 'pool' ? renderPoolCard(item) : null}
))}
)}
); }; // === Получение родительского gateway или интерфейса === const getParentGateway = (parentId, templateGatewaysList = null) => { if (!parentId) return null; // Если передан список templateGateways, сначала ищем там if (templateGatewaysList) { const templateGateway = templateGatewaysList.find(g => g.id === parentId); if (templateGateway) { return { parentType: 'gateway', ...templateGateway, ip: templateGateway.ip }; } } // Сначала ищем в gateway const gateway = config.gateways?.find(g => g.id === parentId); if (gateway) { return { parentType: 'gateway', // Используем parentType чтобы не конфликтовать с type интерфейса ...gateway, ip: gateway.ip // Явно указываем IP для gateway }; } // Если не найден, ищем в интерфейсах const iface = config.tunnelInterfaces?.find(i => i.id === parentId); if (iface) { return { parentType: 'interface', // Используем parentType чтобы не конфликтовать с type интерфейса ...iface, remoteIp: iface.remoteIp, // Явно указываем remoteIp для интерфейса localIp: iface.localIp, name: iface.name, interfaceType: iface.type // Сохраняем тип интерфейса (GRE, WireGuard и т.д.) отдельно }; } // Если не найден ни gateway, ни интерфейс - логируем для отладки console.warn('Parent gateway/interface not found:', parentId, { availableGateways: config.gateways?.map(g => g.id), availableInterfaces: config.tunnelInterfaces?.map(i => i.id), templateGateways: templateGatewaysList?.map(g => g.id) }); return null; }; // === Рендер карточки IP Pool === const renderPoolCard = (pool) => { const server = getServerInfo(pool.serverId); const serverCountry = server?.country || ''; // Определяем цвет индикатора (зеленый для IP пулов) const indicatorColor = 'green'; const IndicatorIcon = IconDatabase; return (
{/* Левая часть: тип индикатора */}
Pool
{/* Центральная часть: информация */}
{/* Верхняя строка: флаг, название */}
{serverCountry && ( {countryToFlag(serverCountry)} )}
{pool.name || '—'}
{/* CIDR под названием */} {pool.cidr && (
{pool.cidr}
)} {/* Нижняя строка: DNS сервера */}
{server && ( {server.dns || server.ip || pool.serverId} )} {!server && pool.serverId && ( {pool.serverId} )}
{/* Правая часть: действия - компактные иконки */}
); }; // === Рендер карточки Gateway === const renderGatewayCard = (gateway) => { const server = getServerInfo(gateway.serverId); const provider = server?.provider || ''; const serverCountry = server?.country || ''; const gatewayCountry = gateway.country || ''; const displayCountry = gatewayCountry || serverCountry; const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; // Поддержка старого формата (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' ? 'Рекурсивный' : 'Прямой'; const indicatorColor = gateway.type === 'recursive' ? 'red' : 'blue'; const IndicatorIcon = gateway.type === 'recursive' ? IconArrowsRightLeft : IconRouter; // Формируем текст для зеленого блока let displayText = ''; 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' && 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 (
{/* Левая часть: тип индикатора (как в ServerCard) */}
{indicatorType}
{/* Центральная часть: информация */}
{/* Верхняя строка: флаг, название */}
{displayCountry && ( {countryToFlag(displayCountry)} )}
{gateway.description || gateway.ip || '—'}
{/* Маршрут ip->ip сразу под названием */} {displayText && (
{displayText}
)} {/* Нижняя строка: DNS */}
{server && ( {server.dns || server.ip || gateway.serverId} )}
{/* Правая часть: действия - компактные иконки */}
); }; // === Рендер карточки Interface === const renderInterfaceCard = (iface, currentServerId) => { const server = getServerInfo(iface.serverId); const server2 = getServerInfo(iface.serverId2); // Определяем, для какого сервера отображается карточка const isForServer2 = currentServerId && iface.serverId2 && (() => { const currentServer = getServerInfo(currentServerId); const targetServer2 = getServerInfo(iface.serverId2); if (!currentServer || !targetServer2) return false; return ( currentServer.id === targetServer2.id || currentServer.ip === targetServer2.ip || currentServer.dns === targetServer2.dns || currentServerId === iface.serverId2 || currentServerId === targetServer2.id || currentServerId === targetServer2.ip || currentServerId === targetServer2.dns ); })(); // Имя интерфейса зависит от того, для какого сервера отображается const interfaceName = isForServer2 ? (iface.name2 || iface.name || '—') : (iface.name || '—'); // IP адреса также зависят от того, для какого сервера отображается const displayLocalIp = isForServer2 ? (iface.remoteIp || '—') : (iface.localIp || '—'); const displayRemoteIp = isForServer2 ? (iface.localIp || '—') : (iface.remoteIp || '—'); // Определяем сервер для отображения const displayServer = isForServer2 ? server2 : server; const displayServerName = displayServer?.dns || displayServer?.ip || (isForServer2 ? iface.serverId2 : iface.serverId); const displayCountry = displayServer?.country || ''; const interfaceTypeColor = getInterfaceTypeColor(iface.type); // Определяем отображаемый IP для зеленого блока (remote IP другого сервера) const displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp; return (
{/* Левая часть: тип интерфейса (как в ServerCard) */}
{iface.type}
{/* Центральная часть: информация */}
{/* Верхняя строка: флаг, название */}
{displayCountry && ( {countryToFlag(displayCountry)} )}
{interfaceName}
{/* Маршрут между Local IP и Remote IP сразу под названием */} {displayLocalIp !== '—' && displayRemoteIp !== '—' && (
{displayLocalIp} → {displayRemoteIp}
)} {/* Нижняя строка: DNS */}
{displayServerName && ( {displayServerName} )}
{/* Правая часть: действия - компактные иконки */}
); }; return (
{/* Page Header */}
Управление

Сетевые настройки

{/* Tabs as button group */}
{/* Toolbar */}
{/* Search */}
setSearchTerm(e.target.value)} /> {searchTerm && ( setSearchTerm('')}> )}
{/* Server filter */} {(activeTab === 'gateways' || activeTab === 'interfaces' || activeTab === 'pools') && uniqueServersInConfig.length > 0 && (
)} {/* Provider filter for gateways - берем уникальные провайдеры из серверов */} {activeTab === 'gateways' && (
)} {/* Type filter for interfaces */} {activeTab === 'interfaces' && (
)} {/* Reset filters */} {hasActiveFilters && (
)}
{/* View mode toggle */} {(activeTab === 'gateways' || activeTab === 'interfaces') && (
)} {/* Add button */} {activeTab === 'gateways' && ( <> )} {activeTab === 'interfaces' && ( <> )} {activeTab === 'pools' && ( )}
{/* Content */} {loading ? (
Загрузка...
) : ( <> {/* Gateways Tab */} {activeTab === 'gateways' && ( <> {filteredGateways.length === 0 ? (

Gateways не найдены

{hasActiveFilters ? 'Попробуйте изменить параметры фильтрации' : 'Добавьте gateway для использования в конфигурациях серверов' }

{!hasActiveFilters && ( )}
) : viewMode === 'cards' ? ( // Grouped cards view with category accordions
{['home', 'jumphost', 'exit', 'other'].map(category => renderCategoryAccordion(category, gatewaysByCategory[category], 'gateway') )}
) : ( // Table view
{filteredGateways.map(gateway => { const server = getServerInfo(gateway.serverId); const provider = server?.provider || ''; const serverCountry = server?.country || ''; const gatewayCountry = gateway.country || ''; const displayCountry = gatewayCountry || serverCountry; const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; // Поддержка старого формата (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 ( ); })}
IP адрес Тип Провайдер Сервер Страна Действия
{gateway.ip || '—'} {gateway.ip && ( )}
{gateway.description && (
{gateway.description}
)}
{gatewayType.label} {firstParent && (
→ {firstParent.parentType === 'interface' ? `Интерфейс ${firstParent.name || firstParent.interfaceType || 'interface'} (${firstParent.remoteIp || '—'})` : firstParent.ip ? `Gateway ${firstParent.ip}` : 'Gateway (не указан IP)' } {parentGatewaysList.length > 1 && ( (+{parentGatewaysList.length - 1}) )}
)}
{provider ? ( {provider} ) : '—'} {server ? (
{server.dns || server.ip}
) : gateway.serverId ? ( {gateway.serverId} ) : ( )}
{displayCountry ? (
{countryToFlag(displayCountry)} {displayCountry} {gatewayCountry && serverCountry && gatewayCountry !== serverCountry && (
сервер: {serverCountry}
)}
) : '—'}
)} )} {/* Interfaces Tab */} {activeTab === 'interfaces' && ( <> {filteredInterfaces.length === 0 ? (

Интерфейсы не найдены

{hasActiveFilters ? 'Попробуйте изменить параметры фильтрации' : 'Добавьте туннельные интерфейсы для серверов' }

{!hasActiveFilters && ( )}
) : viewMode === 'cards' ? ( // Grouped cards view with category accordions
{['home', 'jumphost', 'exit', 'other'].map(category => renderCategoryAccordion(category, interfacesByCategory[category], 'interface') )}
) : ( // Table view
{filteredInterfaces.map(iface => { const server = getServerInfo(iface.serverId); const server2 = getServerInfo(iface.serverId2); return ( ); })}
Имя Тип Local IP Remote IP Серверы Действия
{iface.name || '—'} {iface.type} {iface.localIp || '—'} {iface.localIp && ( )} {iface.remoteIp || '—'} {iface.remoteIp && ( )}
{server ? (
{server.dns || server.ip}
) : iface.serverId ? ( {iface.serverId} ) : ( )} {server2 && (
{server2.dns || server2.ip}
)}
)} )} {/* IP Pools Tab */} {activeTab === 'pools' && ( <> {filteredPools.length === 0 ? (

IP пулы не найдены

{hasActiveFilters ? 'Попробуйте изменить параметры фильтрации' : 'Добавьте пулы IP-адресов для организации сетей' }

{!hasActiveFilters && (
)}
) : ( <> {/* Группировка по серверам */} {(serverFilter || filteredPools.some(p => p.serverId)) && (() => { const poolsByServer = {}; filteredPools.forEach(pool => { const key = pool.serverId || '__unassigned__'; if (!poolsByServer[key]) { poolsByServer[key] = []; } poolsByServer[key].push(pool); }); return Object.entries(poolsByServer) .sort(([a], [b]) => { if (a === '__unassigned__') return 1; if (b === '__unassigned__') return -1; return a.localeCompare(b); }) .map(([serverId, pools]) => renderServerGroup(serverId, pools, 'pool')); })()} {/* Если нет фильтра по серверу и все пулы без сервера - показываем без группировки */} {!serverFilter && !filteredPools.some(p => p.serverId) && (
{filteredPools.map(pool => (
{renderPoolCard(pool)}
))}
)} )} )} {/* IP Registry Tab */} {activeTab === 'ip-registry' && ( <>
setIpRegistrySearch(e.target.value)} />
{filteredIpRegistry.length === 0 ? (

Реестр IP адресов пуст

{ipRegistrySearch ? 'Попробуйте изменить поисковый запрос' : 'Добавьте интерфейсы, чтобы увидеть используемые IP адреса'}

) : (
{filteredIpRegistry.map((item, idx) => ( ))}
IP адрес Тип Сервер PTR зона Интерфейс Действия
{item.ip} {item.hasConflict && ( Конфликт )}
{item.type === 'localIp' ? 'Local' : 'Remote'}
{item.serverName}
{item.ptrZone}
{item.interfaceName}
)} )} )} {/* Gateway Modal */} { setGatewayModalOpen(false); setEditingGateway(null); }} onSubmit={() => editingGateway && handleSaveGateway(editingGateway)} title={gatewayModalMode === 'add' ? 'Добавить Gateway' : 'Редактировать Gateway'} submitLabel={gatewayModalMode === 'add' ? 'Добавить' : 'Сохранить'} submitIcon={gatewayModalMode === 'add' ? IconPlus : IconEdit} size="lg" > {editingGateway && (
setEditingGateway({ ...editingGateway, serverId: val })} servers={servers} placeholder="Выберите сервер..." />
Провайдер будет автоматически взят из данных сервера
setEditingGateway({ ...editingGateway, ip: val })} placeholder="94.142.140.1" required />
IP адрес является главным идентификатором gateway
setEditingGateway({ ...editingGateway, description: val })} placeholder="SWE-IHOR или Основной выход через Cloudflare" />
Название gateway для удобства (отображается в фильтрах)
setEditingGateway({ ...editingGateway, type: val, parentGateways: val === 'direct' ? [] : (editingGateway.parentGateways || []) })} options={GATEWAY_TYPES.map(t => ({ value: t.value, label: `${t.label} - ${t.description}` }))} required />
setEditingGateway({ ...editingGateway, country: val.toUpperCase().slice(0, 2) })} placeholder="SWE" />
2-буквенный код страны (ISO 3166-1 alpha-2)
{editingGateway.type === 'recursive' && (
{(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} serverId={editingGateway.serverId} 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 или интерфейсов. Distance определяет приоритет маршрута (меньше = выше приоритет).
)} {editingGateway.serverId && (() => { const server = getServerInfo(editingGateway.serverId); return server ? (
{server.dns || server.ip} {server.provider && • {server.provider}} {server.country && • {countryToFlag(server.country)} {server.country}}
) : null; })()}
)}
{/* Interface Modal */} { setInterfaceModalOpen(false); setEditingInterface(null); }} onSubmit={() => editingInterface && handleSaveInterface(editingInterface)} title={interfaceModalMode === 'add' ? 'Добавить интерфейс' : 'Редактировать интерфейс'} submitLabel={interfaceModalMode === 'add' ? 'Добавить' : 'Сохранить'} submitIcon={interfaceModalMode === 'add' ? IconPlus : IconEdit} size="lg" > {editingInterface && (
setEditingInterface({ ...editingInterface, serverId: val })} servers={servers} placeholder="Выберите сервер..." />
Интерфейс будет привязан к выбранному серверу
setEditingInterface({ ...editingInterface, name: val })} placeholder="gre-tunnel1" required />
{editingInterface.serverId2 && (
setEditingInterface({ ...editingInterface, name2: val })} placeholder="gre-tunnel2" />
Если не указано, будет использовано имя с сервера 1
)}
setEditingInterface({ ...editingInterface, type: val, ipsecPasswordId: (val !== 'IPSec' && val !== 'GRE') ? '' : editingInterface.ipsecPasswordId })} options={INTERFACE_TYPES} />
{(editingInterface.type === 'IPSec' || editingInterface.type === 'GRE') && (
Выберите сохраненный IPSec пароль или создайте новый (опционально для GRE)
)}
Выберите IP пул для подбора адресов или оставьте автоматический подбор
setEditingInterface({ ...editingInterface, mtu: val })} placeholder="1500" type="number" />
Максимальный размер передаваемого пакета (опционально)
setEditingInterface({ ...editingInterface, localIp: e.target.value })} placeholder="10.10.0.1" />
{editingInterface.localIp && (() => { const conflicts = checkInterfaceIpConflict( { localIp: editingInterface.localIp }, interfaceModalMode === 'edit' ? editingInterface.id : null ).filter(c => c.type === 'localIp'); if (conflicts.length > 0) { return (
⚠️ Этот IP уже используется в интерфейсе "{conflicts[0].conflictingInterface.name || conflicts[0].conflictingInterface.id}"
); } return null; })()}
setEditingInterface({ ...editingInterface, remoteIp: e.target.value })} placeholder="10.10.0.2" />
{editingInterface.remoteIp && (() => { const conflicts = checkInterfaceIpConflict( { remoteIp: editingInterface.remoteIp }, interfaceModalMode === 'edit' ? editingInterface.id : null ).filter(c => c.type === 'remoteIp'); if (conflicts.length > 0) { return (
⚠️ Этот IP уже используется в интерфейсе "{conflicts[0].conflictingInterface.name || conflicts[0].conflictingInterface.id}"
); } return null; })()}
setEditingInterface({ ...editingInterface, serverId2: val })} servers={servers.filter(s => s.id !== editingInterface.serverId && s.ip !== editingInterface.serverId)} placeholder="Выберите второй сервер (для понимания связи между серверами)..." />
Укажите второй сервер, если интерфейс связывает два сервера
{editingInterface.serverId && (() => { const server = getServerInfo(editingInterface.serverId); return server ? (
Сервер 1: {server.dns || server.ip} {server.provider && ( <> {server.provider} )} {server.country && ( <> {countryToFlag(server.country)} {server.country} )}
) : null; })()} {editingInterface.serverId2 && (() => { const server2 = getServerInfo(editingInterface.serverId2); return server2 ? (
Сервер 2: {server2.dns || server2.ip} {server2.provider && ( <> {server2.provider} )} {server2.country && ( <> {countryToFlag(server2.country)} {server2.country} )}
) : null; })()} {/* Поля для замены PTR зоны */}

Настройка PTR зоны
setEditingInterface({ ...editingInterface, ptrZoneReplaceFrom: val })} placeholder="rt.shx" />
Часть DNS домена, которую нужно заменить (например, "rt.shx")
setEditingInterface({ ...editingInterface, ptrZoneReplaceTo: val })} placeholder="shrt" />
На что заменить (например, "shrt"). Результат: DNS "selectel.msk.rt.shx.su" → PTR "selectel.msk.shrt.su"
{editingInterface.serverId && editingInterface.ptrZoneReplaceFrom && editingInterface.ptrZoneReplaceTo && (() => { const server = getServerInfo(editingInterface.serverId); const originalDns = server?.dns || ''; const ptrZone = generatePtrZone(originalDns, editingInterface); return originalDns ? (
DNS: {originalDns}
PTR зона: {ptrZone}
) : null; })()}
)}
{/* Interface Template Modal */} { setInterfaceTemplateModalOpen(false); setTemplateServer1(''); setTemplateServer2(''); setTemplateType('GRE'); setTemplateNamePrefix(''); setTemplateName1(''); setTemplateName2(''); setTemplateIpsecPasswordId(''); setTemplateIpPoolId(''); setTemplateMtu(''); setTemplateTunnelCount(1); setTemplateServerPairs([]); }} onSubmit={handleCreateInterfaceFromTemplate} title="Создать интерфейс из шаблона" submitLabel={templateTunnelCount > 1 ? `Создать ${templateTunnelCount} туннелей` : "Создать интерфейс"} submitIcon={IconSparkles} size="lg" >
{ const value = parseInt(e.target.value) || 1; 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} пар серверов ниже` : 'Сколько туннелей создать между выбранными серверами (1-100)'}
setTemplateType(val)} options={INTERFACE_TYPES} />
setTemplateNamePrefix(e.target.value)} placeholder="tunnel" />
Будет добавлен перед именем интерфейса
{(templateTunnelCount === 1) && ( <>
setTemplateServer1(val)} servers={servers.filter(s => s.id !== templateServer2 && s.ip !== templateServer2)} placeholder="Выберите первый сервер..." /> {templateServer1 && (() => { const server1 = getServerInfo(templateServer1); return server1 ? (
{server1.dns || server1.ip} {server1.country && ` • ${countryToFlag(server1.country)} ${server1.country}`}
) : null; })()}
setTemplateServer2(val)} servers={servers.filter(s => s.id !== templateServer1 && s.ip !== templateServer1)} placeholder="Выберите второй сервер..." /> {templateServer2 && (() => { const server2 = getServerInfo(templateServer2); return server2 ? (
{server2.dns || server2.ip} {server2.country && ` • ${countryToFlag(server2.country)} ${server2.country}`}
) : null; })()}
)} {templateTunnelCount > 1 && (
{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 пул для подбора адресов или оставьте автоматический подбор. Будет использован для всех туннелей, если не указан индивидуально.
setTemplateMtu(e.target.value)} placeholder="1500" />
Максимальный размер передаваемого пакета (опционально). Будет использован для всех туннелей, если не указан индивидуально.
{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).toUpperCase(); const previewName2 = (templateName2.trim() || autoName2).toUpperCase(); 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}
)}
); })()} {templateTunnelCount > 1 && templateServerPairs.length === templateTunnelCount && (
{templateServerPairs.map((pair, index) => { const server1 = getServerInfo(pair.server1); const server2 = getServerInfo(pair.server2); if (!server1 || !server2 || !pair.server1 || !pair.server2 || pair.server1 === pair.server2) { return null; } const baseName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix); const baseName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix); const previewName1 = (pair.name1?.trim() || baseName1).toUpperCase(); const previewName2 = (pair.name2?.trim() || baseName2).toUpperCase(); // Используем настройки из пары или глобальные для предпросмотра const pairIpPoolId = pair.ipPoolId || templateIpPoolId; // Генерируем предпросмотр IP (используем временный Set для предпросмотра) const previewUsedIps = new Set(getAllUsedIpsSet); // Добавляем IP из предыдущих туннелей в предпросмотре for (let i = 0; i < index; i++) { const prevPair = templateServerPairs[i]; if (prevPair.server1 && prevPair.server2) { const prevPairIpPoolId = prevPair.ipPoolId || templateIpPoolId; const prevPreviewLocal = generateFreePrivateIp(null, false, null, prevPairIpPoolId || null, previewUsedIps); if (prevPreviewLocal) { previewUsedIps.add(prevPreviewLocal); const prevPreviewRemote = generateFreePrivateIp(null, true, prevPreviewLocal, prevPairIpPoolId || null, previewUsedIps); if (prevPreviewRemote && prevPreviewRemote !== prevPreviewLocal) { previewUsedIps.add(prevPreviewRemote); } } } } // Генерируем IP для текущего туннеля let previewLocalIp = generateFreePrivateIp(null, false, null, pairIpPoolId || null, previewUsedIps); let previewRemoteIp = null; if (previewLocalIp) { // Генерируем Remote IP на основе Local IP previewRemoteIp = generateFreePrivateIp(null, true, previewLocalIp, pairIpPoolId || null, previewUsedIps); // Если Remote IP не сгенерировался или совпадает с Local IP, пробуем найти другую пару if (!previewRemoteIp || previewRemoteIp === previewLocalIp) { // Добавляем текущий Local IP в использованные и пробуем найти другую пару previewUsedIps.add(previewLocalIp); const altPreviewLocalIp = generateFreePrivateIp(null, false, null, pairIpPoolId || null, previewUsedIps); if (altPreviewLocalIp && altPreviewLocalIp !== previewLocalIp) { const altPreviewRemoteIp = generateFreePrivateIp(null, true, altPreviewLocalIp, pairIpPoolId || null, previewUsedIps); if (altPreviewRemoteIp && altPreviewRemoteIp !== altPreviewLocalIp) { previewLocalIp = altPreviewLocalIp; previewRemoteIp = altPreviewRemoteIp; } else { // Если все равно не получилось, удаляем альтернативный Local IP previewUsedIps.delete(altPreviewLocalIp); } } } } return (
Туннель {index + 1}:
Сервер 1 ({server1.dns || server1.ip}):
Имя: {previewName1}
Local IP: {previewLocalIp || '—'}
Сервер 2 ({server2.dns || server2.ip}):
Имя: {previewName2}
Local IP: {previewRemoteIp || '—'}
{previewLocalIp && previewRemoteIp && (
{previewLocalIp} ↔ {previewRemoteIp}
)}
); })}
)}
{/* IPSec Password Modal */} { setIpsecPasswordModalOpen(false); setEditingIpsecPassword(null); }} onSubmit={handleSaveIpsecPassword} title={ipsecPasswordModalMode === 'add' ? 'Добавить IPSec пароль' : 'Редактировать IPSec пароль'} submitLabel={ipsecPasswordModalMode === 'add' ? 'Создать' : 'Сохранить'} submitIcon={ipsecPasswordModalMode === 'add' ? IconPlus : IconEdit} > {editingIpsecPassword && (
setEditingIpsecPassword({ ...editingIpsecPassword, name: val })} placeholder="Основной IPSec пароль" required />
setEditingIpsecPassword({ ...editingIpsecPassword, password: val })} placeholder="Введите пароль" required />
Пароль будет зашифрован перед сохранением в S3
setEditingIpsecPassword({ ...editingIpsecPassword, description: val })} placeholder="Описание использования пароля" />
)}
{/* IPSec Passwords List Modal */} {ipsecPasswordsListModalOpen && (
Управление IPSec паролями
Сохраненные IPSec пароли
Пароли хранятся в зашифрованном виде в S3
{ipsecPasswords.length === 0 ? (

IPSec пароли не найдены

Создайте первый IPSec пароль для использования в шаблонах интерфейсов

) : (
{ipsecPasswords.map(pwd => ( ))}
Название Описание Действия
{pwd.name} {pwd.description || '—'}
)}
)} {/* Pool Modal */} { setPoolModalOpen(false); setEditingPool(null); }} onSubmit={() => editingPool && handleSavePool(editingPool)} title={poolModalMode === 'add' ? 'Добавить IP пул' : 'Редактировать IP пул'} submitLabel={poolModalMode === 'add' ? 'Добавить' : 'Сохранить'} submitIcon={poolModalMode === 'add' ? IconPlus : IconEdit} > {editingPool && (
setEditingPool({ ...editingPool, name: val })} placeholder="GRE Tunnels" required />
setEditingPool({ ...editingPool, cidr: val })} placeholder="10.10.0.0/24" required />
setEditingPool({ ...editingPool, description: val })} placeholder="Пул для GRE туннелей между серверами" />
)}
{/* Delete Confirmation Modal */} { setDeleteModalOpen(false); setItemToDelete(null); }} onConfirm={executeDelete} title="Подтверждение удаления" message={ itemToDelete ? ( <>

Вы уверены, что хотите удалить {itemToDelete.ip || itemToDelete.name || itemToDelete.cidr || 'этот элемент'}?

Это действие нельзя отменить.

) : null } confirmLabel="Удалить" variant="danger" icon={IconTrash} /> {/* MikroTik Code Modal */} {mikrotikCodeModalOpen && (
Предварительный просмотр конфигурации MikroTik
Конфигурация MikroTik RouterOS для рекурсивных маршрутов и IP адресов интерфейсов
{Object.keys(groupedMikrotikCode).length > 0 ? (
{Object.entries(groupedMikrotikCode) .sort(([a], [b]) => a.localeCompare(b)) .map(([serverName, serverData]) => { const server = serverData.server; const isUnassigned = serverName === '__unassigned__'; return (
{/* Заголовок сервера */}
{isUnassigned ? ( <> Без привязки к серверу ) : ( <>
{serverName}
{server && (
{server.provider && {server.provider}} {server.provider && server.country && } {server.country && ( {countryToFlag(server.country)} {server.country} )} {server.ip && server.ip !== serverName && ( <> {(server.provider || server.country) && } {server.ip} )}
)}
)}
{/* Блоки кода для этого сервера */}
{serverData.blocks.map((block, blockIndex) => { const blockTitle = block.type === 'recursive-routes' ? 'Рекурсивные маршруты' : block.type === 'interface-addresses' ? 'IP адреса интерфейсов' : 'Конфигурация'; return (
{/* Заголовок блока */}
{blockTitle}
{/* Код блока */}
                                        {block.code}
                                      
); })}
); })}
) : (
Нет данных для генерации конфигурации
)}

Инструкция по применению:

  1. Скопируйте конфигурацию в буфер обмена
  2. Подключитесь к MikroTik через SSH или Winbox
  3. Вставьте конфигурацию в терминал
  4. Проверьте применение командой: /ip route print или /ip address print
)} {/* Gateway Template Modal */} { setGatewayTemplateModalOpen(false); setSelectedTemplate(null); setTemplateServerId(''); setTemplateBaseIp(''); setTemplateGateways([]); }} onSubmit={handleCreateGatewaysFromTemplate} title="Создать gateways из шаблона" submitLabel={selectedTemplate ? `Создать ${templateGateways.length} gateways` : "Выберите шаблон"} submitIcon={IconSparkles} size="lg" submitDisabled={!selectedTemplate || !templateServerId || !templateBaseIp} >
{/* Выбор шаблона */}
{GATEWAY_TEMPLATES.map(template => { // Генерируем предпросмотр IP адресов если есть базовый IP и сервер const previewIps = template.gateways.map((gw, idx) => { // Для прямых gateways показываем IP сервера, если выбран if (gw.type === 'direct' && templateServerId) { const server = getServerInfo(templateServerId); if (server && server.ip) { return server.ip; } } // Для рекурсивных gateways генерируем из базового IP if (gw.type === 'recursive' && templateBaseIp && gw.ipTemplate) { const normalizedBaseIp = normalizeBaseIp(templateBaseIp); if (normalizedBaseIp && normalizedBaseIp.split('.').length >= 3) { return gw.ipTemplate.replace('{baseIp}', normalizedBaseIp); } } return '—'; }); return (
handleSelectTemplate(template)} >
{template.name}

{template.description}

{template.gateways.length} {template.gateways.length === 1 ? 'gateway' : 'gateways'} {template.gateways.filter(g => g.type === 'direct').length} прямых, {template.gateways.filter(g => g.type === 'recursive').length} рекурсивных
{templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && templateServerId && (
Предпросмотр IP:
{previewIps.map((ip, idx) => ( {ip} ))}
)}
{selectedTemplate?.id === template.id && ( )}
); })}
{/* Общие параметры */} {selectedTemplate && ( <>

Общие параметры
setTemplateServerId(val)} servers={servers} placeholder="Выберите сервер для всех gateways..." />
setTemplateBaseIp(e.target.value)} placeholder="10.9.9 или 10.9.9.1 (минимум 3 октета, полный IP будет нормализован)" />
Базовый IP для генерации адресов рекурсивных gateways. Можно ввести 3 октета (например, "10.9.9") или полный IP (например, "10.9.9.1") - система автоматически возьмет первые 3 октета. Для "10.9.9" будут созданы IP: 10.9.9.1, 10.9.9.2, 10.9.9.3 и т.д. Прямые gateways будут использовать IP адрес выбранного сервера.
{/* Редактирование отдельных gateways */}

Настройка gateways
{templateGateways.map((gw, index) => { const gatewayType = GATEWAY_TYPES.find(t => t.value === gw.type); return (
{gatewayType?.label || gw.type} Gateway {index + 1}
{ const newGateways = [...templateGateways]; newGateways[index].description = e.target.value; setTemplateGateways(newGateways); }} placeholder="Название gateway" />
{ const newGateways = [...templateGateways]; newGateways[index].ip = e.target.value; setTemplateGateways(newGateways); }} placeholder={ gw.type === 'direct' && templateServerId ? (getServerInfo(templateServerId)?.ip || 'IP сервера') : (gw.ipTemplate ? gw.ipTemplate.replace('{baseIp}', normalizeBaseIp(templateBaseIp) || 'X.X.X') : 'IP адрес') } />
{gw.type === 'direct' ? (templateServerId ? `IP сервера: ${getServerInfo(templateServerId)?.ip || '—'}` : 'Выберите сервер для автоматической подстановки IP') : `Шаблон: ${gw.ipTemplate || '—'}` }
{ const newGateways = [...templateGateways]; newGateways[index].country = e.target.value.toUpperCase().slice(0, 2); setTemplateGateways(newGateways); }} placeholder="SE, FI, RU..." maxLength={2} />
{gw.type === 'recursive' && (
{(gw.parentGateways || []).map((parent, parentIndex) => { const parentInfo = getParentGateway(parent.id, templateGateways); return (
{ const newGateways = [...templateGateways]; if (!newGateways[index].parentGateways) { newGateways[index].parentGateways = []; } newGateways[index].parentGateways[parentIndex] = { ...newGateways[index].parentGateways[parentIndex], id: val }; setTemplateGateways(newGateways); }} gateways={config.gateways} interfaces={config.tunnelInterfaces} templateGateways={templateGateways} serverId={templateServerId} excludeGatewayId={gw.id} placeholder="Выберите родительский gateway или интерфейс..." />
{ const newGateways = [...templateGateways]; if (!newGateways[index].parentGateways) { newGateways[index].parentGateways = []; } newGateways[index].parentGateways[parentIndex] = { ...newGateways[index].parentGateways[parentIndex], distance: e.target.value ? parseInt(e.target.value) : undefined }; setTemplateGateways(newGateways); }} placeholder="Distance" />
); })}
)}
); })}
)}
); } export default NetworkConfigManager;