Files
router-lists-ui/backend/utils/mikrotikInterfaceGenerator.js

405 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Генератор конфигурации 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<Array>}
*/
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<Array>}
*/
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,
};