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
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m36s
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import AutoUrlManager from './AutoUrlManager';
|
||||
import BillingManager from './BillingManager';
|
||||
import CommunitiesManager from './CommunitiesManager';
|
||||
import NetworkConfigManager from './NetworkConfigManager';
|
||||
import MikrotikTools from './MikrotikTools.jsx';
|
||||
import Dashboard from './Dashboard';
|
||||
import MikrotikBackupsManager from './MikrotikBackupsManager.jsx';
|
||||
import './App.css';
|
||||
@@ -206,7 +207,8 @@ function MainLayout() {
|
||||
icon: IconSettings,
|
||||
items: [
|
||||
{ id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload },
|
||||
{ id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase }
|
||||
{ id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase },
|
||||
{ id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork }
|
||||
]
|
||||
},
|
||||
// Убрали неиспользуемые/неработающие разделы
|
||||
@@ -407,6 +409,7 @@ function MainLayout() {
|
||||
<Route path="/network-config" element={<NetworkConfigManager />} />
|
||||
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
||||
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
||||
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import api from './lib/api.js';
|
||||
import { useNotify } from './components/NotifyProvider.jsx';
|
||||
import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx';
|
||||
import GatewayAutocompleteInput from './components/GatewayAutocompleteInput.jsx';
|
||||
import {
|
||||
IconRoute,
|
||||
IconWorld,
|
||||
IconAlertTriangle,
|
||||
IconRefresh,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* Раздел "Инструменты → MikroTik": проверка маршрута (traceroute)
|
||||
* с выбором сервера, gateway и домена/IP.
|
||||
*/
|
||||
function MikrotikTools() {
|
||||
const notify = useNotify();
|
||||
|
||||
const [servers, setServers] = useState([]);
|
||||
const [networkConfig, setNetworkConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [serverId, setServerId] = useState('');
|
||||
const [gatewayRef, setGatewayRef] = useState('');
|
||||
const [gatewayMeta, setGatewayMeta] = useState(null);
|
||||
const [target, setTarget] = useState('');
|
||||
const [maxHops, setMaxHops] = useState(30);
|
||||
|
||||
const [running, setRunning] = useState(false);
|
||||
const [hops, setHops] = useState([]);
|
||||
const [analysis, setAnalysis] = useState([]);
|
||||
|
||||
// Загрузка серверов и сетевого конфига
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [serversRes, netRes] = await Promise.all([
|
||||
api.get('/servers'),
|
||||
api.get('/network-config'),
|
||||
]);
|
||||
setServers(Array.isArray(serversRes.data) ? serversRes.data : []);
|
||||
setNetworkConfig(netRes.data && typeof netRes.data === 'object' ? netRes.data : { gateways: [], tunnelInterfaces: [] });
|
||||
} catch (error) {
|
||||
console.error('[MikrotikTools] failed to load initial data', error);
|
||||
notify.error('Не удалось загрузить данные для инструментов MikroTik');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, [notify]);
|
||||
|
||||
const gateways = useMemo(
|
||||
() => (networkConfig?.gateways && Array.isArray(networkConfig.gateways) ? networkConfig.gateways : []),
|
||||
[networkConfig]
|
||||
);
|
||||
|
||||
const interfaces = useMemo(
|
||||
() => (networkConfig?.tunnelInterfaces && Array.isArray(networkConfig.tunnelInterfaces) ? networkConfig.tunnelInterfaces : []),
|
||||
[networkConfig]
|
||||
);
|
||||
|
||||
const handleSelectGatewayMeta = (meta) => {
|
||||
setGatewayMeta(meta);
|
||||
// Если цель не задана — подставляем IP gateway как target
|
||||
if (!target && meta && meta.ip) {
|
||||
setTarget(meta.ip);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunTraceroute = async () => {
|
||||
if (!serverId) {
|
||||
notify.error('Выберите сервер (jumphost)');
|
||||
return;
|
||||
}
|
||||
if (!target || String(target).trim() === '') {
|
||||
notify.error('Укажите домен или IP для трассировки');
|
||||
return;
|
||||
}
|
||||
|
||||
setRunning(true);
|
||||
setHops([]);
|
||||
setAnalysis([]);
|
||||
|
||||
try {
|
||||
const body = {
|
||||
serverId,
|
||||
target: String(target).trim(),
|
||||
};
|
||||
// gatewayRef может быть как ID gateway, так и IP/строка —
|
||||
// для простоты, если в meta есть ip — используем его как gatewayIp.
|
||||
if (gatewayMeta?.ip) {
|
||||
body.gatewayIp = gatewayMeta.ip;
|
||||
}
|
||||
// Max hops
|
||||
const hopsNum = Number(maxHops);
|
||||
if (Number.isFinite(hopsNum) && hopsNum > 0) {
|
||||
body.maxHops = hopsNum;
|
||||
}
|
||||
|
||||
const res = await api.post('/mikrotik/traceroute', body);
|
||||
const data = res?.data || {};
|
||||
if (!data.ok) {
|
||||
notify.error(data.error || 'Трассировка завершилась с ошибкой');
|
||||
return;
|
||||
}
|
||||
|
||||
const hopsList = Array.isArray(data.hops) ? data.hops : [];
|
||||
setHops(hopsList);
|
||||
setAnalysis(buildTracerouteAnalysis(hopsList, networkConfig, servers, body.target));
|
||||
} catch (error) {
|
||||
console.error('[MikrotikTools] traceroute failed', error);
|
||||
notify.error(error?.message || 'Ошибка при выполнении трассировки');
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setGatewayRef('');
|
||||
setGatewayMeta(null);
|
||||
setTarget('');
|
||||
setHops([]);
|
||||
setAnalysis([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container-xl py-3">
|
||||
<div className="page-header d-print-none mb-3">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title d-flex align-items-center">
|
||||
<IconRoute size={24} className="me-2 text-primary" />
|
||||
Инструменты MikroTik: трассировка
|
||||
</h2>
|
||||
<div className="page-subtitle text-muted">
|
||||
Проверка маршрута до домена/IP через выбранный jumphost и gateway с расшифровкой хопов.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row g-3">
|
||||
<div className="col-md-4">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Параметры проверки</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{loading ? (
|
||||
<div className="text-muted small">Загрузка серверов и сетевых настроек…</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Сервер (jumphost)</label>
|
||||
<ServerAutocompleteInput
|
||||
value={serverId}
|
||||
onChange={setServerId}
|
||||
servers={servers.filter((s) => String(s.type).toLowerCase() === 'jumphost')}
|
||||
placeholder="Начните вводить IP, DNS или провайдера…"
|
||||
/>
|
||||
<div className="form-text">
|
||||
Выберите MikroTik‑jumphost, через который будет выполняться трассировка.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Привязка к gateway / интерфейсу (опционально)</label>
|
||||
<GatewayAutocompleteInput
|
||||
value={gatewayRef}
|
||||
onChange={setGatewayRef}
|
||||
gateways={gateways}
|
||||
interfaces={interfaces}
|
||||
serverId={serverId || null}
|
||||
placeholder="IP gateway или remote IP туннеля…"
|
||||
onSelectMeta={handleSelectGatewayMeta}
|
||||
/>
|
||||
<div className="form-text">
|
||||
Можно привязать трассировку к конкретному прямому gateway или туннельному интерфейсу.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Домен или IP назначения</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<span className="input-group-text">
|
||||
<IconWorld size={16} className="text-muted" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="example.com или 8.8.8.8"
|
||||
value={target}
|
||||
onChange={(e) => setTarget(e.target.value)}
|
||||
/>
|
||||
{gatewayMeta?.ip && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => setTarget(gatewayMeta.ip)}
|
||||
>
|
||||
IP gateway
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-text">
|
||||
Можно трассировать как до внешнего домена, так и до IP gateway/туннеля.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Максимальное число хопов</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
min={1}
|
||||
max={64}
|
||||
value={maxHops}
|
||||
onChange={(e) => setMaxHops(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-footer d-flex justify-content-between">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={handleReset}
|
||||
disabled={running}
|
||||
>
|
||||
<IconRefresh size={16} className="me-1" />
|
||||
Сбросить
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleRunTraceroute}
|
||||
disabled={running || loading}
|
||||
>
|
||||
{running ? 'Трассировка…' : 'Запустить трассировку'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-8">
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Результаты трассировки</h3>
|
||||
</div>
|
||||
<div className="card-body p-0">
|
||||
{hops.length === 0 ? (
|
||||
<div className="p-3 text-muted small">
|
||||
Результаты трассировки появятся здесь после запуска проверки.
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-hover table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 60 }}>Хоп</th>
|
||||
<th>Адрес / узел</th>
|
||||
<th style={{ width: 120 }}>Среднее, мс</th>
|
||||
<th style={{ width: 120 }}>Мин / Макс, мс</th>
|
||||
<th style={{ width: 100 }}>Потери</th>
|
||||
<th style={{ width: 160 }}>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hops.map((h) => (
|
||||
<tr key={h.hop || `${h.host}-${Math.random()}`}>
|
||||
<td>{h.hop}</td>
|
||||
<td>
|
||||
{h.host || '—'}
|
||||
{h._label && (
|
||||
<div className="text-muted small">{h._label}</div>
|
||||
)}
|
||||
</td>
|
||||
<td>{h.avgMs != null ? h.avgMs.toFixed(1) : '—'}</td>
|
||||
<td>
|
||||
{h.bestMs != null ? h.bestMs.toFixed(1) : '—'} /{' '}
|
||||
{h.worstMs != null ? h.worstMs.toFixed(1) : '—'}
|
||||
</td>
|
||||
<td>{h.loss != null ? `${h.loss}%` : '—'}</td>
|
||||
<td>{h.status || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title d-flex align-items-center">
|
||||
<IconAlertTriangle size={18} className="me-2 text-yellow" />
|
||||
Расшифровка трассировки
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{analysis.length === 0 ? (
|
||||
<div className="text-muted small">
|
||||
После выполнения трассировки здесь появится человекочитаемое описание пути и возможных проблем.
|
||||
</div>
|
||||
) : (
|
||||
<ul className="list-unstyled mb-0">
|
||||
{analysis.map((line, idx) => (
|
||||
<li key={idx} className="mb-1">
|
||||
{line}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildTracerouteAnalysis(hops, networkConfig, servers, target) {
|
||||
if (!Array.isArray(hops) || hops.length === 0) return [];
|
||||
const lines = [];
|
||||
const gateways = (networkConfig?.gateways && Array.isArray(networkConfig.gateways)) ? networkConfig.gateways : [];
|
||||
const ifaces = (networkConfig?.tunnelInterfaces && Array.isArray(networkConfig.tunnelInterfaces)) ? networkConfig.tunnelInterfaces : [];
|
||||
|
||||
const findServerLabel = (serverId) => {
|
||||
if (!serverId) return null;
|
||||
const s = (servers || []).find((srv) => srv.id === serverId || srv.ip === serverId || srv.dns === serverId);
|
||||
if (!s) return serverId;
|
||||
return s.dns || s.ip || serverId;
|
||||
};
|
||||
|
||||
const targetStr = String(target || '').trim();
|
||||
lines.push(`Цель трассировки: ${targetStr || 'не указана'}. Хопов в маршруте: ${hops.length}.`);
|
||||
|
||||
let firstTimeoutHop = null;
|
||||
let worstLatencyHop = null;
|
||||
|
||||
hops.forEach((hop, index) => {
|
||||
const host = hop.host || hop.address || '';
|
||||
const avg = hop.avgMs;
|
||||
const loss = hop.loss;
|
||||
|
||||
let descr = `Хоп ${hop.hop || index + 1}: ${host || 'неизвестный узел'}`;
|
||||
const details = [];
|
||||
|
||||
if (avg != null) details.push(`среднее время ≈ ${avg.toFixed(1)} мс`);
|
||||
if (hop.bestMs != null && hop.worstMs != null) {
|
||||
details.push(`мин/макс ${hop.bestMs.toFixed(1)}/${hop.worstMs.toFixed(1)} мс`);
|
||||
}
|
||||
if (loss != null) details.push(`потеря пакетов ${loss}%`);
|
||||
|
||||
// Привязка к gateway
|
||||
const gw = gateways.find((g) => g.ip && g.ip === host);
|
||||
if (gw) {
|
||||
const srvLabel = findServerLabel(gw.serverId);
|
||||
details.push(`gateway "${gw.description || gw.ip}" на сервере ${srvLabel || gw.serverId}`);
|
||||
}
|
||||
|
||||
// Привязка к туннельному интерфейсу
|
||||
const iface = ifaces.find((i) => i.localIp === host || i.remoteIp === host);
|
||||
if (iface) {
|
||||
const s1 = findServerLabel(iface.serverId);
|
||||
const s2 = findServerLabel(iface.serverId2);
|
||||
details.push(
|
||||
`туннель ${iface.type || ''} ${iface.name || ''} (${iface.localIp} ⇄ ${iface.remoteIp})` +
|
||||
(s1 || s2 ? ` между ${s1 || 'сервером 1'} и ${s2 || 'сервером 2'}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
// Статус/таймауты
|
||||
if (hop.status) {
|
||||
const st = String(hop.status).toLowerCase();
|
||||
if (st.includes('timeout') || st.includes('no reply') || st.includes('unreachable')) {
|
||||
details.push('узел не отвечает на ICMP (возможная фильтрация или недоступность)');
|
||||
if (!firstTimeoutHop) firstTimeoutHop = hop;
|
||||
}
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
descr += ` — ${details.join('; ')}`;
|
||||
}
|
||||
|
||||
lines.push(descr);
|
||||
|
||||
if (avg != null) {
|
||||
if (!worstLatencyHop || avg > (worstLatencyHop.avgMs || 0)) {
|
||||
worstLatencyHop = hop;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (worstLatencyHop && worstLatencyHop.avgMs != null && worstLatencyHop.avgMs > 150) {
|
||||
lines.push(
|
||||
`Максимальная средняя задержка на хопе ${worstLatencyHop.hop}: около ${worstLatencyHop.avgMs.toFixed(
|
||||
1
|
||||
)} мс — возможен "узкое место" или удалённый сегмент сети.`
|
||||
);
|
||||
}
|
||||
|
||||
if (firstTimeoutHop) {
|
||||
lines.push(
|
||||
`Начиная с хопа ${firstTimeoutHop.hop} (${firstTimeoutHop.host || 'неизвестный узел'}) наблюдаются таймауты — цель может быть недоступна или где-то по пути фильтруется ICMP.`
|
||||
);
|
||||
} else {
|
||||
lines.push('Таймаутов или явной потери пакетов по пути не обнаружено (по данным MikroTik).');
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export default MikrotikTools;
|
||||
|
||||
Reference in New Issue
Block a user