feat(MikrotikConfigRoutes): enhance gateway and interface matching logic to support additional server IDs and improve route extraction
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m24s

This commit is contained in:
2026-02-22 18:03:58 +07:00
parent f80e962767
commit 8c13ae9df4
+51 -43
View File
@@ -455,12 +455,11 @@ async function runPingViaRouter(serverId, gatewayIp, target, count = 5, interfac
const config = await loadNetworkConfig(); const config = await loadNetworkConfig();
const tunnelIfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : []; const tunnelIfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
// 1) Пытаемся сопоставить gatewayIp с локальным/удалённым IP туннельного интерфейса // 1) Пытаемся сопоставить gatewayIp с локальным/удалённым IP туннельного интерфейса (сервер может быть serverId или serverId2)
const iface = tunnelIfaces.find((i) => { const iface = tunnelIfaces.find((i) => {
const sameServer = const sameServer =
i.serverId === serverId || i.serverId === serverId || i.serverId === server.ip || i.serverId === server.dns ||
i.serverId === server.ip || i.serverId2 === serverId || i.serverId2 === server.ip || i.serverId2 === server.dns;
i.serverId === server.dns;
const sameIp = i.remoteIp === gatewayIp || i.localIp === gatewayIp; const sameIp = i.remoteIp === gatewayIp || i.localIp === gatewayIp;
return sameServer && sameIp; return sameServer && sameIp;
}); });
@@ -474,9 +473,8 @@ async function runPingViaRouter(serverId, gatewayIp, target, count = 5, interfac
const gw = config.gateways.find((g) => { const gw = config.gateways.find((g) => {
if (!g || !g.ip) return false; if (!g || !g.ip) return false;
const sameServer = const sameServer =
g.serverId === serverId || g.serverId === serverId || g.serverId === server.ip || g.serverId === server.dns ||
g.serverId === server.ip || (g.serverId2 && (g.serverId2 === serverId || g.serverId2 === server.ip || g.serverId2 === server.dns));
g.serverId === server.dns;
return sameServer && g.ip === gatewayIp; return sameServer && g.ip === gatewayIp;
}); });
@@ -504,33 +502,40 @@ async function runPingViaRouter(serverId, gatewayIp, target, count = 5, interfac
} }
// 3) Если даже из /network-config не смогли получить interface — // 3) Если даже из /network-config не смогли получить interface —
// пробуем вытащить его напрямую из маршрутов MikroTik по полю gateway. // пробуем вытащить его из маршрутов MikroTik (gateway в формате "IP%interface" или отдельное поле interface).
if (!ifaceName) { if (!ifaceName) {
try { try {
const routeRes = await client.command('ip/route/print', { const routeRes = await client.command('ip/route/print', {
'.proplist': ['gateway'], '.proplist': ['gateway', 'interface'],
'.query': [`gateway~${gatewayIp}`],
}); });
const data = routeRes?.data; const data = routeRes?.data;
const routes = Array.isArray(data) ? data : (data ? [data] : []); const routes = Array.isArray(data) ? data : (data ? [data] : []);
const match = routes.find((r) => typeof r.gateway === 'string' && r.gateway.includes(gatewayIp)); const routeList = Array.isArray(routes) ? routes : (routes && typeof routes === 'object' && routes.ret ? routes.ret : []);
if (match && typeof match.gateway === 'string') { const match = routeList.find((r) => {
const gwStr = match.gateway; const gw = r.gateway ?? r.Gateway;
const percentIdx = gwStr.indexOf('%'); return typeof gw === 'string' && (gw.includes(gatewayIp) || gw.startsWith(gatewayIp));
if (percentIdx >= 0 && percentIdx < gwStr.length - 1) { });
ifaceName = gwStr.slice(percentIdx + 1); if (match) {
const gwStr = String(match.gateway ?? match.Gateway ?? '');
const ifaceFromRoute = match.interface ?? match.Interface;
if (typeof ifaceFromRoute === 'string' && ifaceFromRoute.trim()) {
ifaceName = ifaceFromRoute.trim();
} else {
const percentIdx = gwStr.indexOf('%');
if (percentIdx >= 0 && percentIdx < gwStr.length - 1) {
ifaceName = gwStr.slice(percentIdx + 1).trim();
}
} }
} }
} catch (_) { } catch (_) {
// Если чтение маршрутов не удалось — просто продолжаем без interface, // Если чтение маршрутов не удалось — просто продолжаем без interface.
// чтобы не ломать сам ping.
} }
} }
} }
// 4) Последний фоллбек (только для jumphost или home с указанным шлюзом): ether1/ether2. // 4) При указанном gatewayIp не подставляем ether1/ether2 — иначе пинг пойдёт не в туннель. Оставляем interface пустым, RouterOS выберет маршрут.
// Для home без шлюза интерфейс не задаём — пинг идёт по маршруту по умолчанию. // Фоллбек ether1/ether2 только если шлюз не указан (редкий кейс).
if (!ifaceName && !isHomeNoGateway) { if (!ifaceName && !isHomeNoGateway && !gatewayIp) {
try { try {
const ifRes = await client.command('interface/print', { const ifRes = await client.command('interface/print', {
'.proplist': ['name'], '.proplist': ['name'],
@@ -727,12 +732,11 @@ async function tracerouteViaGateway(req, res) {
const config = await loadNetworkConfig(); const config = await loadNetworkConfig();
const tunnelIfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : []; const tunnelIfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
// 1) Сопоставляем gatewayIp с локальным/удалённым IP туннельного интерфейса // 1) Сопоставляем gatewayIp с локальным/удалённым IP туннельного интерфейса (сервер может быть serverId или serverId2)
const iface = tunnelIfaces.find((i) => { const iface = tunnelIfaces.find((i) => {
const sameServer = const sameServer =
i.serverId === serverId || i.serverId === serverId || i.serverId === server.ip || i.serverId === server.dns ||
i.serverId === server.ip || i.serverId2 === serverId || i.serverId2 === server.ip || i.serverId2 === server.dns;
i.serverId === server.dns;
const sameIp = i.remoteIp === gatewayIp || i.localIp === gatewayIp; const sameIp = i.remoteIp === gatewayIp || i.localIp === gatewayIp;
return sameServer && sameIp; return sameServer && sameIp;
}); });
@@ -746,9 +750,8 @@ async function tracerouteViaGateway(req, res) {
const gw = config.gateways.find((g) => { const gw = config.gateways.find((g) => {
if (!g || !g.ip) return false; if (!g || !g.ip) return false;
const sameServer = const sameServer =
g.serverId === serverId || g.serverId === serverId || g.serverId === server.ip || g.serverId === server.dns ||
g.serverId === server.ip || (g.serverId2 && (g.serverId2 === serverId || g.serverId2 === server.ip || g.serverId2 === server.dns));
g.serverId === server.dns;
return sameServer && g.ip === gatewayIp; return sameServer && g.ip === gatewayIp;
}); });
@@ -776,34 +779,39 @@ async function tracerouteViaGateway(req, res) {
} }
// 3) Если даже из /network-config не смогли получить interface — // 3) Если даже из /network-config не смогли получить interface —
// пробуем вытащить его напрямую из маршрутов MikroTik по полю gateway. // пробуем вытащить его из маршрутов MikroTik (gateway в формате "IP%interface" или отдельное поле interface).
if (!ifaceName) { if (!ifaceName) {
try { try {
const routeRes = await client.command('ip/route/print', { const routeRes = await client.command('ip/route/print', {
'.proplist': ['gateway'], '.proplist': ['gateway', 'interface'],
'.query': [`gateway~${gatewayIp}`],
}); });
const data = routeRes?.data; const data = routeRes?.data;
const routes = Array.isArray(data) ? data : (data ? [data] : []); const routes = Array.isArray(data) ? data : (data ? [data] : []);
const match = routes.find((r) => typeof r.gateway === 'string' && r.gateway.includes(gatewayIp)); const routeList = Array.isArray(routes) ? routes : (routes && typeof routes === 'object' && routes.ret ? routes.ret : []);
if (match && typeof match.gateway === 'string') { const match = routeList.find((r) => {
const gwStr = match.gateway; const gw = r.gateway ?? r.Gateway;
const percentIdx = gwStr.indexOf('%'); return typeof gw === 'string' && (gw.includes(gatewayIp) || gw.startsWith(gatewayIp));
if (percentIdx >= 0 && percentIdx < gwStr.length - 1) { });
ifaceName = gwStr.slice(percentIdx + 1); if (match) {
const gwStr = String(match.gateway ?? match.Gateway ?? '');
const ifaceFromRoute = match.interface ?? match.Interface;
if (typeof ifaceFromRoute === 'string' && ifaceFromRoute.trim()) {
ifaceName = ifaceFromRoute.trim();
} else {
const percentIdx = gwStr.indexOf('%');
if (percentIdx >= 0 && percentIdx < gwStr.length - 1) {
ifaceName = gwStr.slice(percentIdx + 1).trim();
}
} }
} }
} catch (_) { } catch (_) {
// Если чтение маршрутов не удалось — просто продолжаем без interface, // Если чтение маршрутов не удалось — просто продолжаем без interface.
// чтобы не ломать сам traceroute.
} }
} }
} }
// 4) Последний фоллбек: если ifaceName до сих пор не определён, // 4) При указанном gatewayIp не подставляем ether1/ether2. Фоллбек только если шлюз не указан.
// пробуем взять первый физический порт ether1/ether2 (если есть), if (!ifaceName && !gatewayIp) {
// иначе любой интерфейс с префиксом "ether".
if (!ifaceName) {
try { try {
const ifRes = await client.command('interface/print', { const ifRes = await client.command('interface/print', {
'.proplist': ['name'], '.proplist': ['name'],