diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index 6d437a1..b87eb82 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -272,6 +272,113 @@ async function applyMikrotikConfig(req, res) { } } +/** + * POST /api/mikrotik/ping + * Пингует внешний ресурс через указанный MikroTik (jumphost), + * по возможности привязываясь к интерфейсу, соответствующему gateway. + * + * Body: + * - serverId: ID/имя сервера из servers.json (обязательно) + * - gatewayIp?: IP шлюза (remote IP туннеля или прямой gateway) + * - interfaceName?: имя интерфейса RouterOS (если знаем его заранее) + * - srcAddress?: исходный IP (например, local IP туннеля) + * - target?: адрес для ping (по умолчанию www.gstatic.com) + * - count?: количество пакетов (по умолчанию 5) + */ +async function pingViaInterface(req, res) { + try { + const { + serverId, + gatewayIp, + interfaceName, + srcAddress, + target = 'www.gstatic.com', + count = 5, + } = req.body || {}; + + if (!serverId) { + return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); + } + + 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); + + // Пытаемся найти интерфейс по gatewayIp и serverId, если interfaceName не передан + let ifaceName = interfaceName || null; + let srcAddr = srcAddress || null; + + if (!ifaceName && !srcAddr && gatewayIp) { + const config = await loadNetworkConfig(); + const tunnelIfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : []; + + const iface = tunnelIfaces.find((i) => { + const sameServer = + i.serverId === serverId || + i.serverId === server.ip || + i.serverId === server.dns; + const sameIp = i.remoteIp === gatewayIp || i.localIp === gatewayIp; + return sameServer && sameIp; + }); + + if (iface) { + ifaceName = iface.name || null; + srcAddr = iface.localIp || srcAddr; + } + } + + const body = {}; + body.address = target; + body.count = Number(count) > 0 ? Number(count) : 5; + if (ifaceName) body.interface = ifaceName; + if (srcAddr) body['src-address'] = srcAddr; + + const pingRes = await client.command('tool/ping', body); + const raw = pingRes?.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; + }; + + // Ищем строку с аггрегированными полями (avg-rtt, min-rtt, max-rtt, packet-loss) + let summary = rows.find((r) => r['avg-rtt'] || r['packet-loss']) || rows[rows.length - 1] || {}; + + const avgMs = parseMs(summary['avg-rtt'] || summary.time); + const minMs = parseMs(summary['min-rtt']); + const maxMs = parseMs(summary['max-rtt']); + const loss = + summary['packet-loss'] != null + ? Number(String(summary['packet-loss']).replace('%', '')) + : null; + + return sendOk(res, { + ok: true, + avgMs, + minMs, + maxMs, + loss, + }); + } catch (error) { + const msg = error.response?.data?.message || error.message || 'Ping failed'; + const status = error.response?.status; + console.error('pingViaInterface:', error); + return sendError(res, status && status >= 400 ? status : 502, msg, 'E_PING'); + } +} + /** * POST /api/mikrotik/run-script * Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter @@ -318,4 +425,5 @@ module.exports = { testMikrotikConnection, applyMikrotikConfig, runScript, + pingViaInterface, }; diff --git a/backend/server.js b/backend/server.js index 065eb3c..0b70198 100644 --- a/backend/server.js +++ b/backend/server.js @@ -452,7 +452,10 @@ app.post('/api/mikrotik/generate', mikrotikConfigRoutes.generateMikrotikConfig); app.get('/api/mikrotik/generate-interfaces', mikrotikConfigRoutes.generateInterfaces); app.get('/api/mikrotik/generate-recursive-routes', mikrotikConfigRoutes.generateRecursiveRoutes); app.post('/api/mikrotik/test-connection', mikrotikConfigRoutes.testMikrotikConnection); +// Простой health-check (используется NetworkConfigManager при открытии модалки применения) app.get('/api/mikrotik/ping', (req, res) => res.json({ ok: true, ts: Date.now() })); +// Реальный ping через RouterOS по интерфейсу/шлюзу +app.post('/api/mikrotik/ping', writeLimiter, mikrotikConfigRoutes.pingViaInterface); app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig); app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript); diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx index 42208d7..9f1add5 100644 --- a/frontend/src/EasySwitchManager.jsx +++ b/frontend/src/EasySwitchManager.jsx @@ -47,6 +47,8 @@ function EasySwitchManager() { const [hasChanges, setHasChanges] = useState(false); const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам + // Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс + const [pingMap, setPingMap] = useState({}); useEffect(() => { const initData = async () => { @@ -146,6 +148,8 @@ function EasySwitchManager() { // Фильтруем серверы, у которых есть gateways const serversWithGateways = serversWithData.filter(s => s.gateways.length > 0); setServers(serversWithGateways); + // Предзагружаем пинги для всех комбинаций server+gateway + prefetchPings(serversWithGateways, inventory); // Автоматически разворачиваем первый сервер if (serversWithGateways.length > 0 && expandedServers.size === 0) { @@ -176,6 +180,67 @@ function EasySwitchManager() { } }; + /** + * Загрузить реальные пинги для комбинаций (сервер, gateway). + * Пингуем хост www.gstatic.com через соответствующий MikroTik jumphost. + */ + const prefetchPings = async (serversList, inventory) => { + try { + const tasks = []; + const seen = new Set(); + + serversList.forEach((server) => { + const inv = inventory.find(srv => + String(srv.dns || '').trim() === server.name || + String(srv.hostName || '').trim() === server.name || + String(srv.ip || '').trim() === server.name + ); + const routerId = inv?.id || inv?.dns || inv?.ip; + if (!routerId) return; + + (server.gateways || []).forEach((gw) => { + const ip = gw.ip || ''; + if (!ip) return; + const key = `${routerId}:${ip}`; + if (seen.has(key) || pingMap[key] !== undefined) return; + seen.add(key); + tasks.push({ routerId, ip, key }); + }); + }); + + if (tasks.length === 0) return; + + const results = await Promise.all( + tasks.map(async ({ routerId, ip, key }) => { + try { + const { data } = await api.post('/mikrotik/ping', { + serverId: routerId, + gatewayIp: ip, + target: 'www.gstatic.com', + count: 5, + }); + return { key, value: typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null }; + } catch (e) { + console.warn('Failed to fetch ping for', routerId, ip, e?.message || e); + return { key, value: null }; + } + }) + ); + + if (results.length > 0) { + setPingMap(prev => { + const next = { ...prev }; + results.forEach(r => { + next[r.key] = r.value; + }); + return next; + }); + } + } catch (e) { + console.warn('prefetchPings error:', e?.message || e); + } + }; + const handleGatewaySelect = (serverId, community, gateway) => { const key = `${serverId}:${community}`; setActiveGateways(prev => { @@ -711,7 +776,12 @@ function EasySwitchManager() { {server.gateways.map((gw, idx) => { const isActive = activeGw === gw.name; const isFastest = idx === 0; - const ping = 20 + idx * 10 + Math.floor(Math.random() * 15); + const pingKey = (() => { + const inv = getServerMetadata(server.name); + const routerId = inv?.id || inv?.dns || inv?.ip; + return routerId && gw.ip ? `${routerId}:${gw.ip}` : null; + })(); + const ping = pingKey && pingMap[pingKey] !== undefined ? pingMap[pingKey] : null; return (
@@ -785,7 +855,7 @@ function EasySwitchManager() { {/* Метрика (пинг) */}
- {ping} + {ping != null ? ping : '—'}
@@ -846,7 +916,12 @@ function EasySwitchManager() { {server.gateways.map((gw, idx) => { const isActive = activeGw === gw.name; const isFastest = idx === 0; - const ping = 20 + idx * 10 + Math.floor(Math.random() * 15); + const pingKey = (() => { + const inv = getServerMetadata(server.name); + const routerId = inv?.id || inv?.dns || inv?.ip; + return routerId && gw.ip ? `${routerId}:${gw.ip}` : null; + })(); + const ping = pingKey && pingMap[pingKey] !== undefined ? pingMap[pingKey] : null; return (
@@ -920,7 +995,7 @@ function EasySwitchManager() { {/* Метрика (пинг) */}
- {ping} + {ping != null ? ping : '—'}
@@ -970,7 +1045,12 @@ function EasySwitchManager() { {server.gateways.map((gw, idx) => { const isActive = activeGw === gw.name; const isFastest = idx === 0; - const ping = 20 + idx * 10 + Math.floor(Math.random() * 15); + const pingKey = (() => { + const inv = getServerMetadata(server.name); + const routerId = inv?.id || inv?.dns || inv?.ip; + return routerId && gw.ip ? `${routerId}:${gw.ip}` : null; + })(); + const ping = pingKey && pingMap[pingKey] !== undefined ? pingMap[pingKey] : null; return (
@@ -1044,7 +1124,7 @@ function EasySwitchManager() { {/* Метрика (пинг) */}
- {ping} + {ping != null ? ping : '—'}