feat(MikrotikTools): add traceroute functionality via MikroTik API and integrate new tools route in frontend
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m36s

This commit is contained in:
2026-02-12 15:24:53 +07:00
parent c75f5f6b82
commit 6077d3bc5b
4 changed files with 532 additions and 1 deletions
+106
View File
@@ -541,6 +541,111 @@ async function pingViaInterface(req, res) {
}
}
/**
* POST /api/mikrotik/traceroute
* Трассировка до адреса через указанный MikroTik (jumphost) и, при желании, конкретный gateway.
*
* Body:
* - serverId: ID/имя сервера из servers.json (обязательно)
* - target: адрес назначения (обязательно)
* - gatewayIp?: IP шлюза, через который выполнять трассировку
* - maxHops?: максимальное количество хопов (по умолчанию 30)
*/
async function tracerouteViaGateway(req, res) {
try {
let {
serverId,
target,
gatewayIp,
maxHops = 30,
} = req.body || {};
if (!serverId) {
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
}
if (target == null || String(target).trim() === '') {
return sendError(res, 400, 'target is required', 'E_BAD_REQUEST');
}
target = String(target).trim();
const servers = await readServersFromS3();
const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId);
if (!server || server.type !== 'jumphost') {
return sendError(res, 400, 'Jumphost server not found', 'E_NOT_FOUND');
}
const creds = getMikrotikCredentials(server);
if (!creds) {
return sendError(res, 400, 'MikroTik credentials not configured for this server', 'E_CREDENTIALS');
}
const client = createRosClient(creds);
const body = {
address: target,
};
if (gatewayIp) {
body.gateway = gatewayIp;
}
const hopsNum = Number(maxHops);
if (Number.isFinite(hopsNum) && hopsNum > 0) {
body['max-hops'] = hopsNum;
}
try {
const cliParts = [
'/tool/traceroute',
`address=${body.address}`,
];
if (body.gateway) cliParts.push(`gateway=${body.gateway}`);
if (body['max-hops']) cliParts.push(`max-hops=${body['max-hops']}`);
console.log('[mikrotik][tracerouteViaGateway]', {
serverId,
gatewayIp,
body,
cli: cliParts.join(' '),
});
} catch (_) {}
const trRes = await client.command('tool/traceroute', body);
const raw = trRes?.data;
const rows = Array.isArray(raw) ? raw : (raw ? [raw] : []);
const parseMs = (val) => {
if (val == null) return null;
const s = String(val).trim();
const m = s.match(/([\d.]+)/);
return m ? Number(m[1]) : null;
};
const hops = rows.map((r, index) => ({
hop: r.hop != null ? Number(r.hop) : index + 1,
host: r.host || r.address || '',
avgMs: parseMs(r['avg-rtt'] || r.time || r.avg),
bestMs: parseMs(r['best-rtt'] || r['min-rtt']),
worstMs: parseMs(r['worst-rtt'] || r['max-rtt']),
loss:
r['packet-loss'] != null
? Number(String(r['packet-loss']).replace('%', ''))
: null,
status: r.status || '',
raw: r,
}));
return res.json({ ok: true, hops });
} catch (error) {
const msg = error.response?.data?.message || error.message || 'Traceroute failed';
const status = error.response?.status;
console.error('tracerouteViaGateway:', error);
return sendError(res, status && status >= 400 ? status : 502, msg, 'E_TRACEROUTE');
}
}
/**
* POST /api/mikrotik/run-script
* Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter
@@ -587,5 +692,6 @@ module.exports = {
testMikrotikConnection,
applyMikrotikConfig,
runScript,
tracerouteViaGateway,
pingViaInterface,
};
+2
View File
@@ -456,6 +456,8 @@ app.post('/api/mikrotik/test-connection', mikrotikConfigRoutes.testMikrotikConne
app.get('/api/mikrotik/ping', (req, res) => res.json({ ok: true, ts: Date.now() }));
// Реальный ping через RouterOS по интерфейсу/шлюзу
app.post('/api/mikrotik/ping', writeLimiter, mikrotikConfigRoutes.pingViaInterface);
// Traceroute через RouterOS с выбором сервера и шлюза
app.post('/api/mikrotik/traceroute', writeLimiter, mikrotikConfigRoutes.tracerouteViaGateway);
app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig);
app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript);