diff --git a/backend/src/routes/servers.ts b/backend/src/routes/servers.ts index 3a0fc5c1..78930339 100644 --- a/backend/src/routes/servers.ts +++ b/backend/src/routes/servers.ts @@ -12,7 +12,7 @@ import { type TestConnectionRequest, } from "../types/server.js" import { pollServer } from "../services/poller.js" -import { MikrotikClient, MikrotikError } from "../services/mikrotik.js" +import { MikrotikClient, MikrotikError, routerOsAccessHint } from "../services/mikrotik.js" import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js" import { createServer, @@ -56,9 +56,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => { let message = "Не удалось подключиться" if (err instanceof MikrotikError) { - if (err.statusCode === 401) message = "Неверный логин или пароль (401 Unauthorized)" - else if (err.statusCode === 403) message = "Нет прав доступа к REST API (403 Forbidden)" - else message = `Ошибка RouterOS API: ${err.statusCode}` + message = routerOsAccessHint(err.statusCode, err.body) ?? `Ошибка RouterOS API: ${err.statusCode}` } else if (err instanceof Error) { const msg = err.message.toLowerCase() if (msg.includes("timeout") || msg.includes("abort")) diff --git a/backend/src/services/mikrotik-response.test.ts b/backend/src/services/mikrotik-response.test.ts index 6af83373..e8801071 100644 --- a/backend/src/services/mikrotik-response.test.ts +++ b/backend/src/services/mikrotik-response.test.ts @@ -1,7 +1,35 @@ import { test } from "node:test" import assert from "node:assert/strict" import { createServer } from "node:http" -import { MikrotikClient } from "./mikrotik.js" +import { MikrotikClient, MikrotikError, routerOsAccessHint } from "./mikrotik.js" + +test("RouterOS access diagnostics distinguish login saturation without assuming a wrong password or retrying", async () => { + let requests = 0 + const body = '{"detail":"too many concurrent logins (6)","error":500,"message":"Internal Server Error"}' + const server = createServer((req, res) => { requests++; req.resume(); res.writeHead(500); res.end(body) }) + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)) + const address = server.address(); assert(address && typeof address !== "string") + const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" }) + try { + for (const operation of [() => client.get("/interface"), () => client.put("/own", {})]) { + await assert.rejects(operation, (error: unknown) => { + assert(error instanceof MikrotikError) + assert.equal(error.statusCode, 500); assert.equal(error.body, body) + assert.match(error.message, /превышен лимит одновременных входов/) + return true + }) + } + assert.equal(requests, 2, "Neither reads nor writes may silently retry a refused login") + assert.match(routerOsAccessHint(401, '{"message":"Unauthorized"}')!, /ограничения входа/) + assert.doesNotMatch(routerOsAccessHint(401, "")!, /Неверный логин или пароль/) + for (const unknown of ["not JSON", "null", '{"detail":6}', '{"detail":"disk failure"}']) { + assert.equal(routerOsAccessHint(500, unknown), undefined) + } + assert.equal(routerOsAccessHint(400, body), undefined) + } finally { + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) + } +}) test("RouterOS print preserves native query arrays and property selection", async () => { let received: unknown, target = "", method = "" diff --git a/backend/src/services/mikrotik.ts b/backend/src/services/mikrotik.ts index 8dd3c307..86f0a9b4 100644 --- a/backend/src/services/mikrotik.ts +++ b/backend/src/services/mikrotik.ts @@ -1110,13 +1110,29 @@ export class MikrotikClient { // ── Error type ───────────────────────────────────────────────────────────────── +/** HTTP 401 alone does not distinguish wrong credentials from a refused login. */ +export function routerOsAccessHint(statusCode: number, body: string): string | undefined { + if (statusCode === 401) return "RouterOS отклонил авторизацию: проверьте учётные данные, права API/REST и ограничения входа" + if (statusCode === 403) return "RouterOS запретил доступ к REST API" + if (statusCode === 500) { + try { + const detail: unknown = JSON.parse(body)?.detail + if (typeof detail === "string" && /^too many concurrent logins(?:\s*\(\d+\))?$/i.test(detail.trim())) { + return "RouterOS занят: превышен лимит одновременных входов" + } + } catch { /* Keep an unrecognized response unchanged. */ } + } + return undefined +} + export class MikrotikError extends Error { constructor( public readonly statusCode: number, public readonly path: string, public readonly body: string, ) { - super(`RouterOS API error ${statusCode} on ${path}: ${body}`) + const hint = routerOsAccessHint(statusCode, body) + super(`${hint ? `${hint}. ` : ""}RouterOS API error ${statusCode} on ${path}: ${body}`) this.name = "MikrotikError" } } diff --git a/deploy/PRODUCTION-ROUTEROS-273.md b/deploy/PRODUCTION-ROUTEROS-273.md new file mode 100644 index 00000000..07609dd1 --- /dev/null +++ b/deploy/PRODUCTION-ROUTEROS-273.md @@ -0,0 +1,72 @@ +# Production273 — отказы входа RouterOS (2026-10-16) + +Диагностика production выполнена только чтением. Пароли, пользователи, службы, +firewall, задания RouterOS и контейнеры не изменялись. Секреты не включены. + +## Наблюдения + +- `nov-gw01.rtnt.top`, server ID3: RouterOS **7.24.4 stable**. +- Рядом зафиксированы `500 / too many concurrent logins (6)` и `401 Unauthorized` + на `/interface`, `/interface/gre`, `/routing/bgp/session`. Последние отказы серии: + **2026-10-16 01:11:40–44 по Новосибирску** (2026-10-15 18:11:40–44 UTC). +- В последних15 минутах на01:27: traffic30/30 ok, gre_bgp30/30 ok, + internet_path3/3 ok, ошибок этих заданий нет. +- Тот же сохранённый пароль успешно использован для system/resource, user/active, + ip/service, interface/gre (3 записи), routing/bgp/session (3 записи). + Пользователь приложения: full, disabled=false, address пустой, api/rest-api + разрешены. Это подтверждает текущий доступ, но не объясняет каждый старый401. +- FTP/Telnet включены. В кольце журнала01:26:00–01:26:47:10 отказов входа + с посторонних адресов —9 FTP и1 Telnet; отказов с адреса приложения в этой + выборке нет. Следующий снимок показывает **7 внешних Telnet-соединений**. + Подбор учётных данных наблюдается; успешный посторонний вход не установлен. +- У www/www-ssl/api/api-ssl max-sessions=20. Число6 в ошибке нельзя автоматически + отождествлять с этим параметром или количеством TCP-соединений приложения. +- user/active содержит много старых rest-api записей с адреса приложения; + api запись в момент чтения одна. По одному списку нельзя доказывать утечку + живых TCP-соединений или принудительно удалять записи. +- RouterOS scheduler `evofw-sync`: enabled, interval1m, run-count625. + В доступном кольце01:26–01:30:866 изменений от scheduler, из них860 добавлений + address-list и6 изменений filter rule. Два script/job в снимке, включая начатый + 01:22:08. Скрипты не выгружались и не менялись; связь с отказами не доказана. +- Общий HTTP/HTTPS Agent backend ограничивает сокеты до2 **на origin**. + Одно задание планировщика защищено от повторного запуска; разные задания + работают одновременно. Шесть одновременных REST-запросов из основного процесса + приложения не установлены. + +## Вывод и границы + +Неверный пароль как причина текущего доступа не подтвердился. RouterOS явно +сообщил о насыщении механизма входа;401 возникали рядом, затем без изменения +пароля прекратились. Связь с внешним подбором, продлением REST-сессии или локальным +scheduler необходимо различить дальнейшим наблюдением.15 минут успеха не доказывают +устранение причины. + +[Официальная документация REST](https://manual.mikrotik.com/docs/developer-guides/rest-api/) +описывает внутренний API login, повторное использование логина10 минут и политики +api/rest-api. Страница относится к7.26; поведение7.24.4 не считать идентичным +без отдельного подтверждения. + +## Локальная правка + +- 500 с точным JSON detail `too many concurrent logins (...)` получает подсказку + «RouterOS занят: превышен лимит одновременных входов». +- 401 больше не обозначается безусловно как неверный пароль: предлагается проверить + учётные данные, права API/REST и ограничения входа. +- Исходные HTTP status/path/body сохранены; повторов чтения/записи не добавлено. + Другие500 не классифицируются как лимит входов. +- 12 HTTP-проверок и профиль ros-file, backend production build, scoped ESLint, + diff check прошли. Локально, без push/deploy. + +## Следующий шаг, требующий разрешения + +Временно отключить **только** ftp (`*1`,21) и telnet (`*0`,23) на ID3. +Перед записью повторно проверить имя/ID и исходный disabled каждой службы. +Это отключит FTP/Telnet подключения; Winbox, HTTPS, API и маршрутизация сохраняются. +Для этой диагностики production доступ предоставлен на чтение: запись требует +отдельного разрешения владельца. Если эти службы нужны, согласовать доверенные адреса. + +После разрешения: сохранить исходные disabled, отключить две службы, проверить +применение и REST-доступ; наблюдать обычный опрос не менее двух периодов продления +REST-логина, сравнить ошибки и login failures. Не повышать max-sessions, не менять +пароль, не удалять user/active, не перезапускать маршрутизатор. Результат записать +отдельно: отключение ещё не выполнялось и причинная связь не доказана. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 4bc5f8da..7ea6df41 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,5 +1,7 @@ # UX/UI: журнал реализации +Продолжение 2026-10-16: Production273 — исследованы500/401 nov-gw01; сохранённый пароль работает. Наблюдаются внешний подбор через FTP/Telnet и насыщение входа RouterOS; причинная связь пока не доказана. Подсказки исправлены локально, production без изменений. Контролируемое отключение FTP/Telnet ожидает отдельного разрешения. + Актуальное состояние 2026-10-16: Production272 — исправления268/270/271 опубликованы в main, CI2468 success, production backend/frontend обновлены до2.0.2 и healthy. В HTTP/UI подтверждены рекурсивные маршруты всех3 JH; Filters без ложного missing-route warning;4 узла карты и новая легенда. PostgreSQL не перезапускался. Read-only DNS и дальнейшая проверка трафика остаются отдельными задачами. Предыдущее состояние 2026-10-15: Production269 — по отдельному разрешению обновлён production frontend до2.0.1; в браузере подтверждены3JH/1EN и25GRE в данных. Filters268 остаётся локальным;6 профильных E2E/TypeScript passed. Карта и миграция retention опубликованы в main; исправлен CI PostgreSQL, backend-test2467/8417 success. Карта принята в production с ограничением read-only DNS; Filters ещё не развёрнут. Online-индекс266 применён отдельно с разрешения владельца; нагрузка БД снизилась в двух повторных замерах. Проверка трафика и причин остальных тайм-аутов продолжается отдельно. Лабораторная приёмка264 завершена в документированных границах стенда:1396 основных UI и221 context-сценарий;120 live default states30 экранов. Runtime стенда backend258/frontend261. Непроверенные внешние группы сохранены в итоговом акте. Production v2.0.0 подтверждён через UI; [описание релиза](releases/2.0.0.md). @@ -40,6 +42,12 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Завершён2026-10-15 в границах предоставленного стенда:1396 UI/221 context,120 live states30 экранов, production builds/ReUI checklist и реальные интеграции. Ограничения — в [акте](ux-ui-acceptance-candidate.md); production-выпуск не выполнялся. | +## Production273 — диагностика отказов входа RouterOS (2026-10-16) + +На nov-gw01 подтверждены соседние500 `too many concurrent logins (6)` и401; последние в01:11:40–44 по Новосибирску. Тот же сохранённый пароль работает, full/api/rest-api разрешены. На01:27 последние15 минут: traffic30/30, gre_bgp30/30, internet_path3/3 успешны. Наблюдаются внешние попытки входа через открытые FTP/Telnet и7 внешних Telnet-соединений, а также частые изменения от локального evofw-sync. Их причинная связь с отказами пока не доказана; насыщение входа нельзя приравнивать к max-sessions20 или старым user/active записям. Production, RouterOS и пароли не менялись. + +Локально исправлены подсказки:401 не объявляет пароль неверным; точный500/detail распознаётся как лимит одновременных входов. Исходные status/path/body сохранены, повторов операций нет.12 HTTP-проверок, профиль ros-file, backend build, scoped ESLint и diff check прошли. [Протокол и процедура](../deploy/PRODUCTION-ROUTEROS-273.md). Без push/deploy. Следующий шаг — после отдельного разрешения временно отключить только FTP/Telnet на ID3 и сравнить обычный опрос минимум за два периода продления REST-логина. Это ещё не выполненное исправление первопричины. + ## Production272 — публикация и приёмка v2.0.2 (2026-10-16) По прямому разрешению владельца подготовленные коммиты268/269/270/271 отправлены в main до e6a2e400. Все7 jobs CI2468 success; v2.0.2 опубликован. Проверены OCI version/revision и digest обоих образов, затем обновлены только backend и frontend через Compose --no-deps --pull never. Backend healthy с00:41:33, frontend healthy с00:42:11 по Новосибирску; PostgreSQL container ID/StartedAt и SHA256 Compose сохранены. RouterOS-конфигурация и права не менялись; автоматического prune не было.