/** * Генератор конфигурации MikroTik для интерфейсов и маршрутов * Поддерживает два формата вывода: 'text' (как раньше) и 'json' (структурированные операции для API) */ /** * @param {string} serverId * @param {Array} servers * @returns {object|null} */ function getServerInfo(serverId, servers) { if (!serverId || !Array.isArray(servers)) return null; return servers.find(s => s.id === serverId || s.ip === serverId || s.dns === serverId) || null; } /** * @param {string} parentId * @param {object} config * @param {Array} templateGateways * @returns {object|null} */ function getParentGateway(parentId, config, templateGateways = null) { if (!parentId || !config) return null; if (Array.isArray(templateGateways)) { const g = templateGateways.find(x => x.id === parentId); if (g) return { parentType: 'gateway', ...g, ip: g.ip }; } const gateway = config.gateways?.find(g => g.id === parentId); if (gateway) return { parentType: 'gateway', ...gateway, ip: gateway.ip }; const iface = config.tunnelInterfaces?.find(i => i.id === parentId); if (iface) { return { parentType: 'interface', ...iface, remoteIp: iface.remoteIp, localIp: iface.localIp, name: iface.name, interfaceType: iface.type, }; } return null; } /** * Сериализация одной операции в строку RouterOS * @param {object} op - { path, action, params } * @returns {string} */ function operationToRouterOSLine(op) { const { path, action, params } = op; if (!path || !action) return ''; const pathStr = path.replace(/^\//, '').replace(/\//g, ' '); const fullPath = pathStr ? `/${pathStr}` : ''; const parts = (params && typeof params === 'object') ? Object.entries(params) .filter(([, v]) => v != null && v !== '') .map(([k, v]) => { const val = String(v); if (val.includes(' ') || val.includes('"')) return `${k}="${val}"`; return `${k}=${val}`; }) : []; return `${fullPath} ${action} ${parts.join(' ')}`.trim(); } /** * Сериализация массива операций в текст * @param {Array} operations * @returns {string} */ function serializeOperationsToText(operations) { if (!Array.isArray(operations)) return ''; return operations.map(op => operationToRouterOSLine(op)).filter(Boolean).join('\n'); } /** * Построение блоков MikroTik для интерфейсов * @param {object} config - { tunnelInterfaces, gateways } * @param {Array} servers * @param {object} passwordMap - { [ipsecPasswordId]: decryptedPassword } * @param {object} options - { format: 'text'|'json', serverId?: string } * @returns {Promise} */ async function buildMikrotikInterfaceBlocks(config, servers, passwordMap, options = {}) { const { format = 'text', serverId: filterServerId } = options; const interfaces = (config.tunnelInterfaces || []).filter( i => i.serverId && i.serverId2 && i.localIp && i.remoteIp ); if (interfaces.length === 0) return []; const interfacesByServer = {}; for (const iface of interfaces) { const server1 = getServerInfo(iface.serverId, servers); const server2 = getServerInfo(iface.serverId2, servers); if (!server1 || !server2) continue; const server1Name = server1.dns || server1.ip || iface.serverId; const server2Name = server2.dns || server2.ip || iface.serverId2; const interfaceName1 = iface.name || `${iface.type}-tunnel`; const interfaceName2 = iface.name2 || iface.name || `${iface.type}-tunnel`; const ipsecPassword = (iface.ipsecPasswordId && passwordMap[iface.ipsecPasswordId]) || null; 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, ipsecPassword, mtu: iface.mtu || null, }); if (!interfacesByServer[server2Name]) { interfacesByServer[server2Name] = { serverInfo: server2, interfaces: [] }; } interfacesByServer[server2Name].interfaces.push({ interfaceName: interfaceName2, localIp: iface.remoteIp, remoteIp: iface.localIp, type: iface.type, server2Name: server1Name, server2Info: server1, ipsecPassword, mtu: iface.mtu || null, }); } const sortedServers = Object.entries(interfacesByServer).sort(([a], [b]) => a.localeCompare(b)); const blocks = []; for (const [serverName, serverData] of sortedServers) { if (filterServerId) { const s = getServerInfo(filterServerId, servers); const matchName = s ? (s.dns || s.ip || filterServerId) : filterServerId; if (serverName !== matchName && serverData.serverInfo?.id !== filterServerId) continue; } const server = serverData.serverInfo; const isHomeRouter = server.type === 'home'; const hasGreInterfaces = serverData.interfaces.some(i => i.type === 'GRE'); const operations = []; let code = ''; if (format === 'text') { 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'; } if (isHomeRouter && hasGreInterfaces) { if (format === 'text') { code += `# Создание interface list для GRE туннелей (если не существует)\n`; code += `:if ([/interface list find name=GREs] = "") do={ /interface list add name=GREs comment="GRE tunnels list" }\n\n`; } else { operations.push({ path: '/interface/list', action: 'add', params: { name: 'GREs', comment: 'GRE tunnels list' }, meta: { ensureExists: true }, }); } } for (let i = 0; i < serverData.interfaces.length; i++) { const iface = serverData.interfaces[i]; const remoteAddress = iface.server2Info?.dns || iface.server2Info?.ip || iface.server2Name; const hasIpsecPassword = iface.ipsecPassword && String(iface.ipsecPassword).trim() !== ''; const localAddressParam = isHomeRouter && server.ip ? server.ip : null; if (format === 'text') { code += `# Интерфейс ${i + 1}: ${iface.interfaceName} (${iface.type})\n`; code += `# Связь с сервером: ${iface.server2Name}\n`; code += `# Local IP: ${iface.localIp}\n`; code += `# Remote IP: ${iface.remoteIp}\n`; } if (iface.type === 'GRE') { const greParams = { name: iface.interfaceName, 'remote-address': remoteAddress, 'keepalive': '10s', 'allow-fast-path': 'no', }; if (localAddressParam) greParams['local-address'] = localAddressParam; if (iface.mtu) greParams.mtu = String(iface.mtu); if (hasIpsecPassword) greParams['ipsec-secret'] = iface.ipsecPassword; const op = { path: '/interface/gre', action: 'add', params: greParams }; operations.push(op); if (format === 'text') { if (hasIpsecPassword) code += `# Создание GRE туннеля с IPSec\n`; else code += `# Создание GRE туннеля (без IPSec)\n`; code += operationToRouterOSLine(op) + '\n'; } if (isHomeRouter) { const listOp = { path: '/interface/list/member', action: 'add', params: { list: 'GREs', interface: iface.interfaceName, comment: `GRE tunnel to ${iface.server2Name}` }, }; operations.push(listOp); if (format === 'text') code += operationToRouterOSLine(listOp) + '\n'; } } const addrOp = { path: '/ip/address', action: 'add', params: { address: `${iface.localIp}/30`, interface: iface.interfaceName, comment: `Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}`, }, }; operations.push(addrOp); if (format === 'text') code += operationToRouterOSLine(addrOp) + '\n'; } if (format === 'text') { code += '# Проверка настроенных адресов:\n'; code += '# /ip address print\n'; } blocks.push({ type: 'interface-addresses', serverName, server, ...(format === 'text' ? { code } : { operations }), }); } return blocks; } /** * Построение блоков для рекурсивных маршрутов * @param {object} config * @param {Array} servers * @param {object} options - { format: 'text'|'json', serverId?: string } * @returns {Promise} */ async function buildMikrotikRecursiveRoutes(config, servers, options = {}) { const { format = 'text', serverId: filterServerId } = options; const recursiveGateways = (config.gateways || []).filter(g => g.type === 'recursive'); const blocks = []; if (recursiveGateways.length === 0) return []; const gatewaysByServer = {}; for (const gw of recursiveGateways) { const sid = gw.serverId || '__unassigned__'; if (!gatewaysByServer[sid]) gatewaysByServer[sid] = []; gatewaysByServer[sid].push(gw); } for (const [sid, gateways] of Object.entries(gatewaysByServer)) { const server = getServerInfo(sid, servers); const serverName = server?.dns || server?.ip || sid; if (filterServerId && sid !== filterServerId) continue; const operations = []; let code = ''; if (format === 'text') { code += `# Рекурсивные маршруты для сервера: ${serverName}\n`; if (server?.provider) code += `# Провайдер: ${server.provider}\n`; if (server?.country) code += `# Страна: ${server.country}\n`; code += '\n'; code += '# Удаление старых рекурсивных маршрутов (comment начинается с "Recursive: ")\n'; code += '/ip route remove [find comment~"^Recursive: "] \n\n'; } else { operations.push({ path: '/ip/route', action: 'remove', params: {}, meta: { findComment: '^Recursive: ' }, }); } let validCount = 0; for (const gw of gateways) { const parentGatewaysList = (gw.parentGateways && gw.parentGateways.length > 0) ? gw.parentGateways : (gw.parentGatewayId ? [{ id: gw.parentGatewayId, distance: undefined }] : []); if (parentGatewaysList.length === 0 || !gw.ip) continue; for (const parentRef of parentGatewaysList) { const parent = getParentGateway(parentRef.id, config); if (!parent) continue; const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; if (!parentIp) continue; validCount++; const gatewayValue = parent.parentType === 'interface' ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` : parentIp; const distance = parentRef.distance; const comment = gw.description ? `Recursive: ${gw.description} -> ${parentIp}` : `Recursive: ${gw.ip} -> ${parentIp}`; const routeParams = { 'dst-address': `${gw.ip}/32`, gateway: gatewayValue, comment, }; if (distance != null) routeParams.distance = String(distance); const op = { path: '/ip/route', action: 'add', params: routeParams }; operations.push(op); if (format === 'text') { code += `# Рекурсивный gateway: ${gw.ip}\n`; code += operationToRouterOSLine(op) + '\n'; } if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { const defaultParams = { 'dst-address': '0.0.0.0/0', gateway: gatewayValue, comment: `Recursive: ${gw.description || gw.ip} (default)`, }; if (distance != null) defaultParams.distance = String(distance); const defaultOp = { path: '/ip/route', action: 'add', params: defaultParams }; operations.push(defaultOp); if (format === 'text') code += operationToRouterOSLine(defaultOp) + '\n'; } } } if (validCount > 0) { if (format === 'text') { code += '# Проверка: /ip route print where comment~"Recursive"\n'; } blocks.push({ type: 'recursive-routes', serverName, server, ...(format === 'text' ? { code } : { operations }), }); } } return blocks; } /** * Комбинированная генерация: интерфейсы + рекурсивные маршруты * @param {object} config * @param {Array} servers * @param {object} passwordMap * @param {object} options - { format: 'text'|'json', serverId?: string, includeInterfaces?: boolean, includeRecursive?: boolean } */ async function buildMikrotikConfig(config, servers, passwordMap, options = {}) { const { format = 'text', serverId, includeInterfaces = true, includeRecursive = true, } = options; const opts = { format, serverId }; const blocks = []; if (includeInterfaces) { const ifBlocks = await buildMikrotikInterfaceBlocks(config, servers, passwordMap, opts); blocks.push(...ifBlocks); } if (includeRecursive) { const recBlocks = await buildMikrotikRecursiveRoutes(config, servers, opts); blocks.push(...recBlocks); } return blocks; } module.exports = { getServerInfo, getParentGateway, operationToRouterOSLine, serializeOperationsToText, buildMikrotikInterfaceBlocks, buildMikrotikRecursiveRoutes, buildMikrotikConfig, };