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