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

405 lines
14 KiB
JavaScript

/**
* Генератор конфигурации 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 += '# Удаление существующих рекурсивных маршрутов (опционально)\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,
};