fix(ipsec): подтвердить сертификатный клиент и сообщать сбой истории

This commit is contained in:
Denozordec committed 2026-10-09 15:32:10 +07:00
1 parent b9a8615b8b
commit 5d9ef9dcf3
11 files changed
+176 -12

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@
"test:ros-file": "tsx src/services/ros-file-contents.test.ts",
"test:scheduler": "tsx src/services/scheduler-running.test.ts && tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts",
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
"test:ipsec": "tsx src/services/ipsec-config.test.ts && tsx src/services/ipsec-state.test.ts",
"test:ipsec": "tsx src/services/ipsec-config.test.ts && tsx src/services/ipsec-state.test.ts && tsx src/routes/ipsec-history.test.ts",
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
"test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:ipsec && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database",
"test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
+26
View File
@@ -0,0 +1,26 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import { createIpsecRoutes } from "./ipsec.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { fetchIpsecState } from "../services/ipsec-live.js"
const original = MikrotikClient.fromServer
const server = { id: 1, name: "test", host: "192.0.2.1", enabled: true } as Parameters<typeof fetchIpsecState>[0]
let patches = 0, recordings = 0, nativeDisabled = "false", rejectWrite = false
MikrotikClient.fromServer = () => ({ get: async (path: string) => path === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2", disabled: nativeDisabled }] : [], patch: async (_path: string, body: Record<string, string>) => { if (rejectWrite) throw Error("write rejected"); patches++; nativeDisabled = body.disabled! } }) as unknown as MikrotikClient
try {
for (const historySaved of [false, true]) {
patches = recordings = 0; nativeDisabled = "false"
const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
await app.register(createIpsecRoutes({ findServer: async () => server, recordRevision: async () => { recordings++; return historySaved ? { created: false, revision: {} as never } : null } }), { prefix: "/api" })
const response = await app.inject({ method: "PATCH", url: "/api/ipsec/peers/1/*P", payload: { disabled: true } })
assert.equal(response.statusCode, historySaved ? 200 : 502)
assert.equal(patches, 1); assert.equal(recordings, 1); assert.equal(nativeDisabled, "yes")
if (!historySaved) { assert.equal(response.json().state, "applied"); assert.equal(response.json().historyRecorded, false); assert.match(response.json().error, /Обновите данные перед повтором/) }
else assert.deepEqual(response.json(), { ok: true })
rejectWrite = true; const rejected = await app.inject({ method: "PATCH", url: "/api/ipsec/peers/1/*P", payload: { disabled: false } }); rejectWrite = false
assert.equal(rejected.statusCode, 502); assert.equal(rejected.json().state, undefined); assert.equal(recordings, 1)
await app.close()
}
} finally { MikrotikClient.fromServer = original }
console.log("IPsec HTTP: confirmed mutation + failed/saved history, rejected write does not claim applied")
+20 -4
View File
@@ -94,6 +94,7 @@ function rosIdParam(v: string): string {
}
function errReply(reply: FastifyReply, e: unknown) {
if (e instanceof IpsecHistoryError) return reply.status(502).send({ error: e.message, state: "applied", historyRecorded: false })
const msg = e instanceof MikrotikError ? e.message : e instanceof Error ? e.message : String(e)
return reply.status(502).send({ error: `RouterOS: ${msg}` })
}
@@ -114,16 +115,24 @@ function badBodyReply(reply: FastifyReply, error: z.ZodError) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: error.flatten() })
}
async function recordIpsec(
class IpsecHistoryError extends Error {
constructor() {
super("Операция выполнена на устройстве, но сохранение истории не подтверждено. Обновите данные перед повтором")
}
}
async function recordIpsecDefault(
server: NonNullable<Awaited<ReturnType<typeof getEnabledIpsecServerById>>>,
source: ConfigRevisionSource,
record: typeof captureAndAppendRevision,
) {
await captureAndAppendRevision({
const result = await record({
serverId: server.id,
section: "ipsec",
source,
capture: () => captureIpsecSnapshot(server),
})
if (!result && source !== "observed") throw new IpsecHistoryError()
}
/** Существующие статические IP клиентов (персональные mode-config). */
@@ -167,7 +176,12 @@ async function buildCertBundle(
const RevisionIdParamSchema = z.object({ id: z.string().min(1) })
const ipsecRoutes: FastifyPluginAsyncZod = async (app) => {
const ipsecDefaults = { findServer: getEnabledIpsecServerById, recordRevision: captureAndAppendRevision }
export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAsyncZod {
const getEnabledIpsecServerById = dependencies.findServer
const recordIpsec = (server: NonNullable<Awaited<ReturnType<typeof getEnabledIpsecServerById>>>, source: ConfigRevisionSource) => recordIpsecDefault(server, source, dependencies.recordRevision)
return async (app) => {
app.get("/ipsec", async (req, reply) => {
const q = req.query as { serverId?: string }
const result = await listIpsec({ serverId: q.serverId })
@@ -735,4 +749,6 @@ const ipsecRoutes: FastifyPluginAsyncZod = async (app) => {
})
}
export default ipsecRoutes
}
export default createIpsecRoutes()
+4 -1
View File
@@ -280,7 +280,10 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
const comment = identity?.comment ?? ""
const mcName = (identity?.["mode-config"] ?? "").trim()
const cn = String(cert["common-name"] ?? "").trim()
const active = (cn ? findActive(cn) : undefined)
// Our issued certificates have a single CN subject; match the whole DN, never a CN substring.
const singleCn = cn && !/[,+=<>;\\"\u0000]/.test(cn) && state.certs.filter(c => c["common-name"] === cn).length === 1
? activeByRemote.get(`CN=${cn}`) : undefined
const active = (cn ? findActive(cn) : undefined) ?? singleCn
?? (identity?.["remote-id"] ? findActive(String(identity["remote-id"]).trim()) : undefined)
out.push({
id: `${server.id}:${identity ? "identity" : "cert"}:${String(identity?.[".id"] ?? cert[".id"] ?? certName)}`,
+7
View File
@@ -92,4 +92,11 @@ for (const prefix of ["fqdn", "user-fqdn", "address", "unknown"]) {
sessionState.active = [{ ".id": "*ACTIVE", id: "alice", state: "established" }]
assert.equal(mapClients(sessionState)[0]?.online, prefix !== "unknown"); checks++
}
state.active = [{ ".id": "*CERTACTIVE", id: "CN=Original CN", state: "established", "dynamic-address": "198.18.79.250", uptime: "1s" }]
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, true); checks++
state.active[0]!.id = "CN=Original CN,O=Other"
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
state.active[0]!.id = "CN=Original CN"
state.certs.push({ ".id": "*DUPCN", name: "duplicate-cn", "common-name": "Original CN", ca: "ca", "key-usage": "tls-client" })
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
console.log(`ipsec-state: ${checks} checks passed`)
+3 -1
View File
@@ -1,9 +1,11 @@
# UX/UI: протокол внешней приёмки 12.5
Статус на2026-10-09: backend ux-ipsec78/frontend ux-ipsec77, backend healthy; основной блок IPsec77 и VXLAN76 приняты в описанных границах. В12.5.67 исправленный Peer .conf принят как клиентский шаблон: API6/6, browser4/4, server endpoint и placeholders; настоящий RSC roundtrip через UI/native/handshake/Ping принят в12.5.68, .conf с явнымName/отказы400/409/сохранение формы приняты в70; поздний native rejection/подтверждённая компенсация/retry приняты в72; изолированный audit INSERT permission denial принят в74; остальные неизвестные исходы/DB faults открыты. Мобильная ширина WireGuard Sheets принята в12.5.64. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. 12.5.62 принял полный WireGuard UI create/peer/disable/enable/Ping/delete на обоих CHR; устойчивое меню развёрнуто. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md). VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен.
Статус на2026-10-09: backend ux-ipsec79/frontend ux-ipsec77, backend healthy; основной блок IPsec77 и VXLAN76 приняты в описанных границах. В12.5.67 исправленный Peer .conf принят как клиентский шаблон: API6/6, browser4/4, server endpoint и placeholders; настоящий RSC roundtrip через UI/native/handshake/Ping принят в12.5.68, .conf с явнымName/отказы400/409/сохранение формы приняты в70; поздний native rejection/подтверждённая компенсация/retry приняты в72; изолированный audit INSERT permission denial принят в74; остальные неизвестные исходы/DB faults открыты. Мобильная ширина WireGuard Sheets принята в12.5.64. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. 12.5.62 принял полный WireGuard UI create/peer/disable/enable/Ping/delete на обоих CHR; устойчивое меню развёрнуто. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md). VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен.
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
IPsec79: сертификатный responder online=true/static mode-config198.18.79.250 принят; recordRevision=null после native patch даёт502/state=applied. Собственные объекты и private files удалены. [Отчёт](ux-ui/acceptance/ipsec-certificate-2026-10-09.json).
## Паспорт стенда
Перед внешним запуском зафиксировать отдельные PostgreSQL и RouterOS тестовые устройства, версии server/client PostgreSQL и RouterOS, HEAD backend/frontend, роли пользователей, число экземпляров backend/планировщика/отправителей и способ их остановки. Для Telegram использовать тестовый чат, для остальных интеграций — тестовые endpoints и учётные записи. Зафиксировать ожидаемые изменения и способ восстановления стенда.
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-09: 12.5.78 подтвердил реальное PSK IKEv2 подключение с выдачей mode-config и исправил ложный offline в каталоге клиентов. Backend ux-ipsec78, frontend ux-ipsec77; 77 backend-проверок, TypeScript/ESLint, одна backend Docker сборка и native/API приёмка; собственные объекты удалены. Следующий12.5.79 — сертификатный клиент/выдача mode-config, затем поздние ошибки операций и история восстановления. Этап12.5 открыт.
Актуальное продолжение 2026-10-09: 12.5.79 подтвердил сертификатный IKEv2 со статическим mode-config198.18.79.250 и online=true; поздний сбой сохранения ревизии сообщает state=applied/historyRecorded=false. Backend ux-ipsec79, frontend ux-ipsec77. Следующий12.5.80 — исправление и реальная приёмка восстановления истории IPsec; поздние частичные/транспортные ошибки остаются открытыми. Этап12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-09. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+16 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-09: 12.5.78 подтвердил реальное PSK IKEv2 подключение с выдачей mode-config и исправил ложный offline в каталоге клиентов. Backend ux-ipsec78, frontend ux-ipsec77; 77 backend-проверок, TypeScript/ESLint, одна backend Docker сборка и native/API приёмка; собственные объекты удалены. Следующий12.5.79 — сертификатный клиент/выдача mode-config, затем поздние ошибки операций и история восстановления. Этап12.5 открыт.
Актуальное продолжение 2026-10-09: 12.5.79 подтвердил сертификатный IKEv2 со статическим mode-config198.18.79.250 и online=true; поздний сбой сохранения ревизии сообщает state=applied/historyRecorded=false. Backend ux-ipsec79, frontend ux-ipsec77. Следующий12.5.80 — исправление и реальная приёмка восстановления истории IPsec; поздние частичные/транспортные ошибки остаются открытыми. Этап12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -36,7 +36,21 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–78 и ограничения ниже. Runtime backend ux-ipsec78/frontend ux-ipsec77. |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–79 и ограничения ниже. Runtime backend ux-ipsec79/frontend ux-ipsec77. |
## 12.5.79 — IPsec: сертификатный online и поздняя ошибка истории (2026-10-09)
**Результат.** Реальный сертификатный клиент получал198.18.79.250, но отображался offline: active-peers.id возвращается как CN=имя. Mapper сопоставляет полный одноатрибутный DN с единственным CN сертификата; не извлекает CN из составного DN и не угадывает дубликаты/экранированные значения. Статус/адрес/uptime подтверждены через production API. Native match-by=certificate корректно выбрал персональный mode-config несмотря на общую listener identity; менять порядок identities не понадобилось.
**Поздний сбой истории.** captureAndAppendRevision может вернуть null при отказе capture/БД. Все IPsec apply/rollback теперь проверяют результат и отвечают502 со state=applied, historyRecorded=false и пояснением: операция уже выполнена, обновить данные перед повтором. Observed capture остаётся best effort для чтения. Добавлен createIpsecRoutes с узкими зависимостями для проверок реального HTTP-маршрута. Это не подтверждение транспортного разрыва или частичного применения до recordIpsec: эти границы открыты.
**Проверки.** config tests +80 state checks, HTTP PATCH: успешная запись истории/отказ после подтверждённого native изменения/отказ до записи; backend tsc, scoped ESLint, diff check. Реальный сертификат выпущен через API и экспортирован вPKCS12, импортирован наCHR2 с уникальными именами CA/leaf; private material оставался в0700 каталоге стенда и удалён. IKEv2 established на обоих CHR; responder native и API: staticIp/activeAddress198.18.79.250, online=true, uptime1s. Управляемая инъекция recordRevision=null в изолированном Fastify с тем же compiled route дала502/applied после настоящего выключения собственного peer; production PATCH вернул peer обратно. Это контракт сбоя записи, не реальная авария PostgreSQL. [Отчёт](ux-ui/acceptance/ipsec-certificate-2026-10-09.json).
**Очистка.** До удаления проверены точные ID/name/comment всех собственных объектов; после совпали исходные ID10 коллекций на обоих CHR. CA удалялся после leaf; системные каталоги/исходные файлы сохранены. Пароли не менялись. Первоначальный запуск после deploy столкнулся с readiness race до первого API write; повтор выполнен после docker compose --wait. Далее всегда ждать healthy до native сценария.
**Runtime.** Одна backend production сборка: mmapp-test/backend:ux-ipsec79/version1.0.0-test.ipsec79, image sha256:26b5b05122b7b55aa83b1f80b880d1aee6eaf33caa225afe09327b02a219c5dc1. Frontend ux-ipsec77 сохранён. Context предыдущего ipsec78 + пять backend-файлов из рабочего состояния перед коммитом; archiveSHA50cdacc0016810bd8c87c0bf2eaedbe9ddd4e1d6134a960f6ca11207c4faaff6. SHA runtime исходников совпали локально/удалённо: ipsec-live.ts676b51c9ed7edd9bc5d0cf5a455409b445c46235a55e2bedeff9e07208401bb6, routes/ipsec.ts192a3a7c5628c5e68c5cb20c26b55ce97c9a4c2cce8effea8ed1a26c79ee442d. Label revision=ipsec79, не итоговый Git SHA. .env backup приватный; миграций нет.
Следующий12.5.80: исправить restore mode-config без неподдерживаемого comment, порядок зависимостей и проверить реальный откат изменений. Обнаружен воспроизводимый локальный отказ: существующие reserved mode-config планируются как новые PUT; новая профильная проверка для следующего блока пока красная. Остальные ограничения12.5.77, составные/экранированные DN, NAT/интернет/IPv6/клиентские ОС/RBAC/throughput открыты. Один локальный коммит без push.
## 12.5.78 — IPsec: реальный online и адрес mode-config (2026-10-09)
+14
View File
@@ -1898,3 +1898,17 @@ flowchart LR
**Организация.** По замечанию пользователя о длительности предыдущего блока: одна подтверждённая проблема, профильные проверки, одна backend сборка, без повторной frontend сборки и полного набора E2E. Расход пятичасового окна на контрольной точке8%; это квота, не длительность работы. Один локальный коммит без push. Следующий шаг12.5.79: проверить сертификатный active-peer/DN и mode-config, затем late failure receipts/DB/transport и history restore. Остальные ограничения12.5.77 сохраняются.
Референс: [RouterOS IPsec — Active Peers](https://help.mikrotik.com/docs/spaces/ROS/pages/11993097/IPsec), поля state/dynamic-address/remote-address/uptime. Приёмка подтверждена собственным native ответом и API, а не одной документацией.
## 12.5.79 — IPsec: сертификатный online и поздняя ошибка истории (2026-10-09)
**Результат.** Реальный сертификатный клиент получал198.18.79.250, но отображался offline: active-peers.id возвращается как CN=имя. Mapper сопоставляет полный одноатрибутный DN с единственным CN сертификата; не извлекает CN из составного DN и не угадывает дубликаты/экранированные значения. Статус/адрес/uptime подтверждены через production API. Native match-by=certificate корректно выбрал персональный mode-config несмотря на общую listener identity; менять порядок identities не понадобилось.
**Поздний сбой истории.** captureAndAppendRevision может вернуть null при отказе capture/БД. Все IPsec apply/rollback теперь проверяют результат и отвечают502 со state=applied, historyRecorded=false и пояснением: операция уже выполнена, обновить данные перед повтором. Observed capture остаётся best effort для чтения. Добавлен createIpsecRoutes с узкими зависимостями для проверок реального HTTP-маршрута. Это не подтверждение транспортного разрыва или частичного применения до recordIpsec: эти границы открыты.
**Проверки.** config tests +80 state checks, HTTP PATCH: успешная запись истории/отказ после подтверждённого native изменения/отказ до записи; backend tsc, scoped ESLint, diff check. Реальный сертификат выпущен через API и экспортирован вPKCS12, импортирован наCHR2 с уникальными именами CA/leaf; private material оставался в0700 каталоге стенда и удалён. IKEv2 established на обоих CHR; responder native и API: staticIp/activeAddress198.18.79.250, online=true, uptime1s. Управляемая инъекция recordRevision=null в изолированном Fastify с тем же compiled route дала502/applied после настоящего выключения собственного peer; production PATCH вернул peer обратно. Это контракт сбоя записи, не реальная авария PostgreSQL. [Отчёт](ux-ui/acceptance/ipsec-certificate-2026-10-09.json).
**Очистка.** До удаления проверены точные ID/name/comment всех собственных объектов; после совпали исходные ID10 коллекций на обоих CHR. CA удалялся после leaf; системные каталоги/исходные файлы сохранены. Пароли не менялись. Первоначальный запуск после deploy столкнулся с readiness race до первого API write; повтор выполнен после docker compose --wait. Далее всегда ждать healthy до native сценария.
**Runtime.** Одна backend production сборка: mmapp-test/backend:ux-ipsec79/version1.0.0-test.ipsec79, image sha256:26b5b05122b7b55aa83b1f80b880d1aee6eaf33caa225afe09327b02a219c5dc1. Frontend ux-ipsec77 сохранён. Context предыдущего ipsec78 + пять backend-файлов из рабочего состояния перед коммитом; archiveSHA50cdacc0016810bd8c87c0bf2eaedbe9ddd4e1d6134a960f6ca11207c4faaff6. SHA runtime исходников совпали локально/удалённо: ipsec-live.ts676b51c9ed7edd9bc5d0cf5a455409b445c46235a55e2bedeff9e07208401bb6, routes/ipsec.ts192a3a7c5628c5e68c5cb20c26b55ce97c9a4c2cce8effea8ed1a26c79ee442d. Label revision=ipsec79, не итоговый Git SHA. .env backup приватный; миграций нет.
Следующий12.5.80: исправить restore mode-config без неподдерживаемого comment, порядок зависимостей и проверить реальный откат изменений. Обнаружен воспроизводимый локальный отказ: существующие reserved mode-config планируются как новые PUT; новая профильная проверка для следующего блока пока красная. Остальные ограничения12.5.77, составные/экранированные DN, NAT/интернет/IPv6/клиентские ОС/RBAC/throughput открыты. Один локальный коммит без push.
+4 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-09
Сервер161.104.46.222 предоставлен для временного тестирования. Backend ux-ipsec78/version1.0.0-test.ipsec78, frontend ux-ipsec77/version1.0.0-test.ipsec77; backend/PG healthy, frontend HTTP/browser passed. Основной блок IPsec77 принят в границах журнала, own fixtures/fake DB rows удалены; каталог flash CHR2 сохранён из-за неподтверждённого ownership. SSH/оба CHR доступны, пароли прежние.
Сервер161.104.46.222 предоставлен для временного тестирования. Backend ux-ipsec79/version1.0.0-test.ipsec79, frontend ux-ipsec77/version1.0.0-test.ipsec77; backend/PG healthy, frontend HTTP/browser passed. Основной блок IPsec77 принят в границах журнала, own fixtures/fake DB rows удалены; каталог flash CHR2 сохранён из-за неподтверждённого ownership. SSH/оба CHR доступны, пароли прежние.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -14,12 +14,14 @@
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
| Образы приложения | mmapp-test/backend:ux-ipsec78 и mmapp-test/frontend:ux-ipsec77; запущены |
| Образы приложения | mmapp-test/backend:ux-ipsec79 и mmapp-test/frontend:ux-ipsec77; запущены |
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
IPsec78: PSK active-peer/online и mode-config приняты через native и развёрнутый API; точные собственные ID удалены, восемь коллекций восстановлены. Одна backend сборка, без изменения frontend; подробности и границы — в12.5.78 журнала.
IPsec79: сертификатный responder online=true/static mode-config198.18.79.250 принят; recordRevision=null после native patch даёт502/state=applied. Собственные объекты и private files удалены. [Отчёт](ux-ui/acceptance/ipsec-certificate-2026-10-09.json).
## Развёртывание приложения
Чистые frontend/backend build contexts подготовлены по .gitea/workflows/docker.yml из закреплённого Git HEAD;образы собраны на сервере без push. Локально docker CLI сейчас отсутствует, на сервере доступен Docker. В корневом .dockerignore исключён backend, поэтому обычный backend build из корня не повторяет CI: использовать отдельный backend context, как main workflow. Не передавать .env.local,backend/.env,ключи и локальные node_modules/.next.
@@ -0,0 +1,80 @@
{
"date": "2026-10-09",
"name": "mm-ipsec79-1791534554424",
"observations": [
{
"serverId": 1,
"active": [
{
".id": "*1",
"id": "CN=mm-ipsec79-1791534554424",
"state": "established",
"dynamic-address": "198.18.79.250",
"remote-address": "104.165.244.7",
"uptime": "1s"
}
],
"catalog": [
{
"name": "mm-ipsec79-1791534554424",
"commonName": "mm-ipsec79-1791534554424",
"staticIp": "198.18.79.250",
"online": true,
"activeAddress": "198.18.79.250",
"activeSince": "1s"
}
]
},
{
"serverId": 2,
"active": [
{
".id": "*1",
"id": "194.226.121.150",
"state": "established",
"remote-address": "194.226.121.150",
"uptime": "1s"
}
],
"catalog": []
}
],
"cleanup": [
{
"serverId": 1,
"/ip/ipsec/identity": true,
"/ip/ipsec/peer": true,
"/ip/ipsec/policy": true,
"/ip/ipsec/policy/group": true,
"/ip/ipsec/mode-config": true,
"/ip/pool": true,
"/ip/ipsec/proposal": true,
"/ip/ipsec/profile": true,
"/certificate": true,
"/file": true
},
{
"serverId": 2,
"/ip/ipsec/identity": true,
"/ip/ipsec/peer": true,
"/ip/ipsec/policy": true,
"/ip/ipsec/policy/group": true,
"/ip/ipsec/mode-config": true,
"/ip/pool": true,
"/ip/ipsec/proposal": true,
"/ip/ipsec/profile": true,
"/certificate": true,
"/file": true
}
],
"expectedAddress": "198.18.79.250",
"staticLeaseCorrect": true,
"catalogCorrect": true,
"historyFailure": {
"faultInjection": "recordRevision returns null; no live PostgreSQL outage",
"httpStatus": 502,
"state": "applied",
"historyRecorded": false,
"nativeMutationConfirmed": true
}
}