Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m37s
405 lines
14 KiB
JavaScript
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,
|
|
};
|