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 { countryToFlag } from './utils/serverUtils.js'; import { IconNetwork, IconPlus, IconEdit, IconTrash, IconDatabase, IconServer, IconWorld, IconRouter, IconCopy, IconSearch, IconRefresh, IconFilter, IconX, IconCircleFilled, IconLayoutGrid, IconList, IconCode, IconArrowsRightLeft } 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' }, ]; // Пустые объекты const getEmptyGateway = () => ({ id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, ip: '', description: '', serverId: '', country: '', type: 'direct', parentGatewayId: '', // Для рекурсивных - ID родительского gateway }); const getEmptyInterface = () => ({ id: `if-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, name: '', name2: '', // Имя интерфейса на втором сервере (опционально) type: 'GRE', localIp: '', remoteIp: '', serverId: '', serverId2: '', // Второй сервер (опциональный) }); 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' // === 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(''); // === MikroTik Code Generation === const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false); const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]); // === Загрузка данных === useEffect(() => { fetchConfig(); fetchServers(); }, []); const fetchConfig = async () => { setLoading(true); try { const response = await api.get('/network-config'); const data = response.data || getDefaultConfig(); setConfig({ gateways: Array.isArray(data.gateways) ? data.gateways : [], 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 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 uniqueServersInConfig = useMemo(() => { const allServerIds = new Set([ ...(config.gateways || []).map(g => g.serverId).filter(Boolean), ...(config.tunnelInterfaces || []).map(i => i.serverId).filter(Boolean), ]); return Array.from(allServerIds).map(id => ({ id, label: getServerLabel(id), server: getServerInfo(id), })); }, [config.gateways, config.tunnelInterfaces, 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 (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(p => p.name?.toLowerCase().includes(term) || p.cidr?.toLowerCase().includes(term) || p.description?.toLowerCase().includes(term) ); } return result; }, [config.ipPools, searchTerm]); // === Группировка по серверам === 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 resetFilters = () => { setSearchTerm(''); setProviderFilter(''); setServerFilter(''); setTypeFilter(''); setGatewayTypeFilter(''); }; const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter; // === CRUD для Gateways === const handleAddGateway = () => { setEditingGateway(getEmptyGateway()); setGatewayModalMode('add'); setGatewayModalOpen(true); }; const handleEditGateway = (gateway) => { setEditingGateway({ ...gateway }); setGatewayModalMode('edit'); setGatewayModalOpen(true); }; const handleSaveGateway = (gatewayData) => { // Валидация: IP адрес обязателен if (!gatewayData.ip || !gatewayData.ip.trim()) { notify.error('IP адрес обязателен'); return; } if (gatewayModalMode === 'add') { // Проверка на дубликат IP const existingGateway = config.gateways.find(g => g.ip === gatewayData.ip.trim()); if (existingGateway) { notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, gateways: [...prev.gateways, gatewayData], })); notify.success('Gateway добавлен'); } else { // При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway const currentGateway = config.gateways.find(g => g.id === gatewayData.id); if (!currentGateway) { notify.error('Gateway не найден'); return; } // Если IP изменился, проверяем, не занят ли он другим gateway if (gatewayData.ip.trim() !== currentGateway.ip && config.gateways.some(g => g.id !== gatewayData.id && g.ip === gatewayData.ip.trim())) { notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g), })); notify.success('Gateway обновлён'); } setGatewayModalOpen(false); setEditingGateway(null); }; const handleDeleteGateway = (gateway) => { setItemToDelete(gateway); setDeleteType('gateway'); setDeleteModalOpen(true); }; // === CRUD для Interfaces === const handleAddInterface = () => { setEditingInterface(getEmptyInterface()); setInterfaceModalMode('add'); setInterfaceModalOpen(true); }; const handleEditInterface = (iface) => { setEditingInterface({ ...iface }); setInterfaceModalMode('edit'); setInterfaceModalOpen(true); }; const handleSaveInterface = (ifaceData) => { if (interfaceModalMode === 'add') { setConfig(prev => ({ ...prev, tunnelInterfaces: [...prev.tunnelInterfaces, ifaceData], })); notify.success('Интерфейс добавлен'); } else { setConfig(prev => ({ ...prev, tunnelInterfaces: prev.tunnelInterfaces.map(i => i.id === ifaceData.id ? ifaceData : i), })); notify.success('Интерфейс обновлён'); } setInterfaceModalOpen(false); setEditingInterface(null); }; const handleDeleteInterface = (iface) => { setItemToDelete(iface); setDeleteType('interface'); setDeleteModalOpen(true); }; // === 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 для настройки IP адресов интерфейсов === const generateMikrotikInterfaceAddresses = () => { const interfacesWithBothServers = (config.tunnelInterfaces || []).filter(i => i.serverId && i.serverId2 && i.localIp && i.remoteIp ); if (interfacesWithBothServers.length === 0) { return []; } // Группируем по серверам 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 }); // Для второго сервера - МЕНЯЕМ МЕСТАМИ: 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 }); }); // Сортируем серверы по имени для консистентности 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'; 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`; code += `/ip address add address=${iface.localIp}/32 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`; code += '\n'; }); code += '# Проверка настроенных адресов на этом сервере:\n'; code += '# /ip address print\n'; blocks.push({ type: 'interface-addresses', serverName, server, code }); }); return blocks; }; // === Генерация кода MikroTik для рекурсивных маршрутов === const generateMikrotikRecursiveRoutes = () => { 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 => { const parent = getParentGateway(gw.parentGatewayId); if (!parent) { code += `# Gateway "${gw.ip || 'без IP'}" - родительский gateway/интерфейс не найден, пропущен\n`; return; } if (!gw.ip) { code += `# Gateway без IP адреса - пропущен\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}`; code += `# Рекурсивный gateway: ${gw.ip}\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 ? `${gw.description} -> ${parentIp}` : `Recursive: ${gw.ip} -> ${parentIp}`; code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue} 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\n`; const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`; code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue} 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 = generateMikrotikInterfaceAddresses(); if (interfaceBlocks && interfaceBlocks.length > 0) { blocks.push(...interfaceBlocks); } return blocks; }; // === Обработчик кнопки "Код для MikroTik" === const handleGenerateMikrotikCode = () => { const code = generateMikrotikRecursiveRoutes(); setGeneratedMikrotikCode(code); setMikrotikCodeModalOpen(true); }; // === Копирование кода 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 renderServerGroup = (serverId, items, type) => { const server = getServerInfo(serverId); const isUnassigned = serverId === '__unassigned__'; return (
{/* Server header */}
{isUnassigned ? ( <> Без привязки к серверу ) : ( <>
{server?.dns || serverId} {server && ( {server.ip} {server.country && ( {countryToFlag(server.country)} {server.country} )} )}
)}
{items.length}
{/* Items */}
{items.map(item => (
{type === 'gateway' ? renderGatewayCard(item) : renderInterfaceCard(item, serverId)}
))}
); }; // === Получение родительского gateway или интерфейса === const getParentGateway = (parentId) => { if (!parentId) return null; // Сначала ищем в 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) }); return null; }; // === Рендер карточки 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]; const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId ? getParentGateway(gateway.parentGatewayId) : null; // Определяем тип индикатора (как в ServerCard) const indicatorType = gateway.type === 'recursive' ? 'EXIT' : 'JUMP'; 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' && parentGateway) { // Для рекурсивного: IP адрес -> IP адрес родительского gateway const parentIp = parentGateway.parentType === 'interface' ? parentGateway.remoteIp : parentGateway.ip; displayText = gateway.ip && parentIp ? `${gateway.ip} → ${parentIp}` : ''; } return (
{/* Левая часть: тип индикатора (как в ServerCard) */}
{indicatorType}
{/* Центральная часть: информация */}
{/* Верхняя строка: флаг, название, провайдер */}
{displayCountry && ( {countryToFlag(displayCountry)} )}
{gateway.description || provider || gateway.ip || '—'} {provider && ( <> {provider} )}
{/* Зеленый блок с маршрутом */} {displayText && ( {displayText} )}
{/* Нижняя строка: DNS + badges */}
{server && ( {server.dns || server.ip || gateway.serverId} )} {gatewayType.label} {gateway.ip && ( )}
{/* Правая часть: действия */}
); }; // === Рендер карточки 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}
{/* Зеленый блок с IP */} {displayIp && displayIp !== '—' && ( → {displayIp} )}
{/* Нижняя строка: DNS + badges */}
{displayServerName && ( {displayServerName} )} {iface.type} {displayIp && displayIp !== '—' && ( )}
{/* Правая часть: действия */}
); }; return (
{/* Page Header */}
Управление

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

{/* Tabs as button group */}
{/* Toolbar */}
{/* Search */}
setSearchTerm(e.target.value)} /> {searchTerm && ( setSearchTerm('')}> )}
{/* Server filter */} {(activeTab === 'gateways' || activeTab === 'interfaces') && 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
{Object.entries(gatewaysByServer).map(([serverId, gateways]) => renderServerGroup(serverId, gateways, '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]; const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId ? getParentGateway(gateway.parentGatewayId) : null; return ( ); })}
IP адрес Тип Провайдер Сервер Страна Действия
{gateway.ip || '—'} {gateway.ip && ( )}
{gateway.description && (
{gateway.description}
)}
{gatewayType.label} {parentGateway && (
→ {parentGateway.parentType === 'interface' ? `Интерфейс ${parentGateway.name || parentGateway.interfaceType || 'interface'} (${parentGateway.remoteIp || '—'})` : parentGateway.ip ? `Gateway ${parentGateway.ip}` : 'Gateway (не указан IP)' }
)}
{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
{Object.entries(interfacesByServer).map(([serverId, interfaces]) => renderServerGroup(serverId, interfaces, '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 && ( )}
) : (
{filteredPools.map(pool => (
{pool.name || '—'}
{pool.cidr || '—'} {pool.cidr && ( )}
{pool.description && (
{pool.description}
)}
))}
)} )} )} {/* 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, parentGatewayId: val === 'direct' ? '' : editingGateway.parentGatewayId })} 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' && (
setEditingGateway({ ...editingGateway, parentGatewayId: val })} gateways={config.gateways} interfaces={config.tunnelInterfaces} excludeGatewayId={editingGateway.id} placeholder="Выберите родительский gateway или интерфейс..." />
Рекурсивный gateway может ссылаться на прямой gateway или на интерфейс (по его remote IP)
)} {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 })} options={INTERFACE_TYPES} />
setEditingInterface({ ...editingInterface, localIp: val })} placeholder="10.10.0.1" />
setEditingInterface({ ...editingInterface, remoteIp: val })} placeholder="10.10.0.2" />
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; })()}
)}
{/* 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
)}
); } export default NetworkConfigManager;