From d112935c73d30c33ffab2f751b939633fc2e13ff Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 30 Sep 2026 15:25:29 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D0=BE=D1=82=D0=BA=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BD=D0=B5=D1=81=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D1=8B=20IPsec=20=D0=B8=20VXLAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ux-ui-progress.md | 9 ++++++++- docs/ux-ui-stage-9.md | 7 +++++++ lib/vxlan-response.ts | 8 ++++---- shared/api/ipsec.ts | 6 +++++- tests/e2e/ipsec-data.spec.ts | 5 ++++- tests/e2e/vxlan-links.spec.ts | 3 +++ 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 96049bf6..8bf85567 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -25,12 +25,19 @@ | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | -| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.54 — IPsec/GRE/VXLAN, Firewall и операции Filters | +| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.55 — IPsec/GRE/VXLAN, Firewall и операции Filters | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 9.55 — согласованность ответов IPsec/VXLAN (2026-09-30) + +- IPsec дополнительно отклоняет чужой serverId вложенных peer/pool/shared-mode-config, повторные peer/сертификаты и отказ с пустым serverId. VXLAN отклоняет повторные failures, одновременные tunnel/failure одного устройства, несогласованные arpMode/arpProxy и повторные ID inventory. Прежние схемы payload и контекст выборного IPsec ответа сохранены. +- Противоречивый ответ целиком считается ошибкой чтения и не заменяет последние подтверждённые строки; вложенный объект не переносится под другой сервер. Старые ответы без arpMode совместимы. +- 24 E2E прошли: расширенные повреждённые/чужие payload IPsec и регрессия ссылок/сбоев/гонок VXLAN. TypeScript и lint четырёх файлов чистые. Backend в этом блоке не менялся; последний backend build — 9.54, frontend — 9.52. +- Далее — ошибки общего GRE-каталога и его потребители, затем экраны VXLAN/сводная регрессия этапа 9. Реальные RouterOS/JWT/БД не проверены. Локальный коммит без push. + ## Блок 9.54 — корректный фрагмент .rsc VXLAN (2026-09-30) - Генератор вынесен в lib/vxlan-export.ts. Строки RouterOS заключены в кавычки; кавычка/обратный слеш/dollar и управляющие символы экранируются. Заголовки не содержат переводов строки из имени/metadata. Числовые параметры идут из валидированной модели. diff --git a/docs/ux-ui-stage-9.md b/docs/ux-ui-stage-9.md index 5295aa7c..105e360c 100644 --- a/docs/ux-ui-stage-9.md +++ b/docs/ux-ui-stage-9.md @@ -300,3 +300,10 @@ Live-копирование читает актуальный целевой rul - Backend возвращает точный arpMode, обычный arp=enabled больше не означает proxy ARP. Обрабатывается learning=no. Точный arp приоритетен над legacy arp-proxy. Новое поле optional на frontend для совместимости прежних ответов; при его отсутствии старый boolean может описать только proxy-arp/обычный enabled. - Unit генератора проверяет сложные строки и отсутствие посторонних команд/недоступных параметров; backend unit проверяет все ARP modes и частичный опрос. TypeScript frontend/backend, frontend lint, 2 E2E экспорта/свежести и backend build прошли. Последний frontend build — 9.52. - Источники: [VXLAN](https://help.mikrotik.com/docs/spaces/ROS/pages/100007937/VXLAN), [строковые литералы RouterOS](https://help.mikrotik.com/docs/spaces/ROS/pages/47579229/Scripting). Далее — согласованность ответа VXLAN, экраны и общая проверка, затем GRE-каталог. Локальный коммит без push. + +## Блок 9.55 — согласованность ответов IPsec/VXLAN (2026-09-30) + +- IPsec дополнительно отклоняет чужой serverId вложенных peer/pool/shared-mode-config, повторные peer/сертификаты и отказ с пустым serverId. VXLAN отклоняет повторные failures, одновременные tunnel/failure одного устройства, несогласованные arpMode/arpProxy и повторные ID inventory. Прежние схемы payload и контекст выборного IPsec ответа сохранены. +- Противоречивый ответ целиком считается ошибкой чтения и не заменяет последние подтверждённые строки; вложенный объект не переносится под другой сервер. Старые ответы без arpMode совместимы. +- 24 E2E прошли: расширенные повреждённые/чужие payload IPsec и регрессия ссылок/сбоев/гонок VXLAN. TypeScript и lint четырёх файлов чистые. Backend в этом блоке не менялся; последний backend build — 9.54, frontend — 9.52. +- Далее — ошибки общего GRE-каталога и его потребители, затем экраны VXLAN/сводная регрессия этапа 9. Реальные RouterOS/JWT/БД не проверены. Локальный коммит без push. diff --git a/lib/vxlan-response.ts b/lib/vxlan-response.ts index 52890b12..dde1bd06 100644 --- a/lib/vxlan-response.ts +++ b/lib/vxlan-response.ts @@ -8,13 +8,13 @@ export const vxlanResponseSchema = z.object({ vtepIp: z.string(), remoteVteps: z.array(z.string()), l2mtu: z.number().int().nonnegative(), arpProxy: z.boolean(), arpMode: z.enum(["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"]).optional(), macLearning: z.boolean(), comment: z.string(), enabled: z.boolean(), status: z.enum(["up", "down"]), - })).refine(rows => new Set(rows.map(row => JSON.stringify([row.serverId, row.id]))).size === rows.length), - failures: z.array(z.object({ serverId: z.string().min(1), serverName: z.string().optional(), error: z.string() })).default([]), -}) + })).refine(rows => new Set(rows.map(row => JSON.stringify([row.serverId, row.id]))).size === rows.length && rows.every(row => row.arpMode === undefined || row.arpProxy === (row.arpMode === "proxy-arp" || row.arpMode === "local-proxy-arp"))), + failures: z.array(z.object({ serverId: z.string().trim().min(1), serverName: z.string().optional(), error: z.string() })).default([]), +}).refine(data => new Set(data.failures.map(f => f.serverId)).size === data.failures.length && !data.tunnels.some(t => data.failures.some(f => f.serverId === t.serverId))) export const vxlanServersSchema = z.array(z.object({ id: z.number().int().positive(), name: z.string(), host: z.string(), country: z.string(), enabled: z.boolean(), type: z.enum(["jump-host", "exit-node", "home-router"]).optional(), site: z.string().optional(), asn: z.string().optional(), status: z.enum(["online", "offline", "degraded"]).optional(), latency: z.number().finite().nullable().optional(), -})) +})).refine(rows => new Set(rows.map(row => row.id)).size === rows.length) diff --git a/shared/api/ipsec.ts b/shared/api/ipsec.ts index f650c15f..11111990 100644 --- a/shared/api/ipsec.ts +++ b/shared/api/ipsec.ts @@ -26,7 +26,11 @@ export async function listIpsec( const clientIds = data.clients.map(c => c.id) const failedIds = (data.failures ?? []).map(f => f.serverId) if (new Set(serverIds).size !== serverIds.length || new Set(clientIds).size !== clientIds.length || - new Set(failedIds).size !== failedIds.length || serverIds.some(id => failedIds.includes(id)) || + new Set(failedIds).size !== failedIds.length || failedIds.some(id => !id.trim()) || + data.servers.some(s => s.peers.some(p => p.serverId !== s.serverId) || + new Set(s.peers.map(p => p.id)).size !== s.peers.length || + [s.peer, s.pool, s.sharedModeConfig].some(child => child && child.serverId !== s.serverId) || + new Set((s.certs ?? []).map(c => c.name)).size !== (s.certs ?? []).length) || serverIds.some(id => failedIds.includes(id)) || data.clients.some(c => failedIds.includes(c.serverId)) || (opts?.serverId && [...serverIds, ...failedIds, ...data.clients.map(c => c.serverId)].some(id => id !== opts.serverId))) { throw new Error("Несогласованный ответ загрузки IPsec") diff --git a/tests/e2e/ipsec-data.spec.ts b/tests/e2e/ipsec-data.spec.ts index 2df4a694..71ce3777 100644 --- a/tests/e2e/ipsec-data.spec.ts +++ b/tests/e2e/ipsec-data.spec.ts @@ -5,7 +5,7 @@ const client = (id: string, name = "Клиент " + id) => ({ id: id + "-*1", r const servers = [1, 2].map(id => ({ id, name: "Роутер " + id, host: "192.0.2." + id, enabled: true, country: "RU", type: "exit-node", status: "online" })) const full = { servers: [summary("1"), summary("2")], clients: [client("1"), client("2")] } -for (const broken of ["http", "payload", "duplicate", "conflict"]) { +for (const broken of ["http", "payload", "duplicate", "conflict", "peer-context", "pool-context", "blank-failure"]) { test(`IPsec: неизвестное начало и сохранение данных при ${broken}`, async ({ page }) => { let phase = 0 await page.route("**/health", r => r.fulfill({ json: { ok: true } })) @@ -17,6 +17,9 @@ for (const broken of ["http", "payload", "duplicate", "conflict"]) { return broken === "http" ? r.fulfill({ status: 503, json: { error: "Повторный отказ IPsec" } }) : r.fulfill({ json: broken === "payload" ? { servers: [], clients: [{}] } : broken === "duplicate" ? { ...full, clients: [client("1"), client("1")] } + : broken === "peer-context" ? { ...full, servers: [{ ...summary("1"), peers: [{ id: "1-*p1", rosId: "*p1", serverId: "2", serverName: "Чужой роутер", name: "peer", managed: true, disabled: false }] }, summary("2")] } + : broken === "pool-context" ? { ...full, servers: [{ ...summary("1"), pool: { id: "1-*p1", rosId: "*p1", serverId: "2", name: "pool", ranges: "10.0.0.1-10.0.0.20", managed: true } }, summary("2")] } + : broken === "blank-failure" ? { servers: [], clients: [], failures: [{ serverId: "", error: "Неизвестный отказ" }] } : { ...full, failures: [{ serverId: "1", error: "Сбой устройства" }] } }) } return r.fulfill({ json: path === "/api/servers" ? servers : path === "/api/auth/config" ? { required: false } : {} }) diff --git a/tests/e2e/vxlan-links.spec.ts b/tests/e2e/vxlan-links.spec.ts index 30052c34..20d9b72d 100644 --- a/tests/e2e/vxlan-links.spec.ts +++ b/tests/e2e/vxlan-links.spec.ts @@ -171,6 +171,9 @@ test("VXLAN: повреждённые записи не заменяют пос { tunnels: [{ ...valid, status: "unknown" }] }, { tunnels: [valid, valid] }, { tunnels: [], failures: [{ serverId: "1", error: null }] }, + { tunnels: [valid], failures: [{ serverId: "1", error: "Конфликт" }] }, + { tunnels: [], failures: [{ serverId: "1", error: "Сбой" }, { serverId: "1", error: "Повтор" }] }, + { tunnels: [{ ...valid, arpMode: "enabled", arpProxy: true }] }, ]) { control.payload = payload await page.getByRole("button", { name: "Обновить", exact: true }).click()