diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index 73f71fb..30793ae 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -294,81 +294,21 @@ async function applyMikrotikConfig(req, res) { } /** - * POST /api/mikrotik/ping - * Пингует внешний ресурс через указанный MikroTik (jumphost), - * по возможности привязываясь к интерфейсу, соответствующему gateway. - * - * Body: - * - serverId: ID/имя сервера из servers.json (обязательно) - * - gatewayIp?: IP шлюза (remote IP туннеля или прямой gateway) - * - interfaceName?: имя интерфейса RouterOS (если знаем его заранее) - * - target?: адрес для ping (по умолчанию www.gstatic.com) - * - count?: количество пакетов (по умолчанию 5) + * Выполнить один пинг через RouterOS (без HTTP, без кеша). + * @returns Promise<{ avgMs, minMs, maxMs, loss }> */ -async function pingViaInterface(req, res) { - try { - let { - serverId, - gatewayIp, - interfaceName, - target, - count = 5, - } = req.body || {}; - - if (!serverId) { - return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); - } - - const uiSettings = await loadUiSettings(); - - // Цель пинга: из тела запроса, иначе из настроек (Домен для пинга), иначе по умолчанию - if (target == null || String(target).trim() === '') { - const pingDomain = (uiSettings.pingDomain && String(uiSettings.pingDomain).trim()) || ''; - target = pingDomain || 'www.gstatic.com'; - } else { - target = String(target).trim(); - } - - const pingCacheMinutes = Math.max(0, parseInt(uiSettings.pingCacheMinutes, 10) || 0); - const cacheKey = pingCacheMinutes > 0 ? pingCacheKey(serverId, gatewayIp, target) : null; - - if (cacheKey) { - try { - const data = await readS3TextObject(cacheKey).catch(() => null); - if (data?.body) { - const cached = JSON.parse(data.body); - const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0; - const ttlMs = pingCacheMinutes * 60 * 1000; - if (cachedAt && Date.now() - cachedAt < ttlMs) { - return res.json({ - ok: true, - avgMs: cached.avgMs, - minMs: cached.minMs, - maxMs: cached.maxMs, - loss: cached.loss, - }); - } - } - } catch (_) { - // кеш протух или невалиден — выполняем реальный пинг - } - } - - 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; +async function runPingViaRouter(serverId, gatewayIp, target, count = 5, interfaceName = null) { + const servers = await readServersFromS3(); + const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId); + if (!server || server.type !== 'jumphost') { + throw new Error('Jumphost server not found'); + } + const creds = getMikrotikCredentials(server); + if (!creds) { + throw new Error('MikroTik credentials not configured for this server'); + } + const client = createRosClient(creds); + let ifaceName = interfaceName || null; if (!ifaceName && gatewayIp) { const config = await loadNetworkConfig(); @@ -520,18 +460,72 @@ async function pingViaInterface(req, res) { ? Number(String(summary['packet-loss']).replace('%', '')) : null; - const result = { ok: true, avgMs, minMs, maxMs, loss }; + return { ok: true, avgMs, minMs, maxMs, loss }; +} +/** + * POST /api/mikrotik/ping + * Пингует внешний ресурс через указанный MikroTik (jumphost), + * по возможности привязываясь к интерфейсу, соответствующему gateway. + * + * Body: + * - serverId: ID/имя сервера из servers.json (обязательно) + * - gatewayIp?: IP шлюза (remote IP туннеля или прямой gateway) + * - interfaceName?: имя интерфейса RouterOS (если знаем его заранее) + * - target?: адрес для ping (по умолчанию www.gstatic.com) + * - count?: количество пакетов (по умолчанию 5) + */ +async function pingViaInterface(req, res) { + try { + let { + serverId, + gatewayIp, + interfaceName, + target, + count = 5, + } = req.body || {}; + + if (!serverId) { + return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); + } + const uiSettings = await loadUiSettings(); + if (target == null || String(target).trim() === '') { + const pingDomain = (uiSettings.pingDomain && String(uiSettings.pingDomain).trim()) || ''; + target = pingDomain || 'www.gstatic.com'; + } else { + target = String(target).trim(); + } + const pingCacheMinutes = Math.max(0, parseInt(uiSettings.pingCacheMinutes, 10) || 0); + const cacheKey = pingCacheMinutes > 0 ? pingCacheKey(serverId, gatewayIp, target) : null; + if (cacheKey) { + try { + const data = await readS3TextObject(cacheKey).catch(() => null); + if (data?.body) { + const cached = JSON.parse(data.body); + const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0; + const ttlMs = pingCacheMinutes * 60 * 1000; + if (cachedAt && Date.now() - cachedAt < ttlMs) { + return res.json({ + ok: true, + avgMs: cached.avgMs, + minMs: cached.minMs, + maxMs: cached.maxMs, + loss: cached.loss, + }); + } + } + } catch (_) {} + } + const result = await runPingViaRouter(serverId, gatewayIp, target, count, interfaceName); if (cacheKey) { writeS3JsonObject(cacheKey, { - avgMs, - minMs, - maxMs, - loss, + avgMs: result.avgMs, + minMs: result.minMs, + maxMs: result.maxMs, + loss: result.loss, cachedAt: Date.now(), }).catch((err) => console.warn('[mikrotik][pingViaInterface] cache write failed:', err?.message)); } - return res.json(result); } catch (error) { const msg = error.response?.data?.message || error.message || 'Ping failed'; @@ -845,4 +839,5 @@ module.exports = { runScript, tracerouteViaGateway, pingViaInterface, + runPingViaRouter, }; diff --git a/backend/routes/miscRoutes.js b/backend/routes/miscRoutes.js index ad5be57..5c97da2 100644 --- a/backend/routes/miscRoutes.js +++ b/backend/routes/miscRoutes.js @@ -339,9 +339,63 @@ const PING_SERVICES = [ { id: 'instagram', name: 'Instagram', host: 'instagram.com', port: 443 }, ]; -// GET /api/ping-services — RTT до Google, Cloudflare, Yandex, Instagram (для карточек на дашборде) -async function getPingServices(req, res) { +/** Загрузить UI-настройки из S3 (для pingServicesSource и др.) */ +async function loadUiSettingsSync() { try { + const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/rt_ui_settings.json' })); + const jsonText = await streamToString(data.Body); + const parsed = JSON.parse(jsonText || '{}'); + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +} + +// GET /api/ping-services — RTT до Google, Cloudflare, Yandex, Instagram (веб или через RouterOS по настройке) +async function getPingServices(req, res) { + const fallbackPayload = { + google: { id: 'google', name: 'Google', host: '8.8.8.8', ms: null }, + cloudflare: { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', ms: null }, + yandex: { id: 'yandex', name: 'Yandex', host: 'ya.ru', ms: null }, + instagram: { id: 'instagram', name: 'Instagram', host: 'instagram.com', ms: null }, + }; + try { + const uiSettings = await loadUiSettingsSync(); + const viaRouter = String(uiSettings.pingServicesSource || 'web').toLowerCase() === 'router'; + + if (viaRouter) { + const { runPingViaRouter } = require('./mikrotikConfigRoutes'); + const { readServersFromS3 } = require('./serversRoutes'); + const servers = await readServersFromS3(); + const jumphosts = servers.filter((s) => s && String(s.type || '').toLowerCase() === 'jumphost'); + const serverId = (uiSettings.pingServicesServerId && String(uiSettings.pingServicesServerId).trim()) || (jumphosts[0] && (jumphosts[0].id || jumphosts[0].dns || jumphosts[0].ip)); + const gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null; + let gatewayIpResolved = gatewayIp; + if (!gatewayIpResolved && serverId && jumphosts.length > 0) { + const server = jumphosts.find((s) => (s.id || s.dns || s.ip) === serverId) || jumphosts[0]; + const gateways = Array.isArray(server.gateways) ? server.gateways : []; + const primary = gateways.find((g) => g && g.primary) || gateways[0]; + gatewayIpResolved = primary && (primary.ip || primary.remoteIp) ? (primary.ip || primary.remoteIp) : null; + } + if (!serverId || !gatewayIpResolved) { + return res.json(fallbackPayload); + } + const results = await Promise.all( + PING_SERVICES.map(async (svc) => { + try { + const result = await runPingViaRouter(serverId, gatewayIpResolved, svc.host, 3); + const ms = typeof result.avgMs === 'number' ? Math.round(result.avgMs) : null; + return { id: svc.id, name: svc.name, host: svc.host, ms }; + } catch (_) { + return { id: svc.id, name: svc.name, host: svc.host, ms: null }; + } + }) + ); + const byId = {}; + results.forEach((r) => { byId[r.id] = r; }); + return res.json(byId); + } + const results = await Promise.all( PING_SERVICES.map(async (svc) => { const ms = await measureTcpRtt(svc.host, svc.port, 6000); @@ -353,12 +407,7 @@ async function getPingServices(req, res) { res.json(byId); } catch (e) { console.error('ping-services error', e); - res.status(500).json({ - google: { id: 'google', name: 'Google', host: '8.8.8.8', ms: null }, - cloudflare: { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', ms: null }, - yandex: { id: 'yandex', name: 'Yandex', host: 'ya.ru', ms: null }, - instagram: { id: 'instagram', name: 'Instagram', host: 'instagram.com', ms: null }, - }); + res.status(500).json(fallbackPayload); } } diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index e183cee..bfac072 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -22,6 +22,9 @@ export default function SettingsModal({ open, onClose }) { const [pingCacheMinutes, setPingCacheMinutes] = useState(''); const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState(''); const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState(''); + const [pingServicesSource, setPingServicesSource] = useState('web'); + const [pingServicesServerId, setPingServicesServerId] = useState(''); + const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState(''); useEffect(() => { if (!open) return; @@ -41,6 +44,9 @@ export default function SettingsModal({ open, onClose }) { setPingCacheMinutes(data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : ''); setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || '')); setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || '')); + setPingServicesSource(String(data?.pingServicesSource || 'web').toLowerCase() === 'router' ? 'router' : 'web'); + setPingServicesServerId(String(data?.pingServicesServerId || '').trim()); + setPingServicesGatewayIp(String(data?.pingServicesGatewayIp || '').trim()); const e = res?.headers?.etag || res?.headers?.ETag || ''; setEtag(e ? String(e) : ''); } catch (e) { @@ -105,6 +111,9 @@ export default function SettingsModal({ open, onClose }) { pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0), ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(), ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(), + pingServicesSource: pingServicesSource === 'router' ? 'router' : 'web', + pingServicesServerId: String(pingServicesServerId || '').trim(), + pingServicesGatewayIp: String(pingServicesGatewayIp || '').trim(), }; const payload = { settings: mergedSettings, @@ -227,6 +236,53 @@ export default function SettingsModal({ open, onClose }) { min={0} /> +