fix(api): уточнить причины отказа входа на маршрутизатор

This commit is contained in:
Denozordec committed 2026-10-16 01:32:29 +07:00
1 parent 552d5f831f
commit 25ae76dc3f
5 files changed
+128 -6

No files matched your search

+2 -4
View File
@@ -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"))
+29 -1
View File
@@ -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<void>(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<void>((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 = ""
+17 -1
View File
@@ -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"
}
}
+72
View File
@@ -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, не перезапускать маршрутизатор. Результат записать
отдельно: отключение ещё не выполнялось и причинная связь не доказана.
+8
View File
@@ -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 не было.